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

Design documentation for adding a raw-FFI thread manager #31

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
38791cf
Add raw-ffi design
acarbonetto Nov 1, 2023
29e2244
Update to add Shachars changes
acarbonetto Nov 1, 2023
32b751e
Update design documentation
acarbonetto Nov 20, 2023
799b248
Update docs
acarbonetto Nov 20, 2023
a86e424
Add API design doc
jonathanl-bq Nov 21, 2023
265daa0
Update section on supported commands in API design doc
jonathanl-bq Nov 21, 2023
8d43095
Update API design doc with more details
jonathanl-bq Nov 22, 2023
11ea2c7
Update type handling policy in API design doc
jonathanl-bq Nov 22, 2023
d7325a1
Push update
acarbonetto Nov 22, 2023
fe1690a
Update API design doc with Routing info
jonathanl-bq Nov 24, 2023
744e92f
Add example showing how executeRaw would work to API design doc
jonathanl-bq Nov 24, 2023
56ebe90
Add Redis to Java and Go encoding
acarbonetto Nov 26, 2023
64e034a
Change to supporting RESP2 instead of RESP3 for now
jonathanl-bq Nov 27, 2023
2d688c1
Add go and java-specific language
acarbonetto Nov 27, 2023
f6b702b
Clean up section on supported commands in API design doc
jonathanl-bq Nov 29, 2023
af4e2a4
Fix typo in API design doc
jonathanl-bq Nov 29, 2023
df00c54
Update docs/design-api.md
jonathanl-bq Nov 30, 2023
ce3eb6c
Add some more details to API design
jonathanl-bq Nov 30, 2023
28c672b
Add java design documentation
acarbonetto Dec 13, 2023
91490bc
Add use-cases as examples of using the API
acarbonetto Dec 15, 2023
7058b02
Add more examples; return Type directly
acarbonetto Dec 20, 2023
f339390
Update customCommand use case
acarbonetto Dec 20, 2023
c4a13da
Update transactional use-cases
acarbonetto Dec 20, 2023
8bb74ba
Add Go API documentation
aaron-congo Jan 23, 2024
eece5f9
add missing period
aaron-congo Jan 23, 2024
73ba55b
Address PR feedback
aaron-congo Jan 23, 2024
b88cbbd
Update struct diagram
aaron-congo Jan 24, 2024
30406bf
PR suggestions
aaron-congo Jan 24, 2024
041bd39
Add documentation for the Go API design
aaron-congo Jan 24, 2024
a25467e
Add documentation for the Go FFI design
aaron-congo Jan 26, 2024
0062ed9
Update diagrams so that maps and arrays of Redis values include an en…
aaron-congo Jan 26, 2024
ae090e6
Fix mistakes in the FFI request success struct diagram
aaron-congo Jan 26, 2024
a852c04
Scale up diagrams to be more readable
aaron-congo Jan 27, 2024
50bc303
Address PR feedback
aaron-congo Jan 30, 2024
fdd659a
Increase size of API struct diagram to make it more readable
aaron-congo Jan 30, 2024
c3b7ae6
Update request success struct diagram
aaron-congo Jan 30, 2024
d5edd9d
Update connection sequence diagram
aaron-congo Jan 30, 2024
3bd84ba
Update connection sequence diagram
aaron-congo Jan 30, 2024
26bafc1
Add glide-core to connection sequence diagram
aaron-congo Jan 31, 2024
765d220
Add documentation for the Go FFI design
aaron-congo Jan 31, 2024
969a896
Update Go use cases with current configuration implementation
aaron-congo Feb 24, 2024
3896446
Update Go use cases to use config without pointer fields
aaron-congo Feb 27, 2024
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
117 changes: 117 additions & 0 deletions docs/design-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
API Design

# Client Wrapper API design doc

## API requirements:
- The API will be thread-safe.

Choose a reason for hiding this comment

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

may be use present instead of future?

- The API will accept as inputs all of [RESP2 types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md). We plan to add support for RESP3 types when they are available.
- The API will attempt authentication, topology refreshes, reconnections, etc., automatically. In case of failures concrete errors will be returned to the user.
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved

## Command Interface

### Unix Domain Socket solution
For clients based on Unix Domain Sockets (UDS), we will use the existing Rust-core protobuf messages for creating a connection, sending requests, and receiving responses. Supported commands are enumerated in the [protobuf definition for requests](../babushka-core/src/protobuf/redis_request.proto) and we will support them as they are added in the future. Note: the `CustomCommand` request type is also adequate for all commands. As defined in the [protobuf definition for responses](../babushka-core/src/protobuf/response.proto), client wrappers will receive data as a pointer, which can be passed to Rust to marshal the data back into the wrapper language’s native data types. In the future, we plan on optimizing this approach such that small data is returned as an object and large data is returned as a pointer in order to reduce the overhead of FFI calls in the case of small data.

Choose a reason for hiding this comment

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

Reword the first sentense - UDS is a transport, not a base for a client


Transactions will be handled by adding a list of `Command`s to the protobuf request. The response will be a `redis::Value::Bulk`, which should be handled in the same Rust function that marshals the data back into the wrapper language's native data types. This is handled by storing the results in a collection type native to the wrapper language.

When running Redis in Cluster Mode, several routing options will be provided. These are all specified in the protobuf request. The various options are detailed below in the ["Routing Options" section](#routing-options). We will also provide a separate client for handling Cluster Mode responses, which will convert the list of values and nodes into a map, as is done in existing client wrappers.

### Raw FFI solution
jonathanl-bq marked this conversation as resolved.
Show resolved Hide resolved
For clients using a raw FFI solution, in Rust, we will expose a general command that is able to take any command and arguments as strings.

Like in the UDS solution, we will support a separate client for Cluster Mode.

We have 2 options for passing the command, arguments, and any additional configuration to the Rust core from the wrapper language:

#### Protobuf
The wrapper language will pass the commands, arguments, and configuration as protobuf messages using the same definitions as in the UDS solution.

Transactions will be handled by adding a list of `Command`s to the protobuf request. The response will be a `redis::Value::Bulk`, which can be marshalled into a C array of values before being passed from Rust to the wrapper language. The wrapper language is responsible for converting the array of results to its own native collection type.

Cluster Mode support is the same here as in the UDS solution detailed above.

Pros:
- We get to reuse the protobuf definitions, meaning fewer files to update if we make changes to the protobuf definitions
Copy link
Collaborator

Choose a reason for hiding this comment

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

All Redis commands can be presented as a simple string array, so passing protobuf messages from the wrapper to the core adds unnecessary complication when we're talking about a raw FFI solution

Copy link
Collaborator

Choose a reason for hiding this comment

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

e.g. we'll have a generic execute_command function in rust that excepts a string array and all FFI functions from the wrapper will call it

- May be simpler to implement compared to the C data types solution, since we do not need to define our own C data types

Cons:
- There is additional overhead from marshalling data to and from protobuf, which could impact performance significantly

#### C Data Types
The wrapper language will pass commands, arguments, and configuration as C data types.

Transactions will be handled by passing a C array of an array of arguments to Rust from the wrapper language. The response will be a `redis::Value::Bulk`, which can be marshalled in the same way as explained in the protobuf solution.

For Cluster Mode support, [routing options](#routing-options) will be defined as C enums and structs. Like in the protobuf solution, we will provide a separate client for handling Cluster Mode responses, which will convert the list of values and nodes into a map.

Pros:
- No additional overhead from marshalling to and from protobuf, so this should perform better
- May be simpler to implement compared to protobuf solution, since it can be tricky to construct protobuf messages in a performant way and we have to add a varint to the messages as well

Cons:
- Would add an additional file to maintain containing the C definitions (only one file though, since we could share between all raw FFI solutions), which we would need to update every time we want to update the existing protobuf definitions

We will be testing both approaches to see which is easier to implement, as well as the performance impact before deciding on a solution.

To marshal Redis data types back into the corresponding types for the wrapper language, we will convert them into appropriate C types, which can then be translated by the wrapper language into its native data types. Here is what a Redis result might look like:
```
typedef struct redisValue {
enum {NIL, INT, DATA, STATUS, BULK, OKAY} kind;
union Payload {
long intValue;
unsigned char *dataValue;
char *statusValue;
struct redisValue *bulkValue;
} payload;
} RedisValue
```

## Routing Options
We will be supporting routing Redis requests to all nodes, all primary nodes, or a random node. For more specific routing to a node, we will also allow sending a request to a primary or replica node with a specified hash slot or key. When the wrapper given a key route, the key is passed to the Rust core, which will find the corresponding hash slot for it.

Choose a reason for hiding this comment

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

We will

or

We should

or

The client should/will/etc

Also: future or present? (here and below)


## Supported Commands
We will be supporting all Redis commands. Commands with higher usage will be prioritized, as determined by usage numbers from AWS ElastiCache usage logs.

Two different methods of sending commands will be supported:

### Custom Command
We will expose an `executeRaw` method that does no validation of the input types or command on the client side, leaving it up to Redis to reject the command should it be malformed. This gives the user the flexibility to send any type of command they want, including ones not officially supported yet.

For example, if a user wants to implement support for the Redis ZADD command in Java, their implementation might look something like this:
```java
public Long zadd(K key, double score, V member) throws RequestException {
string[] args = { key.toString(), score.toString(), member.toString() };
return (Long) executeRaw(args);
}
```

where `executeRaw` has the following signature:
```java
public Object executeRaw(string[] args) throws RequestException
```

### Explicitly Supported Command
We will expose separate methods for each supported command. There will be a separate version of each method for transactions, as well as another version for Cluster Mode clients. For statically typed languages, we will leverage the compiler of the wrapper language to validate the types of the command arguments as much as possible. Since wrappers should be as lightweight as possible, we will be performing very few to no checks for proper typing for non-statically typed languages.

## Errors

Choose a reason for hiding this comment

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

Reference where these errors came from

ClosingError: Errors that report that the client has closed and is no longer usable.

Choose a reason for hiding this comment

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

Suggested change
ClosingError: Errors that report that the client has closed and is no longer usable.
`ClosingError`: Errors that report that the client has closed and is no longer usable.

And below


RedisError: Errors that were reported during a request.

TimeoutError: Errors that are thrown when a request times out.

ExecAbortError: Errors that are thrown when a transaction is aborted.

ConnectionError: Errors that are thrown when a connection disconnects. These errors can be temporary, as the client will attempt to reconnect.

Errors returned are subject to change as we update the protobuf definitions.

## Java Specific Details
We will be using the UDS solution for communication between the wrapper and the Rust core. This thin layer is implemented using the [jni-rs library](https://github.com/jni-rs/jni-rs) to start the socket listener and marshal Redis values into native Java data types.

Errors in Rust are represented as Algebraic Data Types, which are not supported in Java by default (at least not in the versions of Java we want to support). Instead, we utilise the [jni-rs library](https://github.com/jni-rs/jni-rs) to throw Java `Exception`s where we receive errors from Redis.

## Golang Specific Details
We will be using a raw FFI solution for communication between the wrapper and the Rust core. The FFI layer is implemented using `cgo`.

Golang does not support Algebraic Data Types, so errors will need to be returned as struct values.
252 changes: 252 additions & 0 deletions docs/design-go-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# Go API Design documentation

## Overview

This document presents the high-level user API for the Go-Wrapper client library. Specifically, it demonstrates how the user connects to Redis, executes requests, receives responses, and checks for errors.

## Requirements

- The minimum supported Go version will be 1.18. This version introduces support for generics, including type constraints and type sets.
- The API will be thread-safe.
- The API will accept as inputs all [RESP3 types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md).
- The API will attempt authentication, topology refreshes, reconnections, etc., automatically. In case of failures concrete errors will be returned to the user.

# Use Cases

### Case 1: Create Redis client and connect

```go
var config *api.RedisClientConfiguration = api.NewRedisClientConfiguration().
WithAddress(&api.NodeAddress{"some_host", 1234}).
WithUseTLS(true)

// Create a client and connect
var client *api.RedisClient
var err error
client, err = api.CreateClient(config)
```

### Case 2: Connection to Redis fails with ClosingError
```go
var client *api.RedisClient
var err error
client, err := api.CreateClient(config)

// User can check specifically for a ClosingError:
if err != nil {
closingErr, isClosingErr := err.(api.ClosingError)
if isClosingErr {
log.Fatal("Failed to connect to Redis: " + closingErr.Error())
}
}
```

### Case 3: Connect to Redis with deferred cleanup
```go
func connectAndGet(config *RedisClientConfiguration, key string) string {
var client *api.RedisClient
var err error
client, err = api.CreateClient(config)
if err != nil {
log.Fatal("Redis client failed with: " + err.Error())
}

// client.Close() is executed when the function exits.
// The client is available until the end of the function.
defer client.Close()

result, err := client.Get(key)
if err != nil {
// If we enter this branch, client.Close() will be executed after logging this message.
log.Fatal("Redis Get failed with: " + err.Error())
}

// client.Close() will be executed when the result is returned.
return result
}
```

### Case 4: Connect to Redis cluster
```go
var config *api.RedisClusterClientConfiguration
config = api.NewRedisClusterClientConfiguration().
WithAddress(&api.NodeAddress{Host: "host1", Port: 1234}).
WithAddress(&api.NodeAddress{Host: "host2", Port: 1234}).
WithUseTLS(true)

var client *api.RedisClusterClient
var err error
client, err = api.CreateClusterClient(config)
```

### Case 5: Get(key) from connected RedisClient
```go
result, err := client.Get("apples")
fmt.Println("The value associated with 'apples' is: " + result)
```

### Case 6: Set(key, value) from connected RedisClient
```go
// Without setOptions
err := client.Set("apples", "oranges")

// With setOptions
var setOptions *api.SetOptions
setOptions = api.NewSetOptions().
WithReturnOldValue(true)
oldValue, err := client.SetWithOptions("apples", "oranges", setOptions)
```

### Case 7: Get(key) from a disconnected RedisClient
Return a api.ConnectionError if the RedisClient fails to connect to Redis
```go
result, err := client.Get("apples")
if err != nil {
connErr, isConnErr := err.(api.ConnectionError)
if isConnErr {
log.Fatal("RedisClient get failed with: " + connErr.Error())
}
}
```

### Case 8: Send custom command to RedisClient
```go
var result interface{}
var err error

result, err = client.CustomCommand([]{"GET", "apples"})
if err != nil {
log.Fatal("RedisClient failed to execute custom command with: " + err.Error())
}

strResult, isString := result.(string)
if !isString {
log.Fatal("Expected result to be of type string but the actual type was: " + reflect.TypeOf(result))
}
```

### Case 9: Send transaction to RedisClient
```go
transaction := api.NewTransaction()
transaction.Get("apples")
transaction.Get("pears")
transaction.Set("cherries", "Bing")

var result []interface{}
var err error
result, err = client.Exec(transaction)
if err != nil {
log.Fatal("Redis client transaction failed with: " + err.Error())
}

firstResponse := result[0] // evaluates to a string
secondResponse := result[1] // evaluates to a string
thirdResponse := result[2] // evaluates to nil
```

### Case 10: Send Get request to a RedisClusterClient with one address
```go
var config *api.ClusterClientConfiguration
config = api.NewClusterClientConfiguration().
WithAddress(&api.NodeAddress{Host: "some_host", Port: 1234}).
WithUseTLS(true)

// Create a client and connect
var client *api.RedisClusterClient
var err error
client, err = api.CreateClusterClient(config)
if err != nil {
log.Fatal("Redis client failed with: " + err.Error())
}

result, err := client.Get("apples")
```

### Case 11: Send Ping request to a RedisClusterClient with multiple addresses
```go
var config *api.ClusterClientConfiguration
config = api.NewClusterClientConfiguration().
WithAddress(&api.NodeAddress{Host: "host1", Port: 1234}).
WithAddress(&api.NodeAddress{Host: "host2", Port: 1234}).
WithUseTLS(true)

// Create a client and connect
var client *api.RedisClusterClient
var err error
client, err = api.CreateClusterClient(config)
if err != nil {
log.Fatal("Redis client failed with: " + err.Error())
}

// Without message or route
result, err := client.Ping()

// With message
result, err := client.PingWithMessage("Ping received")

// With route
result, err := client.PingWithRoute(api.NewRoute(api.AllNodes))

// With message and route
result, err := client.PingWithMessageAndRoute("Ping received", api.NewRoute(api.AllNodes))
```

### Case 12: Get(key) encounters a TimeoutError
```go
result, err := client.Get("apples")
if err != nil {
timeoutErr, isTimeoutErr := err.(api.TimeoutError)
if isTimeoutErr {
// Handle error as desired
}
}
```

### Case 13: Get(key) encounters a ConnectionError
```go
result, err := client.Get("apples")
if err != nil {
connErr, isConnErr := err.(api.ConnectionError)
if isConnErr {
// Handle error as desired
}
}
```

# API Design

## Presentation

![API Design](img/design-go-api.svg)

# FFI Design

## Client creation and connection

### Sequence diagram

![FFI Connection Sequence Diagram](img/FFI-conn-sequence.svg)

### Struct diagram

![FFI Connection Struct Diagram](img/FFI-conn-struct-diagram.svg)

## Redis request succeeds

### Sequence diagram

![FFI Request Success Sequence Diagram](img/FFI-request-success-sequence.svg)

### Struct diagram

![FFI Request Success Struct Diagram](img/FFI-request-success-struct-diagram.svg)

## Redis request fails

### Sequence diagram

![FFI Request Failure Sequence Diagram](img/FFI-request-failure-sequence.svg)

### Struct diagram

![FFI Request Failure Struct Diagram](img/FFI-request-failure-struct-diagram.svg)
Loading