Skip to content
This repository was archived by the owner on Feb 26, 2025. It is now read-only.

Commit 1668f29

Browse files
authored
Merge pull request #60 from golemcloud/go-multi-rpc
Go example project with RPC and multiple components
2 parents f483888 + 4e91a11 commit 1668f29

File tree

30 files changed

+1680
-14
lines changed

30 files changed

+1680
-14
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,19 @@ The following fields are required:
2424
The following fields are optional:
2525

2626
- `requiresAdapter` is a boolean, defaults to **true**. If true, the appropriate version of the WASI Preview2 to Preview1 adapter is copied into the generated project (based on the guest language) to an `adapters` directory.
27-
- `requiresGolemHostWIT` is a boolean, defaults to **false**. If true, the Golem specific WIT interface gets copied into `wit/deps`.
27+
- `requiresGolemHostWIT` is a boolean, defaults to **false**. If true, the Golem specific WIT interface gets copied into `wit/deps`.
2828
- `requiresWASI` is a boolean, defaults to **false**. If true, the WASI Preview2 WIT interfaces which are compatible with Golem Cloud get copied into `wit/deps`.
29+
- `witDepsPaths` is an array of directory paths, defaults to **null**. When set, overrides the `wit/deps` directory for the above options and allows to use multiple target dirs for supporting multi-component examples.
2930
- `exclude` is a list of sub-paths and works as a simplified `.gitignore` file. It's primary purpose is to help the development loop of working on examples and in the future it will likely be dropped in favor of just using `.gitignore` files.
31+
- `instructions` is an optional filename, defaults to **null**. When set, overrides the __INSTRUCTIONS__ file used for the example, the file needs to be placed to same directory as the default instructions file.
3032

3133
### Template rules
3234

3335
Golem examples are currently simple and not using any known template language, in order to keep the examples **compilable** as they are - this makes it very convenient to work on existing ones and add new examples as you can immediately verify that it can be compiled into a _Golem template_.
3436

3537
When calling `golem-new` the user specifies a **template name**. The provided component name must use either `PascalCase`, `snake_case` or `kebab-case`.
3638

37-
There is an optional parameter for defining a **package name**, which defaults to `golem:component`. It has to be in the `pack:name` format.
39+
There is an optional parameter for defining a **package name**, which defaults to `golem:component`. It has to be in the `pack:name` format. The first part of the package name is called **package namespace**.
3840

3941
The following occurrences get replaced to the provided component name, applying the casing used in the template:
4042
- `component-name`
@@ -46,6 +48,8 @@ The following occurrences get replaced to the provided component name, applying
4648
- `pack-name`
4749
- `pack/name`
4850
- `PackName`
51+
- `pack-ns`
52+
- `PackNs`
4953

5054
### Testing the examples
5155
The example generation and instructions can be tested with a test [cli app](/src/test/main.rs).

examples/go/INSTRUCTIONS-mage

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
See the documentation about installing tooling: https://learn.golem.cloud/docs/go-language-guide/setup
2+
and see more details in the README.md file about other commands.
3+
4+
Before first time build, generate RPC stubs:
5+
go run mage.go updateRpcStubs
6+
7+
Generate bindings, compile and compose components:
8+
go run mage.go build
9+
10+
Deploy components to default profile:
11+
go run mage.go deploy
12+
13+
Test the deployed components:
14+
go run mage.go testIntegration

examples/go/go-multi-rpc/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
binding
2+
/target/

