From 12e1782d4db4b7645e0bf2281aa18a712a921e0d Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Thu, 20 Jul 2023 16:10:07 +0200 Subject: [PATCH 1/3] Add documentation about service type and service contract --- docs/service_contract.md | 108 +++++++++++++++++++++++++++++++++++++++ docs/service_type.md | 62 ++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 docs/service_contract.md create mode 100644 docs/service_type.md diff --git a/docs/service_contract.md b/docs/service_contract.md new file mode 100644 index 00000000..c3bf0bf8 --- /dev/null +++ b/docs/service_contract.md @@ -0,0 +1,108 @@ +--- +sidebar_position: 7 +--- + +# Restate Service contract + +Every Restate service defines a typed interface using a contract. The interface describes some properties of the service, such as the service methods (in other RPC systems these are called _handlers_) and its input/output message types. + +Restate uses this contract to enable several features, such as: + +* Automatically extract the [service key](./service_type.md), if any +* Accept requests in the [ingress](./ingress.md) in different formats and route them +* Allow code generation of service code and clients +* Support safer [upgrades](./deployment-operations/versioning.md) through incompatibility checks + +The service contract is defined using [Protobuf](https://protobuf.dev/programming-guides/proto3/#services). Refer to their documentation to learn how to use the [Protobuf IDL](https://protobuf.dev/programming-guides/proto3). + +## Protobuf service definition + +Below a sample Protobuf service definition for the service `counter.Counter`: + +```protobuf +syntax = "proto3"; + +package counter; + +// Import the Restate contract extensions +import "dev/restate/ext.proto"; + +service Counter { + // Define the service type + option (dev.restate.ext.service_type) = KEYED; + + // Define the service methods + rpc Get (GetRequest) returns (Response); + rpc Add (AddRequest) returns (Response); +} + +message GetRequest { + // Define the key + string counter_name = 1 [(dev.restate.ext.field) = KEY]; +} + +message AddRequest { + string counter_name = 1 [(dev.restate.ext.field) = KEY]; + int64 value = 2; +} + +message Response { + int64 value = 1; +} +``` + +## Defining service instance and key + +In addition to the standard Protobuf service definition, in Restate service you must specify the service type. Check the [service type](./service_type.md) documentation for more details. + +To define the service type, you must use the `dev.restate.ext.service_type` extension. To define a service as keyed: + +```protobuf +option (dev.restate.ext.service_type) = KEYED; +``` + +As unkeyed: + +```protobuf +option (dev.restate.ext.service_type) = UNKEYED; +``` + +As singleton: + +```protobuf +option (dev.restate.ext.service_type) = SINGLETON; +``` + +For keyed services, you're required to specify in every input message the field to use as key. To mark a field as key, annotate it with `dev.restate.ext.field`. Make sure that: + +* The field type is either a primitive or a message. Repeated field and maps are not supported. +* The field type is the same for every method input message of the same service. + +For example, a primitive key field: + +```protobuf +message GetRequest { + string counter_name = 1 [(dev.restate.ext.field) = KEY]; +} +``` + +A composite key field: + +```protobuf +message GreetingRequest { + Person person = 1 [(dev.restate.ext.field) = KEY]; +} + +message Person { + string first_name = 1; + string last_name = 2; +} +``` + +## How to use the contract + +Once you have the contract, the SDK uses it to generate the code to encode/decode messages and the interface to implement the service. You can import contract of other services, and the SDK will generate clients to invoke them. + +The contract can also be used to generate gRPC/Connect clients to invoke Restate services from your webapp, mobile app, legacy system or in general from any system outside Restate services through the [ingress](./ingress.md). You can check out the [gRPC](https://grpc.io/docs/languages/) and [Connect](https://connect.build/docs/introduction) documentation for more info on the available clients. + +When [registering a service endpoint](./deployment-operations/deployment.md#registering-service-endpoints), Restate automatically _discovers_ all the available service contracts and stores them in an internal registry, no manual input is needed. diff --git a/docs/service_type.md b/docs/service_type.md new file mode 100644 index 00000000..4a061650 --- /dev/null +++ b/docs/service_type.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 7 +--- + +# Service type + +Restate provides different state semantics and concurrency properties for services, that you can choose from when defining the service. + +Services can be categorized in three different types: + +1. **Keyed service**: All service invocations are sharded on a user-defined key. +2. **Singleton service**: All service invocations are executed serially, and the state is shared among every invocation. +3. **Unkeyed service**: All service invocations run in parallel, and there is not state shared among invocations. + +To define the service type and key, check the [service contract](./service_contract.md) documentation. + +## Keyed service + +Keyed services allows to shard state and workload by a user-defined key. Each key will have its own invocations queue and its own subset of the state. There is at most one concurrent invocation per key, but there can be multiple invocations to the same service with different keys executing at the same time. + +You can think to a keyed service as a class, and a service instance as object instance of that class. The key is the field that **uniquely** identifies that instance, and state entries are the other fields of that class. + +A common use case for keyed services is to model an entity of your application. For example, a `CustomerService` could model a customer, where the key is the customer id card number, the state defines the properties of the customer and the methods define the operations on it. + +### Ordering guarantees + +Because keyed services are executed serially on a key basis, it means that invocations will execute in the same order they're enqueued. For example, assume the following code exists in `ServiceA`: + +```typescript +const client = new ServiceB(restateContext); +await restateContext.oneWayCall(() => + client.do(TestRequest.create({ key: "Pete", number: 1 })) +); +await restateContext.oneWayCall(() => + client.do(TestRequest.create({ key: "Pete", number: 2 })) +); +``` + +It is guaranteed that the invocation with `number: 1` will be executed before the invocation with `number: 2`. It is not guaranteed though that invocation `number: 2` will be executed immediately after invocation `number: 1`, as any other service, or a call from the ingress, could interleave these two calls enqueuing a third one. + +### Common pitfalls + +You should take into account some of the limitations of keyed services when designing your applications: + +* Time-consuming operations, such as sleep, lock the service instance for the entire operation, hence they won't allow other enqueued invocations to be executed. +* Keyed service invocations can produce deadlocks when using request/response calls. When this happens, the keys remain locked and the system can't process any more requests. In this situation you'll have to unblock the keys manually by [cancelling invocations](./deployment-operations/manage-invocations.md#cancel-an-invocation). Some example cases: + * Cross deadlock between service A and B: A calls B, and B calls A, both using same keys. + * Cyclical deadlock: A calls B, and B calls C, and C calls A again. + +## Singleton service + +Singleton services are essentially like keyed service where the key is always the same, meaning that every invocation will access the same state and will be enqueued in the same queue. + +Carefully ponder whether a service should be a singleton, given it executes all the invocations serially. If not properly used, it can end up being a quite significant source of resource contention for your application. + +## Unkeyed service + +Unkeyed services have no invocation queue, meaning invocations are executed as soon as they're received without any concurrency and ordering guarantee. + +You should not use state in unkeyed services, as all the stored state will inaccessible forever after the end of the invocation. + +Because unkeyed services don't lock any resource, they are a good fit for long running workflows with many time-consuming operations such as sleeps, or as a coordinator to invoke other keyed services. From c8419f2c3af2d86e71bf855b64e0c609408bcb68 Mon Sep 17 00:00:00 2001 From: Francesco Guardiani Date: Fri, 21 Jul 2023 13:17:50 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Till Rohrmann Co-authored-by: Giselle van Dongen --- docs/service_contract.md | 2 +- docs/service_type.md | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/service_contract.md b/docs/service_contract.md index c3bf0bf8..b82e19a1 100644 --- a/docs/service_contract.md +++ b/docs/service_contract.md @@ -101,7 +101,7 @@ message Person { ## How to use the contract -Once you have the contract, the SDK uses it to generate the code to encode/decode messages and the interface to implement the service. You can import contract of other services, and the SDK will generate clients to invoke them. +Once you have the contract, the SDK uses it to generate the code to encode/decode messages and the interface to implement the service. You can import contracts of other services, and the SDK will generate clients to invoke them. The contract can also be used to generate gRPC/Connect clients to invoke Restate services from your webapp, mobile app, legacy system or in general from any system outside Restate services through the [ingress](./ingress.md). You can check out the [gRPC](https://grpc.io/docs/languages/) and [Connect](https://connect.build/docs/introduction) documentation for more info on the available clients. diff --git a/docs/service_type.md b/docs/service_type.md index 4a061650..bbb1287b 100644 --- a/docs/service_type.md +++ b/docs/service_type.md @@ -10,21 +10,21 @@ Services can be categorized in three different types: 1. **Keyed service**: All service invocations are sharded on a user-defined key. 2. **Singleton service**: All service invocations are executed serially, and the state is shared among every invocation. -3. **Unkeyed service**: All service invocations run in parallel, and there is not state shared among invocations. +3. **Unkeyed service**: All service invocations run in parallel, and there is no shared state among invocations. To define the service type and key, check the [service contract](./service_contract.md) documentation. ## Keyed service -Keyed services allows to shard state and workload by a user-defined key. Each key will have its own invocations queue and its own subset of the state. There is at most one concurrent invocation per key, but there can be multiple invocations to the same service with different keys executing at the same time. +Keyed services allow to shard state and workload by a user-defined key. Each key will have its own invocations queue and its own subset of the service state. There is at most one invocation per key, but there can be multiple invocations to the same service with different keys executing concurrently. -You can think to a keyed service as a class, and a service instance as object instance of that class. The key is the field that **uniquely** identifies that instance, and state entries are the other fields of that class. +You can think of a keyed service as a class, and of a service instance as an object instance of that class. The key is the field that **uniquely** identifies that instance, and state entries are the other fields of that class. A common use case for keyed services is to model an entity of your application. For example, a `CustomerService` could model a customer, where the key is the customer id card number, the state defines the properties of the customer and the methods define the operations on it. ### Ordering guarantees -Because keyed services are executed serially on a key basis, it means that invocations will execute in the same order they're enqueued. For example, assume the following code exists in `ServiceA`: +Because keyed services are executed serially on a per-key basis, it means that invocations will execute in the same order in which they are enqueued. For example, assume the following code in `ServiceA`: ```typescript const client = new ServiceB(restateContext); @@ -49,14 +49,16 @@ You should take into account some of the limitations of keyed services when desi ## Singleton service -Singleton services are essentially like keyed service where the key is always the same, meaning that every invocation will access the same state and will be enqueued in the same queue. +Singleton services are essentially like keyed services where the key is always the same, meaning that every invocation accesses the same state and gets enqueued in the same queue. -Carefully ponder whether a service should be a singleton, given it executes all the invocations serially. If not properly used, it can end up being a quite significant source of resource contention for your application. +:::warning +Carefully ponder whether a service should be a singleton, given it executes all the invocations serially. If not properly used, it can become the throughput bottleneck of your application. +::: ## Unkeyed service Unkeyed services have no invocation queue, meaning invocations are executed as soon as they're received without any concurrency and ordering guarantee. -You should not use state in unkeyed services, as all the stored state will inaccessible forever after the end of the invocation. +You should not use state in unkeyed services, as all the stored state will be inaccessible after the end of the invocation. Because unkeyed services don't lock any resource, they are a good fit for long running workflows with many time-consuming operations such as sleeps, or as a coordinator to invoke other keyed services. From a48758a9f3c37098f11badb399af65e28c779f03 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Fri, 21 Jul 2023 13:23:55 +0200 Subject: [PATCH 3/3] Suggestions --- docs/service_contract.md | 1 + docs/service_type.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/service_contract.md b/docs/service_contract.md index b82e19a1..2974242e 100644 --- a/docs/service_contract.md +++ b/docs/service_contract.md @@ -75,6 +75,7 @@ option (dev.restate.ext.service_type) = SINGLETON; For keyed services, you're required to specify in every input message the field to use as key. To mark a field as key, annotate it with `dev.restate.ext.field`. Make sure that: +* There is only one key field. * The field type is either a primitive or a message. Repeated field and maps are not supported. * The field type is the same for every method input message of the same service. diff --git a/docs/service_type.md b/docs/service_type.md index bbb1287b..d7d6b50c 100644 --- a/docs/service_type.md +++ b/docs/service_type.md @@ -16,7 +16,7 @@ To define the service type and key, check the [service contract](./service_contr ## Keyed service -Keyed services allow to shard state and workload by a user-defined key. Each key will have its own invocations queue and its own subset of the service state. There is at most one invocation per key, but there can be multiple invocations to the same service with different keys executing concurrently. +Keyed services allow to shard state and workload by a user-defined key. Each key will have its own invocations queue and its own state. There is at most one invocation per key, but there can be multiple invocations to the same service with different keys executing concurrently. You can think of a keyed service as a class, and of a service instance as an object instance of that class. The key is the field that **uniquely** identifies that instance, and state entries are the other fields of that class.