Skip to content
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

Merged
merged 3 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions docs/service_contract.md
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];
}
Copy link
Contributor

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...

Copy link
Contributor Author

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


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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more general thought. When we use the word Connect I always wonder whether it's not clearer to use plain HTTP because from the perspective of the end user, connect is an implementation detail right?
But I agree that if we mention it anywhere, this should be the place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, plain HTTP doesn't mean much if you don't specify how to put the request in the http envelope, in particular for protobuf users might be strange to just read plain HTTP.

For me Connect is not the implementation detail, but rather HTTP is, as it's the transport of Connect. Also we use the Connect name all around the docs, in particular in the ingress docs. I would keep it as it is.


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.
64 changes: 64 additions & 0 deletions docs/service_type.md
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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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...

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.