Welcome to the new Golem Cloud Docs! πŸ‘‹
Documentation
Go Language Guide
Worker to Worker Communication

Worker to Worker communication for Go components

See the Worker to Worker communication page for a general overview of how workers can invoke each other in Golem.

Tooling setup

Make sure that all required tools are installed according to the setup page, including the Worker to Worker communication parts.

Setup with using example RPC Go project

The golem-cli new command can be used to generate an example project which contains multiple components that are calling each other. It also has higher level build scripts for adding, connecting, deploying and testing components.

To create such an example use the following command:

$ golem-cli new --example go-multi-rpc --package-name golem:demo go-example

The command will create a new Golem project in the go-example directory, and print short, language-specific instructions on how to build the project. It also contains documentation in the generated README.md with more details about managing components and worker to worker communication.

In case you prefer manually setting up your project, or if you want to learn more about how golem-cli stubgen works, see the next section.

Setup with directly using golem-cli stubgen

A project building two Golem components where one can call the other can be set up with the following primary steps:

Create components

First create two Go components using the techniques described in the defining components page. The two components should be in two separate directories, enclosed by a root directory for the whole project.

⚠️

It is important to use a different package name for each component, otherwise the stub generator will fail.

This can be done by using the --package-name parameter of golem-cli new for example --package-name rpcdemo:component1 and --package-name rpcdemo:component2.

It is also important to use different exported interface names in the component WIT worlds, otherwise there will be name clashes in the generated bindings.

In this document we will use the golem-cli new command for creating the components:

$ golem-cli new --example go-default --package-name rpcdemo:component1 component1
$ golem-cli new --example go-default --package-name rpcdemo:component2 component2

Then to avoid naming problems, we rename the exported interfaces in both component's wit definition.

component1/wit/component1.wit:

package rpcdemo:component1;
 
    // Renamed from api to component1-api
    interface component1-api {
    // ...
}
 
world component1 {
    // ...
 
    // Renamed from api to component1-api
    export component1-api;
}

component2/wit/component2.wit:

package rpcdemo:component2;
 
// Renamed from api to component2-api
interface component2-api {
    // ...
}
 
world component2 {
    // ...
 
    // Renamed from api to component2-api
    export component2-api;
}

Updating the main.go component implementations is also needed based on the above.

Generate RPC client stub definition and component

All components that we want to call from another component will require client stub definitions and components.

Let's create one for component2 using golem-cli stubgen build:

$ golem-cli stubgen build --source-wit-root component2/wit --dest-wasm component2-stub/component2_stub.wasm --dest-wit-root component2-stub/wit

The above command will place the generated WIT definitions and WASM component into the component2-stub folder:

$ tree component2-stub
component2-stub
β”œβ”€β”€ component2_stub.wasm   // stub component
└── wit
    β”œβ”€β”€ _stub.wit          // stub definition
    └── deps
        β”œβ”€β”€ ...
        .
        .
        .

Add the stub dependency to the caller project

Now we add the generated stub definition to the caller project with the golem-cli stubgen add-stub-dependency

golem-cli stubgen add-stub-dependency --stub-wit-root component2-stub/wit  --dest-wit-root component1/wit --overwrite

After this step the stub definitions will be present in the component1/wit/deps directory:

$  tree component1/wit
component1/wit
β”œβ”€β”€ component1.wit
└── deps
    .
    .
    .
    β”œβ”€β”€ rpcdemo_component2         // component2 definition
    β”‚   └── component2.wit
    β”œβ”€β”€ rpcdemo_component2-stub    // component2 stub definition
    β”‚   └── _stub.wit
    .
    .
    .

Import the stub definition in the caller world

To make the stub definition available for binding generation we have to import it in the callers world. To do so we have to edit the component1/wit/component1.wit file:

package rpcdemo:component1;
 
interface api {
    // ...
}
 
