Worker to Worker communication
See the Worker to Worker communication page for a general overview of how workers can invoke each other in Golem.
Setting up
For doing worker to worker communication between Golem components, all the components involved should be defined in the same application created with the golem app new
command. Once the application is created the components can be added using golem component new
.
Creating a project with two components
A project building two Golem components where one can call the other can be set up with the following primary steps:
Create the two components
First create two components using the techniques described in the "defining components" page for the chosen language.
In the following example snippets we will use the following component names:
example:caller
the component that will call another componentexample:provider
the component that provides some API thatexample:caller
will call
Set up the dependencies between the components
In the root golem.yaml
file add a dependencies
section:
dependencies:
example:caller:
- target: example:provider
type: wasm-rpc
The type
field can be one of the following:
wasm-rpc
: Worker to Worker communication using dynamic linking. This is the default and recommended option. The resulting component will import client functions for connecting to the other worker, and these imports will be resolved by Golem automatically when the worker starts.static-wasm-rpc
: Worker to Worker communication using static linking. In this case the client gets generated as Rust source code, and compiled and linked statically with your component. Using this option can be useful for debugging but otherwise deprecated and requires a working Rust toolchain on your computer.
Check it works by running golem app build
in the root directory of the application, which should build all the components.
Sometimes it can be useful for a component to depend on itself:
- Different workers of the same component can communicate with each other
- A worker can schedule a call for later for another worker, or even itself
For this just use the same component name in the target
field of the dependencies
section.
Import the generated client in the caller component
When defining RPC dependencies like above, the golem app build
command will automatically generate client WIT interfaces for all the target components, and import them into the components that are dependent on them.
For example with the above setup, in the end the following import would be added in the example:caller
component's WIT world:
package example:caller;
world example-caller {
import example:provider-client/example-provider-client;
// ...
}
Note that the wit
directory of each component remains untouched and should only be modified by the user. These automatic imports are generated in the wit-generated
directory and should not be modified manually.
The exact name to be imported depends on what package and component names were used when creating the target component.
In this example the package name was example:provider
. This gives the generated client package example:provider-client
with an exported interface example-provider-client
.
Start implementing the components
With this the workspace is set up, and the generated clients can be used from the caller component to call the target component, as it is described in the next section.
Once the code is written, just use golem app build
to build all the components.
Writing blocking remote calls
For each exported function in the target component, the generated client 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)
To use the blocking variants we need to construct a resource from the generated client, passing the remote Golem URI of the target worker to the constructor.
Taking the default template as the target component, which has the following exported interface:
package example:provider;
interface example-providerapi {
add: func(value: u64);
get: func() -> u64;
}
world example-provider {
export example-provider-api;
}
The generated client will contain the following functions:
package example:provider-client;
interface example-provider-client {
use wasi:io/poll@0.2.0.{ pollable as wasi-io-pollable };
use wasi:clocks/wall-clock@0.2.0.{ datetime as wasi-clocks-datetime };
use golem:rpc/types@0.2.0.{ worker-id as golem-rpc-worker-id, cancellation-token as golem-rpc-cancellation-token };
resource future-get-result {
subscribe: func() -> pollable;
get: func() -> option<u64>;
}
resource example-provider-api {
constructor(worker-name: string);
custom: static func(worker-id: golem-rpc-worker-id) -> example-provider-api;
blocking-add: func(value: u64);
add: func(value: u64);
schedule-add: func(value: u64, scheduled-for: wasi-clocks-datetime) -> golem-rpc-cancellation-token;
blocking-get: func() -> u64;
get: func() -> future-get-result;
schedule-get: func(scheduled-for: wasi-clocks-datetime) -> golem-rpc-cancellation-token;
}
}
world wasm-rpc-client-example-provider {
export example-provider-client;
}
If the targeted component is ephemeral, the interface is slightly different: there is no need to pass a worker name to the constructor, and the custom
constructor just takes a component-id
.
To use this generated example-providerapi
resource, the following steps are needed in the caller component (example:caller
in the example):
Import the generated types
The generated bindings for the generated stubs need to be imported in the module where the remote procedure calls are made.
As this import is automatically added to the caller component, we can import the generated bindings in our source code to be able to call the example:provider
workers:
use crate::bindings::example::provider_client::example_provider_client::ExampleProviderApi;
Deciding which constructor to use
Each generated client resource has two constructors:
- The default one (
new
) takes a worker name string for durable components, and nothing for ephemeral components. - The custom one (
custom
) is a static method that takes a worker ID for durable components, or a component ID for ephemeral components.
When using the default constructors, Golem assumes that the target component's name is the one specified in the app manifest (in our example: example:provider
). This is always true if the application was deployed using the golem app deploy
command. The custom constructor is there for advanced use cases where you want to target a separately deployed component with a different name and you know its identifier.
Note that if the client is for a remote resource (and not for the remote component's top-level exports), the constructors will also require passing the resource's constructors beside the ones defining the RPC target, such as the worker name.
Construct and use the API resource
In our example the example:provider
is a durable component, so the ExampleProviderApi
resource can be constructed by passing a worker name and then used to call the remote functions.
let remote_api = ExampleProviderApi::new("worker-1");
remote_api.blocking_add(3);
Writing non-blocking remote calls
Using the non-blocking variants of the generated stub functions requires the same steps as the blocking variants, but the returned value is a special future which needs to be subscribed to and polled using the low-level WASI poll
interface.
The following snippet demonstrates how to get the value of three counters simultaneously, assuming that counter1
, counter2
and counter3
are all insteances of the generated stub's Api
resource:
let remote_api1 = new ExampleRustApi("worker-1");
let remote_api2 = new ExampleRustApi("worker-2");
let remote_api3 = new ExampleRustApi("worker-3");
// Making the non-blocking calls
let future_value1 = remote_api1.get();
let future_value2 = remote_api2.get();
let future_value3 = remote_api3.get();
let futures = &[&future_value1, &future_value2, &future_value3];
// Subscribing to get the results
let poll_value1 = future_value1.subscribe();
let poll_value2 = future_value2.subscribe();
let poll_value3 = future_value3.subscribe();
let mut values = [0u64; 3];
let mut remaining = vec![&poll_value1, &poll_value2, &poll_value3];
let mut mapping = vec![0, 1, 2];
// Repeatedly poll the futures until all of them are ready
while !remaining.is_empty() {
let poll_result = bindings::wasi::io::poll::poll(&remaining);
// poll_result is a list of indexes of the futures that are ready
for idx in &poll_result {
let counter_idx = mapping[*idx as usize];
let future = futures[counter_idx];
let value = future
.get()
.expect("future did not return a value because after marked as completed");
values[counter_idx] = value;
}
// Removing the completed futures from the list
remaining = remaining
.into_iter()
.enumerate()
.filter_map(|(idx, item)| {
if poll_result.contains(&(idx as u32)) {
None
} else {
Some(item)
}
})
.collect();
// Updating the index mapping
mapping = mapping
.into_iter()
.enumerate()
.filter_map(|(idx, item)| {
if poll_result.contains(&(idx as u32)) {
None
} else {
Some(item)
}
})
.collect();
}
// values contains the results of the three calls
Deploying the resulting components
To build deployable WASM files of the involved components, use
golem app build
and upload them using
golem app deploy