examples/go/go-multi-rpc/README.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Golem Go Example with Multiple Components and Worker to Worker RPC Communication
2+
3+
## Building
4+
The project uses [magefile](https://magefile.org/) for building. Either install the tool binary `mage`,
5+
or use the __zero install option__: `go run mage.go`. This readme will use the latter.
6+
7+
To see the available commands use:
8+
9+
```shell
10+
go run mage.go
11+
Targets:
12+
addStubDependency adds generated and built stub dependency to componentGolemCliAddStubDependency
13+
build alias for BuildAllComponents
14+
buildAllComponents builds all components
15+
buildComponent builds component by name
16+
buildStubComponent builds RPC stub for component
17+
clean cleans the projects
18+
deploy adds or updates all the components with golem-cli\'s default profile
19+
generateBinding generates go binding from WIT
20+
generateNewComponent generates a new component based on the component-template
21+
stubCompose composes dependencies
22+
testIntegration tests the deployed components
23+
tinyGoBuildComponentBinary build wasm component binary with tiny go
24+
updateRpcStubs builds rpc stub components and adds them as dependency
25+
wasmToolsComponentEmbed embeds type info into wasm component with wasm-tools
26+
wasmToolsComponentNew create golem component with wasm-tools
27+
```
28+
29+
For building the project for the first time (or after `clean`) use the following commands:
30+
31+
```shell
32+
go run mage.go updateRpcStubs
33+
go run mage.go build
34+
```
35+
36+
After this, using the `build` command is enough, unless there are changes in the RPC dependencies,
37+
in that case `updateRpcStubs` is needed again.
38+
39+
The final components that are usable by golem are placed in the `target/components` folder.
40+
41+
## Deploying and testing the example
42+
43+
In the example 3 simple counter components are defined, which can be familiar from the smaller examples. To showcase the remote calls, the counters `add` functions are connected, apart from increasing their own counter:
44+
- **component one** delegates the add call to **component two** and **three** too,
45+
- and **component two** delegates to **component three**.
46+
47+
In both cases the _current worker name_ will be used as _target worker name_ too.
48+
49+
Apart from _worker name_, remote calls also require the **target components' deployed ID**. For this the example uses environment variables, and uses the `lib/cfg` subpackage (which is shared between the components) to extract it.
50+
51+
The examples assume a configured default `golem-cli` profile, and will use that.
52+
53+
To test, first we have to build the project as seen in the above:
54+
55+
```shell
56+
go run mage.go updateRpcStubs
57+
go run mage.go build
58+
```
59+
60+
Then we can deploy our components with `golem-cli`, for which a wrapper magefile command is provided:
61+
62+
```shell
63+
go run mage.go deploy
64+
```
65+
66+
Once the components are deployed, a simple example integration test suite can be used to test the components.
67+
The tests are in the [/integration/integration_test.go](/integration/integration_test.go) test file, and can be run with:
68+
69+
```shell
70+
go run mage.go testIntegration
71+
```
72+
73+
The `TestDeployed` simply tests if our components metadata is available through `golem-cli component get`.
74+
75+
The `TestCallingAddOnComponentOneCallsToOtherComponents` will:
76+
- get the _component URNs_ with `golem-cli component get`
77+
- generates a _random worker name_, so our tests are starting from a clean state
78+
- adds 1 - 1 worker for component one and component two with the required _environment variables_ containing the other workers' _component ids_
79+
- then makes various component invocations with `golem-cli worker invoke-and-await` and tests if the counters - after increments - are holding the right value according to the delegated `add` function calls.
80+
81+
## Adding Components
82+
83+
Use the `generateNewComponent` command to add new components to the project:
84+
85+
```shell
86+
go run mage.go generateNewComponent component-four
87+
```
88+
89+
The above will create a new component in the `components/component-four` directory based on the template at [/component-template/component](/component-template/component).
90+
91+
After adding a new component the `build` command will also include it.
92+
93+
## Using Worker to Worker RPC calls
94+
95+
### Under the hood
96+
97+
Under the hood the _magefile_ commands below (and for build) use generic `golem-cli stubgen` subcommands:
98+
- `golem-cli stubgen build` for creating remote call _stub WIT_ definitions and _WASM components_ for the stubs
99+
- `golem-cli stubgen add-stub-dependency` for adding the _stub WIT_ definitions to a _component's WIT_ dependencies
100+
- `golem-cli stubgen compose` for _composing_ components with the stub components
101+
102+
### Magefile commands and required manual steps
103+
104+
The dependencies between components are defined in the [/magefiles/magefile.go](/magefiles/magefile.go) build script:
105+
106+
```go
107+
// componentDeps defines the Worker to Worker RPC dependencies
108+
var componentDeps = map[string][]string{
109+
"component-one": {"component-two", "component-three"},
110+
"component-two": {"component-three"},
111+
}
112+
```
113+
114+
After changing dependencies the `updateRpcStubs` command can be used to create the necessary stubs:
115+
116+
```shell
117+
go run mage.go updateRpcStubs
118+
```
119+
120+
The command will create stubs for the dependency projects in the ``/target/stub`` directory and will also place the required stub _WIT_ interfaces on the dependant component's `wit/deps` directory.
121+
122+
To actually use the dependencies in a project it also has to be manually imported in the component's world.
123+
124+
E.g. with the above definitions the following import has to be __manually__ added to `/components/component-one/wit/component-one.wit`:
125+
126+
```wit
127+
import pack-ns:component-two-stub;
128+
import pack-ns:component-three-stub;
129+
```
130+
131+
So the component definition should like similar to this:
132+
133+
```wit
134+
package pack-ns:component-one;
135+
136+
// See https://component-model.bytecodealliance.org/design/wit.html for more details about the WIT syntax
137+
138+
interface component-one-api {
139+
add: func(value: u64);
140+
get: func() -> u64;
141+
}
142+
143+
world component-one {
144+
// Golem dependencies
145+
import golem:api/host@0.2.0;
146+
import golem:rpc/types@0.1.0;
147+
148+
// WASI dependencies
149+
import wasi:blobstore/blobstore;
150+
// .
151+
// .
152+
// .
153+
// other dependencies
154+
import wasi:sockets/instance-network@0.2.0;
155+
156+
// Project Component dependencies
157+
import pack-ns:component-two-stub;
158+
import pack-ns:component-three-stub;
159+
160+
export component-one-api;
161+
}
162+
```
163+
164+
After this `build` (or the `generateBinding`) command can be used to update bindings, which now should include the
165+
required functions for calling other components.
166+
167+
Here's an example that delegates the `Add` call to another component and waits for the result:
168+
169+
```go
170+
import (
171+
"github.com/golemcloud/golem-go/std"
172+
173+
"golem-go-project/components/component-one/binding"
174+
)
175+
176+
177+
func (i *Impl) Add(value uint64) {
178+
std.Init(std.Packages{Os: true, NetHttp: true})
179+
180+
componentTwo := binding.NewComponentTwoApi(binding.GolemRpc0_1_0_TypesUri{Value: "uri"})
181+
defer componentTwo.Drop()
182+
componentTwo.BlockingAdd(value)
183+
184+
i.counter += value
185+
}
186+
```
187+
188+
Once a remote call is in place, the `build` command will also compose the stub components into the caller component.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
func main() {
13+
if len(os.Args) != 3 {
14+
exit(0, fmt.Sprintf("Usage: %s <package"+"-org> <component"+"-name>", os.Args[0]))
15+
}
16+
17+
componentTemplateRoot := "component-template/component"
18+
pkgOrg := os.Args[1]
19+
compName := os.Args[2]
20+
componentDir := filepath.Join("components", compName)
21+
22+
_, err := os.Stat(componentDir)
23+
if err == nil {
24+
exit(1, fmt.Sprintf("Target component directory already exists: %s", componentDir))
25+
}
26+
if err != nil && !os.IsNotExist(err) {
27+
exit(1, err.Error())
28+
}
29+
30+
err = os.MkdirAll(componentDir, 0755)
31+
if err != nil {
32+
exit(1, err.Error())
33+
}
34+
35+
err = fs.WalkDir(
36+
os.DirFS(componentTemplateRoot),
37+
".",
38+
func(path string, d fs.DirEntry, err error) error {
39+
srcFilePath := filepath.Join(componentTemplateRoot, path)
40+
fileInfo, err := os.Stat(srcFilePath)
41+
if err != nil {
42+
return fmt.Errorf("stat failed for template %s, %w", srcFilePath, err)
43+
}
44+
45+
if fileInfo.IsDir() {
46+
return nil
47+
}
48+
49+
switch path {
50+
case "main.go":
51+
err = generateFile(pkgOrg, compName, srcFilePath, filepath.Join(componentDir, path))
52+
case "wit/component.wit":
53+
err = generateFile(pkgOrg, compName, srcFilePath, filepath.Join(componentDir, "wit", compName+".wit"))
54+
default:
55+
err = copyFile(srcFilePath, filepath.Join(componentDir, path))
56+
}
57+
if err != nil {
58+
return fmt.Errorf("template generation failed for %s, %w", srcFilePath, err)
59+
}
60+
61+
return nil
62+
})
63+
if err != nil {
64+
exit(1, err.Error())
65+
}
66+
}
67+
68+
func generateFile(pkgOrg, compName, srcFileName, dstFileName string) error {
69+
pascalPkgOrg := dashToPascal(pkgOrg)
70+
pascalCompName := dashToPascal(compName)
71+
72+
fmt.Printf("Generating from %s to %s\n", srcFileName, dstFileName)
73+
74+
contentsBs, err := os.ReadFile(srcFileName)
75+
if err != nil {
76+
return fmt.Errorf("generateFile: read file failed for %s, %w", srcFileName, err)
77+
}
78+
79+
contents := string(contentsBs)
80+
81+
contents = strings.ReplaceAll(contents, "comp-name", compName)
82+
contents = strings.ReplaceAll(contents, "pck-ns", pkgOrg)
83+
contents = strings.ReplaceAll(contents, "CompName", pascalCompName)
84+
contents = strings.ReplaceAll(contents, "PckNs", pascalPkgOrg)
85+
86+
dstDir := filepath.Dir(dstFileName)
87+
err = os.MkdirAll(dstDir, 0755)
88+
if err != nil {
89+
return fmt.Errorf("generateFile: mkdir failed for %s, %w", dstDir, err)
90+
}
91+
92+
err = os.WriteFile(dstFileName, []byte(contents), 0644)
93+
if err != nil {
94+
return fmt.Errorf("generateFile: write file failed for %s, %w", dstFileName, err)
95+
}
96+
97+
return nil
98+
}
99+
100+
func dashToPascal(s string) string {
101+
parts := strings.Split(s, "-")
102+
for i, part := range parts {
103+
if len(part) > 0 {
104+
parts[i] = strings.ToUpper(string(part[0])) + part[1:]
105+
}
106+
}
107+
return strings.Join(parts, "")
108+
}
109+
110+
func copyFile(srcFileName, dstFileName string) error {
111+
fmt.Printf("Copy %s to %s\n", srcFileName, dstFileName)
112+
113+
src, err := os.Open(srcFileName)
114+
if err != nil {
115+
return fmt.Errorf("copyFile: open failed for %s, %w", srcFileName, err)
116+
}
117+
defer func() { _ = src.Close() }()
118+
119+
dstDir := filepath.Dir(dstFileName)
120+
err = os.MkdirAll(dstDir, 0755)
121+
if err != nil {
122+
return fmt.Errorf("copyFile: mkdir failed for %s, %w", dstDir, err)
123+
}
124+
125+
dst, err := os.Create(dstFileName)
126+
if err != nil {
127+
return fmt.Errorf("copyFile: create file failed for %s, %w", dstFileName, err)
128+
}
129+
defer func() { _ = dst.Close() }()
130+
131+
_, err = io.Copy(dst, src)
132+
if err != nil {
133+
return fmt.Errorf("copyFile: copy failed from %s to %s, %w", srcFileName, dstFileName, err)
134+
}
135+
136+
return nil
137+
}
138+
139+
func exit(code int, message string) {
140+
fmt.Println(message)
141+
os.Exit(code)
142+
}

0 commit comments

Comments
 (0)