diff --git a/README.md b/README.md new file mode 100644 index 0000000..a959df5 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Restate Go SDK + +[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**. + +This SDK is an individual effort to build a golang SDK for restate runtime. The implementation is based on the service protocol documentation found [here](https://github.com/restatedev/service-protocol/blob/main/service-invocation-protocol.md) and a lot of experimentation with the protocol. + +This means that it's not granted that this SDK matches exactly what `restate` has intended but it's a best effort interpretation of the docs + +Since **service discovery** was not documented (or at least I could not find any documentation for it), the implementation is based on reverse engineering the TypeScript SDK. + +This implementation of the SDK **ONLY** supports `dynrpc`. There is noway yet that you can define your service interface with `gRPC` + +Calling other services right now is done completely by name, hence it's not very safe since you can miss up arguments list/type for example but hopefully later on we can generate stubs or use `gRPC` interfaces to define services. + +## Features implemented + +- [x] Log replay (resume of execution on failure) +- [x] State (set/get/clear/clear-all/keys) +- [x] Remote service call over restate runtime +- [X] Delayed execution of remote services +- [X] Sleep +- [x] Side effects + - Implementation might differ from as intended by restate since it's not documented and based on reverse engineering of the TypeScript SDK +- [ ] Awakeable + +## Basic usage + +Please check [example](example) for a fully working example. The example tries to implement the same exact example provided by restate official docs and TypeScript SDK but with few changes. So I recommend you start there first before trying out this example. + +### How to use the example + +Download and run restate [v0.8](https://github.com/restatedev/restate/releases/tag/v0.8.0) + +```bash +restate-server --wipe all +``` + +> Generally you don't have to use `--wipe all` but that is mainly for testing to make sure you starting from clean state + +In another terminal run the example + +```bash +cd restate-sdk-go/example +go run . +``` + +In yet a third terminal do the following steps + +- Add tickets to basket + +```bash +curl -v localhost:8080/UserSession/addTicket \ + -H 'content-type: application/json' \ + -d '{"key": "azmy", "request": "ticket-1"}' + +# {"response":true} +curl -v localhost:8080/UserSession/addTicket \ + -H 'content-type: application/json' \ + -d '{"key": "azmy", "request": "ticket-2"}' +# {"response":true} +``` + +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 inpatient like me change the delay in code to make it shorter) + +Finally checkout + +```bash +curl localhost:8080/UserSession/checkout \ + -H 'content-type: application/json' \ + -d '{"key": "azmy", "request": null}' +#{"response":true} +``` diff --git a/error.go b/error.go index eb3a395..56d19e9 100644 --- a/error.go +++ b/error.go @@ -183,6 +183,8 @@ func WithErrorCode(err error, code Code) error { // TerminalError returns a terminal error with optional code. // code is optional but only one code is allowed. +// By default restate will retry the invocation forever unless a terminal error +// is returned func TerminalError(err error, code ...Code) error { if err == nil { return nil @@ -206,6 +208,7 @@ func TerminalError(err error, code ...Code) error { return err } +// IsTerminalError checks if err is terminal func IsTerminalError(err error) bool { if err == nil { return false @@ -214,6 +217,7 @@ func IsTerminalError(err error) bool { return errors.As(err, &t) } +// ErrorCode returns code associated with error or UNKNOWN func ErrorCode(err error) Code { var e *codeError if errors.As(err, &e) { diff --git a/example/ticket_service.go b/example/ticket_service.go index 495a83d..807ffad 100644 --- a/example/ticket_service.go +++ b/example/ticket_service.go @@ -16,7 +16,6 @@ const ( ) func reserve(ctx restate.Context, ticketId string, _ restate.Void) (bool, error) { - log.Info().Str("ticket", ticketId).Msg("reserving ticket") status, err := restate.GetAs[TicketStatus](ctx, "status") if err != nil && !errors.Is(err, restate.ErrKeyNotFound) { return false, err diff --git a/example/user_session.go b/example/user_session.go index 4d9e035..229232f 100644 --- a/example/user_session.go +++ b/example/user_session.go @@ -71,6 +71,8 @@ func checkout(ctx restate.Context, userId string, _ restate.Void) (bool, error) return false, err } + log.Info().Strs("tickets", tickets).Msg("tickets in basket") + if len(tickets) == 0 { return false, nil } diff --git a/router.go b/router.go index f78b544..1a4cda1 100644 --- a/router.go +++ b/router.go @@ -37,6 +37,7 @@ type Service interface { } type Context interface { + // Context of request. Ctx() context.Context // Set sets key value to bytes array. You can // Note: Use SetAs helper function to seamlessly store @@ -80,21 +81,26 @@ type Handler interface { sealed() } +// Router interface type Router interface { Keyed() bool + // Set of handlers associated with this router Handlers() map[string]Handler } +// UnKeyedRouter implements Router type UnKeyedRouter struct { handlers map[string]Handler } +// NewUnKeyedRouter creates a new UnKeyedRouter func NewUnKeyedRouter() *UnKeyedRouter { return &UnKeyedRouter{ handlers: make(map[string]Handler), } } +// Handler registers a new handler by name func (r *UnKeyedRouter) Handler(name string, handler *UnKeyedHandler) *UnKeyedRouter { r.handlers[name] = handler return r diff --git a/server/restate.go b/server/restate.go index 2263fea..2ff0bf5 100644 --- a/server/restate.go +++ b/server/restate.go @@ -21,6 +21,7 @@ type Restate struct { routers map[string]restate.Router } +// NewRestate creates a new instance of Restate server func NewRestate() *Restate { return &Restate{ routers: make(map[string]restate.Router),