-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add documentation about service type and service contract #78
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
--- | ||
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: | ||
|
||
* 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. | ||
|
||
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 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A more general thought. When we use the word There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure, For me |
||
|
||
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
--- | ||
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 no shared state among invocations. | ||
|
||
To define the service type and key, check the [service contract](./service_contract.md) documentation. | ||
|
||
## 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 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. | ||
|
||
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 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); | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this is also a cycle, just with only 2 participants... But I am being difficult... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depends on your definition of cycle: for me a cycle is when I end up in a deadlock due to only my own actions, while this cross deadlock can happen when two services are independently doing their own thing, perhaps from different ingress requests, and cross-invoking each other they end up in the deadlock. But I might be fuzzy on the names here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I forgot about that option |
||
* Cyclical deadlock: A calls B, and B calls C, and C calls A again. | ||
|
||
## Singleton service | ||
|
||
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. | ||
|
||
:::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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering whether we shouldn't start with unkeyed services in our description because they are the simplest type of services. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsure, it's strange to start a documentation page from the negation of another type. This page is not a "starter" page, so starting from what's easy is less of a concern IMO. It also reads better this way, because explains service types as "one variation of the other". I would keep as it is and perhaps when reorganizing the docs #66 we could give another pass at this. |
||
|
||
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 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it not possible to just put the tag on two fields?
In that case I think we should mention that... Some users might depend on whatever they get from external systems and might not be able to choose the message format. But I guess they can put an unkeyed translator service in between...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some doc to specify that there can be only one key field