world component1 {
    // ... other imports
 
    // Add the following import for the client stub:
    import rpcdemo:component2-stub/stub-component2;
 
    export api;
}

Regenerate bindings

To actually be able to use the imported stub we regenerate our bindings, which can done in the component1 directory with:

$ make bindings
# the above will execute: wit-bindgen tiny-go --world component1 --out-dir component1 ./wit

Creating the worker client

The worker to be invoked is identified by an URI which consists of the component ID and the worker name.

If the worker pointed by the URI does not exists, it is going to be created automatically and it inherits the environment variables of the caller.

A common pattern is to use environment variables to configure the deployed component's component ID and use that to construct the URI. See the defining components page for more information about passing configuration values to workers.

The following code snippet gets the target component's id from an environment variable, and constructs the URI for the target worker with a fixed worker name target-worker:

import "rpcdemo/component1/component1"
 
// We get the component ID from environment variable, because it depends on the deploy
component2ID := os.Getenv("COMPONENT2_ID")
workerName := "target-worker"
 
// Create new worker client with an URN constructed from the component ID and worker name
component2WorkerClient := component1.NewComponent2Api(
    component1.GolemRpc0_1_0_TypesUri{
        Value: fmt.Sprintf("urn:worker:%s/%s", component2ID, workerName),
    },
)
 
// Cleanup the client after use
defer component2WorkerClient.Drop()

Calling worker client functions

For each exported function in the target component, the generated stub contains two exported variants:

  • A blocking variant, prefixed with Blocking, which does not return until the remote worker finished processing the call.
  • A non-blocking variant, which returns a pollable result immediately (unless for remote functions without any return value, in which case it just triggers the invocation but does not return anything)

An example blocking call, using the client created in the previous step:

// Example for a blocking call, this will wait until the other worker finished
component2WorkerClient.BlockingAdd(3)

And a non-blocking one, which assumes two worker clients, and uses the low level WASI poll API:

futureResult1 := component2Worker1Client.Get()
defer futureResult1.Drop()
futureResult2 := component2Worker2Client.Get()
defer futureResult2.Drop()
 
pollResult1 := futureResult1.Subscribe()
defer pollResult1.Drop()
pollResult2 := futureResult2.Subscribe()
defer pollResult2.Drop()
 
futures := []component1.RpcdemoComponent2StubStubComponent2FutureGetResult{futureResult1, futureResult2}
polls := []component1.WasiIo0_2_0_PollPollable{pollResult1, pollResult2}
results := make([]uint64, len(futures))
 
for {
    // Poll returns the indices of the finished future poll handles
    for _, idx := range component1.WasiIo0_2_0_PollPoll(polls) {
    result := futures[idx].Get()
    if result.IsNone() {
        panic("no future result after poll success")
    }
    results[idx] = result.Unwrap()
 
    futures = append(futures[:idx], futures[idx+1:]...)
        polls = append(polls[:idx], polls[idx+1:]...)
    }
    if len(polls) == 0 {
        break
    }
}

Composing the stub client component into the caller component

Before we can deploy our built component we also have to compose the stub client's component into the caller's one.

For this we will add the following golem-cli stubgen compose command as the last step in our makefile:

golem-cli stubgen compose --source-wasm component1.wasm --stub-wasm ../component2-stub/component2_stub.wasm --dest-wasm component1-with-component2-stub.wasm

So the final build step in out makefile will look like this:

build: compile
    wasm-tools component embed ./wit component1.module.wasm --output component1.embed.wasm
    wasm-tools component new component1.embed.wasm -o component1.wasm --adapt adapters/tier1/wasi_snapshot_preview1.wasm
    golem-cli stubgen compose --source-wasm component1.wasm --stub-wasm ../component2-stub/component2_stub.wasm --dest-wasm component1-with-component2-stub.wasm

Now we can use make build to create our component with remote worker calls, then component1-with-component2-stub.wasm is ready to be deployed to golem.