Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduced golem-network plugin api #1126

Merged
merged 3 commits into from
Nov 20, 2024
Merged

Conversation

grisha87
Copy link
Contributor

This PR introduces the plugin API I always wanted to introduce to the project.

The main motivators are described in the feature's documentation (see below).

Documentation

The feature documentation has been specified in the MD file:
https://github.com/golemfactory/golem-js/blob/feature/golem-plugins/docs/PLUGINS.md

Next steps

The implementation of plugin API enables code reuse and modularization, but doesn't influence how Golem Network behaves. This is an aspect that I want to leave to hooks which are another concept that I would like to implement next, that will be then a power combo with the plugin API introduced in this PR.

Example

A large example of how this works is attached here:

import { GolemNetwork, GolemPluginInitializer } from "../src";

const avgOf = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;

const checkGlmPriceBeforeStarting: GolemPluginInitializer<{
  maxPrice: number;
}> = async (_glm, opts) => {
  const response = await fetch("https://api.coinpaprika.com/v1/tickers/glm-golem");

  if (!response.ok) {
    throw new Error("Failed to fetch GLM price");
  } else {
    const data = await response.json();
    console.log("=== GLM Price ===");
    const price = parseFloat(data.quotes.USD.price);
    console.log("GLM price is", price);
    console.log("=== GLM Price ===");

    if (price > opts.maxPrice) {
      throw new Error("GLM price is too high, won't compute today :O");
    }
  }
};

const providerTracker: GolemPluginInitializer = (glm) => {
  const seenProviders: { id: string; name: string }[] = [];

  glm.market.events.on("offerProposalReceived", (event) => {
    const { id, name } = event.offerProposal.provider;

    const providerInfo = { id, name };

    if (!seenProviders.includes(providerInfo)) {
      seenProviders.push(providerInfo);
      console.log("Saw new provider %s named %s", id, name);
    }
  });

  return () => {
    console.log("Provider tracker found a total of %d providers", seenProviders.length);
  };
};

const offerTrackerPlugin: GolemPluginInitializer = (glm) => {
  const startPrices: number[] = [];
  const cpuPrices: number[] = [];
  const envPrices: number[] = [];

  glm.market.events.on("offerProposalReceived", (event) => {
    const { start, envSec, cpuSec } = event.offerProposal.pricing;
    startPrices.push(start);
    cpuPrices.push(cpuSec);
    envPrices.push(envSec);
  });

  console.log("Initialized offer tracker");

  const printStats = () => {
    const avgStart = avgOf(startPrices);
    const avgCpu = avgOf(cpuPrices) * 3600;
    const avgEnv = avgOf(envPrices) * 3600;

    console.log(
      "[OT Stats] Prices: AVG start %f, AVG CPU/h %f, AVG ENV/h %f (based on %d offers)",
      avgStart.toFixed(2),
      avgCpu.toFixed(2),
      avgEnv.toFixed(2),
      startPrices.length,
    );
  };

  const int = setInterval(printStats, 10_000);

  return () => {
    clearInterval(int);
    printStats();
    console.log("Offer tracker finished");
  };
};

(async () => {
  const glm = new GolemNetwork();

  // Example of a plugin with options
  glm.use(offerTrackerPlugin);

  // Example of a plugin without options
  glm.use(providerTracker);

  // Example of an async plugin
  glm.use(checkGlmPriceBeforeStarting, {
    maxPrice: 0.5,
  });

  // If the plugin requires options, not providing them will raise a TS error
  //glm.use(checkGlmPriceBeforeStarting);

  try {
    console.log("Connecting to Golem Network");
    await glm.connect();

    console.log("Acquiring resources from the market");
    const rental = await glm.oneOf({
      order: {
        demand: {
          workload: {
            imageTag: "golem/node:latest",
            minStorageGib: 1,
            // runtime: {
            //   version: "0.5.2",
            // },
          },
        },
        market: {
          rentHours: 15 / 60,
          pricing: {
            avgGlmPerHour: 1,
            model: "burn-rate",
          },
        },
      },
    });

    console.log("Deployng workload to the rental");
    const exe = await rental.getExeUnit();

    console.log("Running command on the workload");
    const result = await exe.run("df -h");
    console.log(result.stdout?.toString());

    console.log("Finalizing the rental and releasing resources to the market");
    await rental.stopAndFinalize();
  } finally {
    await glm.disconnect();
  }
})().catch(console.error);

@grisha87 grisha87 changed the base branch from master to beta November 19, 2024 14:04
@grisha87 grisha87 force-pushed the feature/golem-plugins branch from 7b20dd2 to d9a5569 Compare November 19, 2024 14:43
@grisha87 grisha87 force-pushed the feature/golem-plugins branch from d9a5569 to 7b77bd3 Compare November 19, 2024 16:53
Copy link
Contributor

@mgordel mgordel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job, it looks very clean and readable. I played around with it a bit and it works fine.

There is one scenario where the user can cause the application to hang:
if he calls _glm.disconnect() from a plugin, the cleanup functions will not be executed. I know that we can't protect users from bad and stupid implementations ;)

It might be a good idea to include these examples from the PLUGINS.md in @examples/plugins - it would also be a test for CI

src/golem-network/plugin.ts Outdated Show resolved Hide resolved
@grisha87 grisha87 merged commit 18b549c into beta Nov 20, 2024
8 checks passed
@grisha87
Copy link
Contributor Author

Good job, it looks very clean and readable. I played around with it a bit and it works fine.

There is one scenario where the user can cause the application to hang: if he calls _glm.disconnect() from a plugin, the cleanup functions will not be executed. I know that we can't protect users from bad and stupid implementations ;)

It might be a good idea to include these examples from the PLUGINS.md in @examples/plugins - it would also be a test for CI

I was thinking about this, but resigned from the idea to not have our CI poke on the coinpaprika's API during builds to not make these examples tests even more fragile. But you're right, we can add more examples that won't need that API call. Will consider this for the next PR.

@grisha87 grisha87 deleted the feature/golem-plugins branch November 20, 2024 13:11
@grisha87 grisha87 mentioned this pull request Nov 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants