Skip to main content

Crate rainier_drivers

Crate rainier_drivers 

Source
Expand description

§rainier-drivers

Every external service Rainier talks to is interfaced here, and nowhere else. Redis, Memcached, S3, SQS, DynamoDB — the protocol knowledge, the service operations, the limits, the quirks and the error mapping all live in this crate. The crates that define ports own thin adapters onto it.

§The paradigm

  rainier-cache          rainier-queue         rainier-filesystem
  (and rainier-database, over rainier-orm's Executor port)
  ┌────────────────┐     ┌───────────────┐     ┌──────────────────┐
  │ Cache (port)   │     │ Queue (port)  │     │ Filesystem (port)│
  │  RedisCache    │     │  SqsQueue     │     │  S3Filesystem    │
  │  DynamoDbCache │     │  DatabaseQueue│     │  LocalFilesystem │
  └───────┬────────┘     └──────┬────────┘     └────────┬─────────┘
          │      adapters — they translate, only        │
          └──────────────┬──────────────────────────────┘
                         ▼
                ┌──────────────────┐
                │ rainier-drivers  │  service clients, no port traits
                │  RedisClient     │
                │  MemcachedClient │
                │  S3Client        │
                │  SqsClient       │
                │  DynamoDbClient  │
                │  SeaOrmExecutor  │
                │  D1Executor      │
                └──────────────────┘

Two rules, and they are what make the split hold:

1. A driver never names a port trait. SqsClient has never heard of Queue; RedisClient has never heard of Cache. They expose the service’s vocabulary — send, receive, change_visibility, set_nx, incr_by — and nothing about what a caller intends to build from it.

That is not stylistic. If a driver implemented Queue, this crate would depend on rainier-queue, which depends on this crate: a cycle. Keeping the trait out is what lets one crate hold every connector while each port keeps its own adapter.

2. An adapter holds no protocol knowledge. SqsQueue in rainier-queue decides what an attempt means and where a receipt handle is stashed on a job; it does not know SQS’s attribute names, its fifteen-minute delay cap, or that changing a visibility timeout is how a message gets released. Those are here.

§What this buys

One place to look. “How does Rainier talk to Redis” has one answer, and it is not spread across the cache and the queue.

One client, shared. Redis is wanted by the cache and the queue. Without a shared home each would carry its own client, its own copy of the protocol code, and its own URL parsing — and an application would configure Redis twice and open two sets of connections to one server. The same goes for AWS credentials, which AwsConnector resolves once for S3, SQS and DynamoDB together.

Testable in the right halves. A driver test asks does this speak the protocol correctly — the Memcached client is tested against a stub server, including a value containing CRLF that a line-based reader would truncate. An adapter test asks does this translate correctly, and needs no network at all.

§Where the line falls, concretely

Belongs hereBelongs in the port’s crate
opening a client from a URLdeciding what a key means
SET key value PX 60000choosing the TTL
SQS’s fifteen-minute delay capwhat to do when a job exceeds it
DynamoDB’s TTL lagging 48 hoursfiltering an expired read as a miss
S3 reporting absence two different waystreating absence as Ok(None)
turning a driver error into an Errorretry policy

The rule that settles an argument: this crate must not know what is being stored, only how to store it.

§Errors are 503, not 500

Every driver failure is ServiceUnavailable. A cache or queue being unreachable is a dependency outage: retryable, somebody’s to page about, and not a bug in the request that happened to hit it. A 500 puts it in the wrong bucket on every dashboard you have.

Driver messages are also not passed through verbatim — a Redis or Memcached error frequently contains the connection string, and therefore the password.

§Official SDKs where they exist

The AWS services use the official AWS SDK rather than signed requests of our own. An earlier version of this crate hand-rolled SigV4; it worked, and it could only read static credentials from environment variables. Real workloads use an EC2 instance role, an ECS task role, EKS IRSA, SSO or a profile — each needing a provider that discovers, caches and refreshes a temporary credential before it expires. Reimplementing that is reimplementing the interesting half.

The one client written here rather than taken from a crate is Memcached, for a stated reason: the obvious candidate does not compile on Windows, and six commands of a text protocol is cheaper to carry than a portability problem.

§Features

FeatureGives you
redis-driverRedisConnector and RedisClient against one server
redis-cluster…and against a sharded cluster
memcachedMemcachedConnector
kafkaKafkaConnector and KafkaClient
kafka-tls…and TLS, for a managed cluster
awsAwsConnector — one AWS configuration for every service
aws-s3S3Client
aws-sqsSqsClient
aws-dynamodbDynamoDbClient
sea-orm-executornative MySQL / Postgres / SQLite for rainier-orm
d1-httpCloudflare D1
libsql-httplibSQL / Turso

All off by default, so an application compiles only the clients it uses.