rainier_drivers/lib.rs
1//! # rainier-drivers
2//!
3//! **Every external service Rainier talks to is interfaced here, and nowhere
4//! else.** Redis, Memcached, S3, SQS, DynamoDB — the protocol knowledge, the
5//! service operations, the limits, the quirks and the error mapping all live in
6//! this crate. The crates that define ports own thin adapters onto it.
7//!
8//! ## The paradigm
9//!
10//! ```text
11//! rainier-cache rainier-queue rainier-filesystem
12//! (and rainier-database, over rainier-orm's Executor port)
13//! ┌────────────────┐ ┌───────────────┐ ┌──────────────────┐
14//! │ Cache (port) │ │ Queue (port) │ │ Filesystem (port)│
15//! │ RedisCache │ │ SqsQueue │ │ S3Filesystem │
16//! │ DynamoDbCache │ │ DatabaseQueue│ │ LocalFilesystem │
17//! └───────┬────────┘ └──────┬────────┘ └────────┬─────────┘
18//! │ adapters — they translate, only │
19//! └──────────────┬──────────────────────────────┘
20//! ▼
21//! ┌──────────────────┐
22//! │ rainier-drivers │ service clients, no port traits
23//! │ RedisClient │
24//! │ MemcachedClient │
25//! │ S3Client │
26//! │ SqsClient │
27//! │ DynamoDbClient │
28//! │ SeaOrmExecutor │
29//! │ D1Executor │
30//! └──────────────────┘
31//! ```
32//!
33//! Two rules, and they are what make the split hold:
34//!
35//! **1. A driver never names a port trait.** `SqsClient` has never heard of
36//! `Queue`; `RedisClient` has never heard of `Cache`. They expose the
37//! *service's* vocabulary — `send`, `receive`, `change_visibility`, `set_nx`,
38//! `incr_by` — and nothing about what a caller intends to build from it.
39//!
40//! That is not stylistic. If a driver implemented `Queue`, this crate would
41//! depend on `rainier-queue`, which depends on this crate: a cycle. Keeping the
42//! trait out is what lets **one** crate hold every connector while each port
43//! keeps its own adapter.
44//!
45//! **2. An adapter holds no protocol knowledge.** `SqsQueue` in `rainier-queue`
46//! decides what an attempt means and where a receipt handle is stashed on a job;
47//! it does not know SQS's attribute names, its fifteen-minute delay cap, or that
48//! changing a visibility timeout is how a message gets released. Those are here.
49//!
50//! ## What this buys
51//!
52//! **One place to look.** "How does Rainier talk to Redis" has one answer, and it
53//! is not spread across the cache and the queue.
54//!
55//! **One client, shared.** Redis is wanted by the cache *and* the queue. Without
56//! a shared home each would carry its own client, its own copy of the protocol
57//! code, and its own URL parsing — and an application would configure Redis twice
58//! and open two sets of connections to one server. The same goes for AWS
59//! credentials, which `AwsConnector` resolves once for S3, SQS and DynamoDB
60//! together.
61//!
62//! **Testable in the right halves.** A driver test asks *does this speak the
63//! protocol correctly* — the Memcached client is tested against a
64//! stub server, including a value containing `CRLF` that a line-based reader
65//! would truncate. An adapter test asks *does this translate correctly*, and
66//! needs no network at all.
67//!
68//! ## Where the line falls, concretely
69//!
70//! | Belongs here | Belongs in the port's crate |
71//! |---|---|
72//! | opening a client from a URL | deciding what a key means |
73//! | `SET key value PX 60000` | choosing the TTL |
74//! | SQS's fifteen-minute delay cap | what to do when a job exceeds it |
75//! | DynamoDB's TTL lagging 48 hours | filtering an expired read as a miss |
76//! | S3 reporting absence two different ways | treating absence as `Ok(None)` |
77//! | turning a driver error into an [`Error`](rainier_support::Error) | retry policy |
78//!
79//! The rule that settles an argument: **this crate must not know what is being
80//! stored, only how to store it.**
81//!
82//! ## Errors are `503`, not `500`
83//!
84//! Every driver failure is
85//! [`ServiceUnavailable`](rainier_support::ErrorKind::ServiceUnavailable). A cache
86//! or queue being unreachable is a **dependency outage**: retryable, somebody's to
87//! page about, and not a bug in the request that happened to hit it. A `500` puts
88//! it in the wrong bucket on every dashboard you have.
89//!
90//! Driver messages are also not passed through verbatim — a Redis or Memcached
91//! error frequently contains the connection string, and therefore the password.
92//!
93//! ## Official SDKs where they exist
94//!
95//! The AWS services use the **official AWS SDK** rather than signed requests of
96//! our own. An earlier version of this crate hand-rolled SigV4; it worked, and it
97//! could only read static credentials from environment variables. Real workloads
98//! use an EC2 instance role, an ECS task role, EKS IRSA, SSO or a profile — each
99//! needing a provider that discovers, caches and **refreshes** a temporary
100//! credential before it expires. Reimplementing that is reimplementing the
101//! interesting half.
102//!
103//! The one client written here rather than taken from a crate is
104//! Memcached, for a stated reason: the obvious candidate does not
105//! compile on Windows, and six commands of a text protocol is cheaper to carry
106//! than a portability problem.
107//!
108//! ## Features
109//!
110//! | Feature | Gives you |
111//! |---|---|
112//! | `redis-driver` | `RedisConnector` and `RedisClient` against one server |
113//! | `redis-cluster` | …and against a sharded cluster |
114//! | `memcached` | `MemcachedConnector` |
115//! | `kafka` | `KafkaConnector` and `KafkaClient` |
116//! | `kafka-tls` | …and TLS, for a managed cluster |
117//! | `aws` | `AwsConnector` — one AWS configuration for every service |
118//! | `aws-s3` | `S3Client` |
119//! | `aws-sqs` | `SqsClient` |
120//! | `aws-dynamodb` | `DynamoDbClient` |
121//! | `sea-orm-executor` | native MySQL / Postgres / SQLite for [`rainier-orm`] |
122//! | `d1-http` | Cloudflare D1 |
123//! | `libsql-http` | libSQL / Turso |
124//!
125//! [`rainier-orm`]: https://docs.rs/rainier-orm
126//!
127//! All off by default, so an application compiles only the clients it uses.
128
129#![deny(missing_docs)]
130#![deny(rustdoc::broken_intra_doc_links)]
131
132#[cfg(feature = "aws")]
133pub mod aws;
134#[cfg(feature = "aws-dynamodb")]
135pub mod dynamodb;
136#[cfg(feature = "kafka")]
137pub mod kafka;
138#[cfg(feature = "memcached")]
139pub mod memcached;
140#[cfg(feature = "redis-driver")]
141pub mod redis;
142#[cfg(feature = "redis-driver")]
143pub mod redis_streams;
144#[cfg(feature = "aws-s3")]
145pub mod s3;
146#[cfg(feature = "sql")]
147pub mod sql;
148#[cfg(feature = "aws-sqs")]
149pub mod sqs;
150
151#[cfg(feature = "aws")]
152pub use aws::{sdk_error, AwsConnector};
153#[cfg(feature = "aws-dynamodb")]
154pub use dynamodb::{DynamoDbClient, DynamoItem};
155#[cfg(feature = "kafka")]
156pub use kafka::{
157 partition_for_key, KafkaClient, KafkaConnector, KafkaCredentials, KafkaFetch, KafkaMessage,
158 KafkaOffset, KafkaPosition, KafkaRecord, SaslMechanism,
159};
160#[cfg(feature = "memcached")]
161pub use memcached::{
162 check_key, expiry_seconds, MemcachedConnection, MemcachedConnector, MemcachedGuard, Stored,
163 MAX_RELATIVE_TTL,
164};
165#[cfg(feature = "redis-driver")]
166pub use redis::{RedisClient, RedisConnection, RedisConnector};
167#[cfg(feature = "redis-driver")]
168pub use redis_streams::StreamEntry;
169#[cfg(feature = "aws-s3")]
170pub use s3::{S3Client, S3Head, S3Object};
171#[cfg(feature = "aws-sqs")]
172pub use sqs::{SqsClient, SqsMessage};
173
174/// The Redis client, re-exported so every consumer resolves one version and can
175/// build commands this crate does not wrap.
176#[cfg(feature = "redis-driver")]
177pub use ::redis as redis_client;
178
179#[cfg(all(
180 test,
181 not(any(feature = "redis-driver", feature = "memcached", feature = "aws", feature = "kafka"))
182))]
183mod tests {
184 /// With no driver enabled the crate is empty, which is the point of the
185 /// feature flags — this asserts it still compiles that way.
186 #[test]
187 fn the_crate_compiles_with_no_drivers() {}
188}