├── .github └── workflows │ ├── cla.yml │ ├── docker.yaml │ ├── integration.yaml │ └── test.yaml ├── .gitignore ├── .mockery.yaml ├── LICENSE ├── README.md ├── buf.gen.yaml ├── buf.lock ├── buf.yaml ├── context.go ├── encoding ├── encoding.go ├── encoding_test.go └── internal │ ├── protojsonschema │ ├── schema.go │ ├── schema_test.go │ └── wellknown.go │ └── util │ └── util.go ├── error.go ├── error_test.go ├── examples ├── codegen │ ├── buf.gen.yaml │ ├── buf.lock │ ├── buf.yaml │ ├── main.go │ └── proto │ │ ├── helloworld.pb.go │ │ ├── helloworld.proto │ │ └── helloworld_restate.pb.go ├── helloworld │ ├── .gitignore │ └── main.go ├── otel │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── parallelizework │ └── main.go └── ticketreservation │ ├── .gitignore │ ├── checkout.go │ ├── main.go │ ├── main_test.go │ ├── ticket_service.go │ └── user_session.go ├── facilitators.go ├── generate.go ├── generated └── dev │ └── restate │ └── sdk │ └── go.pb.go ├── go.mod ├── go.sum ├── handler.go ├── internal.buf.gen.yaml ├── internal ├── converters │ └── converters.go ├── discovery.go ├── errors │ └── error.go ├── generated │ └── internal.pb.go ├── identity │ ├── identity.go │ └── v1.go ├── log │ └── log.go ├── options │ └── options.go ├── rand │ ├── rand.go │ └── rand_test.go ├── restatecontext │ ├── async_results.go │ ├── awakeable.go │ ├── call.go │ ├── ctx.go │ ├── execute_invocation.go │ ├── handler.go │ ├── io_helpers.go │ ├── promise.go │ ├── run.go │ ├── select.go │ ├── sleep.go │ └── state.go └── statemachine │ ├── logger.go │ ├── shared_core_golang_wasm_binding.wasm │ └── wasm.go ├── mock.go ├── mocks ├── helpers.go ├── mock_AfterFuture.go ├── mock_AttachFuture.go ├── mock_AwakeableFuture.go ├── mock_Client.go ├── mock_Context.go ├── mock_DurablePromise.go ├── mock_Invocation.go ├── mock_Rand.go ├── mock_ResponseFuture.go ├── mock_RunAsyncFuture.go └── mock_Selector.go ├── options.go ├── proto ├── dev │ └── restate │ │ └── sdk │ │ └── go.proto └── internal.proto ├── protoc-gen-go-restate ├── README.md ├── main.go └── restate.go ├── rcontext └── rcontext.go ├── reflect.go ├── reflect_test.go ├── router.go ├── server ├── conn.go ├── lambda.go └── restate.go ├── shared-core ├── .cargo │ └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ └── lib.rs └── test-services ├── .env ├── README.md ├── awakeableholder.go ├── blockandwaitworkflow.go ├── canceltest.go ├── counter.go ├── exclusions.yaml ├── failing.go ├── interpreter.go ├── kill.go ├── listobject.go ├── main.go ├── mapobject.go ├── nondeterministic.go ├── proxy.go ├── registry.go ├── testutils.go ├── upgradetest.go └── virtualobjectinterpreter.go /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened, closed, synchronize] 7 | 8 | jobs: 9 | CLAAssistant: 10 | uses: restatedev/restate/.github/workflows/cla.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: 7 | - v** 8 | 9 | env: 10 | REPOSITORY_OWNER: ${{ github.repository_owner }} 11 | GHCR_REGISTRY: "ghcr.io" 12 | GHCR_REGISTRY_USERNAME: ${{ github.actor }} 13 | GHCR_REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | 15 | jobs: 16 | sdk-test-docker: 17 | if: github.repository_owner == 'restatedev' 18 | runs-on: ubuntu-latest 19 | name: "Create test-services Docker Image" 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | repository: restatedev/sdk-go 25 | 26 | - name: Setup Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: "1.21.x" 30 | 31 | - name: Setup ko 32 | uses: ko-build/setup-ko@v0.6 33 | with: 34 | version: v0.16.0 35 | 36 | - name: Log into GitHub container registry 37 | uses: docker/login-action@v2 38 | with: 39 | registry: ${{ env.GHCR_REGISTRY }} 40 | username: ${{ env.GHCR_REGISTRY_USERNAME }} 41 | password: ${{ env.GHCR_REGISTRY_TOKEN }} 42 | 43 | - name: Install dependencies 44 | run: go get . 45 | 46 | - name: Build Docker image 47 | run: KO_DOCKER_REPO=restatedev ko build --platform=linux/amd64,linux/arm64 -B -L github.com/restatedev/sdk-go/test-services 48 | 49 | - name: Push restatedev/test-services-java:main image 50 | run: | 51 | docker tag restatedev/test-services ghcr.io/restatedev/test-services-go:main 52 | docker push ghcr.io/restatedev/test-services-go:main 53 | -------------------------------------------------------------------------------- /.github/workflows/integration.yaml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | 3 | # Controls when the workflow will run 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | schedule: 10 | - cron: "0 */6 * * *" # Every 6 hours 11 | workflow_dispatch: 12 | inputs: 13 | restateCommit: 14 | description: "restate commit" 15 | required: false 16 | default: "" 17 | type: string 18 | restateImage: 19 | description: "restate image, superseded by restate commit" 20 | required: false 21 | default: "ghcr.io/restatedev/restate:main" 22 | type: string 23 | workflow_call: 24 | inputs: 25 | restateCommit: 26 | description: "restate commit" 27 | required: false 28 | default: "" 29 | type: string 30 | restateImage: 31 | description: "restate image, superseded by restate commit" 32 | required: false 33 | default: "ghcr.io/restatedev/restate:main" 34 | type: string 35 | 36 | jobs: 37 | sdk-test-suite: 38 | if: github.repository_owner == 'restatedev' 39 | runs-on: ubuntu-latest 40 | name: "Features integration test" 41 | permissions: 42 | contents: read 43 | issues: read 44 | checks: write 45 | pull-requests: write 46 | actions: read 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | repository: restatedev/sdk-go 52 | 53 | # support importing oci-format restate.tar 54 | - name: Set up Docker containerd snapshotter 55 | uses: crazy-max/ghaction-setup-docker@v3 56 | with: 57 | set-host: true 58 | daemon-config: | 59 | { 60 | "features": { 61 | "containerd-snapshotter": true 62 | } 63 | } 64 | 65 | ### Download the Restate container image, if needed 66 | # Setup restate snapshot if necessary 67 | # Due to https://github.com/actions/upload-artifact/issues/53 68 | # We must use download-artifact to get artifacts created during *this* workflow run, ie by workflow call 69 | - name: Download restate snapshot from in-progress workflow 70 | if: ${{ inputs.restateCommit != '' && github.event_name != 'workflow_dispatch' }} 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: restate.tar 74 | 75 | # In the workflow dispatch case where the artifact was created in a previous run, we can download as normal 76 | - name: Download restate snapshot from completed workflow 77 | if: ${{ inputs.restateCommit != '' && github.event_name == 'workflow_dispatch' }} 78 | uses: dawidd6/action-download-artifact@v3 79 | with: 80 | repo: restatedev/restate 81 | workflow: ci.yml 82 | commit: ${{ inputs.restateCommit }} 83 | name: restate.tar 84 | 85 | - name: Install restate snapshot 86 | if: ${{ inputs.restateCommit != '' }} 87 | run: | 88 | output=$(docker load --input restate.tar | head -n 1) 89 | docker tag "${output#*: }" "localhost/restatedev/restate-commit-download:latest" 90 | docker image ls -a 91 | 92 | - name: Setup Go 93 | uses: actions/setup-go@v5 94 | with: 95 | go-version: "1.21.x" 96 | 97 | - name: Setup ko 98 | uses: ko-build/setup-ko@v0.6 99 | with: 100 | version: v0.16.0 101 | 102 | - name: Install dependencies 103 | run: go get . 104 | 105 | - name: Build Docker image 106 | run: KO_DOCKER_REPO=restatedev ko build -B -L github.com/restatedev/sdk-go/test-services 107 | 108 | - name: Run test tool 109 | uses: restatedev/sdk-test-suite@v3.0 110 | with: 111 | restateContainerImage: ${{ inputs.restateCommit != '' && 'localhost/restatedev/restate-commit-download:latest' || (inputs.restateImage != '' && inputs.restateImage || 'ghcr.io/restatedev/restate:main') }} 112 | serviceContainerImage: "restatedev/test-services" 113 | exclusionsFile: "test-services/exclusions.yaml" 114 | testArtifactOutput: "sdk-go-integration-test-report" 115 | serviceContainerEnvFile: "test-services/.env" 116 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | 4 | permissions: 5 | checks: write 6 | pull-requests: write 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: "1.21.x" 18 | - name: Install dependencies 19 | run: go get . 20 | - name: Vet 21 | run: go vet -v ./... 22 | - name: Build 23 | run: go build -v ./... 24 | - name: Test with the Go CLI 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test_report 2 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | with-expecter: true 2 | issue-845-fix: true 3 | resolve-type-alias: false 4 | dir: mocks 5 | outPkg: mocks 6 | packages: 7 | github.com/restatedev/sdk-go/internal/restatecontext: 8 | interfaces: 9 | AfterFuture: 10 | AttachFuture: 11 | AwakeableFuture: 12 | Client: 13 | Context: 14 | DurablePromise: 15 | Invocation: 16 | ResponseFuture: 17 | RunAsyncFuture: 18 | Selector: 19 | github.com/restatedev/sdk-go/internal/rand: 20 | interfaces: 21 | Rand: 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 - Restate Software, Inc., Restate GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/restatedev/sdk-go.svg)](https://pkg.go.dev/github.com/restatedev/sdk-go) 2 | [![Go](https://github.com/restatedev/sdk-go/actions/workflows/test.yaml/badge.svg)](https://github.com/restatedev/sdk-go/actions/workflows/test.yaml) 3 | 4 | # Restate Go SDK 5 | 6 | [Restate](https://restate.dev/) is a system for easily building resilient applications using *distributed durable async/await*. This repository contains the Restate SDK for writing services in **Golang**. 7 | 8 | ## Community 9 | 10 | * 🤗️ [Join our online community](https://discord.gg/skW3AZ6uGd) for help, sharing feedback and talking to the community. 11 | * 📖 [Check out our documentation](https://docs.restate.dev) to get quickly started! 12 | * 📣 [Follow us on Twitter](https://twitter.com/restatedev) for staying up to date. 13 | * 🙋 [Create a GitHub issue](https://github.com/restatedev/sdk-java/issues) for requesting a new feature or reporting a problem. 14 | * 🏠 [Visit our GitHub org](https://github.com/restatedev) for exploring other repositories. 15 | 16 | ## Prerequisites 17 | - Go: >= 1.21.0 18 | 19 | ## Examples 20 | 21 | This repo contains an [example](examples) based on the [Ticket Reservation Service](https://github.com/restatedev/examples/tree/main/tutorials/tour-of-restate-go). 22 | 23 | You can also check a list of examples available here: https://github.com/restatedev/examples?tab=readme-ov-file#go 24 | 25 | ### How to use the example 26 | 27 | Download and run restate, as described here [v1.x](https://github.com/restatedev/restate/releases/) 28 | 29 | ```bash 30 | restate-server 31 | ``` 32 | 33 | In another terminal run the example 34 | 35 | ```bash 36 | cd restate-sdk-go/example 37 | go run . 38 | ``` 39 | 40 | In a third terminal register: 41 | 42 | ```bash 43 | restate deployments register http://localhost:9080 44 | ``` 45 | 46 | And do the following steps 47 | 48 | - Add tickets to basket 49 | 50 | ```bash 51 | curl -v localhost:8080/UserSession/azmy/AddTicket \ 52 | -H 'content-type: application/json' \ 53 | -d '"ticket-1"' 54 | 55 | # true 56 | curl -v localhost:8080/UserSession/azmy/AddTicket \ 57 | -H 'content-type: application/json' \ 58 | -d '"ticket-2"' 59 | # true 60 | ``` 61 | 62 | Trying adding the same tickets again should return `false` since they are already reserved. If you didn't check out the tickets in 15min (if you are impatient change the delay in code to make it shorter) 63 | 64 | - Check out 65 | 66 | ```bash 67 | curl localhost:8080/UserSession/azmy/Checkout 68 | # true 69 | ``` 70 | 71 | ## Versions 72 | 73 | This library follows [Semantic Versioning](https://semver.org/). 74 | 75 | The compatibility with Restate is described in the following table: 76 | 77 | | Restate Server\sdk-go | 0.9 - 0.15 | 0.16 - 0.17 | 78 | |-----------------------|------------|-------------| 79 | | 1.0 | ✅ | ❌ | 80 | | 1.1 | ✅ | ❌ | 81 | | 1.2 | ✅ | ❌ | 82 | | 1.3 | ✅ | ✅ | 83 | 84 | ## Contributing 85 | 86 | We’re excited if you join the Restate community and start contributing! 87 | Whether it is feature requests, bug reports, ideas & feedback or PRs, we appreciate any and all contributions. 88 | We know that your time is precious and, therefore, deeply value any effort to contribute! 89 | 90 | ### Internal core 91 | 92 | To rebuild the internal core: 93 | 94 | ```shell 95 | cd shared-core 96 | cargo build --release 97 | mv target/wasm32-unknown-unknown/release/shared_core_golang_wasm_binding.wasm ../internal/statemachine 98 | ``` 99 | 100 | To regenerate the protobuf contract between core and SDK: 101 | 102 | ```shell 103 | buf generate --template internal.buf.gen.yaml 104 | ``` 105 | 106 | ## Mockery mocks 107 | The `mock` package is mostly autogenerated but will require some very light editing. To generate run `mockery` in the root of this repo. Then check the git diff. 108 | Certain structs (`MockAfterFuture`, `MockAttachFuture`, `MockAwakeableFuture`, `MockDurablePromise`, `MockResponseFuture`) and functions (`NewMockClient`, `NewMockContext`) 109 | have been redefined in `helpers.go` and commented out in their respective files. Please continue this state of affairs if you regenerate mocks - not doing so will be a build 110 | error. 111 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | override: 5 | - file_option: go_package_prefix 6 | value: github.com/restatedev/sdk-go/generated 7 | plugins: 8 | - remote: buf.build/protocolbuffers/go:v1.36.5 9 | out: generated 10 | opt: paths=source_relative 11 | inputs: 12 | - proto_file: proto/dev/restate/sdk/go.proto 13 | -------------------------------------------------------------------------------- /buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v2 3 | deps: 4 | - name: buf.build/protocolbuffers/wellknowntypes 5 | commit: d4f14e5e0a9c40889c90d373c74e95eb 6 | digest: b5:39b4d0887abcd8ee1594086283f4120f688e1c33ec9ccd554ab0362ad9ad482154d0e07e3787d394bb22970930b452aac1c5c105c05efe129cec299ff5b5e05e 7 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | modules: 3 | - path: proto 4 | name: buf.build/restatedev/sdk-go 5 | excludes: 6 | - proto/dev/restate/sdk/go 7 | deps: 8 | - buf.build/protocolbuffers/wellknowntypes:v29.3 9 | breaking: 10 | use: 11 | - FILE 12 | lint: 13 | use: 14 | - DEFAULT 15 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package restate 2 | 3 | import ( 4 | "github.com/restatedev/sdk-go/internal/restatecontext" 5 | ) 6 | 7 | // RunContext is passed to [Run] closures and provides the limited set of Restate operations that are safe to use there. 8 | type RunContext = restatecontext.RunContext 9 | 10 | // Context is an extension of [RunContext] which is passed to Restate service handlers and enables 11 | // interaction with Restate 12 | type Context interface { 13 | RunContext 14 | inner() restatecontext.Context 15 | } 16 | 17 | // ObjectSharedContext is an extension of [Context] which is passed to shared-mode Virtual Object handlers, 18 | // giving read-only access to a snapshot of state. 19 | type ObjectSharedContext interface { 20 | Context 21 | object() 22 | } 23 | 24 | // ObjectContext is an extension of [ObjectSharedContext] which is passed to exclusive-mode Virtual Object handlers. 25 | // giving mutable access to state. 26 | type ObjectContext interface { 27 | ObjectSharedContext 28 | exclusiveObject() 29 | } 30 | 31 | // WorkflowSharedContext is an extension of [ObjectSharedContext] which is passed to shared-mode Workflow handlers, 32 | // giving read-only access to a snapshot of state. 33 | type WorkflowSharedContext interface { 34 | ObjectSharedContext 35 | workflow() 36 | } 37 | 38 | // WorkflowContext is an extension of [WorkflowSharedContext] and [ObjectContext] which is passed to Workflow 'run' handlers, 39 | // giving mutable access to state. 40 | type WorkflowContext interface { 41 | WorkflowSharedContext 42 | ObjectContext 43 | runWorkflow() 44 | } 45 | -------------------------------------------------------------------------------- /encoding/encoding_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestVoid(t *testing.T) { 12 | codecs := map[string]Codec{ 13 | "json": JSONCodec, 14 | "proto": ProtoCodec, 15 | "protojson": ProtoJSONCodec, 16 | "binary": BinaryCodec, 17 | } 18 | for name, codec := range codecs { 19 | t.Run(name, func(t *testing.T) { 20 | bytes, err := Marshal(codec, Void{}) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if bytes != nil { 26 | t.Fatalf("expected bytes to be nil, found %v", bytes) 27 | } 28 | 29 | if err := Unmarshal(codec, []byte{1, 2, 3}, &Void{}); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if err := Unmarshal(codec, []byte{1, 2, 3}, Void{}); err != nil { 34 | t.Fatal(err) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | var jsonSchemaCases = []struct { 41 | object any 42 | schema string 43 | }{ 44 | { 45 | object: "abc", 46 | schema: `{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"}`, 47 | }, 48 | { 49 | object: 123, 50 | schema: `{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"integer"}`, 51 | }, 52 | { 53 | object: 1.1, 54 | schema: `{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"number"}`, 55 | }, 56 | { 57 | object: struct { 58 | Foo string `json:"foo"` 59 | }{}, 60 | schema: `{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"foo":{"type":"string"}},"additionalProperties":false,"type":"object","required":["foo"]}`, 61 | }, 62 | { 63 | object: recursive{}, 64 | schema: `{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/restatedev/sdk-go/encoding/recursive","$defs":{"recursive":{"$ref":"#"}},"properties":{"inner":{"$ref":"#/$defs/recursive"}},"additionalProperties":false,"type":"object","required":["inner"]}`, 65 | }, 66 | { 67 | object: nestedRecursiveA{}, 68 | schema: `{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/restatedev/sdk-go/encoding/nested-recursive-a","$defs":{"nestedRecursiveA":{"$ref":"#"},"nestedRecursiveB":{"properties":{"inner":{"$ref":"#/$defs/nestedRecursiveC"}},"additionalProperties":false,"type":"object","required":["inner"]},"nestedRecursiveC":{"properties":{"inner":{"$ref":"#/$defs/nestedRecursiveA"}},"additionalProperties":false,"type":"object","required":["inner"]}},"properties":{"inner":{"$ref":"#/$defs/nestedRecursiveB"}},"additionalProperties":false,"type":"object","required":["inner"]}`, 69 | }, 70 | } 71 | 72 | type recursive struct { 73 | Inner *recursive `json:"inner"` 74 | } 75 | 76 | type nestedRecursiveA struct { 77 | Inner *nestedRecursiveB `json:"inner"` 78 | } 79 | 80 | type nestedRecursiveB struct { 81 | Inner *nestedRecursiveC `json:"inner"` 82 | } 83 | 84 | type nestedRecursiveC struct { 85 | Inner *nestedRecursiveA `json:"inner"` 86 | } 87 | 88 | func TestGenerateJsonSchema(t *testing.T) { 89 | for _, test := range jsonSchemaCases { 90 | t.Run(reflect.TypeOf(test.object).String(), func(t *testing.T) { 91 | schema := generateJsonSchema(test.object) 92 | data, err := json.Marshal(schema) 93 | require.NoError(t, err) 94 | require.Equal(t, test.schema, string(data)) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /encoding/internal/protojsonschema/schema.go: -------------------------------------------------------------------------------- 1 | package protojsonschema 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/invopop/jsonschema" 7 | "github.com/restatedev/sdk-go/encoding/internal/util" 8 | "google.golang.org/protobuf/reflect/protoreflect" 9 | ) 10 | 11 | var protoMessageType = reflect.TypeOf((*protoreflect.ProtoMessage)(nil)).Elem() 12 | var protoEnumType = reflect.TypeOf((*protoreflect.Enum)(nil)).Elem() 13 | 14 | func descriptor(typ reflect.Type) protoreflect.Descriptor { 15 | if typ.Implements(protoEnumType) { 16 | zero := reflect.Zero(typ).Interface().(protoreflect.Enum) 17 | return zero.Descriptor() 18 | } 19 | 20 | pointerTyp := reflect.PointerTo(typ) 21 | if pointerTyp.Implements(protoMessageType) { 22 | zero := reflect.Zero(pointerTyp).Interface().(protoreflect.ProtoMessage) 23 | return zero.ProtoReflect().Descriptor() 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func GenerateSchema(v any) *jsonschema.Schema { 30 | reflector := jsonschema.Reflector{ 31 | // Unfortunately we can't enable this due to a panic bug https://github.com/invopop/jsonschema/issues/163 32 | // So we use ExpandSchema instead, which has the same effect but without the panic 33 | // ExpandedStruct: true, 34 | KeyNamer: func(fieldName string) string { 35 | return jsonCamelCase(fieldName) 36 | }, 37 | Lookup: func(typ reflect.Type) jsonschema.ID { 38 | desc := descriptor(typ) 39 | if desc == nil { 40 | return jsonschema.EmptyID 41 | } 42 | 43 | id := jsonschema.ID("https://" + typ.PkgPath()) 44 | if err := id.Validate(); err != nil { 45 | return jsonschema.EmptyID 46 | } 47 | 48 | return id.Add(string(desc.FullName())) 49 | }, 50 | Mapper: func(typ reflect.Type) *jsonschema.Schema { 51 | desc := descriptor(typ) 52 | if desc == nil { 53 | return nil 54 | } 55 | 56 | schemaFn, ok := wellKnownToSchemaFns[string(desc.FullName())] 57 | if !ok { 58 | return nil 59 | } 60 | 61 | return schemaFn(desc) 62 | }, 63 | Namer: func(typ reflect.Type) string { 64 | desc := descriptor(typ) 65 | if desc == nil { 66 | return "" 67 | } 68 | 69 | return string(desc.FullName()) 70 | }, 71 | } 72 | return util.ExpandSchema(reflector.Reflect(v)) 73 | } 74 | 75 | // jsonCamelCase converts a snake_case identifier to a camelCase identifier, 76 | // according to the protobuf JSON specification. 77 | func jsonCamelCase(s string) string { 78 | var b []byte 79 | var wasUnderscore bool 80 | for i := 0; i < len(s); i++ { // proto identifiers are always ASCII 81 | c := s[i] 82 | if c != '_' { 83 | if wasUnderscore && 'a' <= c && c <= 'z' { 84 | c -= 'a' - 'A' // convert to uppercase 85 | } 86 | b = append(b, c) 87 | } 88 | wasUnderscore = c == '_' 89 | } 90 | return string(b) 91 | } 92 | -------------------------------------------------------------------------------- /encoding/internal/protojsonschema/wellknown.go: -------------------------------------------------------------------------------- 1 | package protojsonschema 2 | 3 | import ( 4 | "github.com/invopop/jsonschema" 5 | orderedmap "github.com/wk8/go-ordered-map/v2" 6 | "google.golang.org/protobuf/reflect/protoreflect" 7 | ) 8 | 9 | var wellKnownToSchemaFns = map[string]func(protoreflect.Descriptor) *jsonschema.Schema{ 10 | "google.protobuf.Duration": func(d protoreflect.Descriptor) *jsonschema.Schema { 11 | return &jsonschema.Schema{ 12 | Type: "string", 13 | Format: "regex", 14 | Pattern: `^[-\+]?([0-9]+\.?[0-9]*|\.[0-9]+)s$`, 15 | } 16 | }, 17 | "google.protobuf.Timestamp": func(d protoreflect.Descriptor) *jsonschema.Schema { 18 | return &jsonschema.Schema{ 19 | Type: "string", 20 | Format: "date-time", 21 | } 22 | }, 23 | "google.protobuf.Empty": func(d protoreflect.Descriptor) *jsonschema.Schema { 24 | return &jsonschema.Schema{ 25 | Type: "object", 26 | AdditionalProperties: jsonschema.FalseSchema, 27 | } 28 | }, 29 | "google.protobuf.Any": func(d protoreflect.Descriptor) *jsonschema.Schema { 30 | return &jsonschema.Schema{ 31 | Type: "object", 32 | Properties: orderedmap.New[string, *jsonschema.Schema](orderedmap.WithInitialData[string, *jsonschema.Schema]( 33 | orderedmap.Pair[string, *jsonschema.Schema]{ 34 | Key: "@type", 35 | Value: &jsonschema.Schema{Type: "string"}, 36 | }, 37 | orderedmap.Pair[string, *jsonschema.Schema]{ 38 | Key: "value", 39 | Value: &jsonschema.Schema{Type: "string", Format: "binary"}, 40 | }, 41 | )), 42 | AdditionalProperties: jsonschema.TrueSchema, 43 | } 44 | }, 45 | "google.protobuf.FieldMask": func(d protoreflect.Descriptor) *jsonschema.Schema { 46 | return &jsonschema.Schema{ 47 | Type: "string", 48 | } 49 | }, 50 | 51 | "google.protobuf.Struct": func(d protoreflect.Descriptor) *jsonschema.Schema { 52 | return &jsonschema.Schema{ 53 | Type: "object", 54 | AdditionalProperties: jsonschema.TrueSchema, 55 | } 56 | }, 57 | "google.protobuf.Value": func(d protoreflect.Descriptor) *jsonschema.Schema { 58 | return jsonschema.TrueSchema 59 | }, 60 | "google.protobuf.NullValue": func(d protoreflect.Descriptor) *jsonschema.Schema { 61 | return &jsonschema.Schema{ 62 | Type: "null", 63 | } 64 | }, 65 | "google.protobuf.StringValue": func(d protoreflect.Descriptor) *jsonschema.Schema { 66 | return &jsonschema.Schema{ 67 | Type: "string", 68 | } 69 | }, 70 | "google.protobuf.BytesValue": func(d protoreflect.Descriptor) *jsonschema.Schema { 71 | return &jsonschema.Schema{ 72 | Type: "string", 73 | Format: "binary", 74 | } 75 | }, 76 | "google.protobuf.BoolValue": func(d protoreflect.Descriptor) *jsonschema.Schema { 77 | return &jsonschema.Schema{ 78 | Type: "boolean", 79 | } 80 | }, 81 | "google.protobuf.DoubleValue": google64BitNumberValue, 82 | "google.protobuf.Int64Value": google64BitNumberValue, 83 | "google.protobuf.UInt64Value": google64BitNumberValue, 84 | "google.protobuf.FloatValue": google64BitNumberValue, 85 | "google.protobuf.Int32Value": google32BitNumberValue, 86 | "google.protobuf.UInt32Value": google32BitNumberValue, 87 | } 88 | 89 | var google64BitNumberValue = func(d protoreflect.Descriptor) *jsonschema.Schema { 90 | return &jsonschema.Schema{ 91 | OneOf: []*jsonschema.Schema{ 92 | &jsonschema.Schema{Type: "number"}, 93 | &jsonschema.Schema{Type: "string"}, 94 | }, 95 | } 96 | } 97 | var google32BitNumberValue = func(d protoreflect.Descriptor) *jsonschema.Schema { 98 | return &jsonschema.Schema{ 99 | Type: "number", 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /encoding/internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/invopop/jsonschema" 7 | ) 8 | 9 | // Schemas that have a top-level ref can be problematic for some parsers, like the playground in the UI. 10 | // To be more forgiving, we can yank the definition up to the top level 11 | func ExpandSchema(rootSchema *jsonschema.Schema) *jsonschema.Schema { 12 | if !strings.HasPrefix(rootSchema.Ref, `#/$defs/`) { 13 | return rootSchema 14 | } 15 | defName := rootSchema.Ref[len(`#/$defs/`):] 16 | def, ok := rootSchema.Definitions[defName] 17 | if !ok { 18 | return rootSchema 19 | } 20 | // allow references to #/$defs/name to still work by redirecting to the root 21 | rootSchema.Definitions[defName] = &jsonschema.Schema{Ref: "#"} 22 | 23 | expandedSchema := &*def 24 | expandedSchema.ID = rootSchema.ID 25 | expandedSchema.Version = rootSchema.Version 26 | expandedSchema.Definitions = rootSchema.Definitions 27 | 28 | return expandedSchema 29 | } 30 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package restate 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/restatedev/sdk-go/internal/errors" 7 | ) 8 | 9 | // Code is a numeric status code for an error, typically a HTTP status code. 10 | type Code = errors.Code 11 | 12 | // WithErrorCode returns an error with specific [Code] attached. 13 | func WithErrorCode(err error, code Code) error { 14 | if err == nil { 15 | return nil 16 | } 17 | 18 | return &errors.CodeError{ 19 | Inner: err, 20 | Code: code, 21 | } 22 | } 23 | 24 | // TerminalError returns a terminal error with optional code. Code is optional but only one code is allowed. 25 | // By default, restate will retry the invocation or Run function forever unless a terminal error is returned 26 | func TerminalError(err error, code ...errors.Code) error { 27 | return errors.NewTerminalError(err, code...) 28 | } 29 | 30 | // TerminalErrorf is a shorthand for combining fmt.Errorf with TerminalError 31 | func TerminalErrorf(format string, a ...any) error { 32 | return TerminalError(fmt.Errorf(format, a...)) 33 | } 34 | 35 | // IsTerminalError checks if err is terminal - ie, that returning it in a handler or Run function will finish 36 | // the invocation with the error as a result. 37 | func IsTerminalError(err error) bool { 38 | return errors.IsTerminalError(err) 39 | } 40 | 41 | // ErrorCode returns [Code] associated with error, defaulting to 500 42 | func ErrorCode(err error) errors.Code { 43 | return errors.ErrorCode(err) 44 | } 45 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package restate 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTerminal(t *testing.T) { 12 | require.False(t, IsTerminalError(fmt.Errorf("not terminal"))) 13 | 14 | err := TerminalErrorf("failed terminally") 15 | require.True(t, IsTerminalError(err)) 16 | 17 | //terminal with code 18 | err = TerminalError(fmt.Errorf("terminal with code"), 500) 19 | 20 | require.True(t, IsTerminalError(err)) 21 | require.EqualValues(t, 500, ErrorCode(err)) 22 | } 23 | 24 | func TestCode(t *testing.T) { 25 | 26 | err := WithErrorCode(fmt.Errorf("some error"), 16) 27 | 28 | code := ErrorCode(err) 29 | 30 | require.EqualValues(t, 16, code) 31 | 32 | require.EqualValues(t, http.StatusInternalServerError, ErrorCode(fmt.Errorf("unknown error"))) 33 | } 34 | 35 | func TestCombine(t *testing.T) { 36 | err := WithErrorCode(TerminalError(fmt.Errorf("some error")), 100) 37 | 38 | require.True(t, IsTerminalError(err)) 39 | require.EqualValues(t, 100, ErrorCode(err)) 40 | } 41 | -------------------------------------------------------------------------------- /examples/codegen/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | plugins: 5 | - remote: buf.build/protocolbuffers/go:v1.34.2 6 | out: . 7 | opt: paths=source_relative 8 | - local: protoc-gen-go-restate 9 | out: . 10 | opt: 11 | - paths=source_relative 12 | - use_go_service_names=false 13 | inputs: 14 | - directory: . 15 | -------------------------------------------------------------------------------- /examples/codegen/buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v2 3 | deps: 4 | - name: buf.build/restatedev/sdk-go 5 | commit: 9ea0b54286dd4f35b0cb96ecdf09b402 6 | digest: b5:822b9362e943c827c36e44b0db519542259439382f94817989349d0ee590617ba70e35975840c5d96ceff278254806435e7d570db81548f9703c00b01eec398e 7 | -------------------------------------------------------------------------------- /examples/codegen/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | lint: 3 | use: 4 | - DEFAULT 5 | breaking: 6 | use: 7 | - FILE 8 | deps: 9 | - buf.build/restatedev/sdk-go 10 | -------------------------------------------------------------------------------- /examples/codegen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "time" 9 | 10 | restate "github.com/restatedev/sdk-go" 11 | helloworld "github.com/restatedev/sdk-go/examples/codegen/proto" 12 | "github.com/restatedev/sdk-go/server" 13 | ) 14 | 15 | type greeter struct { 16 | helloworld.UnimplementedGreeterServer 17 | } 18 | 19 | func (greeter) SayHello(ctx restate.Context, req *helloworld.HelloRequest) (*helloworld.HelloResponse, error) { 20 | counter := helloworld.NewCounterClient(ctx, req.Name) 21 | count, err := counter.Add(). 22 | Request(&helloworld.AddRequest{Delta: 1}) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &helloworld.HelloResponse{ 27 | Message: fmt.Sprintf("Hello, %s! Call number: %d", req.Name, count.Value), 28 | }, nil 29 | } 30 | 31 | type counter struct { 32 | helloworld.UnimplementedCounterServer 33 | } 34 | 35 | func (c counter) Add(ctx restate.ObjectContext, req *helloworld.AddRequest) (*helloworld.GetResponse, error) { 36 | count, err := restate.Get[int64](ctx, "counter") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | watchers, err := restate.Get[[]string](ctx, "watchers") 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | count += req.Delta 47 | restate.Set(ctx, "counter", count) 48 | 49 | for _, awakeableID := range watchers { 50 | restate.ResolveAwakeable(ctx, awakeableID, count) 51 | } 52 | restate.Clear(ctx, "watchers") 53 | 54 | return &helloworld.GetResponse{Value: count}, nil 55 | } 56 | 57 | func (c counter) Get(ctx restate.ObjectSharedContext, _ *helloworld.GetRequest) (*helloworld.GetResponse, error) { 58 | count, err := restate.Get[int64](ctx, "counter") 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return &helloworld.GetResponse{Value: count}, nil 64 | } 65 | 66 | func (c counter) AddWatcher(ctx restate.ObjectContext, req *helloworld.AddWatcherRequest) (*helloworld.AddWatcherResponse, error) { 67 | watchers, err := restate.Get[[]string](ctx, "watchers") 68 | if err != nil { 69 | return nil, err 70 | } 71 | watchers = append(watchers, req.AwakeableId) 72 | restate.Set(ctx, "watchers", watchers) 73 | return &helloworld.AddWatcherResponse{}, nil 74 | } 75 | 76 | func (c counter) Watch(ctx restate.ObjectSharedContext, req *helloworld.WatchRequest) (*helloworld.GetResponse, error) { 77 | awakeable := restate.Awakeable[int64](ctx) 78 | 79 | // since this is a shared handler, we need to use a separate exclusive handler to store the awakeable ID 80 | // if there is an in-flight Add call, this will take effect after it completes 81 | // we could add a version counter check here to detect changes that happen mid-request and return immediately 82 | if _, err := helloworld.NewCounterClient(ctx, restate.Key(ctx)). 83 | AddWatcher(). 84 | Request(&helloworld.AddWatcherRequest{AwakeableId: awakeable.Id()}); err != nil { 85 | return nil, err 86 | } 87 | 88 | timeout := time.Duration(req.TimeoutMillis) * time.Millisecond 89 | if timeout == 0 { 90 | // infinite timeout case; just await the next value 91 | next, err := awakeable.Result() 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return &helloworld.GetResponse{Value: next}, nil 97 | } 98 | 99 | after := restate.After(ctx, timeout) 100 | 101 | // this is the safe way to race two results 102 | selector := restate.Select(ctx, after, awakeable) 103 | 104 | if selector.Select() == after { 105 | // the timeout won 106 | if err := after.Done(); err != nil { 107 | // an error here implies this invocation was cancelled 108 | return nil, err 109 | } 110 | return nil, restate.TerminalError(context.DeadlineExceeded, 408) 111 | } 112 | 113 | // otherwise, the awakeable won 114 | next, err := awakeable.Result() 115 | if err != nil { 116 | return nil, err 117 | } 118 | return &helloworld.GetResponse{Value: next}, nil 119 | } 120 | 121 | type workflow struct { 122 | helloworld.UnimplementedWorkflowServer 123 | } 124 | 125 | func (workflow) Run(ctx restate.WorkflowContext, _ *helloworld.RunRequest) (*helloworld.RunResponse, error) { 126 | restate.Set(ctx, "status", "waiting") 127 | _, err := restate.Promise[restate.Void](ctx, "promise").Result() 128 | if err != nil { 129 | return nil, err 130 | } 131 | restate.Set(ctx, "status", "finished") 132 | return &helloworld.RunResponse{Status: "finished"}, nil 133 | } 134 | 135 | func (workflow) Finish(ctx restate.WorkflowSharedContext, _ *helloworld.FinishRequest) (*helloworld.FinishResponse, error) { 136 | return nil, restate.Promise[restate.Void](ctx, "promise").Resolve(restate.Void{}) 137 | } 138 | 139 | func (workflow) Status(ctx restate.WorkflowSharedContext, _ *helloworld.StatusRequest) (*helloworld.StatusResponse, error) { 140 | status, err := restate.Get[string](ctx, "status") 141 | if err != nil { 142 | return nil, err 143 | } 144 | return &helloworld.StatusResponse{Status: status}, nil 145 | } 146 | 147 | func main() { 148 | server := server.NewRestate(). 149 | Bind(helloworld.NewGreeterServer(greeter{})). 150 | Bind(helloworld.NewCounterServer(counter{})). 151 | Bind(helloworld.NewWorkflowServer(workflow{})) 152 | 153 | if err := server.Start(context.Background(), ":9080"); err != nil { 154 | slog.Error("application exited unexpectedly", "err", err.Error()) 155 | os.Exit(1) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /examples/codegen/proto/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/restatedev/sdk-go/examples/codegen/proto"; 4 | 5 | import "dev/restate/sdk/go.proto"; 6 | 7 | package helloworld; 8 | 9 | service Greeter { 10 | rpc SayHello (HelloRequest) returns (HelloResponse) {} 11 | } 12 | 13 | service Counter { 14 | option (dev.restate.sdk.go.service_type) = VIRTUAL_OBJECT; 15 | // Mutate the value 16 | rpc Add (AddRequest) returns (GetResponse) {} 17 | // Get the current value 18 | rpc Get (GetRequest) returns (GetResponse) { 19 | option (dev.restate.sdk.go.handler_type) = SHARED; 20 | } 21 | // Internal method to store an awakeable ID for the Watch method 22 | rpc AddWatcher (AddWatcherRequest) returns (AddWatcherResponse) {} 23 | // Wait for the counter to change and then return the new value 24 | rpc Watch (WatchRequest) returns (GetResponse) { 25 | option (dev.restate.sdk.go.handler_type) = SHARED; 26 | } 27 | } 28 | 29 | service Workflow { 30 | option (dev.restate.sdk.go.service_type) = WORKFLOW; 31 | // Execute the workflow 32 | rpc Run (RunRequest) returns (RunResponse) {} 33 | // Unblock the workflow 34 | rpc Finish(FinishRequest) returns (FinishResponse) {} 35 | // Check the current status 36 | rpc Status (StatusRequest) returns (StatusResponse) {} 37 | } 38 | 39 | message HelloRequest { 40 | string name = 1; 41 | } 42 | 43 | message HelloResponse { 44 | string message = 1; 45 | } 46 | 47 | message AddRequest { 48 | int64 delta = 1; 49 | } 50 | 51 | message GetRequest {} 52 | 53 | message GetResponse { 54 | int64 value = 1; 55 | } 56 | 57 | message AddWatcherRequest { 58 | string awakeable_id = 1; 59 | } 60 | 61 | message AddWatcherResponse {} 62 | 63 | message WatchRequest { 64 | int64 timeout_millis = 1; 65 | } 66 | 67 | message RunRequest {} 68 | 69 | message RunResponse { 70 | string status = 1; 71 | } 72 | 73 | message StatusRequest {} 74 | 75 | message StatusResponse { 76 | string status = 1; 77 | } 78 | 79 | message FinishRequest {} 80 | 81 | message FinishResponse {} 82 | -------------------------------------------------------------------------------- /examples/helloworld/.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /examples/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | restate "github.com/restatedev/sdk-go" 7 | "github.com/restatedev/sdk-go/server" 8 | "log/slog" 9 | "os" 10 | ) 11 | 12 | type Greeter struct{} 13 | 14 | func (Greeter) Greet(ctx restate.Context, name string) (string, error) { 15 | return "You said hi to " + name + "!", nil 16 | } 17 | 18 | type GreeterCounter struct{} 19 | 20 | func (GreeterCounter) Greet(ctx restate.ObjectContext, name string) (string, error) { 21 | count, err := restate.Get[uint32](ctx, "count") 22 | if err != nil { 23 | return "", err 24 | } 25 | count++ 26 | 27 | restate.Set[uint32](ctx, "count", count) 28 | 29 | return fmt.Sprintf("You said hi to %s for the %d time!", name, count), nil 30 | } 31 | 32 | func main() { 33 | server := server.NewRestate(). 34 | // Handlers can be inferred from object methods 35 | Bind(restate.Reflect(Greeter{})). 36 | Bind(restate.Reflect(GreeterCounter{})) 37 | 38 | if err := server.Start(context.Background(), ":9080"); err != nil { 39 | slog.Error("application exited unexpectedly", "err", err.Error()) 40 | os.Exit(1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/otel/README.md: -------------------------------------------------------------------------------- 1 | # Distributed tracing example 2 | 3 | To test out distributed tracing, you can run Jaeger locally: 4 | ```shell 5 | docker run -d --name jaeger \ 6 | -e COLLECTOR_OTLP_ENABLED=true \ 7 | -p 4317:4317 -p 16686:16686 \ 8 | jaegertracing/all-in-one:1.46 9 | ``` 10 | 11 | And start the Restate server configured to send traces to Jaeger: 12 | ```shell 13 | npx @restatedev/restate-server --tracing-endpoint http://localhost:4317 14 | ``` 15 | 16 | Finally start this example service and register it with the Restate server: 17 | ```shell 18 | go run ./examples/otel 19 | restate dep register http://localhost:9080 20 | ``` 21 | 22 | And you can now make invocations with `curl localhost:8080/Greeter/Greet --json '"hello"'`, 23 | and they should appear in the [Jaeger UI](http://localhost:16686) with spans from both the 24 | Restate server and the Go service. 25 | -------------------------------------------------------------------------------- /examples/otel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/restatedev/sdk-go/examples/otel 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/restatedev/sdk-go v0.9.1 9 | go.opentelemetry.io/otel v1.28.0 10 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 11 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 12 | go.opentelemetry.io/otel/sdk v1.28.0 13 | ) 14 | 15 | require ( 16 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 17 | github.com/go-logr/logr v1.4.2 // indirect 18 | github.com/go-logr/stdr v1.2.2 // indirect 19 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 22 | github.com/mr-tron/base58 v1.2.0 // indirect 23 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 24 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 25 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 26 | golang.org/x/net v0.26.0 // indirect 27 | golang.org/x/sys v0.21.0 // indirect 28 | golang.org/x/text v0.16.0 // indirect 29 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect 30 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect 31 | google.golang.org/grpc v1.64.0 // indirect 32 | google.golang.org/protobuf v1.36.5 // indirect 33 | ) 34 | 35 | replace ( 36 | github.com/restatedev/sdk-go => ../../ 37 | github.com/restatedev/sdk-go/server => ../../server 38 | ) 39 | -------------------------------------------------------------------------------- /examples/otel/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 2 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 6 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 7 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 8 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 9 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 10 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 11 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 12 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 13 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 17 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 18 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 19 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 25 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 26 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= 27 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 28 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= 29 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= 30 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 31 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 32 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 33 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 34 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 35 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 36 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 37 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 38 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 39 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 40 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 41 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 42 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 43 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 44 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 45 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 46 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 47 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 48 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= 49 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= 50 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= 51 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 52 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= 53 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= 54 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 55 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 56 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 57 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 58 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 59 | -------------------------------------------------------------------------------- /examples/otel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | restate "github.com/restatedev/sdk-go" 9 | "github.com/restatedev/sdk-go/server" 10 | "go.opentelemetry.io/otel" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 13 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 14 | "go.opentelemetry.io/otel/propagation" 15 | "go.opentelemetry.io/otel/sdk/resource" 16 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 17 | ) 18 | 19 | type Greeter struct{} 20 | 21 | func (Greeter) Greet(ctx restate.Context, message string) (string, error) { 22 | _, span := otel.Tracer("").Start(ctx, "Greet") 23 | defer span.End() 24 | 25 | return fmt.Sprintf("%s!", message), nil 26 | } 27 | 28 | func main() { 29 | exporter, err := otlptrace.New( 30 | context.Background(), 31 | otlptracegrpc.NewClient( 32 | otlptracegrpc.WithInsecure(), 33 | otlptracegrpc.WithEndpoint("localhost:4317"), 34 | ), 35 | ) 36 | 37 | if err != nil { 38 | log.Fatalf("Could not set exporter: %v", err) 39 | } 40 | 41 | resources, err := resource.New( 42 | context.Background(), 43 | resource.WithAttributes( 44 | attribute.String("service.name", "restate-sdk-go-otel-example-greeter"), 45 | ), 46 | ) 47 | if err != nil { 48 | log.Fatalf("Could not set resources: %v", err) 49 | } 50 | 51 | otel.SetTracerProvider( 52 | sdktrace.NewTracerProvider( 53 | sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample())), 54 | sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(exporter)), 55 | sdktrace.WithResource(resources), 56 | ), 57 | ) 58 | 59 | otel.SetTextMapPropagator(propagation.TraceContext{}) 60 | 61 | if err := server.NewRestate(). 62 | Bind(restate.Reflect(Greeter{})). 63 | Start(context.Background(), ":9080"); err != nil { 64 | log.Fatal(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/parallelizework/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "math/rand" 9 | "os" 10 | "strings" 11 | 12 | restate "github.com/restatedev/sdk-go" 13 | "github.com/restatedev/sdk-go/server" 14 | ) 15 | 16 | type fanOutWorker struct{} 17 | 18 | func (c *fanOutWorker) ServiceName() string { 19 | return FanOutWorkerServiceName 20 | } 21 | 22 | const FanOutWorkerServiceName = "FanOutWorker" 23 | 24 | func (c *fanOutWorker) Run(ctx restate.Context, commaSeparatedTasks string) (aggregatedResults string, err error) { 25 | tasks := strings.Split(commaSeparatedTasks, ",") 26 | 27 | // Run tasks in parallel 28 | var futs []restate.Selectable 29 | for _, task := range tasks { 30 | futs = append(futs, restate.RunAsync[string](ctx, func(ctx restate.RunContext) (string, error) { 31 | log.Printf("Heavy task %s running", task) 32 | if rand.Intn(2) == 1 { 33 | log.Printf("Heavy task %s failed", task) 34 | panic(fmt.Errorf("failed to complete heavy task %s", task)) 35 | } 36 | log.Printf("Heavy task %s done", task) 37 | return task, nil 38 | })) 39 | } 40 | 41 | // Aggregate 42 | var results []string 43 | selector := restate.Select(ctx, futs...) 44 | for selector.Remaining() { 45 | result, err := selector.Select().(restate.RunAsyncFuture[string]).Result() 46 | if err != nil { 47 | return "", err 48 | } 49 | results = append(results, result) 50 | } 51 | 52 | return strings.Join(results, "-"), nil 53 | } 54 | 55 | func main() { 56 | slog.SetLogLoggerLevel(slog.LevelDebug) 57 | server := server.NewRestate().Bind(restate.Reflect(&fanOutWorker{})) 58 | 59 | if err := server.Start(context.Background(), ":9080"); err != nil { 60 | slog.Error("application exited unexpectedly", "err", err.Error()) 61 | os.Exit(1) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/ticketreservation/.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /examples/ticketreservation/checkout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | restate "github.com/restatedev/sdk-go" 8 | ) 9 | 10 | type PaymentRequest struct { 11 | UserID string `json:"userId"` 12 | Tickets []string `json:"tickets"` 13 | } 14 | 15 | type PaymentResponse struct { 16 | ID string `json:"id"` 17 | Price int `json:"price"` 18 | } 19 | 20 | type checkout struct{} 21 | 22 | func (c *checkout) ServiceName() string { 23 | return CheckoutServiceName 24 | } 25 | 26 | const CheckoutServiceName = "Checkout" 27 | 28 | func (c *checkout) Payment(ctx restate.Context, request PaymentRequest) (response PaymentResponse, err error) { 29 | uuid := restate.Rand(ctx).UUID().String() 30 | 31 | response.ID = uuid 32 | 33 | // We are a uniform shop where everything costs 30 USD 34 | // that is cheaper than the official example :P 35 | price := len(request.Tickets) * 30 36 | 37 | response.Price = price 38 | _, err = restate.Run(ctx, func(ctx restate.RunContext) (bool, error) { 39 | log := ctx.Log().With("uuid", uuid, "price", price) 40 | if rand.Float64() < 0.5 { 41 | log.Info("payment succeeded") 42 | return true, nil 43 | } else { 44 | log.Error("payment failed") 45 | return false, fmt.Errorf("failed to pay") 46 | } 47 | }) 48 | 49 | if err != nil { 50 | return response, err 51 | } 52 | 53 | // todo: send email 54 | 55 | return response, nil 56 | } 57 | -------------------------------------------------------------------------------- /examples/ticketreservation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | 8 | restate "github.com/restatedev/sdk-go" 9 | "github.com/restatedev/sdk-go/server" 10 | ) 11 | 12 | func main() { 13 | server := server.NewRestate(). 14 | // Handlers can be inferred from object methods 15 | Bind(restate.Reflect(&userSession{})). 16 | Bind(restate.Reflect(&ticketService{})). 17 | Bind(restate.Reflect(&checkout{})) 18 | 19 | if err := server.Start(context.Background(), ":9080"); err != nil { 20 | slog.Error("application exited unexpectedly", "err", err.Error()) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/ticketreservation/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | restate "github.com/restatedev/sdk-go" 10 | "github.com/restatedev/sdk-go/mocks" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestPayment(t *testing.T) { 15 | mockCtx := mocks.NewMockContext(t) 16 | 17 | mockCtx.EXPECT().MockRand().UUID().Return(uuid.Max) 18 | 19 | mockCtx.EXPECT().RunAndExpect(mockCtx, true, nil) 20 | mockCtx.EXPECT().Log().Return(slog.Default()) 21 | 22 | resp, err := (&checkout{}).Payment(restate.WithMockContext(mockCtx), PaymentRequest{Tickets: []string{"abc"}}) 23 | assert.NoError(t, err) 24 | assert.Equal(t, resp, PaymentResponse{ID: "ffffffff-ffff-ffff-ffff-ffffffffffff", Price: 30}) 25 | } 26 | 27 | func TestReserve(t *testing.T) { 28 | mockCtx := mocks.NewMockContext(t) 29 | 30 | mockCtx.EXPECT().GetAndReturn("status", TicketAvailable) 31 | mockCtx.EXPECT().Set("status", TicketReserved) 32 | 33 | ok, err := (&ticketService{}).Reserve(restate.WithMockContext(mockCtx), restate.Void{}) 34 | assert.NoError(t, err) 35 | assert.True(t, ok) 36 | } 37 | 38 | func TestUnreserve(t *testing.T) { 39 | mockCtx := mocks.NewMockContext(t) 40 | 41 | mockCtx.EXPECT().Key().Return("foo") 42 | mockCtx.EXPECT().Log().Return(slog.Default()) 43 | mockCtx.EXPECT().GetAndReturn("status", TicketAvailable) 44 | mockCtx.EXPECT().Clear("status") 45 | 46 | _, err := (&ticketService{}).Unreserve(restate.WithMockContext(mockCtx), restate.Void{}) 47 | assert.NoError(t, err) 48 | } 49 | 50 | func TestMarkAsSold(t *testing.T) { 51 | mockCtx := mocks.NewMockContext(t) 52 | 53 | mockCtx.EXPECT().Key().Return("foo") 54 | mockCtx.EXPECT().Log().Return(slog.Default()) 55 | mockCtx.EXPECT().GetAndReturn("status", TicketReserved) 56 | mockCtx.EXPECT().Set("status", TicketSold) 57 | 58 | _, err := (&ticketService{}).MarkAsSold(restate.WithMockContext(mockCtx), restate.Void{}) 59 | assert.NoError(t, err) 60 | } 61 | 62 | func TestStatus(t *testing.T) { 63 | mockCtx := mocks.NewMockContext(t) 64 | 65 | mockCtx.EXPECT().Key().Return("foo") 66 | mockCtx.EXPECT().Log().Return(slog.Default()) 67 | mockCtx.EXPECT().GetAndReturn("status", TicketReserved) 68 | 69 | status, err := (&ticketService{}).Status(restate.WithMockContext(mockCtx), restate.Void{}) 70 | assert.NoError(t, err) 71 | assert.Equal(t, status, TicketReserved) 72 | } 73 | 74 | func TestAddTicket(t *testing.T) { 75 | mockCtx := mocks.NewMockContext(t) 76 | 77 | mockCtx.EXPECT().Key().Return("userID") 78 | mockCtx.EXPECT().MockObjectClient(TicketServiceName, "ticket2", "Reserve").RequestAndReturn("userID", true, nil) 79 | 80 | mockCtx.EXPECT().GetAndReturn("tickets", []string{"ticket1"}) 81 | mockCtx.EXPECT().Set("tickets", []string{"ticket1", "ticket2"}) 82 | mockCtx.EXPECT().MockObjectClient(UserSessionServiceName, "userID", "ExpireTicket"). 83 | MockSend("ticket2", restate.WithDelay(15*time.Minute)) 84 | 85 | ok, err := (&userSession{}).AddTicket(restate.WithMockContext(mockCtx), "ticket2") 86 | assert.NoError(t, err) 87 | assert.True(t, ok) 88 | } 89 | 90 | func TestExpireTicket(t *testing.T) { 91 | mockCtx := mocks.NewMockContext(t) 92 | 93 | mockCtx.EXPECT().GetAndReturn("tickets", []string{"ticket1", "ticket2"}) 94 | mockCtx.EXPECT().Set("tickets", []string{"ticket1"}) 95 | 96 | mockCtx.EXPECT().MockObjectClient(TicketServiceName, "ticket2", "Unreserve").MockSend(restate.Void{}) 97 | 98 | _, err := (&userSession{}).ExpireTicket(restate.WithMockContext(mockCtx), "ticket2") 99 | assert.NoError(t, err) 100 | } 101 | 102 | func TestCheckout(t *testing.T) { 103 | mockCtx := mocks.NewMockContext(t) 104 | 105 | mockCtx.EXPECT().Key().Return("userID") 106 | mockCtx.EXPECT().GetAndReturn("tickets", []string{"ticket1"}) 107 | mockCtx.EXPECT().Log().Return(slog.Default()) 108 | 109 | mockAfter := mockCtx.EXPECT().MockAfter(time.Minute) 110 | 111 | mockResponseFuture := mockCtx.EXPECT().MockObjectClient(CheckoutServiceName, "", "Payment"). 112 | MockResponseFuture(PaymentRequest{UserID: "userID", Tickets: []string{"ticket1"}}) 113 | 114 | mockCtx.EXPECT().MockSelector(mockAfter, mockResponseFuture). 115 | Select().Return(mockResponseFuture) 116 | 117 | mockResponseFuture.EXPECT().ResponseAndReturn(PaymentResponse{ID: "paymentID", Price: 30}, nil) 118 | 119 | mockCtx.EXPECT().MockObjectClient(TicketServiceName, "ticket1", "MarkAsSold").MockSend(restate.Void{}) 120 | 121 | mockCtx.EXPECT().Clear("tickets") 122 | 123 | ok, err := (&userSession{}).Checkout(restate.WithMockContext(mockCtx), restate.Void{}) 124 | assert.NoError(t, err) 125 | assert.True(t, ok) 126 | } 127 | -------------------------------------------------------------------------------- /examples/ticketreservation/ticket_service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | ) 6 | 7 | type TicketStatus int 8 | 9 | const ( 10 | TicketAvailable TicketStatus = 0 11 | TicketReserved TicketStatus = 1 12 | TicketSold TicketStatus = 2 13 | ) 14 | 15 | const TicketServiceName = "TicketService" 16 | 17 | type ticketService struct{} 18 | 19 | func (t *ticketService) ServiceName() string { return TicketServiceName } 20 | 21 | func (t *ticketService) Reserve(ctx restate.ObjectContext, _ restate.Void) (bool, error) { 22 | status, err := restate.Get[TicketStatus](ctx, "status") 23 | if err != nil { 24 | return false, err 25 | } 26 | 27 | if status == TicketAvailable { 28 | restate.Set(ctx, "status", TicketReserved) 29 | return true, nil 30 | } 31 | 32 | return false, nil 33 | } 34 | 35 | func (t *ticketService) Unreserve(ctx restate.ObjectContext, _ restate.Void) (void restate.Void, err error) { 36 | ticketId := restate.Key(ctx) 37 | ctx.Log().Info("un-reserving ticket", "ticket", ticketId) 38 | status, err := restate.Get[TicketStatus](ctx, "status") 39 | if err != nil { 40 | return void, err 41 | } 42 | 43 | if status != TicketSold { 44 | restate.Clear(ctx, "status") 45 | return void, nil 46 | } 47 | 48 | return void, nil 49 | } 50 | 51 | func (t *ticketService) MarkAsSold(ctx restate.ObjectContext, _ restate.Void) (void restate.Void, err error) { 52 | ticketId := restate.Key(ctx) 53 | ctx.Log().Info("mark ticket as sold", "ticket", ticketId) 54 | 55 | status, err := restate.Get[TicketStatus](ctx, "status") 56 | if err != nil { 57 | return void, err 58 | } 59 | 60 | if status == TicketReserved { 61 | restate.Set(ctx, "status", TicketSold) 62 | return void, nil 63 | } 64 | 65 | return void, nil 66 | } 67 | 68 | func (t *ticketService) Status(ctx restate.ObjectSharedContext, _ restate.Void) (TicketStatus, error) { 69 | ticketId := restate.Key(ctx) 70 | ctx.Log().Info("mark ticket as sold", "ticket", ticketId) 71 | 72 | return restate.Get[TicketStatus](ctx, "status") 73 | } 74 | -------------------------------------------------------------------------------- /examples/ticketreservation/user_session.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "slices" 5 | "time" 6 | 7 | restate "github.com/restatedev/sdk-go" 8 | ) 9 | 10 | const UserSessionServiceName = "UserSession" 11 | 12 | type userSession struct{} 13 | 14 | func (u *userSession) ServiceName() string { 15 | return UserSessionServiceName 16 | } 17 | 18 | func (u *userSession) AddTicket(ctx restate.ObjectContext, ticketId string) (bool, error) { 19 | userId := restate.Key(ctx) 20 | 21 | success, err := restate.Object[bool](ctx, TicketServiceName, ticketId, "Reserve").Request(userId) 22 | if err != nil { 23 | return false, err 24 | } 25 | 26 | if !success { 27 | return false, nil 28 | } 29 | 30 | // add ticket to list of tickets 31 | tickets, err := restate.Get[[]string](ctx, "tickets") 32 | if err != nil { 33 | return false, err 34 | } 35 | 36 | tickets = append(tickets, ticketId) 37 | 38 | restate.Set(ctx, "tickets", tickets) 39 | restate.ObjectSend(ctx, UserSessionServiceName, userId, "ExpireTicket").Send(ticketId, restate.WithDelay(15*time.Minute)) 40 | 41 | return true, nil 42 | } 43 | 44 | func (u *userSession) ExpireTicket(ctx restate.ObjectContext, ticketId string) (void restate.Void, err error) { 45 | tickets, err := restate.Get[[]string](ctx, "tickets") 46 | if err != nil { 47 | return void, err 48 | } 49 | 50 | deleted := false 51 | tickets = slices.DeleteFunc(tickets, func(ticket string) bool { 52 | if ticket == ticketId { 53 | deleted = true 54 | return true 55 | } 56 | return false 57 | }) 58 | if !deleted { 59 | return void, nil 60 | } 61 | 62 | restate.Set(ctx, "tickets", tickets) 63 | restate.ObjectSend(ctx, TicketServiceName, ticketId, "Unreserve").Send(restate.Void{}) 64 | 65 | return void, nil 66 | } 67 | 68 | func (u *userSession) Checkout(ctx restate.ObjectContext, _ restate.Void) (bool, error) { 69 | userId := restate.Key(ctx) 70 | tickets, err := restate.Get[[]string](ctx, "tickets") 71 | if err != nil { 72 | return false, err 73 | } 74 | 75 | ctx.Log().Info("tickets in basket", "tickets", tickets) 76 | 77 | if len(tickets) == 0 { 78 | return false, nil 79 | } 80 | 81 | timeout := restate.After(ctx, time.Minute) 82 | 83 | request := restate.Object[PaymentResponse](ctx, CheckoutServiceName, "", "Payment"). 84 | RequestFuture(PaymentRequest{UserID: userId, Tickets: tickets}) 85 | 86 | // race between the request and the timeout 87 | switch restate.Select(ctx, timeout, request).Select() { 88 | case request: 89 | // happy path 90 | case timeout: 91 | // we could choose to fail here with terminal error, but we'd also have to refund the payment! 92 | ctx.Log().Warn("slow payment") 93 | } 94 | 95 | // block on the eventual response 96 | response, err := request.Response() 97 | if err != nil { 98 | return false, err 99 | } 100 | 101 | ctx.Log().Info("payment details", "id", response.ID, "price", response.Price) 102 | 103 | for _, ticket := range tickets { 104 | restate.ObjectSend(ctx, TicketServiceName, ticket, "MarkAsSold").Send(restate.Void{}) 105 | } 106 | 107 | restate.Clear(ctx, "tickets") 108 | return true, nil 109 | } 110 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package restate 2 | 3 | //go:generate buf generate 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/restatedev/sdk-go 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/golang-jwt/jwt/v5 v5.2.1 9 | github.com/google/uuid v1.6.0 10 | github.com/invopop/jsonschema v0.13.0 11 | github.com/mr-tron/base58 v1.2.0 12 | github.com/stretchr/testify v1.9.0 13 | github.com/tetratelabs/wazero v1.9.0 14 | github.com/wk8/go-ordered-map/v2 v2.1.8 15 | go.opentelemetry.io/otel v1.28.0 16 | golang.org/x/net v0.23.0 17 | google.golang.org/protobuf v1.36.5 18 | ) 19 | 20 | require ( 21 | github.com/bahlo/generic-list-go v0.2.0 // indirect 22 | github.com/buger/jsonparser v1.1.1 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/mailru/easyjson v0.7.7 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/stretchr/objx v0.5.2 // indirect 29 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 30 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 31 | golang.org/x/text v0.14.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= 2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 8 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 9 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 10 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 11 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 12 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 13 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 14 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 15 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= 19 | github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= 20 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 21 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 22 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 23 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 24 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 28 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 29 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 30 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 31 | github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= 32 | github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 33 | github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= 34 | github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 35 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 36 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 37 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 38 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 39 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 40 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 41 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 42 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 43 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 44 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 45 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 46 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 50 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | -------------------------------------------------------------------------------- /internal.buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | plugins: 5 | - remote: buf.build/protocolbuffers/go:v1.36.5 6 | out: internal/generated 7 | opt: 8 | - paths=source_relative 9 | - default_api_level=API_OPAQUE 10 | inputs: 11 | - proto_file: proto/internal.proto 12 | -------------------------------------------------------------------------------- /internal/converters/converters.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "github.com/restatedev/sdk-go/internal/restatecontext" 5 | ) 6 | 7 | type ToInnerFuture interface { 8 | InnerFuture() restatecontext.Selectable 9 | } 10 | 11 | type ResponseFuture[O any] struct { 12 | restatecontext.ResponseFuture 13 | } 14 | 15 | func (t ResponseFuture[O]) Response() (output O, err error) { 16 | err = t.ResponseFuture.Response(&output) 17 | return 18 | } 19 | 20 | func (t ResponseFuture[O]) InnerFuture() restatecontext.Selectable { 21 | return t.ResponseFuture 22 | } 23 | 24 | type RunAsyncFuture[O any] struct { 25 | restatecontext.RunAsyncFuture 26 | } 27 | 28 | func (t RunAsyncFuture[O]) Result() (output O, err error) { 29 | err = t.RunAsyncFuture.Result(&output) 30 | return 31 | } 32 | 33 | func (t RunAsyncFuture[O]) InnerFuture() restatecontext.Selectable { 34 | return t.RunAsyncFuture 35 | } 36 | 37 | type AttachFuture[O any] struct { 38 | restatecontext.AttachFuture 39 | } 40 | 41 | func (t AttachFuture[O]) Response() (output O, err error) { 42 | err = t.AttachFuture.Response(&output) 43 | return 44 | } 45 | 46 | func (t AttachFuture[O]) InnerFuture() restatecontext.Selectable { 47 | return t.AttachFuture 48 | } 49 | 50 | type AwakeableFuture[T any] struct { 51 | restatecontext.AwakeableFuture 52 | } 53 | 54 | func (t AwakeableFuture[T]) Result() (output T, err error) { 55 | err = t.AwakeableFuture.Result(&output) 56 | return 57 | } 58 | 59 | func (t AwakeableFuture[T]) InnerFuture() restatecontext.Selectable { 60 | return t.AwakeableFuture 61 | } 62 | -------------------------------------------------------------------------------- /internal/discovery.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "github.com/restatedev/sdk-go/encoding" 4 | 5 | type ProtocolMode string 6 | 7 | const ( 8 | ProtocolMode_BIDI_STREAM ProtocolMode = "BIDI_STREAM" 9 | ProtocolMode_REQUEST_RESPONSE ProtocolMode = "REQUEST_RESPONSE" 10 | ) 11 | 12 | type ServiceType string 13 | 14 | const ( 15 | ServiceType_VIRTUAL_OBJECT ServiceType = "VIRTUAL_OBJECT" 16 | ServiceType_SERVICE ServiceType = "SERVICE" 17 | ServiceType_WORKFLOW ServiceType = "WORKFLOW" 18 | ) 19 | 20 | type ServiceHandlerType string 21 | 22 | const ( 23 | ServiceHandlerType_WORKFLOW ServiceHandlerType = "WORKFLOW" 24 | ServiceHandlerType_EXCLUSIVE ServiceHandlerType = "EXCLUSIVE" 25 | ServiceHandlerType_SHARED ServiceHandlerType = "SHARED" 26 | ) 27 | 28 | type Handler struct { 29 | Name string `json:"name,omitempty"` 30 | // If unspecified, defaults to EXCLUSIVE for Virtual Object. This should be unset for Services. 31 | Ty *ServiceHandlerType `json:"ty,omitempty"` 32 | Input *encoding.InputPayload `json:"input,omitempty"` 33 | Output *encoding.OutputPayload `json:"output,omitempty"` 34 | Metadata map[string]string `json:"metadata,omitempty"` 35 | Documentation string `json:"documentation,omitempty"` 36 | } 37 | 38 | type Service struct { 39 | Name string `json:"name"` 40 | Ty ServiceType `json:"ty"` 41 | Handlers []Handler `json:"handlers"` 42 | Metadata map[string]string `json:"metadata,omitempty"` 43 | Documentation string `json:"documentation,omitempty"` 44 | } 45 | 46 | type Endpoint struct { 47 | ProtocolMode ProtocolMode `json:"protocolMode"` 48 | MinProtocolVersion int32 `json:"minProtocolVersion"` 49 | MaxProtocolVersion int32 `json:"maxProtocolVersion"` 50 | Services []Service `json:"services"` 51 | } 52 | -------------------------------------------------------------------------------- /internal/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | type Code uint16 9 | 10 | type CodeError struct { 11 | Code Code 12 | Inner error 13 | } 14 | 15 | func (e *CodeError) Error() string { 16 | return fmt.Sprintf("[%d] %s", e.Code, e.Inner) 17 | } 18 | 19 | func (e *CodeError) Unwrap() error { 20 | return e.Inner 21 | } 22 | 23 | func ErrorCode(err error) Code { 24 | var e *CodeError 25 | if errors.As(err, &e) { 26 | return e.Code 27 | } 28 | 29 | return 500 30 | } 31 | 32 | type TerminalError struct { 33 | Inner error 34 | } 35 | 36 | func (e *TerminalError) Error() string { 37 | return e.Inner.Error() 38 | } 39 | 40 | func (e *TerminalError) Unwrap() error { 41 | return e.Inner 42 | } 43 | 44 | func IsTerminalError(err error) bool { 45 | if err == nil { 46 | return false 47 | } 48 | var t *TerminalError 49 | return errors.As(err, &t) 50 | } 51 | 52 | func NewTerminalError(err error, code ...Code) error { 53 | if err == nil { 54 | return nil 55 | } 56 | 57 | if len(code) > 1 { 58 | panic("only single code is allowed") 59 | } 60 | 61 | err = &TerminalError{ 62 | Inner: err, 63 | } 64 | 65 | if len(code) == 1 { 66 | err = &CodeError{ 67 | Inner: err, 68 | Code: code[0], 69 | } 70 | } 71 | 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /internal/identity/identity.go: -------------------------------------------------------------------------------- 1 | package identity 2 | 3 | import "fmt" 4 | 5 | const SIGNATURE_SCHEME_HEADER = "X-Restate-Signature-Scheme" 6 | 7 | type SignatureScheme string 8 | 9 | var ( 10 | SchemeUnsigned SignatureScheme = "unsigned" 11 | errMissingIdentity = fmt.Errorf("request has no identity") 12 | ) 13 | 14 | func ValidateRequestIdentity(keySet KeySetV1, path string, headers map[string][]string) error { 15 | switch len(headers[SIGNATURE_SCHEME_HEADER]) { 16 | case 0: 17 | return errMissingIdentity 18 | case 1: 19 | switch SignatureScheme(headers[SIGNATURE_SCHEME_HEADER][0]) { 20 | case SchemeV1: 21 | return validateV1(keySet, path, headers) 22 | case SchemeUnsigned: 23 | return errMissingIdentity 24 | default: 25 | return fmt.Errorf("unexpected signature scheme %v, allowed values are [%s %s]", headers[SIGNATURE_SCHEME_HEADER][0], SchemeUnsigned, SchemeV1) 26 | } 27 | default: 28 | return fmt.Errorf("unexpected multi-value signature scheme header: %v", headers[SIGNATURE_SCHEME_HEADER]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/identity/v1.go: -------------------------------------------------------------------------------- 1 | package identity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | "strings" 7 | 8 | jwt "github.com/golang-jwt/jwt/v5" 9 | "github.com/mr-tron/base58" 10 | ) 11 | 12 | const ( 13 | JWT_HEADER = "X-Restate-Jwt-V1" 14 | SchemeV1 SignatureScheme = "v1" 15 | ) 16 | 17 | type KeySetV1 = map[string]ed25519.PublicKey 18 | 19 | func validateV1(keySet KeySetV1, path string, headers map[string][]string) error { 20 | switch len(headers[JWT_HEADER]) { 21 | case 0: 22 | return fmt.Errorf("v1 signature scheme expects the following headers: [%s]", JWT_HEADER) 23 | case 1: 24 | default: 25 | return fmt.Errorf("unexpected multi-value JWT header: %v", headers[JWT_HEADER]) 26 | } 27 | 28 | token, err := jwt.Parse(headers[JWT_HEADER][0], func(token *jwt.Token) (interface{}, error) { 29 | if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { 30 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 31 | } 32 | 33 | kid, ok := token.Header["kid"] 34 | if !ok { 35 | return nil, fmt.Errorf("Token missing 'kid' header field") 36 | } 37 | 38 | kidS, ok := kid.(string) 39 | if !ok { 40 | return nil, fmt.Errorf("Token 'kid' header field was not a string: %v", kid) 41 | } 42 | 43 | key, ok := keySet[kidS] 44 | if !ok { 45 | return nil, fmt.Errorf("Key ID %s is not present in key set", kid) 46 | } 47 | 48 | return key, nil 49 | }, jwt.WithValidMethods([]string{"EdDSA"}), jwt.WithAudience(path), jwt.WithExpirationRequired()) 50 | if err != nil { 51 | return fmt.Errorf("failed to validate v1 request identity jwt: %w", err) 52 | } 53 | 54 | nbf, _ := token.Claims.GetNotBefore() 55 | if nbf == nil { 56 | // jwt library only validates nbf if its present, so we should check it was present 57 | return fmt.Errorf("'nbf' claim is missing in v1 request identity jwt") 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func ParseKeySetV1(keys []string) (KeySetV1, error) { 64 | out := make(KeySetV1, len(keys)) 65 | for _, key := range keys { 66 | if !strings.HasPrefix(key, "publickeyv1_") { 67 | return nil, fmt.Errorf("v1 public key must start with 'publickeyv1_'") 68 | } 69 | 70 | pubBytes, err := base58.Decode(key[len("publickeyv1_"):]) 71 | if err != nil { 72 | return nil, fmt.Errorf("v1 public key must be valid base58: %w", err) 73 | } 74 | 75 | if len(pubBytes) != ed25519.PublicKeySize { 76 | return nil, fmt.Errorf("v1 public key must have exactly %d bytes, found %d", ed25519.PublicKeySize, len(pubBytes)) 77 | } 78 | 79 | out[key] = ed25519.PublicKey(pubBytes) 80 | } 81 | 82 | return out, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "reflect" 8 | "sync/atomic" 9 | 10 | "github.com/restatedev/sdk-go/rcontext" 11 | ) 12 | 13 | const ( 14 | LevelTrace slog.Level = -8 15 | ) 16 | 17 | type typeValue struct{ inner any } 18 | 19 | func (t typeValue) LogValue() slog.Value { 20 | return slog.StringValue(reflect.TypeOf(t.inner).String()) 21 | } 22 | 23 | func Type(key string, value any) slog.Attr { 24 | return slog.Any(key, typeValue{value}) 25 | } 26 | 27 | type stringerValue[T fmt.Stringer] struct{ inner T } 28 | 29 | func (t stringerValue[T]) LogValue() slog.Value { 30 | return slog.StringValue(t.inner.String()) 31 | } 32 | 33 | func Stringer[T fmt.Stringer](key string, value T) slog.Attr { 34 | return slog.Any(key, stringerValue[T]{value}) 35 | } 36 | 37 | func Error(err error) slog.Attr { 38 | return slog.String("err", err.Error()) 39 | } 40 | 41 | type contextInjectingHandler struct { 42 | logContext *atomic.Pointer[rcontext.LogContext] 43 | dropReplay bool 44 | inner slog.Handler 45 | } 46 | 47 | func NewUserContextHandler(logContext *atomic.Pointer[rcontext.LogContext], dropReplay bool, inner slog.Handler) slog.Handler { 48 | return &contextInjectingHandler{logContext, dropReplay, inner} 49 | } 50 | 51 | func NewRestateContextHandler(inner slog.Handler) slog.Handler { 52 | logContext := atomic.Pointer[rcontext.LogContext]{} 53 | logContext.Store(&rcontext.LogContext{Source: rcontext.LogSourceRestate, IsReplaying: false}) 54 | return &contextInjectingHandler{&logContext, false, inner} 55 | } 56 | 57 | func (d *contextInjectingHandler) Enabled(ctx context.Context, l slog.Level) bool { 58 | lc := d.logContext.Load() 59 | if d.dropReplay && lc.IsReplaying { 60 | return false 61 | } 62 | return d.inner.Enabled(rcontext.WithLogContext(ctx, lc), l) 63 | } 64 | 65 | func (d *contextInjectingHandler) Handle(ctx context.Context, record slog.Record) error { 66 | return d.inner.Handle(rcontext.WithLogContext(ctx, d.logContext.Load()), record) 67 | } 68 | 69 | func (d *contextInjectingHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 70 | return &contextInjectingHandler{d.logContext, d.dropReplay, d.inner.WithAttrs(attrs)} 71 | } 72 | 73 | func (d *contextInjectingHandler) WithGroup(name string) slog.Handler { 74 | return &contextInjectingHandler{d.logContext, d.dropReplay, d.inner.WithGroup(name)} 75 | } 76 | 77 | var _ slog.Handler = &contextInjectingHandler{} 78 | -------------------------------------------------------------------------------- /internal/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/restatedev/sdk-go/encoding" 7 | ) 8 | 9 | type SleepOptions struct { 10 | // Name used for observability. 11 | Name string 12 | } 13 | 14 | type SleepOption interface { 15 | BeforeSleep(*SleepOptions) 16 | } 17 | 18 | type AwakeableOptions struct { 19 | Codec encoding.Codec 20 | } 21 | 22 | type AwakeableOption interface { 23 | BeforeAwakeable(*AwakeableOptions) 24 | } 25 | 26 | type PromiseOptions struct { 27 | Codec encoding.Codec 28 | } 29 | 30 | type PromiseOption interface { 31 | BeforePromise(*PromiseOptions) 32 | } 33 | 34 | type ResolveAwakeableOptions struct { 35 | Codec encoding.Codec 36 | } 37 | 38 | type ResolveAwakeableOption interface { 39 | BeforeResolveAwakeable(*ResolveAwakeableOptions) 40 | } 41 | 42 | type GetOptions struct { 43 | Codec encoding.Codec 44 | } 45 | 46 | type GetOption interface { 47 | BeforeGet(*GetOptions) 48 | } 49 | 50 | type SetOptions struct { 51 | Codec encoding.Codec 52 | } 53 | 54 | type SetOption interface { 55 | BeforeSet(*SetOptions) 56 | } 57 | 58 | type ClientOptions struct { 59 | Codec encoding.Codec 60 | } 61 | 62 | type ClientOption interface { 63 | BeforeClient(*ClientOptions) 64 | } 65 | 66 | type RequestOptions struct { 67 | IdempotencyKey string 68 | Headers map[string]string 69 | } 70 | 71 | type RequestOption interface { 72 | BeforeRequest(*RequestOptions) 73 | } 74 | 75 | type SendOptions struct { 76 | IdempotencyKey string 77 | Headers map[string]string 78 | Delay time.Duration 79 | } 80 | 81 | type SendOption interface { 82 | BeforeSend(*SendOptions) 83 | } 84 | 85 | type RunOptions struct { 86 | // MaxRetryAttempts before giving up. 87 | // 88 | // When giving up, Run will return a TerminalError wrapping the original error message. 89 | MaxRetryAttempts *uint 90 | 91 | // MaxRetryDuration before giving up. 92 | // 93 | // When giving up, Run will return a TerminalError wrapping the original error message. 94 | MaxRetryDuration *time.Duration 95 | 96 | // InitialRetryInterval for the first retry attempt. 97 | // 98 | // The retry interval will grow by a factor specified in RetryIntervalFactor. 99 | // 100 | // If any of the other retry options are set, this will be set by default to 50 milliseconds. 101 | InitialRetryInterval *time.Duration 102 | 103 | // RetryIntervalFactor to use when computing the next retry delay. 104 | // 105 | // If any of the other retry options are set, this will be set by default to 2, meaning retry interval will double at each attempt. 106 | RetryIntervalFactor *float32 107 | 108 | // MaxRetryInterval between retries. 109 | // Retry interval will grow by a factor specified in RetryIntervalFactor up to the interval specified in this value. 110 | // 111 | // If any of the other retry options are set, this will be set by default to 2 seconds. 112 | MaxRetryInterval *time.Duration 113 | 114 | // Name used for observability. 115 | Name string 116 | 117 | // Codec used to encode/decode the run result. 118 | Codec encoding.Codec 119 | } 120 | 121 | type RunOption interface { 122 | BeforeRun(*RunOptions) 123 | } 124 | 125 | type AttachOptions struct { 126 | Codec encoding.Codec 127 | } 128 | 129 | type AttachOption interface { 130 | BeforeAttach(*AttachOptions) 131 | } 132 | 133 | type HandlerOptions struct { 134 | Codec encoding.PayloadCodec 135 | Metadata map[string]string 136 | Documentation string 137 | } 138 | 139 | type HandlerOption interface { 140 | BeforeHandler(*HandlerOptions) 141 | } 142 | 143 | type ServiceDefinitionOptions struct { 144 | DefaultCodec encoding.PayloadCodec 145 | Metadata map[string]string 146 | Documentation string 147 | } 148 | 149 | type ServiceDefinitionOption interface { 150 | BeforeServiceDefinition(*ServiceDefinitionOptions) 151 | } 152 | -------------------------------------------------------------------------------- /internal/rand/rand.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/binary" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type Rand interface { 11 | UUID() uuid.UUID 12 | Float64() float64 13 | Uint64() uint64 14 | // Source returns a deterministic random source that can be provided to math/rand.New() 15 | // and math/rand/v2.New(). The v2 version of rand is strongly recommended where Go 1.22 16 | // is used, and once this library begins to depend on 1.22, it will be embedded in Rand. 17 | Source() Source 18 | } 19 | 20 | type rand struct { 21 | source *source 22 | } 23 | 24 | func New(invocationID []byte) *rand { 25 | return &rand{newSource(invocationID)} 26 | } 27 | 28 | func (r *rand) UUID() uuid.UUID { 29 | var uuid [16]byte 30 | binary.LittleEndian.PutUint64(uuid[:8], r.Uint64()) 31 | binary.LittleEndian.PutUint64(uuid[8:], r.Uint64()) 32 | uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 33 | uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 34 | return uuid 35 | } 36 | 37 | func (r *rand) Float64() float64 { 38 | // use the math/rand/v2 implementation of Float64() which is more correct 39 | // and also matches our TS implementation 40 | return float64(r.Uint64()<<11>>11) / (1 << 53) 41 | } 42 | 43 | func (r *rand) Uint64() uint64 { 44 | return r.source.Uint64() 45 | } 46 | 47 | func (r *rand) Source() Source { 48 | return r.source 49 | } 50 | 51 | type Source interface { 52 | Int63() int64 53 | Uint64() uint64 54 | 55 | // only the v1 rand package requires this method; we do *not* support it and will panic if its called. 56 | Seed(int64) 57 | } 58 | 59 | type source struct { 60 | state [4]uint64 61 | } 62 | 63 | func newSource(invocationID []byte) *source { 64 | hash := sha256.New() 65 | hash.Write(invocationID) 66 | var sum [32]byte 67 | hash.Sum(sum[:0]) 68 | 69 | return &source{state: [4]uint64{ 70 | binary.LittleEndian.Uint64(sum[:8]), 71 | binary.LittleEndian.Uint64(sum[8:16]), 72 | binary.LittleEndian.Uint64(sum[16:24]), 73 | binary.LittleEndian.Uint64(sum[24:32]), 74 | }} 75 | } 76 | 77 | func (s *source) Int63() int64 { 78 | return int64(s.Uint64() & ((1 << 63) - 1)) 79 | } 80 | 81 | // only the v1 rand package has this method 82 | func (s *source) Seed(int64) { 83 | panic("The Restate random source is already deterministic based on invocation ID and must not be seeded") 84 | } 85 | 86 | func (s *source) Uint64() uint64 { 87 | result := rotl((s.state[0]+s.state[3]), 23) + s.state[0] 88 | 89 | t := (s.state[1] << 17) 90 | 91 | s.state[2] ^= s.state[0] 92 | s.state[3] ^= s.state[1] 93 | s.state[1] ^= s.state[2] 94 | s.state[0] ^= s.state[3] 95 | 96 | s.state[2] ^= t 97 | 98 | s.state[3] = rotl(s.state[3], 45) 99 | 100 | return result 101 | } 102 | 103 | func rotl(x uint64, k uint64) uint64 { 104 | return (x << k) | (x >> (64 - k)) 105 | } 106 | -------------------------------------------------------------------------------- /internal/rand/rand_test.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | ) 7 | 8 | func TestUint64(t *testing.T) { 9 | id, err := hex.DecodeString("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2") 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | rand := New(id) 14 | 15 | expected := []uint64{ 16 | 6541268553928124324, 17 | 1632128201851599825, 18 | 3999496359968271420, 19 | 9099219592091638755, 20 | 2609122094717920550, 21 | 16569362788292807660, 22 | 14955958648458255954, 23 | 15581072429430901841, 24 | 4951852598761288088, 25 | 2380816196140950843, 26 | } 27 | 28 | for _, e := range expected { 29 | if found := rand.Uint64(); e != found { 30 | t.Fatalf("Unexpected uint64 %d, expected %d", found, e) 31 | } 32 | } 33 | } 34 | 35 | func TestFloat64(t *testing.T) { 36 | source := &source{state: [4]uint64{1, 2, 3, 4}} 37 | rand := &rand{source} 38 | 39 | expected := []float64{ 40 | 4.656612984099695e-9, 6.519269457605503e-9, 0.39843750651926946, 41 | 0.3986824029416509, 0.5822761557370711, 0.2997488042907357, 42 | 0.5336032865255543, 0.36335061693258097, 0.5968067925950846, 43 | 0.18570456306457928, 44 | } 45 | 46 | for _, e := range expected { 47 | if found := rand.Float64(); e != found { 48 | t.Fatalf("Unexpected float64 %v, expected %v", found, e) 49 | } 50 | } 51 | } 52 | 53 | func TestUUID(t *testing.T) { 54 | source := &source{state: [4]uint64{1, 2, 3, 4}} 55 | rand := &rand{source} 56 | 57 | expected := []string{ 58 | "01008002-0000-4000-a700-800300000000", 59 | "67008003-00c0-4c00-b200-449901c20c00", 60 | "cd33c49a-01a2-4280-ba33-eecd8a97698a", 61 | "bd4a1533-4713-41c2-979e-167991a02bac", 62 | "d83f078f-0a19-43db-a092-22b24af10591", 63 | "677c91f7-146e-4769-a4fd-df3793e717e8", 64 | "f15179b2-f220-4427-8d90-7b5437d9828d", 65 | "9e97720f-42b8-4d09-a449-914cf221df26", 66 | "09d0a109-6f11-4ef9-93fa-f013d0ad3808", 67 | "41eb0e0c-41c9-4828-85d0-59fb901b4df4", 68 | } 69 | 70 | for _, e := range expected { 71 | if found := rand.UUID().String(); e != found { 72 | t.Fatalf("Unexpected uuid %s, expected %s", found, e) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/restatecontext/async_results.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "fmt" 5 | "github.com/restatedev/sdk-go/internal/errors" 6 | pbinternal "github.com/restatedev/sdk-go/internal/generated" 7 | "github.com/restatedev/sdk-go/internal/statemachine" 8 | "io" 9 | "sync" 10 | "sync/atomic" 11 | ) 12 | 13 | var CancelledFailureValue = func() statemachine.Value { 14 | failure := pbinternal.Failure{} 15 | failure.SetCode(409) 16 | failure.SetMessage("Cancelled") 17 | return statemachine.ValueFailure{Failure: &failure} 18 | }() 19 | 20 | func errorFromFailure(failure statemachine.ValueFailure) error { 21 | return &errors.CodeError{Inner: &errors.TerminalError{Inner: fmt.Errorf(failure.Failure.GetMessage())}, Code: errors.Code(failure.Failure.GetCode())} 22 | } 23 | 24 | type Selectable interface { 25 | handle() uint32 26 | } 27 | 28 | type asyncResult struct { 29 | ctx *ctx 30 | coreHandle uint32 31 | poll sync.Once 32 | result atomic.Value // statemachine.Value 33 | } 34 | 35 | func newAsyncResult(ctx *ctx, handle uint32) asyncResult { 36 | return asyncResult{ 37 | ctx: ctx, 38 | coreHandle: handle, 39 | } 40 | } 41 | 42 | func (a *asyncResult) handle() uint32 { 43 | return a.coreHandle 44 | } 45 | 46 | func (a *asyncResult) pollProgress() { 47 | if a.result.Load() != nil { 48 | return 49 | } 50 | a.poll.Do(func() { 51 | cancelled := a.ctx.pollProgress([]uint32{a.coreHandle}) 52 | if cancelled { 53 | a.result.Store(CancelledFailureValue) 54 | } else { 55 | value, err := a.ctx.stateMachine.TakeNotification(a.ctx, a.coreHandle) 56 | if value == nil { 57 | panic("The value should not be nil anymore") 58 | } 59 | if err != nil { 60 | panic(err) 61 | } 62 | a.result.Store(value) 63 | } 64 | }) 65 | } 66 | 67 | func (a *asyncResult) mustLoadValue() statemachine.Value { 68 | value := a.result.Load() 69 | if value == nil { 70 | panic("value is not expected to be nil at this point") 71 | } 72 | return value.(statemachine.Value) 73 | } 74 | 75 | func (a *asyncResult) pollProgressAndLoadValue() statemachine.Value { 76 | a.pollProgress() 77 | return a.mustLoadValue() 78 | } 79 | 80 | func (restateCtx *ctx) pollProgress(handles []uint32) bool { 81 | // Pump output once 82 | if err := takeOutputAndWriteOut(restateCtx, restateCtx.stateMachine, restateCtx.conn); err != nil { 83 | panic(err) 84 | } 85 | 86 | for { 87 | progressResult, err := restateCtx.stateMachine.DoProgress(restateCtx, handles) 88 | if err != nil { 89 | panic(err) 90 | } 91 | if _, ok := progressResult.(statemachine.DoProgressAnyCompleted); ok { 92 | return false 93 | } 94 | _, isPendingRun := progressResult.(statemachine.DoProgressWaitingPendingRun) 95 | _, isReadFromInput := progressResult.(statemachine.DoProgressReadFromInput) 96 | if isPendingRun || isReadFromInput { 97 | // Either wait for at least one read or for run proposals 98 | select { 99 | case readRes := <-restateCtx.readChan: 100 | if readRes.err == io.EOF { 101 | // Got EOF, notify and break 102 | if err = restateCtx.stateMachine.NotifyInputClosed(restateCtx); err != nil { 103 | panic(err) 104 | } 105 | break 106 | } else if err != nil { 107 | // Cannot read input anymore 108 | panic(fmt.Errorf("error when reading the input stream %e", err)) 109 | } 110 | if err = restateCtx.stateMachine.NotifyInput(restateCtx, readRes.buf[0:readRes.nRead]); err != nil { 111 | panic(err) 112 | } 113 | BufPool.Put(readRes.buf) 114 | break 115 | case proposal := <-restateCtx.runClosureCompletions: 116 | // Propose completion 117 | if err := restateCtx.stateMachine.ProposeRunCompletion(restateCtx, proposal); err != nil { 118 | panic(err) 119 | } 120 | 121 | // Pump output once. This is needed for the run completion to be effectively written 122 | if err := takeOutputAndWriteOut(restateCtx, restateCtx.stateMachine, restateCtx.conn); err != nil { 123 | panic(err) 124 | } 125 | break 126 | case <-restateCtx.Done(): 127 | panic(restateCtx.Err()) 128 | } 129 | } 130 | if _, ok := progressResult.(statemachine.DoProgressCancelSignalReceived); ok { 131 | // Pump output once. This is needed for cancel commands to be effectively written 132 | if err := takeOutputAndWriteOut(restateCtx, restateCtx.stateMachine, restateCtx.conn); err != nil { 133 | panic(err) 134 | } 135 | 136 | return true 137 | } 138 | if executeRun, ok := progressResult.(statemachine.DoProgressExecuteRun); ok { 139 | closure, ok := restateCtx.runClosures[executeRun.Handle] 140 | if !ok { 141 | panic(fmt.Sprintf("Need to run a Run closure with coreHandle %d, but it doesn't exist. This is an SDK bug.", executeRun.Handle)) 142 | } 143 | 144 | // Delete this closure from the running list 145 | delete(restateCtx.runClosures, executeRun.Handle) 146 | 147 | // Run closure in a separate goroutine, proposing the result to runClosureCompletions 148 | go func(runClosureCompletions chan *pbinternal.VmProposeRunCompletionParameters, closure func() *pbinternal.VmProposeRunCompletionParameters) { 149 | runClosureCompletions <- closure() 150 | }(restateCtx.runClosureCompletions, closure) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /internal/restatecontext/awakeable.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "fmt" 5 | "github.com/restatedev/sdk-go/encoding" 6 | "github.com/restatedev/sdk-go/internal/errors" 7 | pbinternal "github.com/restatedev/sdk-go/internal/generated" 8 | "github.com/restatedev/sdk-go/internal/options" 9 | "github.com/restatedev/sdk-go/internal/statemachine" 10 | ) 11 | 12 | func (restateCtx *ctx) Awakeable(opts ...options.AwakeableOption) AwakeableFuture { 13 | o := options.AwakeableOptions{} 14 | for _, opt := range opts { 15 | opt.BeforeAwakeable(&o) 16 | } 17 | if o.Codec == nil { 18 | o.Codec = encoding.JSONCodec 19 | } 20 | 21 | id, handle, err := restateCtx.stateMachine.SysAwakeable(restateCtx) 22 | if err != nil { 23 | panic(err) 24 | } 25 | restateCtx.checkStateTransition() 26 | 27 | return &awakeableFuture{ 28 | asyncResult: newAsyncResult(restateCtx, handle), 29 | id: id, 30 | codec: o.Codec, 31 | } 32 | } 33 | 34 | type AwakeableFuture interface { 35 | Selectable 36 | Id() string 37 | Result(output any) error 38 | } 39 | 40 | type awakeableFuture struct { 41 | asyncResult 42 | id string 43 | codec encoding.Codec 44 | } 45 | 46 | func (d *awakeableFuture) Id() string { return d.id } 47 | 48 | func (d *awakeableFuture) Result(output any) error { 49 | switch result := d.pollProgressAndLoadValue().(type) { 50 | case statemachine.ValueSuccess: 51 | { 52 | if err := encoding.Unmarshal(d.codec, result.Success, output); err != nil { 53 | panic(fmt.Errorf("failed to unmarshal awakeable result into output: %w", err)) 54 | } 55 | return nil 56 | } 57 | case statemachine.ValueFailure: 58 | return errorFromFailure(result) 59 | default: 60 | panic(fmt.Errorf("unexpected value %s", result)) 61 | 62 | } 63 | } 64 | 65 | func (restateCtx *ctx) ResolveAwakeable(id string, value any, opts ...options.ResolveAwakeableOption) { 66 | o := options.ResolveAwakeableOptions{} 67 | for _, opt := range opts { 68 | opt.BeforeResolveAwakeable(&o) 69 | } 70 | if o.Codec == nil { 71 | o.Codec = encoding.JSONCodec 72 | } 73 | bytes, err := encoding.Marshal(o.Codec, value) 74 | if err != nil { 75 | panic(fmt.Errorf("failed to marshal ResolveAwakeable value: %w", err)) 76 | } 77 | 78 | input := pbinternal.VmSysCompleteAwakeableParameters{} 79 | input.SetId(id) 80 | input.SetSuccess(bytes) 81 | if err := restateCtx.stateMachine.SysCompleteAwakeable(restateCtx, &input); err != nil { 82 | panic(err) 83 | } 84 | restateCtx.checkStateTransition() 85 | } 86 | 87 | func (restateCtx *ctx) RejectAwakeable(id string, reason error) { 88 | failure := pbinternal.Failure{} 89 | failure.SetCode(uint32(errors.ErrorCode(reason))) 90 | failure.SetMessage(reason.Error()) 91 | 92 | input := pbinternal.VmSysCompleteAwakeableParameters{} 93 | input.SetId(id) 94 | input.SetFailure(&failure) 95 | if err := restateCtx.stateMachine.SysCompleteAwakeable(restateCtx, &input); err != nil { 96 | panic(err) 97 | } 98 | restateCtx.checkStateTransition() 99 | } 100 | -------------------------------------------------------------------------------- /internal/restatecontext/ctx.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "context" 5 | pbinternal "github.com/restatedev/sdk-go/internal/generated" 6 | "github.com/restatedev/sdk-go/internal/log" 7 | "github.com/restatedev/sdk-go/internal/options" 8 | "github.com/restatedev/sdk-go/internal/rand" 9 | "github.com/restatedev/sdk-go/internal/statemachine" 10 | "github.com/restatedev/sdk-go/rcontext" 11 | "io" 12 | "log/slog" 13 | "sync/atomic" 14 | "time" 15 | ) 16 | 17 | type Request struct { 18 | // The unique id that identifies the current function invocation. This id is guaranteed to be 19 | // unique across invocations, but constant across reties and suspensions. 20 | ID string 21 | // Request headers - the following headers capture the original invocation headers, as provided to 22 | // the ingress. 23 | Headers map[string]string 24 | // Attempt headers - the following headers are sent by the restate runtime. 25 | // These headers are attempt specific, generated by the restate runtime uniquely for each attempt. 26 | // These headers might contain information such as the W3C trace context, and attempt specific information. 27 | AttemptHeaders map[string][]string 28 | // Raw unparsed request body 29 | Body []byte 30 | } 31 | 32 | type Context interface { 33 | context.Context 34 | Log() *slog.Logger 35 | Request() *Request 36 | 37 | // available outside of .Run() 38 | Rand() rand.Rand 39 | Sleep(d time.Duration, opts ...options.SleepOption) error 40 | After(d time.Duration, opts ...options.SleepOption) AfterFuture 41 | Service(service, method string, options ...options.ClientOption) Client 42 | Object(service, key, method string, options ...options.ClientOption) Client 43 | Workflow(seservice, workflowID, method string, options ...options.ClientOption) Client 44 | CancelInvocation(invocationId string) 45 | AttachInvocation(invocationId string, opts ...options.AttachOption) AttachFuture 46 | Awakeable(options ...options.AwakeableOption) AwakeableFuture 47 | ResolveAwakeable(id string, value any, options ...options.ResolveAwakeableOption) 48 | RejectAwakeable(id string, reason error) 49 | Select(futs ...Selectable) Selector 50 | Run(fn func(ctx RunContext) (any, error), output any, options ...options.RunOption) error 51 | RunAsync(fn func(ctx RunContext) (any, error), options ...options.RunOption) RunAsyncFuture 52 | 53 | // available on all keyed handlers 54 | Get(key string, output any, options ...options.GetOption) (bool, error) 55 | Keys() ([]string, error) 56 | Key() string 57 | 58 | // available on non-shared keyed handlers 59 | Set(key string, value any, options ...options.SetOption) 60 | Clear(key string) 61 | ClearAll() 62 | 63 | // available on workflow handlers 64 | Promise(name string, options ...options.PromiseOption) DurablePromise 65 | } 66 | 67 | type ctx struct { 68 | context.Context 69 | 70 | conn io.ReadWriteCloser 71 | readChan chan readResult 72 | 73 | stateMachine *statemachine.StateMachine 74 | 75 | // Info about the invocation 76 | key string 77 | request Request 78 | isProcessing bool 79 | 80 | // Logging 81 | userLogContext atomic.Pointer[rcontext.LogContext] 82 | internalLogger *slog.Logger 83 | userLogger *slog.Logger 84 | 85 | // Random 86 | rand rand.Rand 87 | 88 | // Run implementation 89 | runClosures map[uint32]func() *pbinternal.VmProposeRunCompletionParameters 90 | runClosureCompletions chan *pbinternal.VmProposeRunCompletionParameters 91 | } 92 | 93 | var _ Context = (*ctx)(nil) 94 | 95 | func newContext(inner context.Context, machine *statemachine.StateMachine, invocationInput *pbinternal.VmSysInputReturn_Input, conn io.ReadWriteCloser, attemptHeaders map[string][]string, dropReplayLogs bool, logHandler slog.Handler) *ctx { 96 | request := Request{ 97 | ID: invocationInput.GetInvocationId(), 98 | Headers: make(map[string]string), 99 | AttemptHeaders: attemptHeaders, 100 | Body: invocationInput.GetInput(), 101 | } 102 | for _, h := range invocationInput.GetHeaders() { 103 | request.Headers[h.GetKey()] = h.GetValue() 104 | } 105 | 106 | logHandler = logHandler.WithAttrs([]slog.Attr{slog.String("invocationID", invocationInput.GetInvocationId())}) 107 | internalLogger := slog.New(log.NewRestateContextHandler(logHandler)) 108 | inner = statemachine.WithLogger(inner, internalLogger) 109 | 110 | // will be cancelled when the http2 stream is cancelled 111 | // but NOT when we just doSuspend - just because we can't get completions doesn't mean we can't make 112 | // progress towards producing an output message 113 | ctx := &ctx{ 114 | Context: inner, 115 | conn: conn, 116 | readChan: make(chan readResult), 117 | stateMachine: machine, 118 | key: invocationInput.GetKey(), 119 | request: request, 120 | internalLogger: internalLogger, 121 | userLogContext: atomic.Pointer[rcontext.LogContext]{}, 122 | rand: rand.New([]byte(invocationInput.GetInvocationId())), 123 | userLogger: nil, 124 | isProcessing: false, 125 | runClosures: make(map[uint32]func() *pbinternal.VmProposeRunCompletionParameters), 126 | runClosureCompletions: make(chan *pbinternal.VmProposeRunCompletionParameters, 10), 127 | } 128 | ctx.userLogger = slog.New(log.NewUserContextHandler(&ctx.userLogContext, dropReplayLogs, logHandler)) 129 | 130 | // Prepare logger 131 | ctx.userLogContext.Store(&rcontext.LogContext{Source: rcontext.LogSourceUser, IsReplaying: true}) 132 | 133 | // It might be we're already in processing phase 134 | ctx.checkStateTransition() 135 | 136 | return ctx 137 | } 138 | 139 | func (restateCtx *ctx) Log() *slog.Logger { 140 | return restateCtx.userLogger 141 | } 142 | 143 | func (restateCtx *ctx) Request() *Request { 144 | return &restateCtx.request 145 | } 146 | 147 | func (restateCtx *ctx) Rand() rand.Rand { 148 | return restateCtx.rand 149 | } 150 | 151 | func (restateCtx *ctx) Key() string { 152 | return restateCtx.key 153 | } 154 | 155 | func (restateCtx *ctx) checkStateTransition() { 156 | if restateCtx.isProcessing { 157 | return 158 | } 159 | // Check if we transitioned to processing 160 | processing, err := restateCtx.stateMachine.IsProcessing(restateCtx) 161 | if err != nil { 162 | panic(err) 163 | } 164 | if processing { 165 | restateCtx.userLogContext.Store(&rcontext.LogContext{Source: rcontext.LogSourceUser, IsReplaying: false}) 166 | restateCtx.isProcessing = true 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /internal/restatecontext/execute_invocation.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/restatedev/sdk-go/internal/errors" 7 | pbinternal "github.com/restatedev/sdk-go/internal/generated" 8 | "github.com/restatedev/sdk-go/internal/log" 9 | "github.com/restatedev/sdk-go/internal/statemachine" 10 | "io" 11 | "log/slog" 12 | "runtime/debug" 13 | ) 14 | 15 | func ExecuteInvocation(ctx context.Context, logger *slog.Logger, stateMachine *statemachine.StateMachine, conn io.ReadWriteCloser, handler Handler, dropReplayLogs bool, logHandler slog.Handler, attemptHeaders map[string][]string) error { 16 | // Let's read the input entry 17 | invocationInput, err := stateMachine.SysInput(ctx) 18 | if err != nil { 19 | logger.WarnContext(ctx, "Error when reading invocation input", log.Error(err)) 20 | if err = consumeOutput(ctx, stateMachine, conn); err != nil { 21 | logger.WarnContext(ctx, "Error when consuming output", log.Error(err)) 22 | return err 23 | } 24 | return err 25 | } 26 | 27 | // Instantiate the restate context 28 | restateCtx := newContext(ctx, stateMachine, invocationInput, conn, attemptHeaders, dropReplayLogs, logHandler) 29 | 30 | // Invoke the handler 31 | invoke(restateCtx, handler) 32 | return nil 33 | } 34 | 35 | func invoke(restateCtx *ctx, handler Handler) { 36 | // Run read loop on a goroutine 37 | go func(restateCtx *ctx) { restateCtx.readInputLoop() }(restateCtx) 38 | 39 | defer func() { 40 | // recover will return a non-nil object 41 | // if there was a panic 42 | // 43 | recovered := recover() 44 | 45 | switch typ := recovered.(type) { 46 | case nil: 47 | // nothing to do, just exit 48 | break 49 | case *statemachine.SuspensionError: 50 | case statemachine.SuspensionError: 51 | restateCtx.internalLogger.LogAttrs(restateCtx, slog.LevelInfo, "Suspending invocation") 52 | break 53 | default: 54 | restateCtx.internalLogger.LogAttrs(restateCtx, slog.LevelError, "Invocation panicked, returning error to Restate", slog.Any("err", typ)) 55 | 56 | if err := restateCtx.stateMachine.NotifyError(restateCtx, fmt.Sprint(typ), string(debug.Stack())); err != nil { 57 | restateCtx.internalLogger.WarnContext(restateCtx, "Error when notifying error to state restateContext", log.Error(err)) 58 | } 59 | 60 | break 61 | } 62 | 63 | // Consume all the state restateContext output as last step 64 | if err := consumeOutput(restateCtx, restateCtx.stateMachine, restateCtx.conn); err != nil { 65 | restateCtx.internalLogger.WarnContext(restateCtx, "Error when consuming output", log.Error(err)) 66 | } 67 | }() 68 | 69 | restateCtx.internalLogger.InfoContext(restateCtx, "Handling invocation") 70 | 71 | var bytes []byte 72 | var err error 73 | bytes, err = handler.Call(restateCtx, restateCtx.request.Body) 74 | 75 | if err != nil && errors.IsTerminalError(err) { 76 | restateCtx.internalLogger.LogAttrs(restateCtx, slog.LevelError, "Invocation returned a terminal failure", log.Error(err)) 77 | 78 | failure := pbinternal.Failure{} 79 | failure.SetCode(uint32(errors.ErrorCode(err))) 80 | failure.SetMessage(err.Error()) 81 | outputParameters := pbinternal.VmSysWriteOutputParameters{} 82 | outputParameters.SetFailure(&failure) 83 | if err := restateCtx.stateMachine.SysWriteOutput(restateCtx, &outputParameters); err != nil { 84 | // This is handled by the panic catcher above 85 | panic(err) 86 | } 87 | } else if err != nil { 88 | restateCtx.internalLogger.LogAttrs(restateCtx, slog.LevelError, "Invocation returned a non-terminal failure", log.Error(err)) 89 | 90 | // This is handled by the panic catcher above 91 | panic(err) 92 | } else { 93 | restateCtx.internalLogger.InfoContext(restateCtx, "Invocation completed successfully") 94 | 95 | outputParameters := pbinternal.VmSysWriteOutputParameters{} 96 | outputParameters.SetSuccess(bytes) 97 | if err := restateCtx.stateMachine.SysWriteOutput(restateCtx, &outputParameters); err != nil { 98 | // This is handled by the panic catcher above 99 | panic(err) 100 | } 101 | } 102 | 103 | // Sys_end the state restateContext 104 | if err := restateCtx.stateMachine.SysEnd(restateCtx); err != nil { 105 | // This is handled by the panic catcher above 106 | panic(err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/restatecontext/handler.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "github.com/restatedev/sdk-go/encoding" 5 | "github.com/restatedev/sdk-go/internal" 6 | "github.com/restatedev/sdk-go/internal/options" 7 | ) 8 | 9 | // Handler is implemented by all Restate handlers 10 | type Handler interface { 11 | GetOptions() *options.HandlerOptions 12 | InputPayload() *encoding.InputPayload 13 | OutputPayload() *encoding.OutputPayload 14 | HandlerType() *internal.ServiceHandlerType 15 | Call(ctx Context, request []byte) (output []byte, err error) 16 | } 17 | -------------------------------------------------------------------------------- /internal/restatecontext/io_helpers.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/restatedev/sdk-go/internal/statemachine" 7 | "io" 8 | "sync" 9 | ) 10 | 11 | var BufPool sync.Pool 12 | 13 | func init() { 14 | BufPool = sync.Pool{New: func() interface{} { 15 | return make([]byte, 1024) 16 | }} 17 | } 18 | 19 | func takeOutputAndWriteOut(ctx context.Context, machine *statemachine.StateMachine, conn io.WriteCloser) error { 20 | buffer, err := machine.TakeOutput(ctx) 21 | if err == io.EOF { 22 | return conn.Close() 23 | } else if err != nil { 24 | return err 25 | } 26 | _, err = conn.Write(buffer) 27 | return err 28 | } 29 | 30 | func consumeOutput(ctx context.Context, machine *statemachine.StateMachine, conn io.WriteCloser) error { 31 | for { 32 | buffer, err := machine.TakeOutput(ctx) 33 | if err == io.EOF { 34 | return conn.Close() 35 | } else if err != nil { 36 | return err 37 | } 38 | 39 | _, err = conn.Write(buffer) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | } 45 | 46 | type readResult struct { 47 | nRead int 48 | buf []byte 49 | err error 50 | } 51 | 52 | func (restateCtx *ctx) readInputLoop() { 53 | for { 54 | // Acquire buf 55 | tempBuf := BufPool.Get().([]byte) 56 | read, err := restateCtx.conn.Read(tempBuf) 57 | if err != nil { 58 | BufPool.Put(tempBuf) 59 | if err == io.EOF { 60 | restateCtx.readChan <- readResult{ 61 | nRead: 0, 62 | buf: nil, 63 | err: io.EOF, 64 | } 65 | } else { 66 | restateCtx.readChan <- readResult{ 67 | nRead: 0, 68 | buf: nil, 69 | err: fmt.Errorf("error when reading the input stream %e", err), 70 | } 71 | } 72 | return 73 | } 74 | if read != 0 { 75 | restateCtx.readChan <- readResult{ 76 | nRead: read, 77 | buf: tempBuf, 78 | err: nil, 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/restatecontext/promise.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "fmt" 5 | "github.com/restatedev/sdk-go/encoding" 6 | "github.com/restatedev/sdk-go/internal/errors" 7 | pbinternal "github.com/restatedev/sdk-go/internal/generated" 8 | "github.com/restatedev/sdk-go/internal/options" 9 | "github.com/restatedev/sdk-go/internal/statemachine" 10 | ) 11 | 12 | func (restateCtx *ctx) Promise(key string, opts ...options.PromiseOption) DurablePromise { 13 | o := options.PromiseOptions{} 14 | for _, opt := range opts { 15 | opt.BeforePromise(&o) 16 | } 17 | if o.Codec == nil { 18 | o.Codec = encoding.JSONCodec 19 | } 20 | 21 | handle, err := restateCtx.stateMachine.SysPromiseGet(restateCtx, key) 22 | if err != nil { 23 | panic(err) 24 | } 25 | restateCtx.checkStateTransition() 26 | 27 | return &durablePromise{ 28 | asyncResult: newAsyncResult(restateCtx, handle), 29 | key: key, 30 | codec: o.Codec, 31 | } 32 | } 33 | 34 | type DurablePromise interface { 35 | Selectable 36 | Result(output any) (err error) 37 | Peek(output any) (ok bool, err error) 38 | Resolve(value any) error 39 | Reject(reason error) error 40 | } 41 | 42 | type durablePromise struct { 43 | asyncResult 44 | key string 45 | codec encoding.Codec 46 | } 47 | 48 | func (d *durablePromise) Result(output any) (err error) { 49 | switch result := d.pollProgressAndLoadValue().(type) { 50 | case statemachine.ValueSuccess: 51 | { 52 | if err := encoding.Unmarshal(d.codec, result.Success, output); err != nil { 53 | panic(fmt.Errorf("failed to unmarshal promise result into output: %w", err)) 54 | } 55 | return nil 56 | } 57 | case statemachine.ValueFailure: 58 | return errorFromFailure(result) 59 | default: 60 | panic(fmt.Errorf("unexpected value %s", result)) 61 | 62 | } 63 | } 64 | 65 | func (d *durablePromise) Peek(output any) (ok bool, err error) { 66 | handle, err := d.ctx.stateMachine.SysPromisePeek(d.ctx, d.key) 67 | if err != nil { 68 | panic(err) 69 | } 70 | d.ctx.checkStateTransition() 71 | 72 | ar := newAsyncResult(d.ctx, handle) 73 | switch result := ar.pollProgressAndLoadValue().(type) { 74 | case statemachine.ValueVoid: 75 | return false, nil 76 | case statemachine.ValueSuccess: 77 | { 78 | if err := encoding.Unmarshal(d.codec, result.Success, output); err != nil { 79 | panic(fmt.Errorf("failed to unmarshal promise result into output: %w", err)) 80 | } 81 | return true, nil 82 | } 83 | case statemachine.ValueFailure: 84 | return false, errorFromFailure(result) 85 | default: 86 | panic(fmt.Errorf("unexpected value %s", result)) 87 | } 88 | } 89 | 90 | func (d *durablePromise) Resolve(value any) error { 91 | bytes, err := encoding.Marshal(d.codec, value) 92 | if err != nil { 93 | panic(fmt.Errorf("failed to marshal Promise Resolve value: %w", err)) 94 | } 95 | 96 | input := pbinternal.VmSysPromiseCompleteParameters{} 97 | input.SetId(d.key) 98 | input.SetSuccess(bytes) 99 | handle, err := d.ctx.stateMachine.SysPromiseComplete(d.ctx, &input) 100 | if err != nil { 101 | panic(err) 102 | } 103 | d.ctx.checkStateTransition() 104 | 105 | ar := newAsyncResult(d.ctx, handle) 106 | switch result := ar.pollProgressAndLoadValue().(type) { 107 | case statemachine.ValueVoid: 108 | return nil 109 | case statemachine.ValueFailure: 110 | return errorFromFailure(result) 111 | default: 112 | panic(fmt.Errorf("unexpected value %s", result)) 113 | } 114 | } 115 | 116 | func (d *durablePromise) Reject(reason error) error { 117 | failure := pbinternal.Failure{} 118 | failure.SetCode(uint32(errors.ErrorCode(reason))) 119 | failure.SetMessage(reason.Error()) 120 | 121 | input := pbinternal.VmSysPromiseCompleteParameters{} 122 | input.SetId(d.key) 123 | input.SetFailure(&failure) 124 | handle, err := d.ctx.stateMachine.SysPromiseComplete(d.ctx, &input) 125 | if err != nil { 126 | panic(err) 127 | } 128 | d.ctx.checkStateTransition() 129 | 130 | ar := newAsyncResult(d.ctx, handle) 131 | switch result := ar.pollProgressAndLoadValue().(type) { 132 | case statemachine.ValueVoid: 133 | return nil 134 | case statemachine.ValueFailure: 135 | return errorFromFailure(result) 136 | default: 137 | panic(fmt.Errorf("unexpected value %s", result)) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /internal/restatecontext/run.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/restatedev/sdk-go/encoding" 7 | "github.com/restatedev/sdk-go/internal/errors" 8 | pbinternal "github.com/restatedev/sdk-go/internal/generated" 9 | "github.com/restatedev/sdk-go/internal/options" 10 | "github.com/restatedev/sdk-go/internal/statemachine" 11 | "log/slog" 12 | "runtime/debug" 13 | "time" 14 | ) 15 | 16 | func (restateCtx *ctx) Run(fn func(ctx RunContext) (any, error), output any, opts ...options.RunOption) error { 17 | return restateCtx.RunAsync(fn, opts...).Result(output) 18 | } 19 | 20 | func (restateCtx *ctx) RunAsync(fn func(ctx RunContext) (any, error), opts ...options.RunOption) RunAsyncFuture { 21 | o := options.RunOptions{} 22 | for _, opt := range opts { 23 | opt.BeforeRun(&o) 24 | } 25 | if o.Codec == nil { 26 | o.Codec = encoding.JSONCodec 27 | } 28 | 29 | params := pbinternal.VmSysRunParameters{} 30 | params.SetName(o.Name) 31 | 32 | handle, err := restateCtx.stateMachine.SysRun(restateCtx, o.Name) 33 | if err != nil { 34 | panic(err) 35 | } 36 | restateCtx.checkStateTransition() 37 | 38 | restateCtx.runClosures[handle] = func() *pbinternal.VmProposeRunCompletionParameters { 39 | now := time.Now() 40 | 41 | // Run the user closure 42 | output, err := runWrapPanic(fn)(runContext{Context: restateCtx, log: restateCtx.userLogger, request: &restateCtx.request}) 43 | 44 | // Let's prepare the proposal of the run completion 45 | proposal := pbinternal.VmProposeRunCompletionParameters{} 46 | proposal.SetHandle(handle) 47 | proposal.SetAttemptDurationMillis(uint64(time.Now().Sub(now).Milliseconds())) 48 | 49 | // Set retry policy if any of the retry policy config options are set 50 | if o.MaxRetryAttempts != nil || o.MaxRetryInterval != nil || o.MaxRetryDuration != nil || o.RetryIntervalFactor != nil || o.InitialRetryInterval != nil { 51 | retryPolicy := pbinternal.VmProposeRunCompletionParameters_RetryPolicy{} 52 | retryPolicy.SetInitialInternalMillis(50) 53 | retryPolicy.SetFactor(2) 54 | retryPolicy.SetMaxIntervalMillis(2000) 55 | 56 | if o.MaxRetryDuration != nil { 57 | retryPolicy.SetMaxDurationMillis(uint64((*o.MaxRetryDuration).Milliseconds())) 58 | } 59 | if o.MaxRetryInterval != nil { 60 | retryPolicy.SetMaxIntervalMillis(uint64((*o.MaxRetryInterval).Milliseconds())) 61 | } 62 | if o.RetryIntervalFactor != nil { 63 | retryPolicy.SetFactor(*o.RetryIntervalFactor) 64 | } 65 | if o.MaxRetryAttempts != nil { 66 | retryPolicy.SetMaxAttempts(uint32(*o.MaxRetryAttempts)) 67 | } 68 | if o.InitialRetryInterval != nil { 69 | retryPolicy.SetInitialInternalMillis(uint64((*o.InitialRetryInterval).Milliseconds())) 70 | } 71 | proposal.SetRetryPolicy(&retryPolicy) 72 | } 73 | 74 | if errors.IsTerminalError(err) { 75 | // Terminal error 76 | failure := pbinternal.Failure{} 77 | failure.SetCode(uint32(errors.ErrorCode(err))) 78 | failure.SetMessage(err.Error()) 79 | proposal.SetTerminalFailure(&failure) 80 | } else if err != nil { 81 | // Retryable error 82 | failure := pbinternal.FailureWithStacktrace{} 83 | failure.SetCode(uint32(errors.ErrorCode(err))) 84 | failure.SetMessage(err.Error()) 85 | proposal.SetRetryableFailure(&failure) 86 | } else { 87 | // Success 88 | bytes, err := encoding.Marshal(o.Codec, output) 89 | if err != nil { 90 | panic(fmt.Errorf("failed to marshal Run output: %w", err)) 91 | } 92 | 93 | proposal.SetSuccess(bytes) 94 | } 95 | 96 | return &proposal 97 | } 98 | 99 | return &runAsyncFuture{ 100 | asyncResult: newAsyncResult(restateCtx, handle), 101 | codec: o.Codec, 102 | } 103 | } 104 | 105 | func runWrapPanic(fn func(ctx RunContext) (any, error)) func(ctx RunContext) (any, error) { 106 | return func(ctx RunContext) (res any, err error) { 107 | defer func() { 108 | recovered := recover() 109 | 110 | switch typ := recovered.(type) { 111 | case nil: 112 | // nothing to do, just exit 113 | break 114 | case *statemachine.SuspensionError: 115 | case statemachine.SuspensionError: 116 | err = typ 117 | break 118 | default: 119 | err = fmt.Errorf("panic occurred while executing Run: %s\nStack: %s", fmt.Sprint(typ), string(debug.Stack())) 120 | break 121 | } 122 | }() 123 | res, err = fn(ctx) 124 | return 125 | } 126 | } 127 | 128 | type RunAsyncFuture interface { 129 | Selectable 130 | Result(output any) error 131 | } 132 | 133 | type runAsyncFuture struct { 134 | asyncResult 135 | codec encoding.Codec 136 | } 137 | 138 | func (d *runAsyncFuture) Result(output any) error { 139 | switch result := d.pollProgressAndLoadValue().(type) { 140 | case statemachine.ValueSuccess: 141 | { 142 | if err := encoding.Unmarshal(d.codec, result.Success, output); err != nil { 143 | panic(fmt.Errorf("failed to unmarshal runAsync result into output: %w", err)) 144 | } 145 | return nil 146 | } 147 | case statemachine.ValueFailure: 148 | return errorFromFailure(result) 149 | default: 150 | panic(fmt.Errorf("unexpected value %s", result)) 151 | 152 | } 153 | } 154 | 155 | // RunContext is passed to [Run] closures and provides the limited set of Restate operations that are safe to use there. 156 | type RunContext interface { 157 | context.Context 158 | 159 | // Log obtains a coreHandle on a slog.Logger which already has some useful fields (invocationID and method) 160 | // By default, this logger will not output messages if the invocation is currently replaying 161 | // The log handler can be set with `.WithLogger()` on the server object 162 | Log() *slog.Logger 163 | 164 | // Request gives extra information about the request that started this invocation 165 | Request() *Request 166 | } 167 | 168 | type runContext struct { 169 | context.Context 170 | log *slog.Logger 171 | request *Request 172 | } 173 | 174 | func (r runContext) Log() *slog.Logger { return r.log } 175 | func (r runContext) Request() *Request { return r.request } 176 | -------------------------------------------------------------------------------- /internal/restatecontext/select.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | // Selector is an iterator over a list of blocking Restate operations that are running 4 | // in the background. 5 | type Selector interface { 6 | // Remaining returns whether there are still operations that haven't been returned by Select(). 7 | // There will always be exactly the same number of results as there were operations 8 | // given to Context.Select 9 | Remaining() bool 10 | // Select blocks on the next completed operation or returns nil if there are none left 11 | Select() Selectable 12 | } 13 | 14 | type selector struct { 15 | restateCtx *ctx 16 | indexedFuts map[uint32]Selectable 17 | } 18 | 19 | func (restateCtx *ctx) Select(futs ...Selectable) Selector { 20 | indexedFuts := make(map[uint32]Selectable, len(futs)) 21 | for i := range futs { 22 | handle := futs[i].handle() 23 | indexedFuts[handle] = futs[i] 24 | } 25 | 26 | return &selector{ 27 | restateCtx: restateCtx, 28 | indexedFuts: indexedFuts, 29 | } 30 | } 31 | 32 | func (s *selector) Select() Selectable { 33 | if !s.Remaining() { 34 | return nil 35 | } 36 | 37 | remainingHandles := make([]uint32, len(s.indexedFuts)) 38 | for k := range s.indexedFuts { 39 | remainingHandles = append(remainingHandles, k) 40 | } 41 | 42 | // Do progress 43 | s.restateCtx.pollProgress(remainingHandles) 44 | 45 | // If we exit, one of them is completed, gotta figure out which one 46 | for _, handle := range remainingHandles { 47 | completed, err := s.restateCtx.stateMachine.IsCompleted(s.restateCtx, handle) 48 | if err != nil { 49 | panic(err) 50 | } 51 | if completed { 52 | fut := s.indexedFuts[handle] 53 | delete(s.indexedFuts, handle) 54 | return fut 55 | } 56 | } 57 | 58 | panic("Unexpectedly none of the remaining handles completed, this looks like a bug") 59 | } 60 | 61 | func (s *selector) Remaining() bool { 62 | return len(s.indexedFuts) != 0 63 | } 64 | -------------------------------------------------------------------------------- /internal/restatecontext/sleep.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | "fmt" 5 | "github.com/restatedev/sdk-go/internal/options" 6 | "github.com/restatedev/sdk-go/internal/statemachine" 7 | "time" 8 | ) 9 | 10 | func (restateCtx *ctx) Sleep(d time.Duration, opts ...options.SleepOption) error { 11 | return restateCtx.After(d, opts...).Done() 12 | } 13 | 14 | // After is a coreHandle on a Sleep operation which allows you to do other work concurrently 15 | // with the sleep. 16 | type AfterFuture interface { 17 | // Done blocks waiting on the remaining duration of the sleep. 18 | // It is *not* safe to call this in a goroutine - use Context.Select if you want to wait on multiple 19 | // results at once. Can return a terminal error in the case where the invocation was cancelled mid-sleep, 20 | // hence Done() should always be called, even afterFuture using Context.Select. 21 | Done() error 22 | Selectable 23 | } 24 | 25 | func (restateCtx *ctx) After(d time.Duration, opts ...options.SleepOption) AfterFuture { 26 | o := options.SleepOptions{} 27 | for _, opt := range opts { 28 | opt.BeforeSleep(&o) 29 | } 30 | 31 | handle, err := restateCtx.stateMachine.SysSleep(restateCtx, o.Name, d) 32 | if err != nil { 33 | panic(err) 34 | } 35 | restateCtx.checkStateTransition() 36 | 37 | return &afterFuture{ 38 | asyncResult: newAsyncResult(restateCtx, handle), 39 | } 40 | } 41 | 42 | type afterFuture struct { 43 | asyncResult 44 | } 45 | 46 | func (a *afterFuture) Done() error { 47 | switch result := a.pollProgressAndLoadValue().(type) { 48 | case statemachine.ValueVoid: 49 | return nil 50 | case statemachine.ValueFailure: 51 | return errorFromFailure(result) 52 | default: 53 | panic(fmt.Errorf("unexpected value %s", result)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/restatecontext/state.go: -------------------------------------------------------------------------------- 1 | package restatecontext 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/restatedev/sdk-go/encoding" 7 | "github.com/restatedev/sdk-go/internal/options" 8 | "github.com/restatedev/sdk-go/internal/statemachine" 9 | ) 10 | 11 | func (restateCtx *ctx) Set(key string, value any, opts ...options.SetOption) { 12 | o := options.SetOptions{} 13 | for _, opt := range opts { 14 | opt.BeforeSet(&o) 15 | } 16 | if o.Codec == nil { 17 | o.Codec = encoding.JSONCodec 18 | } 19 | 20 | bytes, err := encoding.Marshal(o.Codec, value) 21 | if err != nil { 22 | panic(fmt.Errorf("failed to marshal Set value: %w", err)) 23 | } 24 | 25 | err = restateCtx.stateMachine.SysStateSet(restateCtx, key, bytes) 26 | if err != nil { 27 | panic(err) 28 | } 29 | restateCtx.checkStateTransition() 30 | } 31 | 32 | func (restateCtx *ctx) Clear(key string) { 33 | err := restateCtx.stateMachine.SysStateClear(restateCtx, key) 34 | if err != nil { 35 | panic(err) 36 | } 37 | restateCtx.checkStateTransition() 38 | } 39 | 40 | // ClearAll drops all associated keys 41 | func (restateCtx *ctx) ClearAll() { 42 | err := restateCtx.stateMachine.SysStateClearAll(restateCtx) 43 | if err != nil { 44 | panic(err) 45 | } 46 | restateCtx.checkStateTransition() 47 | } 48 | 49 | func (restateCtx *ctx) Get(key string, output any, opts ...options.GetOption) (bool, error) { 50 | o := options.GetOptions{} 51 | for _, opt := range opts { 52 | opt.BeforeGet(&o) 53 | } 54 | if o.Codec == nil { 55 | o.Codec = encoding.JSONCodec 56 | } 57 | 58 | handle, err := restateCtx.stateMachine.SysStateGet(restateCtx, key) 59 | if err != nil { 60 | panic(err) 61 | } 62 | restateCtx.checkStateTransition() 63 | 64 | ar := newAsyncResult(restateCtx, handle) 65 | switch result := ar.pollProgressAndLoadValue().(type) { 66 | case statemachine.ValueVoid: 67 | return false, nil 68 | case statemachine.ValueSuccess: 69 | { 70 | if err := encoding.Unmarshal(o.Codec, result.Success, output); err != nil { 71 | panic(fmt.Errorf("failed to unmarshal Get state into output: %w", err)) 72 | } 73 | return true, err 74 | } 75 | case statemachine.ValueFailure: 76 | return true, errorFromFailure(result) 77 | default: 78 | panic(fmt.Errorf("unexpected value %s", result)) 79 | 80 | } 81 | } 82 | 83 | func (restateCtx *ctx) Keys() ([]string, error) { 84 | handle, err := restateCtx.stateMachine.SysStateGetKeys(restateCtx) 85 | if err != nil { 86 | panic(err) 87 | } 88 | restateCtx.checkStateTransition() 89 | 90 | ar := newAsyncResult(restateCtx, handle) 91 | switch result := ar.pollProgressAndLoadValue().(type) { 92 | case statemachine.ValueStateKeys: 93 | return result.Keys, nil 94 | case statemachine.ValueFailure: 95 | return nil, errorFromFailure(result) 96 | default: 97 | panic(fmt.Errorf("unexpected value %s", result)) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/statemachine/logger.go: -------------------------------------------------------------------------------- 1 | package statemachine 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | ) 7 | 8 | type loggerKey struct{} 9 | 10 | func WithLogger(ctx context.Context, logger *slog.Logger) context.Context { 11 | return context.WithValue(ctx, loggerKey{}, logger) 12 | } 13 | 14 | func getLogger(ctx context.Context) *slog.Logger { 15 | val, _ := ctx.Value(loggerKey{}).(*slog.Logger) 16 | return val 17 | } 18 | -------------------------------------------------------------------------------- /internal/statemachine/shared_core_golang_wasm_binding.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restatedev/sdk-go/6596a8707065af998c40527cefd6df6fa44178e0/internal/statemachine/shared_core_golang_wasm_binding.wasm -------------------------------------------------------------------------------- /mock.go: -------------------------------------------------------------------------------- 1 | package restate 2 | 3 | import ( 4 | "github.com/restatedev/sdk-go/internal/restatecontext" 5 | ) 6 | 7 | type mockContext struct { 8 | restatecontext.Context 9 | } 10 | 11 | func (m mockContext) inner() restatecontext.Context { 12 | return m.Context 13 | } 14 | func (m mockContext) object() {} 15 | func (m mockContext) exclusiveObject() {} 16 | func (m mockContext) workflow() {} 17 | func (m mockContext) runWorkflow() {} 18 | 19 | var _ RunContext = mockContext{} 20 | var _ Context = mockContext{} 21 | var _ ObjectSharedContext = mockContext{} 22 | var _ ObjectContext = mockContext{} 23 | var _ WorkflowSharedContext = mockContext{} 24 | var _ WorkflowContext = mockContext{} 25 | 26 | // WithMockContext allows providing a mocked state.Context to handlers 27 | func WithMockContext(ctx restatecontext.Context) mockContext { 28 | return mockContext{ctx} 29 | } 30 | -------------------------------------------------------------------------------- /mocks/mock_AfterFuture.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | /* moved to helpers.go 8 | // MockAfterFuture is an autogenerated mock type for the AfterFuture type 9 | type MockAfterFuture struct { 10 | mock.Mock 11 | } 12 | */ 13 | 14 | type MockAfterFuture_Expecter struct { 15 | mock *mock.Mock 16 | } 17 | 18 | func (_m *MockAfterFuture) EXPECT() *MockAfterFuture_Expecter { 19 | return &MockAfterFuture_Expecter{mock: &_m.Mock} 20 | } 21 | 22 | // Done provides a mock function with no fields 23 | func (_m *MockAfterFuture) Done() error { 24 | ret := _m.Called() 25 | 26 | if len(ret) == 0 { 27 | panic("no return value specified for Done") 28 | } 29 | 30 | var r0 error 31 | if rf, ok := ret.Get(0).(func() error); ok { 32 | r0 = rf() 33 | } else { 34 | r0 = ret.Error(0) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // MockAfterFuture_Done_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Done' 41 | type MockAfterFuture_Done_Call struct { 42 | *mock.Call 43 | } 44 | 45 | // Done is a helper method to define mock.On call 46 | func (_e *MockAfterFuture_Expecter) Done() *MockAfterFuture_Done_Call { 47 | return &MockAfterFuture_Done_Call{Call: _e.mock.On("Done")} 48 | } 49 | 50 | func (_c *MockAfterFuture_Done_Call) Run(run func()) *MockAfterFuture_Done_Call { 51 | _c.Call.Run(func(args mock.Arguments) { 52 | run() 53 | }) 54 | return _c 55 | } 56 | 57 | func (_c *MockAfterFuture_Done_Call) Return(_a0 error) *MockAfterFuture_Done_Call { 58 | _c.Call.Return(_a0) 59 | return _c 60 | } 61 | 62 | func (_c *MockAfterFuture_Done_Call) RunAndReturn(run func() error) *MockAfterFuture_Done_Call { 63 | _c.Call.Return(run) 64 | return _c 65 | } 66 | 67 | // handle provides a mock function with no fields 68 | func (_m *MockAfterFuture) handle() uint32 { 69 | ret := _m.Called() 70 | 71 | if len(ret) == 0 { 72 | panic("no return value specified for handle") 73 | } 74 | 75 | var r0 uint32 76 | if rf, ok := ret.Get(0).(func() uint32); ok { 77 | r0 = rf() 78 | } else { 79 | r0 = ret.Get(0).(uint32) 80 | } 81 | 82 | return r0 83 | } 84 | 85 | // MockAfterFuture_handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'handle' 86 | type MockAfterFuture_handle_Call struct { 87 | *mock.Call 88 | } 89 | 90 | // handle is a helper method to define mock.On call 91 | func (_e *MockAfterFuture_Expecter) handle() *MockAfterFuture_handle_Call { 92 | return &MockAfterFuture_handle_Call{Call: _e.mock.On("handle")} 93 | } 94 | 95 | func (_c *MockAfterFuture_handle_Call) Run(run func()) *MockAfterFuture_handle_Call { 96 | _c.Call.Run(func(args mock.Arguments) { 97 | run() 98 | }) 99 | return _c 100 | } 101 | 102 | func (_c *MockAfterFuture_handle_Call) Return(_a0 uint32) *MockAfterFuture_handle_Call { 103 | _c.Call.Return(_a0) 104 | return _c 105 | } 106 | 107 | func (_c *MockAfterFuture_handle_Call) RunAndReturn(run func() uint32) *MockAfterFuture_handle_Call { 108 | _c.Call.Return(run) 109 | return _c 110 | } 111 | 112 | // NewMockAfterFuture creates a new instance of MockAfterFuture. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 113 | // The first argument is typically a *testing.T value. 114 | func NewMockAfterFuture(t interface { 115 | mock.TestingT 116 | Cleanup(func()) 117 | }) *MockAfterFuture { 118 | mock := &MockAfterFuture{} 119 | mock.Mock.Test(t) 120 | 121 | t.Cleanup(func() { mock.AssertExpectations(t) }) 122 | 123 | return mock 124 | } 125 | -------------------------------------------------------------------------------- /mocks/mock_AttachFuture.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | /* moved to helpers.go 8 | // MockAttachFuture is an autogenerated mock type for the AttachFuture type 9 | type MockAttachFuture struct { 10 | mock.Mock 11 | } 12 | */ 13 | 14 | type MockAttachFuture_Expecter struct { 15 | mock *mock.Mock 16 | } 17 | 18 | func (_m *MockAttachFuture) EXPECT() *MockAttachFuture_Expecter { 19 | return &MockAttachFuture_Expecter{mock: &_m.Mock} 20 | } 21 | 22 | // Response provides a mock function with given fields: output 23 | func (_m *MockAttachFuture) Response(output any) error { 24 | ret := _m.Called(output) 25 | 26 | if len(ret) == 0 { 27 | panic("no return value specified for Response") 28 | } 29 | 30 | var r0 error 31 | if rf, ok := ret.Get(0).(func(any) error); ok { 32 | r0 = rf(output) 33 | } else { 34 | r0 = ret.Error(0) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // MockAttachFuture_Response_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Response' 41 | type MockAttachFuture_Response_Call struct { 42 | *mock.Call 43 | } 44 | 45 | // Response is a helper method to define mock.On call 46 | // - output any 47 | func (_e *MockAttachFuture_Expecter) Response(output interface{}) *MockAttachFuture_Response_Call { 48 | return &MockAttachFuture_Response_Call{Call: _e.mock.On("Response", output)} 49 | } 50 | 51 | func (_c *MockAttachFuture_Response_Call) Run(run func(output any)) *MockAttachFuture_Response_Call { 52 | _c.Call.Run(func(args mock.Arguments) { 53 | run(args[0].(any)) 54 | }) 55 | return _c 56 | } 57 | 58 | func (_c *MockAttachFuture_Response_Call) Return(_a0 error) *MockAttachFuture_Response_Call { 59 | _c.Call.Return(_a0) 60 | return _c 61 | } 62 | 63 | func (_c *MockAttachFuture_Response_Call) RunAndReturn(run func(any) error) *MockAttachFuture_Response_Call { 64 | _c.Call.Return(run) 65 | return _c 66 | } 67 | 68 | // handle provides a mock function with no fields 69 | func (_m *MockAttachFuture) handle() uint32 { 70 | ret := _m.Called() 71 | 72 | if len(ret) == 0 { 73 | panic("no return value specified for handle") 74 | } 75 | 76 | var r0 uint32 77 | if rf, ok := ret.Get(0).(func() uint32); ok { 78 | r0 = rf() 79 | } else { 80 | r0 = ret.Get(0).(uint32) 81 | } 82 | 83 | return r0 84 | } 85 | 86 | // MockAttachFuture_handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'handle' 87 | type MockAttachFuture_handle_Call struct { 88 | *mock.Call 89 | } 90 | 91 | // handle is a helper method to define mock.On call 92 | func (_e *MockAttachFuture_Expecter) handle() *MockAttachFuture_handle_Call { 93 | return &MockAttachFuture_handle_Call{Call: _e.mock.On("handle")} 94 | } 95 | 96 | func (_c *MockAttachFuture_handle_Call) Run(run func()) *MockAttachFuture_handle_Call { 97 | _c.Call.Run(func(args mock.Arguments) { 98 | run() 99 | }) 100 | return _c 101 | } 102 | 103 | func (_c *MockAttachFuture_handle_Call) Return(_a0 uint32) *MockAttachFuture_handle_Call { 104 | _c.Call.Return(_a0) 105 | return _c 106 | } 107 | 108 | func (_c *MockAttachFuture_handle_Call) RunAndReturn(run func() uint32) *MockAttachFuture_handle_Call { 109 | _c.Call.Return(run) 110 | return _c 111 | } 112 | 113 | // NewMockAttachFuture creates a new instance of MockAttachFuture. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 114 | // The first argument is typically a *testing.T value. 115 | func NewMockAttachFuture(t interface { 116 | mock.TestingT 117 | Cleanup(func()) 118 | }) *MockAttachFuture { 119 | mock := &MockAttachFuture{} 120 | mock.Mock.Test(t) 121 | 122 | t.Cleanup(func() { mock.AssertExpectations(t) }) 123 | 124 | return mock 125 | } 126 | -------------------------------------------------------------------------------- /mocks/mock_AwakeableFuture.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | /* moved to helpers.go 8 | // MockAwakeableFuture is an autogenerated mock type for the AwakeableFuture type 9 | type MockAwakeableFuture struct { 10 | mock.Mock 11 | } 12 | */ 13 | 14 | type MockAwakeableFuture_Expecter struct { 15 | mock *mock.Mock 16 | } 17 | 18 | func (_m *MockAwakeableFuture) EXPECT() *MockAwakeableFuture_Expecter { 19 | return &MockAwakeableFuture_Expecter{mock: &_m.Mock} 20 | } 21 | 22 | // Id provides a mock function with no fields 23 | func (_m *MockAwakeableFuture) Id() string { 24 | ret := _m.Called() 25 | 26 | if len(ret) == 0 { 27 | panic("no return value specified for Id") 28 | } 29 | 30 | var r0 string 31 | if rf, ok := ret.Get(0).(func() string); ok { 32 | r0 = rf() 33 | } else { 34 | r0 = ret.Get(0).(string) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // MockAwakeableFuture_Id_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Id' 41 | type MockAwakeableFuture_Id_Call struct { 42 | *mock.Call 43 | } 44 | 45 | // Id is a helper method to define mock.On call 46 | func (_e *MockAwakeableFuture_Expecter) Id() *MockAwakeableFuture_Id_Call { 47 | return &MockAwakeableFuture_Id_Call{Call: _e.mock.On("Id")} 48 | } 49 | 50 | func (_c *MockAwakeableFuture_Id_Call) Run(run func()) *MockAwakeableFuture_Id_Call { 51 | _c.Call.Run(func(args mock.Arguments) { 52 | run() 53 | }) 54 | return _c 55 | } 56 | 57 | func (_c *MockAwakeableFuture_Id_Call) Return(_a0 string) *MockAwakeableFuture_Id_Call { 58 | _c.Call.Return(_a0) 59 | return _c 60 | } 61 | 62 | func (_c *MockAwakeableFuture_Id_Call) RunAndReturn(run func() string) *MockAwakeableFuture_Id_Call { 63 | _c.Call.Return(run) 64 | return _c 65 | } 66 | 67 | // Result provides a mock function with given fields: output 68 | func (_m *MockAwakeableFuture) Result(output any) error { 69 | ret := _m.Called(output) 70 | 71 | if len(ret) == 0 { 72 | panic("no return value specified for Result") 73 | } 74 | 75 | var r0 error 76 | if rf, ok := ret.Get(0).(func(any) error); ok { 77 | r0 = rf(output) 78 | } else { 79 | r0 = ret.Error(0) 80 | } 81 | 82 | return r0 83 | } 84 | 85 | // MockAwakeableFuture_Result_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Result' 86 | type MockAwakeableFuture_Result_Call struct { 87 | *mock.Call 88 | } 89 | 90 | // Result is a helper method to define mock.On call 91 | // - output any 92 | func (_e *MockAwakeableFuture_Expecter) Result(output interface{}) *MockAwakeableFuture_Result_Call { 93 | return &MockAwakeableFuture_Result_Call{Call: _e.mock.On("Result", output)} 94 | } 95 | 96 | func (_c *MockAwakeableFuture_Result_Call) Run(run func(output any)) *MockAwakeableFuture_Result_Call { 97 | _c.Call.Run(func(args mock.Arguments) { 98 | run(args[0].(any)) 99 | }) 100 | return _c 101 | } 102 | 103 | func (_c *MockAwakeableFuture_Result_Call) Return(_a0 error) *MockAwakeableFuture_Result_Call { 104 | _c.Call.Return(_a0) 105 | return _c 106 | } 107 | 108 | func (_c *MockAwakeableFuture_Result_Call) RunAndReturn(run func(any) error) *MockAwakeableFuture_Result_Call { 109 | _c.Call.Return(run) 110 | return _c 111 | } 112 | 113 | // handle provides a mock function with no fields 114 | func (_m *MockAwakeableFuture) handle() uint32 { 115 | ret := _m.Called() 116 | 117 | if len(ret) == 0 { 118 | panic("no return value specified for handle") 119 | } 120 | 121 | var r0 uint32 122 | if rf, ok := ret.Get(0).(func() uint32); ok { 123 | r0 = rf() 124 | } else { 125 | r0 = ret.Get(0).(uint32) 126 | } 127 | 128 | return r0 129 | } 130 | 131 | // MockAwakeableFuture_handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'handle' 132 | type MockAwakeableFuture_handle_Call struct { 133 | *mock.Call 134 | } 135 | 136 | // handle is a helper method to define mock.On call 137 | func (_e *MockAwakeableFuture_Expecter) handle() *MockAwakeableFuture_handle_Call { 138 | return &MockAwakeableFuture_handle_Call{Call: _e.mock.On("handle")} 139 | } 140 | 141 | func (_c *MockAwakeableFuture_handle_Call) Run(run func()) *MockAwakeableFuture_handle_Call { 142 | _c.Call.Run(func(args mock.Arguments) { 143 | run() 144 | }) 145 | return _c 146 | } 147 | 148 | func (_c *MockAwakeableFuture_handle_Call) Return(_a0 uint32) *MockAwakeableFuture_handle_Call { 149 | _c.Call.Return(_a0) 150 | return _c 151 | } 152 | 153 | func (_c *MockAwakeableFuture_handle_Call) RunAndReturn(run func() uint32) *MockAwakeableFuture_handle_Call { 154 | _c.Call.Return(run) 155 | return _c 156 | } 157 | 158 | // NewMockAwakeableFuture creates a new instance of MockAwakeableFuture. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 159 | // The first argument is typically a *testing.T value. 160 | func NewMockAwakeableFuture(t interface { 161 | mock.TestingT 162 | Cleanup(func()) 163 | }) *MockAwakeableFuture { 164 | mock := &MockAwakeableFuture{} 165 | mock.Mock.Test(t) 166 | 167 | t.Cleanup(func() { mock.AssertExpectations(t) }) 168 | 169 | return mock 170 | } 171 | -------------------------------------------------------------------------------- /mocks/mock_Invocation.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // MockInvocation is an autogenerated mock type for the Invocation type 8 | type MockInvocation struct { 9 | mock.Mock 10 | } 11 | 12 | type MockInvocation_Expecter struct { 13 | mock *mock.Mock 14 | } 15 | 16 | func (_m *MockInvocation) EXPECT() *MockInvocation_Expecter { 17 | return &MockInvocation_Expecter{mock: &_m.Mock} 18 | } 19 | 20 | // GetInvocationId provides a mock function with no fields 21 | func (_m *MockInvocation) GetInvocationId() string { 22 | ret := _m.Called() 23 | 24 | if len(ret) == 0 { 25 | panic("no return value specified for GetInvocationId") 26 | } 27 | 28 | var r0 string 29 | if rf, ok := ret.Get(0).(func() string); ok { 30 | r0 = rf() 31 | } else { 32 | r0 = ret.Get(0).(string) 33 | } 34 | 35 | return r0 36 | } 37 | 38 | // MockInvocation_GetInvocationId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInvocationId' 39 | type MockInvocation_GetInvocationId_Call struct { 40 | *mock.Call 41 | } 42 | 43 | // GetInvocationId is a helper method to define mock.On call 44 | func (_e *MockInvocation_Expecter) GetInvocationId() *MockInvocation_GetInvocationId_Call { 45 | return &MockInvocation_GetInvocationId_Call{Call: _e.mock.On("GetInvocationId")} 46 | } 47 | 48 | func (_c *MockInvocation_GetInvocationId_Call) Run(run func()) *MockInvocation_GetInvocationId_Call { 49 | _c.Call.Run(func(args mock.Arguments) { 50 | run() 51 | }) 52 | return _c 53 | } 54 | 55 | func (_c *MockInvocation_GetInvocationId_Call) Return(_a0 string) *MockInvocation_GetInvocationId_Call { 56 | _c.Call.Return(_a0) 57 | return _c 58 | } 59 | 60 | func (_c *MockInvocation_GetInvocationId_Call) RunAndReturn(run func() string) *MockInvocation_GetInvocationId_Call { 61 | _c.Call.Return(run) 62 | return _c 63 | } 64 | 65 | // NewMockInvocation creates a new instance of MockInvocation. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 66 | // The first argument is typically a *testing.T value. 67 | func NewMockInvocation(t interface { 68 | mock.TestingT 69 | Cleanup(func()) 70 | }) *MockInvocation { 71 | mock := &MockInvocation{} 72 | mock.Mock.Test(t) 73 | 74 | t.Cleanup(func() { mock.AssertExpectations(t) }) 75 | 76 | return mock 77 | } 78 | -------------------------------------------------------------------------------- /mocks/mock_Rand.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | rand "github.com/restatedev/sdk-go/internal/rand" 7 | mock "github.com/stretchr/testify/mock" 8 | 9 | uuid "github.com/google/uuid" 10 | ) 11 | 12 | // MockRand is an autogenerated mock type for the Rand type 13 | type MockRand struct { 14 | mock.Mock 15 | } 16 | 17 | type MockRand_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *MockRand) EXPECT() *MockRand_Expecter { 22 | return &MockRand_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // Float64 provides a mock function with no fields 26 | func (_m *MockRand) Float64() float64 { 27 | ret := _m.Called() 28 | 29 | if len(ret) == 0 { 30 | panic("no return value specified for Float64") 31 | } 32 | 33 | var r0 float64 34 | if rf, ok := ret.Get(0).(func() float64); ok { 35 | r0 = rf() 36 | } else { 37 | r0 = ret.Get(0).(float64) 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // MockRand_Float64_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Float64' 44 | type MockRand_Float64_Call struct { 45 | *mock.Call 46 | } 47 | 48 | // Float64 is a helper method to define mock.On call 49 | func (_e *MockRand_Expecter) Float64() *MockRand_Float64_Call { 50 | return &MockRand_Float64_Call{Call: _e.mock.On("Float64")} 51 | } 52 | 53 | func (_c *MockRand_Float64_Call) Run(run func()) *MockRand_Float64_Call { 54 | _c.Call.Run(func(args mock.Arguments) { 55 | run() 56 | }) 57 | return _c 58 | } 59 | 60 | func (_c *MockRand_Float64_Call) Return(_a0 float64) *MockRand_Float64_Call { 61 | _c.Call.Return(_a0) 62 | return _c 63 | } 64 | 65 | func (_c *MockRand_Float64_Call) RunAndReturn(run func() float64) *MockRand_Float64_Call { 66 | _c.Call.Return(run) 67 | return _c 68 | } 69 | 70 | // Source provides a mock function with no fields 71 | func (_m *MockRand) Source() rand.Source { 72 | ret := _m.Called() 73 | 74 | if len(ret) == 0 { 75 | panic("no return value specified for Source") 76 | } 77 | 78 | var r0 rand.Source 79 | if rf, ok := ret.Get(0).(func() rand.Source); ok { 80 | r0 = rf() 81 | } else { 82 | if ret.Get(0) != nil { 83 | r0 = ret.Get(0).(rand.Source) 84 | } 85 | } 86 | 87 | return r0 88 | } 89 | 90 | // MockRand_Source_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Source' 91 | type MockRand_Source_Call struct { 92 | *mock.Call 93 | } 94 | 95 | // Source is a helper method to define mock.On call 96 | func (_e *MockRand_Expecter) Source() *MockRand_Source_Call { 97 | return &MockRand_Source_Call{Call: _e.mock.On("Source")} 98 | } 99 | 100 | func (_c *MockRand_Source_Call) Run(run func()) *MockRand_Source_Call { 101 | _c.Call.Run(func(args mock.Arguments) { 102 | run() 103 | }) 104 | return _c 105 | } 106 | 107 | func (_c *MockRand_Source_Call) Return(_a0 rand.Source) *MockRand_Source_Call { 108 | _c.Call.Return(_a0) 109 | return _c 110 | } 111 | 112 | func (_c *MockRand_Source_Call) RunAndReturn(run func() rand.Source) *MockRand_Source_Call { 113 | _c.Call.Return(run) 114 | return _c 115 | } 116 | 117 | // UUID provides a mock function with no fields 118 | func (_m *MockRand) UUID() uuid.UUID { 119 | ret := _m.Called() 120 | 121 | if len(ret) == 0 { 122 | panic("no return value specified for UUID") 123 | } 124 | 125 | var r0 uuid.UUID 126 | if rf, ok := ret.Get(0).(func() uuid.UUID); ok { 127 | r0 = rf() 128 | } else { 129 | if ret.Get(0) != nil { 130 | r0 = ret.Get(0).(uuid.UUID) 131 | } 132 | } 133 | 134 | return r0 135 | } 136 | 137 | // MockRand_UUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UUID' 138 | type MockRand_UUID_Call struct { 139 | *mock.Call 140 | } 141 | 142 | // UUID is a helper method to define mock.On call 143 | func (_e *MockRand_Expecter) UUID() *MockRand_UUID_Call { 144 | return &MockRand_UUID_Call{Call: _e.mock.On("UUID")} 145 | } 146 | 147 | func (_c *MockRand_UUID_Call) Run(run func()) *MockRand_UUID_Call { 148 | _c.Call.Run(func(args mock.Arguments) { 149 | run() 150 | }) 151 | return _c 152 | } 153 | 154 | func (_c *MockRand_UUID_Call) Return(_a0 uuid.UUID) *MockRand_UUID_Call { 155 | _c.Call.Return(_a0) 156 | return _c 157 | } 158 | 159 | func (_c *MockRand_UUID_Call) RunAndReturn(run func() uuid.UUID) *MockRand_UUID_Call { 160 | _c.Call.Return(run) 161 | return _c 162 | } 163 | 164 | // Uint64 provides a mock function with no fields 165 | func (_m *MockRand) Uint64() uint64 { 166 | ret := _m.Called() 167 | 168 | if len(ret) == 0 { 169 | panic("no return value specified for Uint64") 170 | } 171 | 172 | var r0 uint64 173 | if rf, ok := ret.Get(0).(func() uint64); ok { 174 | r0 = rf() 175 | } else { 176 | r0 = ret.Get(0).(uint64) 177 | } 178 | 179 | return r0 180 | } 181 | 182 | // MockRand_Uint64_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Uint64' 183 | type MockRand_Uint64_Call struct { 184 | *mock.Call 185 | } 186 | 187 | // Uint64 is a helper method to define mock.On call 188 | func (_e *MockRand_Expecter) Uint64() *MockRand_Uint64_Call { 189 | return &MockRand_Uint64_Call{Call: _e.mock.On("Uint64")} 190 | } 191 | 192 | func (_c *MockRand_Uint64_Call) Run(run func()) *MockRand_Uint64_Call { 193 | _c.Call.Run(func(args mock.Arguments) { 194 | run() 195 | }) 196 | return _c 197 | } 198 | 199 | func (_c *MockRand_Uint64_Call) Return(_a0 uint64) *MockRand_Uint64_Call { 200 | _c.Call.Return(_a0) 201 | return _c 202 | } 203 | 204 | func (_c *MockRand_Uint64_Call) RunAndReturn(run func() uint64) *MockRand_Uint64_Call { 205 | _c.Call.Return(run) 206 | return _c 207 | } 208 | 209 | // NewMockRand creates a new instance of MockRand. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 210 | // The first argument is typically a *testing.T value. 211 | func NewMockRand(t interface { 212 | mock.TestingT 213 | Cleanup(func()) 214 | }) *MockRand { 215 | mock := &MockRand{} 216 | mock.Mock.Test(t) 217 | 218 | t.Cleanup(func() { mock.AssertExpectations(t) }) 219 | 220 | return mock 221 | } 222 | -------------------------------------------------------------------------------- /mocks/mock_ResponseFuture.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | /* moved to helpers.go 8 | // MockResponseFuture is an autogenerated mock type for the ResponseFuture type 9 | type MockResponseFuture struct { 10 | mock.Mock 11 | } 12 | */ 13 | 14 | type MockResponseFuture_Expecter struct { 15 | mock *mock.Mock 16 | } 17 | 18 | func (_m *MockResponseFuture) EXPECT() *MockResponseFuture_Expecter { 19 | return &MockResponseFuture_Expecter{mock: &_m.Mock} 20 | } 21 | 22 | // GetInvocationId provides a mock function with no fields 23 | func (_m *MockResponseFuture) GetInvocationId() string { 24 | ret := _m.Called() 25 | 26 | if len(ret) == 0 { 27 | panic("no return value specified for GetInvocationId") 28 | } 29 | 30 | var r0 string 31 | if rf, ok := ret.Get(0).(func() string); ok { 32 | r0 = rf() 33 | } else { 34 | r0 = ret.Get(0).(string) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // MockResponseFuture_GetInvocationId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInvocationId' 41 | type MockResponseFuture_GetInvocationId_Call struct { 42 | *mock.Call 43 | } 44 | 45 | // GetInvocationId is a helper method to define mock.On call 46 | func (_e *MockResponseFuture_Expecter) GetInvocationId() *MockResponseFuture_GetInvocationId_Call { 47 | return &MockResponseFuture_GetInvocationId_Call{Call: _e.mock.On("GetInvocationId")} 48 | } 49 | 50 | func (_c *MockResponseFuture_GetInvocationId_Call) Run(run func()) *MockResponseFuture_GetInvocationId_Call { 51 | _c.Call.Run(func(args mock.Arguments) { 52 | run() 53 | }) 54 | return _c 55 | } 56 | 57 | func (_c *MockResponseFuture_GetInvocationId_Call) Return(_a0 string) *MockResponseFuture_GetInvocationId_Call { 58 | _c.Call.Return(_a0) 59 | return _c 60 | } 61 | 62 | func (_c *MockResponseFuture_GetInvocationId_Call) RunAndReturn(run func() string) *MockResponseFuture_GetInvocationId_Call { 63 | _c.Call.Return(run) 64 | return _c 65 | } 66 | 67 | // Response provides a mock function with given fields: output 68 | func (_m *MockResponseFuture) Response(output any) error { 69 | ret := _m.Called(output) 70 | 71 | if len(ret) == 0 { 72 | panic("no return value specified for Response") 73 | } 74 | 75 | var r0 error 76 | if rf, ok := ret.Get(0).(func(any) error); ok { 77 | r0 = rf(output) 78 | } else { 79 | r0 = ret.Error(0) 80 | } 81 | 82 | return r0 83 | } 84 | 85 | // MockResponseFuture_Response_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Response' 86 | type MockResponseFuture_Response_Call struct { 87 | *mock.Call 88 | } 89 | 90 | // Response is a helper method to define mock.On call 91 | // - output any 92 | func (_e *MockResponseFuture_Expecter) Response(output interface{}) *MockResponseFuture_Response_Call { 93 | return &MockResponseFuture_Response_Call{Call: _e.mock.On("Response", output)} 94 | } 95 | 96 | func (_c *MockResponseFuture_Response_Call) Run(run func(output any)) *MockResponseFuture_Response_Call { 97 | _c.Call.Run(func(args mock.Arguments) { 98 | run(args[0].(any)) 99 | }) 100 | return _c 101 | } 102 | 103 | func (_c *MockResponseFuture_Response_Call) Return(_a0 error) *MockResponseFuture_Response_Call { 104 | _c.Call.Return(_a0) 105 | return _c 106 | } 107 | 108 | func (_c *MockResponseFuture_Response_Call) RunAndReturn(run func(any) error) *MockResponseFuture_Response_Call { 109 | _c.Call.Return(run) 110 | return _c 111 | } 112 | 113 | // handle provides a mock function with no fields 114 | func (_m *MockResponseFuture) handle() uint32 { 115 | ret := _m.Called() 116 | 117 | if len(ret) == 0 { 118 | panic("no return value specified for handle") 119 | } 120 | 121 | var r0 uint32 122 | if rf, ok := ret.Get(0).(func() uint32); ok { 123 | r0 = rf() 124 | } else { 125 | r0 = ret.Get(0).(uint32) 126 | } 127 | 128 | return r0 129 | } 130 | 131 | // MockResponseFuture_handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'handle' 132 | type MockResponseFuture_handle_Call struct { 133 | *mock.Call 134 | } 135 | 136 | // handle is a helper method to define mock.On call 137 | func (_e *MockResponseFuture_Expecter) handle() *MockResponseFuture_handle_Call { 138 | return &MockResponseFuture_handle_Call{Call: _e.mock.On("handle")} 139 | } 140 | 141 | func (_c *MockResponseFuture_handle_Call) Run(run func()) *MockResponseFuture_handle_Call { 142 | _c.Call.Run(func(args mock.Arguments) { 143 | run() 144 | }) 145 | return _c 146 | } 147 | 148 | func (_c *MockResponseFuture_handle_Call) Return(_a0 uint32) *MockResponseFuture_handle_Call { 149 | _c.Call.Return(_a0) 150 | return _c 151 | } 152 | 153 | func (_c *MockResponseFuture_handle_Call) RunAndReturn(run func() uint32) *MockResponseFuture_handle_Call { 154 | _c.Call.Return(run) 155 | return _c 156 | } 157 | 158 | // NewMockResponseFuture creates a new instance of MockResponseFuture. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 159 | // The first argument is typically a *testing.T value. 160 | func NewMockResponseFuture(t interface { 161 | mock.TestingT 162 | Cleanup(func()) 163 | }) *MockResponseFuture { 164 | mock := &MockResponseFuture{} 165 | mock.Mock.Test(t) 166 | 167 | t.Cleanup(func() { mock.AssertExpectations(t) }) 168 | 169 | return mock 170 | } 171 | -------------------------------------------------------------------------------- /mocks/mock_RunAsyncFuture.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | /* moved to helpers.go 8 | // MockRunAsyncFuture is an autogenerated mock type for the RunAsyncFuture type 9 | type MockRunAsyncFuture struct { 10 | mock.Mock 11 | } 12 | */ 13 | 14 | type MockRunAsyncFuture_Expecter struct { 15 | mock *mock.Mock 16 | } 17 | 18 | func (_m *MockRunAsyncFuture) EXPECT() *MockRunAsyncFuture_Expecter { 19 | return &MockRunAsyncFuture_Expecter{mock: &_m.Mock} 20 | } 21 | 22 | // Result provides a mock function with given fields: output 23 | func (_m *MockRunAsyncFuture) Result(output any) error { 24 | ret := _m.Called(output) 25 | 26 | if len(ret) == 0 { 27 | panic("no return value specified for Result") 28 | } 29 | 30 | var r0 error 31 | if rf, ok := ret.Get(0).(func(any) error); ok { 32 | r0 = rf(output) 33 | } else { 34 | r0 = ret.Error(0) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // MockRunAsyncFuture_Result_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Result' 41 | type MockRunAsyncFuture_Result_Call struct { 42 | *mock.Call 43 | } 44 | 45 | // Result is a helper method to define mock.On call 46 | // - output any 47 | func (_e *MockRunAsyncFuture_Expecter) Result(output interface{}) *MockRunAsyncFuture_Result_Call { 48 | return &MockRunAsyncFuture_Result_Call{Call: _e.mock.On("Result", output)} 49 | } 50 | 51 | func (_c *MockRunAsyncFuture_Result_Call) Run(run func(output any)) *MockRunAsyncFuture_Result_Call { 52 | _c.Call.Run(func(args mock.Arguments) { 53 | run(args[0].(any)) 54 | }) 55 | return _c 56 | } 57 | 58 | func (_c *MockRunAsyncFuture_Result_Call) Return(_a0 error) *MockRunAsyncFuture_Result_Call { 59 | _c.Call.Return(_a0) 60 | return _c 61 | } 62 | 63 | func (_c *MockRunAsyncFuture_Result_Call) RunAndReturn(run func(any) error) *MockRunAsyncFuture_Result_Call { 64 | _c.Call.Return(run) 65 | return _c 66 | } 67 | 68 | // handle provides a mock function with no fields 69 | func (_m *MockRunAsyncFuture) handle() uint32 { 70 | ret := _m.Called() 71 | 72 | if len(ret) == 0 { 73 | panic("no return value specified for handle") 74 | } 75 | 76 | var r0 uint32 77 | if rf, ok := ret.Get(0).(func() uint32); ok { 78 | r0 = rf() 79 | } else { 80 | r0 = ret.Get(0).(uint32) 81 | } 82 | 83 | return r0 84 | } 85 | 86 | // MockRunAsyncFuture_handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'handle' 87 | type MockRunAsyncFuture_handle_Call struct { 88 | *mock.Call 89 | } 90 | 91 | // handle is a helper method to define mock.On call 92 | func (_e *MockRunAsyncFuture_Expecter) handle() *MockRunAsyncFuture_handle_Call { 93 | return &MockRunAsyncFuture_handle_Call{Call: _e.mock.On("handle")} 94 | } 95 | 96 | func (_c *MockRunAsyncFuture_handle_Call) Run(run func()) *MockRunAsyncFuture_handle_Call { 97 | _c.Call.Run(func(args mock.Arguments) { 98 | run() 99 | }) 100 | return _c 101 | } 102 | 103 | func (_c *MockRunAsyncFuture_handle_Call) Return(_a0 uint32) *MockRunAsyncFuture_handle_Call { 104 | _c.Call.Return(_a0) 105 | return _c 106 | } 107 | 108 | func (_c *MockRunAsyncFuture_handle_Call) RunAndReturn(run func() uint32) *MockRunAsyncFuture_handle_Call { 109 | _c.Call.Return(run) 110 | return _c 111 | } 112 | 113 | // NewMockRunAsyncFuture creates a new instance of MockRunAsyncFuture. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 114 | // The first argument is typically a *testing.T value. 115 | func NewMockRunAsyncFuture(t interface { 116 | mock.TestingT 117 | Cleanup(func()) 118 | }) *MockRunAsyncFuture { 119 | mock := &MockRunAsyncFuture{} 120 | mock.Mock.Test(t) 121 | 122 | t.Cleanup(func() { mock.AssertExpectations(t) }) 123 | 124 | return mock 125 | } 126 | -------------------------------------------------------------------------------- /mocks/mock_Selector.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.52.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | restatecontext "github.com/restatedev/sdk-go/internal/restatecontext" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockSelector is an autogenerated mock type for the Selector type 11 | type MockSelector struct { 12 | mock.Mock 13 | } 14 | 15 | type MockSelector_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *MockSelector) EXPECT() *MockSelector_Expecter { 20 | return &MockSelector_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // Remaining provides a mock function with no fields 24 | func (_m *MockSelector) Remaining() bool { 25 | ret := _m.Called() 26 | 27 | if len(ret) == 0 { 28 | panic("no return value specified for Remaining") 29 | } 30 | 31 | var r0 bool 32 | if rf, ok := ret.Get(0).(func() bool); ok { 33 | r0 = rf() 34 | } else { 35 | r0 = ret.Get(0).(bool) 36 | } 37 | 38 | return r0 39 | } 40 | 41 | // MockSelector_Remaining_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remaining' 42 | type MockSelector_Remaining_Call struct { 43 | *mock.Call 44 | } 45 | 46 | // Remaining is a helper method to define mock.On call 47 | func (_e *MockSelector_Expecter) Remaining() *MockSelector_Remaining_Call { 48 | return &MockSelector_Remaining_Call{Call: _e.mock.On("Remaining")} 49 | } 50 | 51 | func (_c *MockSelector_Remaining_Call) Run(run func()) *MockSelector_Remaining_Call { 52 | _c.Call.Run(func(args mock.Arguments) { 53 | run() 54 | }) 55 | return _c 56 | } 57 | 58 | func (_c *MockSelector_Remaining_Call) Return(_a0 bool) *MockSelector_Remaining_Call { 59 | _c.Call.Return(_a0) 60 | return _c 61 | } 62 | 63 | func (_c *MockSelector_Remaining_Call) RunAndReturn(run func() bool) *MockSelector_Remaining_Call { 64 | _c.Call.Return(run) 65 | return _c 66 | } 67 | 68 | // Select provides a mock function with no fields 69 | func (_m *MockSelector) Select() restatecontext.Selectable { 70 | ret := _m.Called() 71 | 72 | if len(ret) == 0 { 73 | panic("no return value specified for Select") 74 | } 75 | 76 | var r0 restatecontext.Selectable 77 | if rf, ok := ret.Get(0).(func() restatecontext.Selectable); ok { 78 | r0 = rf() 79 | } else { 80 | if ret.Get(0) != nil { 81 | r0 = ret.Get(0).(restatecontext.Selectable) 82 | } 83 | } 84 | 85 | return r0 86 | } 87 | 88 | // MockSelector_Select_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Select' 89 | type MockSelector_Select_Call struct { 90 | *mock.Call 91 | } 92 | 93 | // Select is a helper method to define mock.On call 94 | func (_e *MockSelector_Expecter) Select() *MockSelector_Select_Call { 95 | return &MockSelector_Select_Call{Call: _e.mock.On("Select")} 96 | } 97 | 98 | func (_c *MockSelector_Select_Call) Run(run func()) *MockSelector_Select_Call { 99 | _c.Call.Run(func(args mock.Arguments) { 100 | run() 101 | }) 102 | return _c 103 | } 104 | 105 | func (_c *MockSelector_Select_Call) Return(_a0 restatecontext.Selectable) *MockSelector_Select_Call { 106 | _c.Call.Return(_a0) 107 | return _c 108 | } 109 | 110 | func (_c *MockSelector_Select_Call) RunAndReturn(run func() restatecontext.Selectable) *MockSelector_Select_Call { 111 | _c.Call.Return(run) 112 | return _c 113 | } 114 | 115 | // NewMockSelector creates a new instance of MockSelector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 116 | // The first argument is typically a *testing.T value. 117 | func NewMockSelector(t interface { 118 | mock.TestingT 119 | Cleanup(func()) 120 | }) *MockSelector { 121 | mock := &MockSelector{} 122 | mock.Mock.Test(t) 123 | 124 | t.Cleanup(func() { mock.AssertExpectations(t) }) 125 | 126 | return mock 127 | } 128 | -------------------------------------------------------------------------------- /proto/dev/restate/sdk/go.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH 3 | * 4 | * This file is part of the Restate SDK for Go, 5 | * which is released under the MIT license. 6 | * 7 | * You can find a copy of the license in file LICENSE in the root 8 | * directory of this repository or package, or at 9 | * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE 10 | */ 11 | 12 | syntax = "proto3"; 13 | 14 | package dev.restate.sdk.go; 15 | 16 | import "google/protobuf/descriptor.proto"; 17 | 18 | option go_package = "github.com/restatedev/sdk-go/generated/dev/restate/sdk"; 19 | 20 | enum ServiceType { 21 | // SERVICE is the default and need not be provided 22 | SERVICE = 0; 23 | VIRTUAL_OBJECT = 1; 24 | WORKFLOW = 2; 25 | } 26 | 27 | enum HandlerType { 28 | // Handler type is ignored for service type SERVICE. 29 | // For VIRTUAL_OBJECT, defaults to EXCLUSIVE. 30 | // For WORKFLOW, defaults to SHARED. 31 | UNSET = 0; 32 | EXCLUSIVE = 1; 33 | SHARED = 2; 34 | // Signifies that this is the primary function for the workflow, typically named 'Run'. 35 | WORKFLOW_RUN = 3; 36 | } 37 | 38 | extend google.protobuf.MethodOptions { 39 | HandlerType handler_type = 2051; 40 | } 41 | 42 | extend google.protobuf.ServiceOptions { 43 | ServiceType service_type = 2051; 44 | } 45 | -------------------------------------------------------------------------------- /proto/internal.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 - Restate Software, Inc., Restate GmbH 3 | * 4 | * This file is part of the Restate SDK for Go, 5 | * which is released under the MIT license. 6 | * 7 | * You can find a copy of the license in file LICENSE in the root 8 | * directory of this repository or package, or at 9 | * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE 10 | */ 11 | 12 | syntax = "proto3"; 13 | 14 | option go_package = "github.com/restatedev/sdk-go/internal/generated"; 15 | 16 | message Empty {} 17 | 18 | message Header { 19 | string key = 1; 20 | string value = 2; 21 | } 22 | 23 | message Failure { 24 | uint32 code = 1; 25 | string message = 2; 26 | } 27 | 28 | message FailureWithStacktrace { 29 | uint32 code = 1; 30 | string message = 2; 31 | string stacktrace = 3; 32 | } 33 | 34 | message VmNewParameters { 35 | repeated Header headers = 1; 36 | } 37 | 38 | message VmNewReturn { 39 | oneof result { 40 | uint32 pointer = 1; 41 | Failure failure = 2; 42 | } 43 | } 44 | 45 | message VmGetResponseHeadReturn { 46 | uint32 status_code = 1; 47 | repeated Header headers = 2; 48 | } 49 | 50 | message VmNotifyError { 51 | string message = 1; 52 | string stacktrace = 2; 53 | } 54 | 55 | message VmTakeOutputReturn { 56 | oneof result { 57 | bytes bytes = 1; 58 | Empty EOF = 2; 59 | } 60 | } 61 | 62 | message VmIsReadyToExecuteReturn { 63 | oneof result { 64 | bool ready = 1; 65 | Failure failure = 2; 66 | } 67 | } 68 | 69 | message VmDoProgressParameters { 70 | repeated uint32 handles = 1; 71 | } 72 | 73 | message VmDoProgressReturn { 74 | oneof result { 75 | Empty any_completed = 1; 76 | Empty read_from_input = 2; 77 | Empty waiting_pending_run = 3; 78 | uint32 execute_run = 4; 79 | Empty cancel_signal_received = 5; 80 | Empty suspended = 6; 81 | Failure failure = 7; 82 | } 83 | } 84 | 85 | message Value { 86 | message StateKeys { 87 | repeated string keys = 1; 88 | } 89 | 90 | oneof value { 91 | Empty void = 1; 92 | bytes success = 2; 93 | Failure failure = 3; 94 | StateKeys state_keys = 4; 95 | string invocation_id = 5; 96 | } 97 | } 98 | 99 | message VmTakeNotificationReturn { 100 | oneof result { 101 | Empty not_ready = 1; 102 | Value value = 2; 103 | Empty suspended = 3; 104 | Failure failure = 4; 105 | } 106 | } 107 | 108 | message VmSysInputReturn { 109 | message Input { 110 | string invocation_id = 1; 111 | string key = 2; 112 | repeated Header headers = 3; 113 | bytes input = 4; 114 | } 115 | 116 | oneof result { 117 | Input ok = 1; 118 | Failure failure = 2; 119 | } 120 | } 121 | 122 | message VmSysStateGetParameters { 123 | string key = 1; 124 | } 125 | 126 | message VmSysStateSetParameters { 127 | string key = 1; 128 | bytes value = 2; 129 | } 130 | 131 | message VmSysStateClearParameters { 132 | string key = 1; 133 | } 134 | 135 | message VmSysSleepParameters { 136 | uint64 wake_up_time_since_unix_epoch_millis = 1; 137 | uint64 now_since_unix_epoch_millis = 2; 138 | string name = 3; 139 | } 140 | 141 | message VmSysAwakeableReturn { 142 | message Awakeable { 143 | string id = 1; 144 | uint32 handle = 2; 145 | } 146 | 147 | oneof result { 148 | Awakeable ok = 1; 149 | Failure failure = 2; 150 | } 151 | } 152 | 153 | message VmSysCompleteAwakeableParameters { 154 | string id = 1; 155 | 156 | oneof result { 157 | bytes success = 2; 158 | Failure failure = 3; 159 | } 160 | } 161 | 162 | message VmSysCallParameters { 163 | string service = 1; 164 | string handler = 2; 165 | optional string key = 3; 166 | optional string idempotency_key = 4; 167 | repeated Header headers = 5; 168 | 169 | bytes input = 6; 170 | } 171 | 172 | message VmSysCallReturn { 173 | message CallHandles { 174 | uint32 invocation_id_handle = 1; 175 | uint32 result_handle = 2; 176 | } 177 | 178 | oneof result { 179 | CallHandles ok = 1; 180 | Failure failure = 2; 181 | } 182 | } 183 | 184 | message VmSysSendParameters { 185 | string service = 1; 186 | string handler = 2; 187 | optional string key = 3; 188 | optional string idempotency_key = 4; 189 | repeated Header headers = 5; 190 | 191 | bytes input = 6; 192 | 193 | optional uint64 execution_time_since_unix_epoch_millis = 7; 194 | } 195 | 196 | message VmSysCancelInvocation { 197 | string invocation_id = 1; 198 | } 199 | 200 | message VmSysAttachInvocation { 201 | string invocation_id = 1; 202 | } 203 | 204 | message VmSysPromiseGetParameters { 205 | string key = 1; 206 | } 207 | 208 | message VmSysPromisePeekParameters { 209 | string key = 1; 210 | } 211 | 212 | message VmSysPromiseCompleteParameters { 213 | string id = 1; 214 | 215 | oneof result { 216 | bytes success = 2; 217 | Failure failure = 3; 218 | } 219 | } 220 | 221 | message VmSysRunParameters { 222 | string name = 1; 223 | } 224 | 225 | message VmProposeRunCompletionParameters { 226 | message RetryPolicy { 227 | uint64 initial_internal_millis = 1; 228 | float factor = 2; 229 | optional uint64 max_interval_millis = 3; 230 | optional uint32 max_attempts = 4; 231 | optional uint64 max_duration_millis = 5; 232 | } 233 | 234 | uint32 handle = 1; 235 | 236 | oneof result { 237 | bytes success = 2; 238 | Failure terminal_failure = 3; 239 | FailureWithStacktrace retryable_failure = 4; 240 | } 241 | 242 | RetryPolicy retry_policy = 5; 243 | uint64 attempt_duration_millis = 6; 244 | } 245 | 246 | message VmSysWriteOutputParameters { 247 | oneof result { 248 | bytes success = 1; 249 | Failure failure = 2; 250 | } 251 | } 252 | 253 | message SimpleSysAsyncResultReturn { 254 | oneof result { 255 | uint32 handle = 1; 256 | Failure failure = 2; 257 | } 258 | } 259 | 260 | message GenericEmptyReturn { 261 | oneof result { 262 | Empty ok = 1; 263 | Failure failure = 2; 264 | } 265 | } -------------------------------------------------------------------------------- /protoc-gen-go-restate/README.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-go-grpc 2 | 3 | This tool generates Go language bindings of `service`s in protobuf definition 4 | files for Restate. 5 | 6 | An example of their use can be found in [examples/codegen](../examples/codegen) 7 | 8 | ## Usage 9 | Via protoc: 10 | ```shell 11 | go install github.com/restatedev/sdk-go/protoc-gen-go-restate@latest 12 | protoc --go_out=. --go_opt=paths=source_relative \ 13 | --go-restate_out=. --go-restate_opt=paths=source_relative service.proto 14 | ``` 15 | 16 | Via [buf](https://buf.build/): 17 | ```yaml 18 | # buf.gen.yaml 19 | plugins: 20 | - remote: buf.build/protocolbuffers/go:v1.34.2 21 | out: . 22 | opt: paths=source_relative 23 | - local: protoc-gen-go-restate 24 | out: . 25 | opt: paths=source_relative 26 | ``` 27 | 28 | # Providing options 29 | This protoc plugin supports the service and method extensions defined in 30 | [proto/dev/restate/sdk/go.proto](../proto/dev/restate/sdk/go.proto). 31 | You will need to use these extensions to define virtual objects in proto. 32 | 33 | You can import the extensions with the statement `import "dev/restate/sdk/go.proto";`. Protoc will expect an equivalent directory 34 | structure containing the go.proto file either locally, or under any of the 35 | paths provided with `--proto_path`. It may be easier to use 36 | [buf](https://buf.build/docs/bsr/module/dependency-management) to import: 37 | ```yaml 38 | # buf.yaml 39 | version: v2 40 | deps: 41 | - buf.build/restatedev/sdk-go 42 | ``` 43 | 44 | # Upgrading from pre-v0.14 45 | This generator used to create Restate services and methods using the Go names (eg `Greeter/SayHello`) instead of the fully qualified protobuf names (eg `helloworld.Greeter/SayHello`). 46 | This was changed to make this package more compatible with gRPC. 47 | To maintain the old behaviour, pass `--go-restate_opt=use_go_service_names=true` to `protoc`. With buf: 48 | ```yaml 49 | ... 50 | - local: protoc-gen-go-restate 51 | out: . 52 | opt: 53 | - paths=source_relative 54 | - use_go_service_names=true 55 | ``` 56 | -------------------------------------------------------------------------------- /protoc-gen-go-restate/main.go: -------------------------------------------------------------------------------- 1 | // protoc-gen-go-restate is a plugin for the Google protocol buffer compiler to 2 | // generate Restate servers and clients. Install it by building this program and 3 | // making it accessible within your PATH with the name: 4 | // 5 | // protoc-gen-go-restate 6 | // 7 | // The 'go-restate' suffix becomes part of the argument for the protocol compiler, 8 | // such that it can be invoked as: 9 | // 10 | // protoc --go-restate_out=. path/to/file.proto 11 | // 12 | // This generates Restate service definitions for the protocol buffer defined by 13 | // file.proto. With that input, the output will be written to: 14 | // 15 | // path/to/file_restate.pb.go 16 | // 17 | // Lots of code copied from protoc-gen-go-grpc: 18 | // https://github.com/grpc/grpc-go/tree/master/cmd/protoc-gen-go-grpc 19 | // ! License Apache-2.0 20 | package main 21 | 22 | import ( 23 | "flag" 24 | "fmt" 25 | 26 | "google.golang.org/protobuf/compiler/protogen" 27 | "google.golang.org/protobuf/types/pluginpb" 28 | ) 29 | 30 | var version = "0.1" 31 | 32 | var requireUnimplemented *bool 33 | var useGoServiceNames *bool 34 | 35 | func main() { 36 | showVersion := flag.Bool("version", false, "print the version and exit") 37 | flag.Parse() 38 | if *showVersion { 39 | fmt.Printf("protoc-gen-go-grpc %v\n", version) 40 | return 41 | } 42 | 43 | var flags flag.FlagSet 44 | requireUnimplemented = flags.Bool("require_unimplemented_servers", false, "set to true to disallow servers that have unimplemented fields") 45 | useGoServiceNames = flags.Bool("use_go_service_names", false, "set to true to use Go names for service and method names instead of the Protobuf fully qualified names. This used to be the default behaviour") 46 | 47 | protogen.Options{ 48 | ParamFunc: flags.Set, 49 | }.Run(func(gen *protogen.Plugin) error { 50 | gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) 51 | for _, f := range gen.Files { 52 | if !f.Generate { 53 | continue 54 | } 55 | generateFile(gen, f) 56 | } 57 | return nil 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /rcontext/rcontext.go: -------------------------------------------------------------------------------- 1 | package rcontext 2 | 3 | import "context" 4 | 5 | // LogSource is an enum to describe the source of a logline 6 | type LogSource int 7 | 8 | const ( 9 | // LogSourceRestate logs come from the sdk-go library 10 | LogSourceRestate = iota 11 | // LogSourceUser logs come from user handlers that use the Context.Log() logger. 12 | LogSourceUser 13 | ) 14 | 15 | // LogContext contains information stored in the context that is passed to loggers 16 | type LogContext struct { 17 | // The source of the logline 18 | Source LogSource 19 | // Whether the user code is currently replaying 20 | IsReplaying bool 21 | } 22 | 23 | type logContextKey struct{} 24 | 25 | // WithLogContext stores a [LogContext] in the provided [context.Context], returning a new context 26 | func WithLogContext(parent context.Context, logContext *LogContext) context.Context { 27 | return context.WithValue(parent, logContextKey{}, logContext) 28 | } 29 | 30 | // LogContextFrom retrieves the [LogContext] stored in this [context.Context], or otherwise returns nil 31 | func LogContextFrom(ctx context.Context) *LogContext { 32 | if val, ok := ctx.Value(logContextKey{}).(*LogContext); ok { 33 | return val 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /reflect_test.go: -------------------------------------------------------------------------------- 1 | package restate_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | restate "github.com/restatedev/sdk-go" 9 | "github.com/restatedev/sdk-go/internal" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type reflectTestParams struct { 14 | rcvr interface{} 15 | opts []restate.ServiceDefinitionOption 16 | serviceName string 17 | serviceType internal.ServiceType 18 | expectedMethods expectedMethods 19 | shouldPanic bool 20 | panicContains string 21 | } 22 | 23 | type expectedMethods = map[string]*internal.ServiceHandlerType 24 | 25 | var exclusive = internal.ServiceHandlerType_EXCLUSIVE 26 | var workflowRun = internal.ServiceHandlerType_WORKFLOW 27 | var shared = internal.ServiceHandlerType_SHARED 28 | 29 | var tests []reflectTestParams = []reflectTestParams{ 30 | {rcvr: validObject{}, serviceName: "validObject", expectedMethods: expectedMethods{ 31 | "Greet": &exclusive, 32 | "GreetShared": &shared, 33 | "NoInput": &exclusive, 34 | "NoError": &exclusive, 35 | "NoOutput": &exclusive, 36 | "NoOutputNoError": &exclusive, 37 | "NoInputNoError": &exclusive, 38 | "NoInputNoOutput": &exclusive, 39 | "NoInputNoOutputNoError": &exclusive, 40 | }}, 41 | {rcvr: validService{}, serviceName: "validService", expectedMethods: expectedMethods{ 42 | "Greet": nil, 43 | }}, 44 | {rcvr: namedService{}, serviceName: "foobar", expectedMethods: expectedMethods{ 45 | "Greet": nil, 46 | }}, 47 | {rcvr: validWorkflow{}, serviceName: "validWorkflow", expectedMethods: expectedMethods{ 48 | "Run": &workflowRun, 49 | "Status": &shared, 50 | }}, 51 | {rcvr: mixed{}, shouldPanic: true, panicContains: "found a mix of object context arguments and other context arguments"}, 52 | {rcvr: empty{}, shouldPanic: true, panicContains: "no valid handlers could be found"}, 53 | {rcvr: notExported{}, shouldPanic: true, panicContains: "no valid handlers could be found"}, 54 | {rcvr: firstParamNotContext{}, shouldPanic: true, panicContains: "no valid handlers could be found"}, 55 | {rcvr: secondReturnNotError{}, shouldPanic: true, panicContains: "the second must be an error"}, 56 | {rcvr: tooManyReturns{}, shouldPanic: true, panicContains: "at most 2 parameters are allowed"}, 57 | } 58 | 59 | func TestReflect(t *testing.T) { 60 | for _, test := range tests { 61 | t.Run(test.serviceName, func(t *testing.T) { 62 | defer func() { 63 | if err := recover(); err != nil { 64 | if test.shouldPanic { 65 | require.Contains(t, fmt.Sprintf("%v", err), test.panicContains) 66 | return 67 | } else { 68 | panic(err) 69 | } 70 | } else if test.shouldPanic { 71 | t.Fatal("test should have panicked") 72 | } 73 | }() 74 | def := restate.Reflect(test.rcvr, test.opts...) 75 | foundMethods := make(map[string]*internal.ServiceHandlerType, len(def.Handlers())) 76 | for k, foundHandler := range def.Handlers() { 77 | t.Run(k, func(t *testing.T) { 78 | foundMethods[k] = foundHandler.HandlerType() 79 | // check for panics 80 | _ = foundHandler.InputPayload() 81 | _ = foundHandler.OutputPayload() 82 | _, err := foundHandler.Call(nil, []byte(`""`)) 83 | require.NoError(t, err) 84 | }) 85 | } 86 | require.Equal(t, test.expectedMethods, foundMethods) 87 | require.Equal(t, test.serviceName, def.Name()) 88 | }) 89 | } 90 | } 91 | 92 | type validObject struct{} 93 | 94 | func (validObject) Greet(ctx restate.ObjectContext, _ string) (string, error) { 95 | return "", nil 96 | } 97 | 98 | func (validObject) GreetShared(ctx restate.ObjectSharedContext, _ string) (string, error) { 99 | return "", nil 100 | } 101 | 102 | func (validObject) NoInput(ctx restate.ObjectContext) (string, error) { 103 | return "", nil 104 | } 105 | 106 | func (validObject) NoError(ctx restate.ObjectContext, _ string) string { 107 | return "" 108 | } 109 | 110 | func (validObject) NoOutput(ctx restate.ObjectContext, _ string) error { 111 | return nil 112 | } 113 | 114 | func (validObject) NoOutputNoError(ctx restate.ObjectContext, _ string) { 115 | } 116 | 117 | func (validObject) NoInputNoError(ctx restate.ObjectContext) string { 118 | return "" 119 | } 120 | 121 | func (validObject) NoInputNoOutput(ctx restate.ObjectContext) error { 122 | return nil 123 | } 124 | 125 | func (validObject) NoInputNoOutputNoError(ctx restate.ObjectContext) { 126 | } 127 | 128 | func (validObject) SkipNoArguments() (string, error) { 129 | return "", nil 130 | } 131 | 132 | func (validObject) SkipInvalidCtx(ctx context.Context, _ string) (string, error) { 133 | return "", nil 134 | } 135 | 136 | func (validObject) skipUnexported(_ string) (string, error) { 137 | return "", nil 138 | } 139 | 140 | type validService struct{} 141 | 142 | func (validService) Greet(ctx restate.Context, _ string) (string, error) { 143 | return "", nil 144 | } 145 | 146 | type namedService struct{} 147 | 148 | func (namedService) ServiceName() string { 149 | return "foobar" 150 | } 151 | 152 | type validWorkflow struct{} 153 | 154 | func (validWorkflow) Run(ctx restate.WorkflowContext) error { 155 | return nil 156 | } 157 | 158 | func (validWorkflow) Status(ctx restate.WorkflowSharedContext, _ string) (string, error) { 159 | return "", nil 160 | } 161 | 162 | func (namedService) Greet(ctx restate.Context, _ string) (string, error) { 163 | return "", nil 164 | } 165 | 166 | type mixed struct{} 167 | 168 | func (mixed) Greet(ctx restate.Context, _ string) (string, error) { 169 | return "", nil 170 | } 171 | func (mixed) GreetShared(ctx restate.ObjectSharedContext, _ string) (string, error) { 172 | return "", nil 173 | } 174 | 175 | type empty struct{} 176 | 177 | type notExported struct{} 178 | 179 | func (notExported) notExported(ctx restate.Context) {} 180 | 181 | type firstParamNotContext struct{} 182 | 183 | func (firstParamNotContext) FirstParamNotContext(foo string) {} 184 | 185 | type secondReturnNotError struct{} 186 | 187 | func (secondReturnNotError) SecondReturnNotError(ctx restate.Context) (string, string) { 188 | return "", "" 189 | } 190 | 191 | type tooManyReturns struct{} 192 | 193 | func (tooManyReturns) TooManyReturns(ctx restate.Context) (string, string, string) { 194 | return "", "", "" 195 | } 196 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package restate 2 | 3 | import ( 4 | "github.com/restatedev/sdk-go/encoding" 5 | "github.com/restatedev/sdk-go/internal" 6 | "github.com/restatedev/sdk-go/internal/options" 7 | "github.com/restatedev/sdk-go/internal/restatecontext" 8 | ) 9 | 10 | // ServiceDefinition is the set of methods implemented by both services and virtual objects 11 | type ServiceDefinition interface { 12 | Name() string 13 | Type() internal.ServiceType 14 | // Set of handlers associated with this service definition 15 | Handlers() map[string]restatecontext.Handler 16 | GetOptions() *options.ServiceDefinitionOptions 17 | } 18 | 19 | // serviceDefinition stores a list of handlers under a named service 20 | type serviceDefinition struct { 21 | name string 22 | handlers map[string]restatecontext.Handler 23 | options options.ServiceDefinitionOptions 24 | typ internal.ServiceType 25 | } 26 | 27 | var _ ServiceDefinition = &serviceDefinition{} 28 | 29 | // Name returns the name of the service described in this definition 30 | func (r *serviceDefinition) Name() string { 31 | return r.name 32 | } 33 | 34 | // Handlers returns the list of handlers in this service definition 35 | func (r *serviceDefinition) Handlers() map[string]restatecontext.Handler { 36 | return r.handlers 37 | } 38 | 39 | // Options returns the configured options 40 | func (r *serviceDefinition) GetOptions() *options.ServiceDefinitionOptions { 41 | return &r.options 42 | } 43 | 44 | // Type returns the type of this service definition (Service or Virtual Object) 45 | func (r *serviceDefinition) Type() internal.ServiceType { 46 | return r.typ 47 | } 48 | 49 | type service struct { 50 | serviceDefinition 51 | } 52 | 53 | // NewService creates a new named Service 54 | func NewService(name string, opts ...options.ServiceDefinitionOption) *service { 55 | o := options.ServiceDefinitionOptions{} 56 | for _, opt := range opts { 57 | opt.BeforeServiceDefinition(&o) 58 | } 59 | if o.DefaultCodec == nil { 60 | o.DefaultCodec = encoding.JSONCodec 61 | } 62 | return &service{ 63 | serviceDefinition: serviceDefinition{ 64 | name: name, 65 | handlers: make(map[string]restatecontext.Handler), 66 | options: o, 67 | typ: internal.ServiceType_SERVICE, 68 | }, 69 | } 70 | } 71 | 72 | // Handler registers a new Service handler by name 73 | func (r *service) Handler(name string, handler restatecontext.Handler) *service { 74 | if handler.GetOptions().Codec == nil { 75 | handler.GetOptions().Codec = r.options.DefaultCodec 76 | } 77 | r.handlers[name] = handler 78 | return r 79 | } 80 | 81 | type object struct { 82 | serviceDefinition 83 | } 84 | 85 | // NewObject creates a new named Virtual Object 86 | func NewObject(name string, opts ...options.ServiceDefinitionOption) *object { 87 | o := options.ServiceDefinitionOptions{} 88 | for _, opt := range opts { 89 | opt.BeforeServiceDefinition(&o) 90 | } 91 | if o.DefaultCodec == nil { 92 | o.DefaultCodec = encoding.JSONCodec 93 | } 94 | return &object{ 95 | serviceDefinition: serviceDefinition{ 96 | name: name, 97 | handlers: make(map[string]restatecontext.Handler), 98 | options: o, 99 | typ: internal.ServiceType_VIRTUAL_OBJECT, 100 | }, 101 | } 102 | } 103 | 104 | // Handler registers a new Virtual Object handler by name 105 | func (r *object) Handler(name string, handler restatecontext.Handler) *object { 106 | if handler.GetOptions().Codec == nil { 107 | handler.GetOptions().Codec = r.options.DefaultCodec 108 | } 109 | r.handlers[name] = handler 110 | return r 111 | } 112 | 113 | type workflow struct { 114 | serviceDefinition 115 | } 116 | 117 | // NewWorkflow creates a new named Workflow 118 | func NewWorkflow(name string, opts ...options.ServiceDefinitionOption) *workflow { 119 | o := options.ServiceDefinitionOptions{} 120 | for _, opt := range opts { 121 | opt.BeforeServiceDefinition(&o) 122 | } 123 | if o.DefaultCodec == nil { 124 | o.DefaultCodec = encoding.JSONCodec 125 | } 126 | return &workflow{ 127 | serviceDefinition: serviceDefinition{ 128 | name: name, 129 | handlers: make(map[string]restatecontext.Handler), 130 | options: o, 131 | typ: internal.ServiceType_WORKFLOW, 132 | }, 133 | } 134 | } 135 | 136 | // Handler registers a new Workflow handler by name 137 | func (r *workflow) Handler(name string, handler restatecontext.Handler) *workflow { 138 | if handler.GetOptions().Codec == nil { 139 | handler.GetOptions().Codec = r.options.DefaultCodec 140 | } 141 | r.handlers[name] = handler 142 | return r 143 | } 144 | -------------------------------------------------------------------------------- /server/conn.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "sync" 8 | ) 9 | 10 | type connection struct { 11 | r io.ReadCloser 12 | flusher http.Flusher 13 | w http.ResponseWriter 14 | cancel func() 15 | 16 | wLock sync.Mutex 17 | rLock sync.Mutex 18 | } 19 | 20 | func newConnection(w http.ResponseWriter, r *http.Request) *connection { 21 | ctx, cancel := context.WithCancel(r.Context()) 22 | flusher, _ := w.(http.Flusher) 23 | 24 | c := &connection{r: r.Body, flusher: flusher, w: w, cancel: cancel} 25 | 26 | // Update the request context with the connection context. 27 | // If the connection is closed by the server, it will also notify everything that waits on the request context. 28 | *r = *r.WithContext(ctx) 29 | 30 | return c 31 | } 32 | 33 | func (c *connection) Write(data []byte) (int, error) { 34 | c.wLock.Lock() 35 | defer c.wLock.Unlock() 36 | n, err := c.w.Write(data) 37 | if c.flusher != nil { 38 | c.flusher.Flush() 39 | } 40 | return n, err 41 | } 42 | 43 | func (c *connection) Read(data []byte) (int, error) { 44 | c.rLock.Lock() 45 | defer c.rLock.Unlock() 46 | n, err := c.r.Read(data) 47 | if err == http.ErrBodyReadAfterClose { 48 | // make our state machine a bit more generic by avoiding this http error which to us means the same as EOF 49 | return n, io.EOF 50 | } 51 | return n, err 52 | } 53 | 54 | func (c *connection) Close() error { 55 | c.cancel() 56 | c.r.Close() // unblock readers 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /server/lambda.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/restatedev/sdk-go/internal/identity" 13 | ) 14 | 15 | type LambdaRequest struct { 16 | Path string `json:"path"` 17 | RawPath string `json:"rawPath"` 18 | Body string `json:"body"` 19 | IsBase64Encoded bool `json:"isBase64Encoded"` 20 | Headers map[string]string `json:"headers"` 21 | } 22 | 23 | type LambdaResponse struct { 24 | StatusCode int `json:"statusCode"` 25 | Headers map[string]string `json:"headers"` 26 | Body string `json:"body"` 27 | IsBase64Encoded bool `json:"isBase64Encoded"` 28 | } 29 | 30 | type LambdaHandlerFunc func(ctx context.Context, event LambdaRequest) (LambdaResponse, error) 31 | 32 | type lambdaResponseWriter struct { 33 | headers http.Header 34 | body bytes.Buffer 35 | status int 36 | } 37 | 38 | func (r *lambdaResponseWriter) Header() http.Header { 39 | return r.headers 40 | } 41 | 42 | func (r *lambdaResponseWriter) Write(body []byte) (int, error) { 43 | if r.status == -1 { 44 | r.status = http.StatusOK 45 | } 46 | 47 | // if the content type header is not set when we write the body we try to 48 | // detect one and set it by default. If the content type cannot be detected 49 | // it is automatically set to "application/octet-stream" by the 50 | // DetectContentType method 51 | if r.Header().Get("Content-Type") == "" { 52 | r.Header().Add("Content-Type", http.DetectContentType(body)) 53 | } 54 | 55 | return (&r.body).Write(body) 56 | } 57 | 58 | func (r *lambdaResponseWriter) WriteHeader(statusCode int) { 59 | r.status = statusCode 60 | } 61 | 62 | func (r *lambdaResponseWriter) Flush() {} 63 | 64 | func (r *lambdaResponseWriter) LambdaResponse() LambdaResponse { 65 | headers := make(map[string]string, len(r.headers)) 66 | for k, v := range r.headers { 67 | if len(v) == 0 { 68 | continue 69 | } 70 | headers[k] = v[0] 71 | } 72 | 73 | return LambdaResponse{ 74 | Headers: headers, 75 | StatusCode: r.status, 76 | IsBase64Encoded: true, 77 | Body: base64.StdEncoding.EncodeToString(r.body.Bytes()), 78 | } 79 | } 80 | 81 | // LambdaHandler obtains a Lambda handler function representing the bound services 82 | // .Bidirectional(false) will be set on your behalf as Lambda only supports request-response communication 83 | func (r *Restate) LambdaHandler() (LambdaHandlerFunc, error) { 84 | r.Bidirectional(false) 85 | 86 | if r.keyIDs == nil { 87 | r.systemLog.Warn("Accepting requests without validating request signatures; Invoke must be restricted") 88 | } else { 89 | ks, err := identity.ParseKeySetV1(r.keyIDs) 90 | if err != nil { 91 | return nil, fmt.Errorf("invalid request identity keys: %w", err) 92 | } 93 | r.keySet = ks 94 | r.systemLog.Info("Validating requests using signing keys", "keys", r.keyIDs) 95 | } 96 | 97 | return LambdaHandlerFunc(r.lambdaHandler), nil 98 | } 99 | 100 | func (r *Restate) lambdaHandler(ctx context.Context, event LambdaRequest) (LambdaResponse, error) { 101 | var path string 102 | if event.Path != "" { 103 | path = event.Path 104 | } else if event.RawPath != "" { 105 | path = event.RawPath 106 | } 107 | 108 | var body io.Reader 109 | if event.Body != "" { 110 | if event.IsBase64Encoded { 111 | body = base64.NewDecoder(base64.StdEncoding, strings.NewReader(event.Body)) 112 | } else { 113 | body = strings.NewReader(event.Body) 114 | } 115 | } 116 | 117 | // method is not read so just set POST as a default 118 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, body) 119 | if err != nil { 120 | return LambdaResponse{StatusCode: http.StatusBadGateway}, err 121 | } 122 | req.RequestURI = path 123 | for k, v := range event.Headers { 124 | req.Header.Add(k, v) 125 | } 126 | 127 | rw := lambdaResponseWriter{headers: make(http.Header, 2), status: -1} 128 | 129 | r.handler(&rw, req) 130 | 131 | return rw.LambdaResponse(), nil 132 | } 133 | -------------------------------------------------------------------------------- /shared-core/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | rustflags = [ 4 | # Make stack size of 1 WASM page, 5 | # to avoid allocating like crazy on module instantiation 6 | "-C", "link-arg=-zstack-size=65536" 7 | ] -------------------------------------------------------------------------------- /shared-core/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /shared-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared-core-golang-wasm-binding" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | restate-sdk-shared-core = { version = "0.3.0" } 11 | bytes = "1.10" 12 | tracing = "0.1.40" 13 | tracing-subscriber = { version = "0.3.18", default-features = false, features = ["fmt", "std"] } 14 | prost = "0.13.5" 15 | 16 | [build-dependencies] 17 | prost-build = "0.13.5" 18 | 19 | # Below settings dramatically reduce wasm output size 20 | # See https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-sizewasm-opt -Oz -o 21 | # See https://doc.rust-lang.org/cargo/reference/profiles.html#codegen-units 22 | [profile.release] 23 | opt-level = 3 24 | lto = true 25 | -------------------------------------------------------------------------------- /shared-core/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | 3 | fn main() -> Result<()> { 4 | prost_build::Config::new() 5 | .bytes(["."]) 6 | .compile_protos(&["../proto/internal.proto"], &["../proto"])?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /test-services/.env: -------------------------------------------------------------------------------- 1 | RESTATE_LOGGING=trace 2 | CORE_TRACE_LOGGING_ENABLED=true -------------------------------------------------------------------------------- /test-services/README.md: -------------------------------------------------------------------------------- 1 | # Test services to run the sdk-test-suite 2 | 3 | ## To run locally 4 | 5 | * Grab the release of sdk-test-suite: https://github.com/restatedev/sdk-test-suite/releases 6 | 7 | * Prepare the docker image: 8 | ```shell 9 | KO_DOCKER_REPO=restatedev ko build -B -L github.com/restatedev/sdk-go/test-services 10 | ``` 11 | 12 | * Run the tests (requires JVM >= 17): 13 | ```shell 14 | java -jar restate-sdk-test-suite.jar run --exclusions-file exclusions.yaml restatedev/test-services 15 | ``` 16 | 17 | ## To debug a single test: 18 | 19 | * Run the golang service using your IDE 20 | * Run the test runner in debug mode specifying test suite and test: 21 | ```shell 22 | java -jar restate-sdk-test-suite.jar debug --image-pull-policy=CACHED --test-config=lazyState --test-name=dev.restate.sdktesting.tests.State default-service=9080 23 | ``` 24 | 25 | For more info: https://github.com/restatedev/sdk-test-suite -------------------------------------------------------------------------------- /test-services/awakeableholder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | restate "github.com/restatedev/sdk-go" 7 | ) 8 | 9 | const ID_KEY = "id" 10 | 11 | func init() { 12 | REGISTRY.AddDefinition( 13 | restate.NewObject("AwakeableHolder"). 14 | Handler("hold", restate.NewObjectHandler( 15 | func(ctx restate.ObjectContext, id string) (restate.Void, error) { 16 | restate.Set(ctx, ID_KEY, id) 17 | return restate.Void{}, nil 18 | })). 19 | Handler("hasAwakeable", restate.NewObjectHandler( 20 | func(ctx restate.ObjectContext, _ restate.Void) (bool, error) { 21 | id, err := restate.Get[string](ctx, ID_KEY) 22 | if err != nil { 23 | return false, err 24 | } 25 | return id != "", nil 26 | })). 27 | Handler("unlock", restate.NewObjectHandler( 28 | func(ctx restate.ObjectContext, payload string) (restate.Void, error) { 29 | id, err := restate.Get[string](ctx, ID_KEY) 30 | if err != nil { 31 | return restate.Void{}, err 32 | } 33 | if id == "" { 34 | return restate.Void{}, restate.TerminalError(fmt.Errorf("No awakeable registered"), 404) 35 | } 36 | restate.ResolveAwakeable(ctx, id, payload) 37 | restate.Clear(ctx, ID_KEY) 38 | return restate.Void{}, nil 39 | }))) 40 | } 41 | -------------------------------------------------------------------------------- /test-services/blockandwaitworkflow.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | ) 6 | 7 | const MY_STATE = "my-state" 8 | const MY_DURABLE_PROMISE = "durable-promise" 9 | 10 | func init() { 11 | REGISTRY.AddDefinition( 12 | restate.NewWorkflow("BlockAndWaitWorkflow"). 13 | Handler("run", restate.NewWorkflowHandler( 14 | func(ctx restate.WorkflowContext, input string) (string, error) { 15 | restate.Set(ctx, MY_STATE, input) 16 | output, err := restate.Promise[string](ctx, MY_DURABLE_PROMISE).Result() 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | peek, err := restate.Promise[*string](ctx, MY_DURABLE_PROMISE).Peek() 22 | if peek == nil { 23 | return "", restate.TerminalErrorf("Durable promise should be completed") 24 | } 25 | 26 | return output, nil 27 | })). 28 | Handler("unblock", restate.NewWorkflowSharedHandler( 29 | func(ctx restate.WorkflowSharedContext, output string) (restate.Void, error) { 30 | return restate.Void{}, restate.Promise[string](ctx, MY_DURABLE_PROMISE).Resolve(output) 31 | })). 32 | Handler("getState", restate.NewWorkflowSharedHandler( 33 | func(ctx restate.WorkflowSharedContext, input restate.Void) (*string, error) { 34 | return restate.Get[*string](ctx, MY_STATE) 35 | }))) 36 | } 37 | -------------------------------------------------------------------------------- /test-services/canceltest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | restate "github.com/restatedev/sdk-go" 8 | ) 9 | 10 | const CanceledState = "canceled" 11 | 12 | type BlockingOperation string 13 | 14 | const ( 15 | CallOp BlockingOperation = "CALL" 16 | SleepOp BlockingOperation = "SLEEP" 17 | AwakeableOp BlockingOperation = "AWAKEABLE" 18 | ) 19 | 20 | func init() { 21 | REGISTRY.AddDefinition( 22 | restate.NewObject("CancelTestRunner"). 23 | Handler("startTest", restate.NewObjectHandler( 24 | func(ctx restate.ObjectContext, operation BlockingOperation) (restate.Void, error) { 25 | if _, err := restate.Object[restate.Void](ctx, "CancelTestBlockingService", restate.Key(ctx), "block").Request(operation); err != nil { 26 | if restate.ErrorCode(err) == 409 { 27 | restate.Set(ctx, CanceledState, true) 28 | return restate.Void{}, nil 29 | } 30 | return restate.Void{}, err 31 | } 32 | return restate.Void{}, nil 33 | })). 34 | Handler("verifyTest", restate.NewObjectHandler( 35 | func(ctx restate.ObjectContext, _ restate.Void) (bool, error) { 36 | return restate.Get[bool](ctx, CanceledState) 37 | }))) 38 | REGISTRY.AddDefinition( 39 | restate.NewObject("CancelTestBlockingService"). 40 | Handler("block", restate.NewObjectHandler( 41 | func(ctx restate.ObjectContext, operation BlockingOperation) (restate.Void, error) { 42 | awakeable := restate.Awakeable[restate.Void](ctx) 43 | if _, err := restate.Object[restate.Void](ctx, "AwakeableHolder", restate.Key(ctx), "hold").Request(awakeable.Id()); err != nil { 44 | return restate.Void{}, err 45 | } 46 | if _, err := awakeable.Result(); err != nil { 47 | return restate.Void{}, err 48 | } 49 | switch operation { 50 | case CallOp: 51 | return restate.Object[restate.Void](ctx, "CancelTestBlockingService", restate.Key(ctx), "block").Request(operation) 52 | case SleepOp: 53 | return restate.Void{}, restate.Sleep(ctx, 1024*time.Hour*24) 54 | case AwakeableOp: 55 | return restate.Awakeable[restate.Void](ctx).Result() 56 | default: 57 | return restate.Void{}, restate.TerminalError(fmt.Errorf("unexpected operation %s", operation), 400) 58 | } 59 | })). 60 | Handler("isUnlocked", restate.NewObjectHandler( 61 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 62 | // no-op 63 | return restate.Void{}, nil 64 | }))) 65 | } 66 | -------------------------------------------------------------------------------- /test-services/counter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | ) 6 | 7 | const COUNTER_KEY = "counter" 8 | 9 | type CounterUpdateResponse struct { 10 | OldValue int64 `json:"oldValue"` 11 | NewValue int64 `json:"newValue"` 12 | } 13 | 14 | func init() { 15 | REGISTRY.AddDefinition( 16 | restate.NewObject("Counter"). 17 | Handler("reset", restate.NewObjectHandler( 18 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 19 | restate.Clear(ctx, COUNTER_KEY) 20 | return restate.Void{}, nil 21 | })). 22 | Handler("get", restate.NewObjectSharedHandler( 23 | func(ctx restate.ObjectSharedContext, _ restate.Void) (int64, error) { 24 | return restate.Get[int64](ctx, COUNTER_KEY) 25 | })). 26 | Handler("add", restate.NewObjectHandler( 27 | func(ctx restate.ObjectContext, addend int64) (CounterUpdateResponse, error) { 28 | oldValue, err := restate.Get[int64](ctx, COUNTER_KEY) 29 | if err != nil { 30 | return CounterUpdateResponse{}, err 31 | } 32 | 33 | newValue := oldValue + addend 34 | restate.Set(ctx, COUNTER_KEY, newValue) 35 | 36 | return CounterUpdateResponse{ 37 | OldValue: oldValue, 38 | NewValue: newValue, 39 | }, nil 40 | })). 41 | Handler("addThenFail", restate.NewObjectHandler( 42 | func(ctx restate.ObjectContext, addend int64) (restate.Void, error) { 43 | oldValue, err := restate.Get[int64](ctx, COUNTER_KEY) 44 | if err != nil { 45 | return restate.Void{}, err 46 | } 47 | 48 | newValue := oldValue + addend 49 | restate.Set(ctx, COUNTER_KEY, newValue) 50 | 51 | return restate.Void{}, restate.TerminalErrorf("%s", restate.Key(ctx)) 52 | }))) 53 | } 54 | -------------------------------------------------------------------------------- /test-services/exclusions.yaml: -------------------------------------------------------------------------------- 1 | exclusions: {} 2 | -------------------------------------------------------------------------------- /test-services/failing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | 8 | restate "github.com/restatedev/sdk-go" 9 | ) 10 | 11 | func init() { 12 | var eventualSuccessCalls atomic.Int32 13 | var eventualSuccessSideEffectCalls atomic.Int32 14 | var eventualFailureSideEffectCalls atomic.Int32 15 | 16 | REGISTRY.AddDefinition( 17 | restate.NewObject("Failing"). 18 | Handler("terminallyFailingCall", restate.NewObjectHandler( 19 | func(ctx restate.ObjectContext, errorMessage string) (restate.Void, error) { 20 | return restate.Void{}, restate.TerminalErrorf(errorMessage) 21 | })). 22 | Handler("callTerminallyFailingCall", restate.NewObjectHandler( 23 | func(ctx restate.ObjectContext, errorMessage string) (string, error) { 24 | if _, err := restate.Object[restate.Void](ctx, "Failing", restate.Rand(ctx).UUID().String(), "terminallyFailingCall").Request(errorMessage); err != nil { 25 | return "", err 26 | } 27 | 28 | return "", restate.TerminalErrorf("This should be unreachable") 29 | })). 30 | Handler("failingCallWithEventualSuccess", restate.NewObjectHandler( 31 | func(ctx restate.ObjectContext, _ restate.Void) (int32, error) { 32 | currentAttempt := eventualSuccessCalls.Add(1) 33 | if currentAttempt >= 4 { 34 | eventualSuccessCalls.Store(0) 35 | return currentAttempt, nil 36 | } else { 37 | return 0, fmt.Errorf("Failed at attempt: %d", currentAttempt) 38 | } 39 | })). 40 | Handler("terminallyFailingSideEffect", restate.NewObjectHandler( 41 | func(ctx restate.ObjectContext, errorMessage string) (restate.Void, error) { 42 | return restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) { 43 | return restate.Void{}, restate.TerminalErrorf(errorMessage) 44 | }) 45 | })). 46 | Handler("sideEffectSucceedsAfterGivenAttempts", restate.NewObjectHandler( 47 | func(ctx restate.ObjectContext, minimumAttempts int32) (int32, error) { 48 | return restate.Run(ctx, func(ctx restate.RunContext) (int32, error) { 49 | currentAttempt := eventualSuccessSideEffectCalls.Add(1) 50 | if currentAttempt >= minimumAttempts { 51 | eventualSuccessSideEffectCalls.Store(0) 52 | return currentAttempt, nil 53 | } else { 54 | return 0, fmt.Errorf("Failed at attempt: %d", currentAttempt) 55 | } 56 | }, 57 | restate.WithName("failing_side_effect"), 58 | restate.WithInitialRetryInterval(time.Millisecond*10), 59 | restate.WithRetryIntervalFactor(1.0)) 60 | })). 61 | Handler("sideEffectFailsAfterGivenAttempts", restate.NewObjectHandler( 62 | func(ctx restate.ObjectContext, retryPolicyMaxRetryCount uint) (int32, error) { 63 | _, err := restate.Run(ctx, func(ctx restate.RunContext) (int32, error) { 64 | currentAttempt := eventualFailureSideEffectCalls.Add(1) 65 | return 0, fmt.Errorf("Failed at attempt: %d", currentAttempt) 66 | }, restate.WithName("failing_side_effect"), restate.WithInitialRetryInterval(time.Millisecond*10), restate.WithRetryIntervalFactor(1.0), restate.WithMaxRetryAttempts(retryPolicyMaxRetryCount)) 67 | if err != nil { 68 | return eventualFailureSideEffectCalls.Load(), nil 69 | } 70 | return 0, restate.TerminalErrorf("Expecting the side effect to fail!") 71 | }))) 72 | } 73 | -------------------------------------------------------------------------------- /test-services/kill.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | ) 6 | 7 | func init() { 8 | REGISTRY.AddDefinition(restate.NewObject("KillTestRunner").Handler("startCallTree", restate.NewObjectHandler(func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 9 | return restate.Object[restate.Void](ctx, "KillTestSingleton", restate.Key(ctx), "recursiveCall").Request(restate.Void{}) 10 | }))) 11 | 12 | REGISTRY.AddDefinition( 13 | restate.NewObject("KillTestSingleton"). 14 | Handler("recursiveCall", restate.NewObjectHandler( 15 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 16 | awakeable := restate.Awakeable[restate.Void](ctx) 17 | restate.ObjectSend(ctx, "AwakeableHolder", restate.Key(ctx), "hold").Send(awakeable.Id()) 18 | if _, err := awakeable.Result(); err != nil { 19 | return restate.Void{}, err 20 | } 21 | 22 | return restate.Object[restate.Void](ctx, "KillTestSingleton", restate.Key(ctx), "recursiveCall").Request(restate.Void{}) 23 | })). 24 | Handler("isUnlocked", restate.NewObjectHandler( 25 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 26 | // no-op 27 | return restate.Void{}, nil 28 | }))) 29 | } 30 | -------------------------------------------------------------------------------- /test-services/listobject.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | ) 6 | 7 | const LIST_KEY = "list" 8 | 9 | func init() { 10 | REGISTRY.AddDefinition( 11 | restate.NewObject("ListObject"). 12 | Handler("append", restate.NewObjectHandler( 13 | func(ctx restate.ObjectContext, value string) (restate.Void, error) { 14 | list, err := restate.Get[[]string](ctx, LIST_KEY) 15 | if err != nil { 16 | return restate.Void{}, err 17 | } 18 | list = append(list, value) 19 | restate.Set(ctx, LIST_KEY, list) 20 | return restate.Void{}, nil 21 | })). 22 | Handler("get", restate.NewObjectHandler( 23 | func(ctx restate.ObjectContext, _ restate.Void) ([]string, error) { 24 | list, err := restate.Get[[]string](ctx, LIST_KEY) 25 | if err != nil { 26 | return nil, err 27 | } 28 | if list == nil { 29 | // or go would encode this as JSON null 30 | list = []string{} 31 | } 32 | 33 | return list, nil 34 | })). 35 | Handler("clear", restate.NewObjectHandler( 36 | func(ctx restate.ObjectContext, _ restate.Void) ([]string, error) { 37 | list, err := restate.Get[[]string](ctx, LIST_KEY) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if list == nil { 42 | // or go would encode this as JSON null 43 | list = []string{} 44 | } 45 | restate.Clear(ctx, LIST_KEY) 46 | return list, nil 47 | }))) 48 | } 49 | -------------------------------------------------------------------------------- /test-services/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/restatedev/sdk-go/internal/log" 6 | "log/slog" 7 | "os" 8 | "strings" 9 | 10 | "github.com/restatedev/sdk-go/server" 11 | ) 12 | 13 | func main() { 14 | // Accommodating for verification tests here 15 | logging := strings.ToLower(os.Getenv("RESTATE_LOGGING")) 16 | if logging == "error" { 17 | slog.SetLogLoggerLevel(slog.LevelError) 18 | } else if logging == "warn" { 19 | slog.SetLogLoggerLevel(slog.LevelWarn) 20 | } else if logging == "info" { 21 | slog.SetLogLoggerLevel(slog.LevelInfo) 22 | } else if logging == "debug" { 23 | slog.SetLogLoggerLevel(slog.LevelDebug) 24 | } else if logging == "trace" { 25 | slog.SetLogLoggerLevel(log.LevelTrace) 26 | } 27 | 28 | services := "*" 29 | if os.Getenv("SERVICES") != "" { 30 | services = os.Getenv("SERVICES") 31 | } 32 | 33 | server := server.NewRestate() 34 | 35 | if services == "*" { 36 | REGISTRY.RegisterAll(server) 37 | } else { 38 | fqdns := strings.Split(services, ",") 39 | set := make(map[string]struct{}, len(fqdns)) 40 | for _, fqdn := range fqdns { 41 | set[fqdn] = struct{}{} 42 | } 43 | REGISTRY.Register(set, server) 44 | } 45 | 46 | port := os.Getenv("PORT") 47 | if port == "" { 48 | port = "9080" 49 | } 50 | 51 | if err := server.Start(context.Background(), ":"+port); err != nil { 52 | slog.Error("application exited unexpectedly", "err", err) 53 | os.Exit(1) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test-services/mapobject.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | ) 6 | 7 | type Entry struct { 8 | Key string `json:"key"` 9 | Value string `json:"value"` 10 | } 11 | 12 | func init() { 13 | REGISTRY.AddDefinition( 14 | restate.NewObject("MapObject"). 15 | Handler("set", restate.NewObjectHandler( 16 | func(ctx restate.ObjectContext, value Entry) (restate.Void, error) { 17 | restate.Set(ctx, value.Key, value.Value) 18 | return restate.Void{}, nil 19 | })). 20 | Handler("get", restate.NewObjectHandler( 21 | func(ctx restate.ObjectContext, key string) (string, error) { 22 | return restate.Get[string](ctx, key) 23 | })). 24 | Handler("clearAll", restate.NewObjectHandler( 25 | func(ctx restate.ObjectContext, _ restate.Void) ([]Entry, error) { 26 | keys, err := restate.Keys(ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | out := make([]Entry, 0, len(keys)) 31 | for _, k := range keys { 32 | value, err := restate.Get[string](ctx, k) 33 | if err != nil { 34 | return nil, err 35 | } 36 | out = append(out, Entry{Key: k, Value: value}) 37 | } 38 | restate.ClearAll(ctx) 39 | return out, nil 40 | }))) 41 | } 42 | -------------------------------------------------------------------------------- /test-services/nondeterministic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | restate "github.com/restatedev/sdk-go" 8 | ) 9 | 10 | const STATE_A = "a" 11 | const STATE_B = "b" 12 | 13 | func init() { 14 | invocationCounts := map[string]int32{} 15 | invocationCountsMtx := sync.RWMutex{} 16 | 17 | doLeftAction := func(ctx restate.ObjectContext) bool { 18 | countKey := restate.Key(ctx) 19 | invocationCountsMtx.Lock() 20 | defer invocationCountsMtx.Unlock() 21 | 22 | invocationCounts[countKey] += 1 23 | return invocationCounts[countKey]%2 == 1 24 | } 25 | incrementCounter := func(ctx restate.ObjectContext) { 26 | restate.ObjectSend(ctx, "Counter", restate.Key(ctx), "add").Send(int64(1)) 27 | } 28 | 29 | REGISTRY.AddDefinition( 30 | restate.NewObject("NonDeterministic"). 31 | Handler("eitherSleepOrCall", restate.NewObjectHandler( 32 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 33 | if doLeftAction(ctx) { 34 | restate.Sleep(ctx, 100*time.Millisecond) 35 | } else { 36 | if _, err := restate.Object[restate.Void](ctx, "Counter", "abc", "get").Request(restate.Void{}); err != nil { 37 | return restate.Void{}, err 38 | } 39 | } 40 | 41 | // This is required to cause a suspension after the non-deterministic operation 42 | restate.Sleep(ctx, 100*time.Millisecond) 43 | incrementCounter(ctx) 44 | return restate.Void{}, nil 45 | })). 46 | Handler("callDifferentMethod", restate.NewObjectHandler( 47 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 48 | if doLeftAction(ctx) { 49 | if _, err := restate.Object[restate.Void](ctx, "Counter", "abc", "get").Request(restate.Void{}); err != nil { 50 | return restate.Void{}, err 51 | } 52 | } else { 53 | if _, err := restate.Object[restate.Void](ctx, "Counter", "abc", "reset").Request(restate.Void{}); err != nil { 54 | return restate.Void{}, err 55 | } 56 | } 57 | 58 | // This is required to cause a suspension after the non-deterministic operation 59 | restate.Sleep(ctx, 100*time.Millisecond) 60 | incrementCounter(ctx) 61 | return restate.Void{}, nil 62 | })). 63 | Handler("backgroundInvokeWithDifferentTargets", restate.NewObjectHandler( 64 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 65 | if doLeftAction(ctx) { 66 | restate.ObjectSend(ctx, "Counter", "abc", "get").Send(restate.Void{}) 67 | } else { 68 | restate.ObjectSend(ctx, "Counter", "abc", "reset").Send(restate.Void{}) 69 | } 70 | 71 | // This is required to cause a suspension after the non-deterministic operation 72 | restate.Sleep(ctx, 100*time.Millisecond) 73 | incrementCounter(ctx) 74 | return restate.Void{}, nil 75 | })). 76 | Handler("setDifferentKey", restate.NewObjectHandler( 77 | func(ctx restate.ObjectContext, _ restate.Void) (restate.Void, error) { 78 | if doLeftAction(ctx) { 79 | restate.Set(ctx, STATE_A, "my-state") 80 | } else { 81 | restate.Set(ctx, STATE_B, "my-state") 82 | } 83 | 84 | // This is required to cause a suspension after the non-deterministic operation 85 | restate.Sleep(ctx, 100*time.Millisecond) 86 | incrementCounter(ctx) 87 | return restate.Void{}, nil 88 | }))) 89 | } 90 | -------------------------------------------------------------------------------- /test-services/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | restate "github.com/restatedev/sdk-go" 5 | "github.com/restatedev/sdk-go/internal/options" 6 | "time" 7 | ) 8 | 9 | type ProxyRequest struct { 10 | ServiceName string `json:"serviceName"` 11 | VirtualObjectKey *string `json:"virtualObjectKey,omitempty"` 12 | HandlerName string `json:"handlerName"` 13 | // We need to use []int because Golang takes the opinionated choice of treating []byte as Base64 14 | Message []int `json:"message"` 15 | IdempotencyKey *string `json:"idempotencyKey,omitempty"` 16 | DelayMillis *uint64 `json:"delayMillis,omitempty"` 17 | } 18 | 19 | func (req *ProxyRequest) ToTarget(ctx restate.Context) restate.Client[[]byte, []byte] { 20 | if req.VirtualObjectKey != nil { 21 | return restate.WithRequestType[[]byte](restate.Object[[]byte]( 22 | ctx, 23 | req.ServiceName, 24 | *req.VirtualObjectKey, 25 | req.HandlerName, 26 | restate.WithBinary)) 27 | } else { 28 | return restate.WithRequestType[[]byte](restate.Service[[]byte]( 29 | ctx, 30 | req.ServiceName, 31 | req.HandlerName, 32 | restate.WithBinary)) 33 | } 34 | } 35 | 36 | type ManyCallRequest struct { 37 | ProxyRequest ProxyRequest `json:"proxyRequest"` 38 | OneWayCall bool `json:"oneWayCall"` 39 | AwaitAtTheEnd bool `json:"awaitAtTheEnd"` 40 | } 41 | 42 | func init() { 43 | REGISTRY.AddDefinition( 44 | restate.NewService("Proxy"). 45 | Handler("call", restate.NewServiceHandler( 46 | // We need to use []int because Golang takes the opinionated choice of treating []byte as Base64 47 | func(ctx restate.Context, req ProxyRequest) ([]int, error) { 48 | input := intArrayToByteArray(req.Message) 49 | var opts []options.RequestOption 50 | if req.IdempotencyKey != nil { 51 | opts = append(opts, restate.WithIdempotencyKey(*req.IdempotencyKey)) 52 | } 53 | bytes, err := req.ToTarget(ctx).Request(input, opts...) 54 | return byteArrayToIntArray(bytes), err 55 | })). 56 | Handler("oneWayCall", restate.NewServiceHandler( 57 | // We need to use []int because Golang takes the opinionated choice of treating []byte as Base64 58 | func(ctx restate.Context, req ProxyRequest) (string, error) { 59 | input := intArrayToByteArray(req.Message) 60 | var opts []options.SendOption 61 | if req.IdempotencyKey != nil { 62 | opts = append(opts, restate.WithIdempotencyKey(*req.IdempotencyKey)) 63 | } 64 | if req.DelayMillis != nil { 65 | opts = append(opts, restate.WithDelay(time.Millisecond*time.Duration(*req.DelayMillis))) 66 | } 67 | return req.ToTarget(ctx).Send(input, opts...).GetInvocationId(), nil 68 | })). 69 | Handler("manyCalls", restate.NewServiceHandler( 70 | // We need to use []int because Golang takes the opinionated choice of treating []byte as Base64 71 | func(ctx restate.Context, requests []ManyCallRequest) (restate.Void, error) { 72 | var toAwait []restate.Selectable 73 | 74 | for _, req := range requests { 75 | input := intArrayToByteArray(req.ProxyRequest.Message) 76 | if req.OneWayCall { 77 | var opts []options.SendOption 78 | if req.ProxyRequest.IdempotencyKey != nil { 79 | opts = append(opts, restate.WithIdempotencyKey(*req.ProxyRequest.IdempotencyKey)) 80 | } 81 | if req.ProxyRequest.DelayMillis != nil { 82 | opts = append(opts, restate.WithDelay(time.Millisecond*time.Duration(*req.ProxyRequest.DelayMillis))) 83 | } 84 | req.ProxyRequest.ToTarget(ctx).Send(input, opts...) 85 | } else { 86 | var opts []options.RequestOption 87 | if req.ProxyRequest.IdempotencyKey != nil { 88 | opts = append(opts, restate.WithIdempotencyKey(*req.ProxyRequest.IdempotencyKey)) 89 | } 90 | fut := req.ProxyRequest.ToTarget(ctx).RequestFuture(input, opts...) 91 | if req.AwaitAtTheEnd { 92 | toAwait = append(toAwait, fut) 93 | } 94 | } 95 | } 96 | 97 | selector := restate.Select(ctx, toAwait...) 98 | for selector.Remaining() { 99 | result := selector.Select() 100 | if _, err := result.(restate.ResponseFuture[[]byte]).Response(); err != nil { 101 | return restate.Void{}, err 102 | } 103 | } 104 | 105 | return restate.Void{}, nil 106 | }))) 107 | } 108 | 109 | func intArrayToByteArray(in []int) []byte { 110 | out := make([]byte, len(in)) 111 | for idx, val := range in { 112 | out[idx] = byte(val) 113 | } 114 | return out 115 | } 116 | 117 | func byteArrayToIntArray(in []byte) []int { 118 | out := make([]int, len(in)) 119 | for idx, val := range in { 120 | out[idx] = int(val) 121 | } 122 | return out 123 | } 124 | -------------------------------------------------------------------------------- /test-services/registry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | restate "github.com/restatedev/sdk-go" 7 | "github.com/restatedev/sdk-go/server" 8 | ) 9 | 10 | var REGISTRY = Registry{components: map[string]Component{}} 11 | 12 | type Registry struct { 13 | components map[string]Component 14 | } 15 | 16 | type Component struct { 17 | Fqdn string 18 | Binder func(endpoint *server.Restate) 19 | } 20 | 21 | func (r *Registry) Add(c Component) { 22 | r.components[c.Fqdn] = c 23 | } 24 | 25 | func (r *Registry) AddDefinition(definition restate.ServiceDefinition) { 26 | r.Add(Component{ 27 | Fqdn: definition.Name(), 28 | Binder: func(e *server.Restate) { e.Bind(definition) }, 29 | }) 30 | } 31 | 32 | func (r *Registry) RegisterAll(e *server.Restate) { 33 | for _, c := range r.components { 34 | c.Binder(e) 35 | } 36 | } 37 | 38 | func (r *Registry) Register(fqdns map[string]struct{}, e *server.Restate) { 39 | for fqdn := range fqdns { 40 | c, ok := r.components[fqdn] 41 | if !ok { 42 | log.Fatalf("unknown fqdn %s. Did you remember to register it?", fqdn) 43 | } 44 | c.Binder(e) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test-services/testutils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "sync/atomic" 7 | "time" 8 | 9 | restate "github.com/restatedev/sdk-go" 10 | ) 11 | 12 | func init() { 13 | REGISTRY.AddDefinition( 14 | restate.NewService("TestUtilsService"). 15 | Handler("echo", restate.NewServiceHandler( 16 | func(ctx restate.Context, input string) (string, error) { 17 | return input, nil 18 | })). 19 | Handler("uppercaseEcho", restate.NewServiceHandler( 20 | func(ctx restate.Context, input string) (string, error) { 21 | return strings.ToUpper(input), nil 22 | })). 23 | Handler("echoHeaders", restate.NewServiceHandler( 24 | func(ctx restate.Context, _ restate.Void) (map[string]string, error) { 25 | return ctx.Request().Headers, nil 26 | })). 27 | Handler("rawEcho", restate.NewServiceHandler( 28 | func(ctx restate.Context, input []byte) ([]byte, error) { 29 | return input, nil 30 | }, restate.WithBinary)). 31 | Handler("sleepConcurrently", restate.NewServiceHandler( 32 | func(ctx restate.Context, millisDuration []int64) (restate.Void, error) { 33 | timers := make([]restate.Selectable, 0, len(millisDuration)) 34 | for _, d := range millisDuration { 35 | timers = append(timers, restate.After(ctx, time.Duration(d)*time.Millisecond)) 36 | } 37 | selector := restate.Select(ctx, timers...) 38 | i := 0 39 | for selector.Remaining() { 40 | _ = selector.Select() 41 | i++ 42 | } 43 | if i != len(timers) { 44 | return restate.Void{}, restate.TerminalErrorf("unexpected number of timers fired: %d", i) 45 | } 46 | return restate.Void{}, nil 47 | })). 48 | Handler("countExecutedSideEffects", restate.NewServiceHandler( 49 | func(ctx restate.Context, increments int32) (int32, error) { 50 | invokedSideEffects := atomic.Int32{} 51 | for i := int32(0); i < increments; i++ { 52 | restate.Run(ctx, func(ctx restate.RunContext) (int32, error) { 53 | return invokedSideEffects.Add(1), nil 54 | }) 55 | } 56 | return invokedSideEffects.Load(), nil 57 | })). 58 | Handler("getEnvVariable", restate.NewServiceHandler(getEnvVariable)). 59 | Handler("cancelInvocation", restate.NewServiceHandler( 60 | func(ctx restate.Context, invocationId string) (restate.Void, error) { 61 | restate.CancelInvocation(ctx, invocationId) 62 | return restate.Void{}, nil 63 | })), 64 | ) 65 | } 66 | 67 | func getEnvVariable(ctx restate.Context, envName string) (string, error) { 68 | return restate.Run(ctx, func(ctx restate.RunContext) (string, error) { 69 | return os.Getenv(envName), nil 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /test-services/upgradetest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | restate "github.com/restatedev/sdk-go" 9 | ) 10 | 11 | func init() { 12 | version := func() string { 13 | return strings.TrimSpace(os.Getenv("E2E_UPGRADETEST_VERSION")) 14 | } 15 | REGISTRY.AddDefinition( 16 | restate.NewService("UpgradeTest"). 17 | Handler("executeSimple", restate.NewServiceHandler( 18 | func(ctx restate.Context, _ restate.Void) (string, error) { 19 | return version(), nil 20 | })). 21 | Handler("executeComplex", restate.NewServiceHandler( 22 | func(ctx restate.Context, _ restate.Void) (string, error) { 23 | if version() != "v1" { 24 | return "", fmt.Errorf("executeComplex should not be invoked with version different from 1!") 25 | } 26 | awakeable := restate.Awakeable[string](ctx) 27 | restate.ObjectSend(ctx, "AwakeableHolder", "upgrade", "hold").Send(awakeable.Id()) 28 | if _, err := awakeable.Result(); err != nil { 29 | return "", err 30 | } 31 | restate.ObjectSend(ctx, "ListObject", "upgrade-test", "append").Send(version()) 32 | return version(), nil 33 | }))) 34 | } 35 | --------------------------------------------------------------------------------