JS Quickstart Explained
Introduction
This tutorial is based on the same example as Quickstart and includes thorough explanations of the script code.
Prerequisites
Yagna service is installed and running with the try_golem
app-key configured.
Setting up the project
Create a project folder, initialize a Node.js project, and install the JS SDK libraries.
mkdir try_golem
cd try_golem
npm init
npm install @golem-sdk/golem-js
npm install @golem-sdk/pino-logger
JS script structure
The basic structure of the script:
import { GolemNetwork } from '@golem-sdk/golem-js'
import { pinoPrettyLogger } from '@golem-sdk/pino-logger'
// order definition will be placed here
;(async () => {
//... Function body in here
})().catch(console.error)
Here we do two things:
- import
GolemNetwork
from @golem-sdk/golem-js andpinoPrettyLogger
from "@golem-sdk/pino-logger" libraries, - create IIAFE (Immediately Invoked Async Function Expression). It has an async function declaration because
GolemNetwork
provides async methods.
Utilizing the GolemNetwork instance
The GolemNetwork
is the main entry point for golem-js
. We will create a new instance of this object, then use the .connect()
method before interacting with the network and .disconnect()
to properly close all dealings.
import { GolemNetwork } from '@golem-sdk/golem-js'
const glm = new GolemNetwork()
try {
await glm.connect() // Do your work here
} catch (err) {
// Handle any errors
} finally {
await glm.disconnect()
}
The GolemNetwork
constructor accepts some parameters:
const glm = new GolemNetwork({
logger: pinoPrettyLogger({
level: 'info',
}),
api: { key: 'try_golem' },
})
- api-key value - a key that will give us access to
yagna
REST API.yagna
is a service that connects us to the network. In this example, we will use api-key that was generated in the process of Yagna installation - logger instance - we use a special logger that produces logs easier to read and understand when scripts are run in the terminal, without this line a default logger would be used, which is the debug logger.
Once we connect to the network (in reality, connecting to the locally installed yagna
), we can leverage the API of the GolemNetwork
object. In this example, we will use .manyOf()
to acquire computational resources, if we needed a single provider we could alternatively get a rental
directly from the GolemNetowk
using the .oneOf()
method. (Note, there is also a low-level API available, that lets you dive deep into various subdomains within the Golem Network domain space.)
const pool = await glm.manyOf({
// I want to have a minimum of one machine in the pool, but only a maximum of 3 machines can work at the same time
poolSize: { min: 1, max: 3 },
order,
})
The manyOf()
method returns us a pool of ResourceRental
aggregates. The ResourceRental
wraps around the details of different entities and processes needed to manage the rental of resources. If you are interested in the details (also accessible from the Golem Network object) and would like to learn more about Agreements, Allocations, Activities, Invoices, DebitNotes, and related conversations required by the protocol please read articles like this one.
Our pool will consist of a minimum of 1 and a maximum of 3 providers available at the same time. Providers will have their environments defined by the order
. It is an object that contains information about the environment we want to run on the provider, and potentially, criteria for the provider selection. In our example, when describing resources needed, we will only indicate the image to be run on a remote node. We will use an image publicly available on the registry portal, therefore it is enough to provide a tag golem/alpine:latest
- it indicates an image based on alpine
distribution. Users can also specify other parameters like the number of threads, memory, or disk size. For the provider selection, our example precises also the maximum acceptable prices using the linear
price model. Finally, the rentHour
defines the maximum duration of the engagements with providers before automatic termination.
const order = {
demand: {
workload: { imageTag: 'golem/alpine:latest' },
},
market: {
rentHours: 0.5,
pricing: {
model: 'linear',
maxStartPrice: 0.5,
maxCpuPerHourPrice: 1.0,
maxEnvPerHourPrice: 0.5,
},
},
}
The pool's .withRental()
method wraps operations on the pool exposing a rental instance. The rental gives us access to the ExeUnit
- a representation of the execution environment on the provider node. The ExeUnit
models the commands supported by the runtime. In our example, we will execute a command in the default shell using the .run()
method. The command: echo "Part #${i} computed on provider ${exe.provider.name} with CPU:" && cat /proc/cpuinfo | grep 'model name'
is a Linux command that will produce filtered content of the /proc/cpuinfo
file from the node. Note how we accessed the name of the provider node: the ExeUnit
instance offers the whole context including the provider's details.
pool.withRental((rental) =>
rental
.getExeUnit()
.then((exe) =>
exe.run(
`echo "Part #${i} computed on provider ${exe.provider.name} with CPU:" && cat /proc/cpuinfo | grep 'model name'`
)
)
)
The output of the commands executed on the remote node is a Promise
of a result
object. Once it is resolved and fulfilled
it contains the output of the command we run, available as a stdout
property.
To run the command 5 times, in parallel on the maximum of 3 providers, we utilize the map()
method and for each data
array element we will acquire a rental
and its exe
to execute the command. We use allSettled()
to handle all promises from each exe.run()
and produce the result
array. Then we can iterate over the result
and get the actual command outputs (if the command execution succeeded).
const data = [...Array(5).keys()]
const results = await Promise.allSettled(
data.map((item) =>
pool.withRental((rental) =>
rental
.getExeUnit()
.then((exe) =>
exe.run(
` echo ${exe.provider.name} && cat /proc/cpuinfo | grep 'model name' `
)
)
)
)
)
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('Success', result.value.stdout)
} else {
console.log('Failure', result.reason)
}
})
Here there is the whole code:
/**
* This example demonstrates how easily lease multiple machines at once.
*/
import { GolemNetwork } from "@golem-sdk/golem-js";
import { pinoPrettyLogger } from "@golem-sdk/pino-logger";
const order = {
demand: {
workload: { imageTag: "golem/alpine:latest" },
},
market: {
rentHours: 0.5,
pricing: {
model: "linear",
maxStartPrice: 0.5,
maxCpuPerHourPrice: 1.0,
maxEnvPerHourPrice: 0.5,
},
},
};
(async () => {
const glm = new GolemNetwork({
logger: pinoPrettyLogger({
level: "info",
}),
api: { key: "try_golem" },
});
try {
await glm.connect();
const pool = await glm.manyOf({
// I want to have a minimum of one machine in the pool,
// but only a maximum of 3 machines can work at the same time
poolSize: { min: 1, max: 3 },
order,
});
// I have 5 parts of the task to perform in parallel
const data = [...Array(5).keys()];
const results = await Promise.allSettled(
data.map((i) =>
pool.withRental((rental) =>
rental
.getExeUnit()
.then((exe) =>
exe.run(
`echo "Part #${i} computed on provider ${exe.provider.name} with CPU:" && cat /proc/cpuinfo | grep 'model name'`,
),
),
),
),
);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Success:", result.value.stdout);
} else {
console.log("Failure:", result.reason);
}
});
} catch (err) {
console.error("Failed to run the example", err);
} finally {
await glm.disconnect();
}
})().catch(console.error);
Summary
We had created the simple requestor script, that ran the same command on a number of remote computers. To achieve it we had:
- imported
@golem-sdk/golem-js
lib - utilized Immediately Invoked Async Function Expression
- created GolemNetwork instance
- created a pool of
rental
objects - acquired
rentals
and accessed relatedExeUnits
, where we ran the command - finally, we collected the results from the
result
objects and provided them to the user.
In this example, we ran a simple command in a shell on the remote computer. You can run other executable programs in more advanced scenarios. See other examples for other interesting features.