1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! # 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
//!
//! ```text
//! 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 here | Belongs in the port's crate |
//! |---|---|
//! | opening a client from a URL | deciding what a key means |
//! | `SET key value PX 60000` | choosing the TTL |
//! | SQS's fifteen-minute delay cap | what to do when a job exceeds it |
//! | DynamoDB's TTL lagging 48 hours | filtering an expired read as a miss |
//! | S3 reporting absence two different ways | treating absence as `Ok(None)` |
//! | turning a driver error into an [`Error`](rainier_support::Error) | retry 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`](rainier_support::ErrorKind::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
//!
//! | Feature | Gives you |
//! |---|---|
//! | `redis-driver` | `RedisConnector` and `RedisClient` against one server |
//! | `redis-cluster` | …and against a sharded cluster |
//! | `memcached` | `MemcachedConnector` |
//! | `kafka` | `KafkaConnector` and `KafkaClient` |
//! | `kafka-tls` | …and TLS, for a managed cluster |
//! | `aws` | `AwsConnector` — one AWS configuration for every service |
//! | `aws-s3` | `S3Client` |
//! | `aws-sqs` | `SqsClient` |
//! | `aws-dynamodb` | `DynamoDbClient` |
//! | `sea-orm-executor` | native MySQL / Postgres / SQLite for [`rainier-orm`] |
//! | `d1-http` | Cloudflare D1 |
//! | `libsql-http` | libSQL / Turso |
//!
//! [`rainier-orm`]: https://docs.rs/rainier-orm
//!
//! All off by default, so an application compiles only the clients it uses.
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use StreamEntry;
pub use ;
pub use ;
/// The Redis client, re-exported so every consumer resolves one version and can
/// build commands this crate does not wrap.
pub use redis as redis_client;