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.