1use gonzalo_core::{
13 CoreError, DeleteResult, PutResult, Record, RecordKey, Result, Revision, Store,
14};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicUsize, Ordering};
17
18pub struct Dispatcher {
20 replicas: Vec<Arc<dyn Store>>,
21 next: AtomicUsize,
22}
23
24impl Dispatcher {
25 pub fn new(replicas: Vec<Arc<dyn Store>>) -> Self {
27 assert!(
28 !replicas.is_empty(),
29 "dispatcher needs at least one replica"
30 );
31 Self {
32 replicas,
33 next: AtomicUsize::new(0),
34 }
35 }
36
37 pub fn len(&self) -> usize {
39 self.replicas.len()
40 }
41
42 pub fn is_empty(&self) -> bool {
44 self.replicas.is_empty()
45 }
46
47 pub fn replicas(&self) -> &[Arc<dyn Store>] {
50 &self.replicas
51 }
52
53 fn start(&self) -> usize {
54 self.next.fetch_add(1, Ordering::Relaxed) % self.replicas.len()
55 }
56
57 async fn with_failover<T, F, Fut>(&self, op: F) -> Result<T>
62 where
63 F: Fn(Arc<dyn Store>) -> Fut,
64 Fut: std::future::Future<Output = Result<T>>,
65 {
66 let n = self.replicas.len();
67 let start = self.start();
68 let mut last_err = None;
69 for offset in 0..n {
70 match op(self.replicas[(start + offset) % n].clone()).await {
71 Err(e @ CoreError::Backend(_)) => last_err = Some(e),
72 other => return other,
73 }
74 }
75 Err(last_err.expect("at least one replica was attempted"))
76 }
77
78 pub async fn get(&self, key: &RecordKey) -> Result<Option<Record>> {
80 self.with_failover(|s| {
81 let key = key.clone();
82 async move { s.get(&key).await }
83 })
84 .await
85 }
86
87 pub async fn put(&self, record: Record, expected: Option<Revision>) -> Result<PutResult> {
91 self.with_failover(|s| {
92 let record = record.clone();
93 let expected = expected.clone();
94 async move { s.put(record, expected).await }
95 })
96 .await
97 }
98
99 pub async fn delete(
104 &self,
105 key: &RecordKey,
106 expected: Option<Revision>,
107 ) -> Result<DeleteResult> {
108 self.with_failover(|s| {
109 let key = key.clone();
110 let expected = expected.clone();
111 async move { s.delete(&key, expected).await }
112 })
113 .await
114 }
115
116 pub async fn get_raw(&self, key: &RecordKey) -> Result<Option<Record>> {
118 self.with_failover(|s| {
119 let key = key.clone();
120 async move { s.get_raw(&key).await }
121 })
122 .await
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use async_trait::async_trait;
130 use gonzalo_core::{
131 Body, CoreError, DeleteResult, Identity, KeyPrefix, Meta, RecordKind, store::Conflict,
132 };
133 use std::collections::BTreeMap;
134 use std::sync::atomic::AtomicBool;
135
136 struct MockStore {
138 alive: AtomicBool,
139 calls: AtomicUsize,
140 }
141 impl MockStore {
142 fn alive() -> Arc<Self> {
143 Arc::new(Self {
144 alive: AtomicBool::new(true),
145 calls: AtomicUsize::new(0),
146 })
147 }
148 fn dead() -> Arc<Self> {
149 Arc::new(Self {
150 alive: AtomicBool::new(false),
151 calls: AtomicUsize::new(0),
152 })
153 }
154 fn calls(&self) -> usize {
155 self.calls.load(Ordering::SeqCst)
156 }
157 }
158 #[async_trait]
159 impl Store for MockStore {
160 async fn get(&self, _key: &RecordKey) -> Result<Option<Record>> {
161 self.calls.fetch_add(1, Ordering::SeqCst);
162 if self.alive.load(Ordering::SeqCst) {
163 Ok(None)
164 } else {
165 Err(CoreError::Backend("connection refused".into()))
166 }
167 }
168 async fn put(&self, _record: Record, _expected: Option<Revision>) -> Result<PutResult> {
169 self.calls.fetch_add(1, Ordering::SeqCst);
170 if self.alive.load(Ordering::SeqCst) {
171 Ok(PutResult::Committed(Revision::initial(b"x")))
172 } else {
173 Err(CoreError::Backend("connection refused".into()))
174 }
175 }
176 async fn list(&self, _prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
177 Ok(Vec::new())
178 }
179 async fn delete_as(
180 &self,
181 _key: &RecordKey,
182 _expected: Option<Revision>,
183 _author: Option<Identity>,
184 ) -> Result<DeleteResult> {
185 self.calls.fetch_add(1, Ordering::SeqCst);
186 if self.alive.load(Ordering::SeqCst) {
187 Ok(DeleteResult::Deleted)
188 } else {
189 Err(CoreError::Backend("connection refused".into()))
190 }
191 }
192 async fn put_raw(&self, record: Record, expected: Option<Revision>) -> Result<PutResult> {
193 <Self as Store>::put(self, record, expected).await
194 }
195 async fn get_raw(&self, key: &RecordKey) -> Result<Option<Record>> {
196 self.get(key).await
197 }
198 async fn list_raw(&self, prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
199 self.list(prefix).await
200 }
201 async fn purge(&self, key: &RecordKey, expected: Revision) -> Result<DeleteResult> {
202 self.delete(key, Some(expected)).await
203 }
204 }
205
206 fn as_store(m: &Arc<MockStore>) -> Arc<dyn Store> {
207 m.clone()
208 }
209
210 fn rec() -> Record {
211 let body = Body::Inline(b"x".to_vec());
212 Record {
213 revision: Revision::initial(body.bytes()),
214 parent: None,
215 body,
216 kind: RecordKind::Topic,
217 meta: Meta {
218 author: Identity::new("soak"),
219 origin_system: "soak".into(),
220 created: 0,
221 updated: 0,
222 labels: BTreeMap::new(),
223 },
224 links: Vec::new(),
225 key: RecordKey::new("ns", "col", "k"),
226 ancestors: Vec::new(),
227 deleted_at: None,
228 deleted_blob: None,
229 deleted_kind: None,
230 }
231 }
232
233 struct Answering {
236 calls: AtomicUsize,
237 put: fn() -> Result<PutResult>,
238 delete: fn() -> Result<DeleteResult>,
239 }
240 impl Answering {
241 fn new(put: fn() -> Result<PutResult>, delete: fn() -> Result<DeleteResult>) -> Arc<Self> {
242 Arc::new(Self {
243 calls: AtomicUsize::new(0),
244 put,
245 delete,
246 })
247 }
248 }
249 #[async_trait]
250 impl Store for Answering {
251 async fn get(&self, _key: &RecordKey) -> Result<Option<Record>> {
252 Ok(None)
253 }
254 async fn put(&self, _record: Record, _expected: Option<Revision>) -> Result<PutResult> {
255 self.calls.fetch_add(1, Ordering::SeqCst);
256 (self.put)()
257 }
258 async fn list(&self, _prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
259 Ok(Vec::new())
260 }
261 async fn delete_as(
262 &self,
263 _key: &RecordKey,
264 _expected: Option<Revision>,
265 _author: Option<Identity>,
266 ) -> Result<DeleteResult> {
267 self.calls.fetch_add(1, Ordering::SeqCst);
268 (self.delete)()
269 }
270 async fn get_raw(&self, _key: &RecordKey) -> Result<Option<Record>> {
271 Ok(None)
272 }
273 async fn put_raw(&self, _record: Record, _expected: Option<Revision>) -> Result<PutResult> {
274 Ok(PutResult::Committed(Revision::initial(b"x")))
275 }
276 async fn list_raw(&self, _prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
277 Ok(Vec::new())
278 }
279 async fn purge(&self, _key: &RecordKey, _expected: Revision) -> Result<DeleteResult> {
280 Ok(DeleteResult::Deleted)
281 }
282 }
283
284 #[tokio::test]
285 async fn failover_skips_dead_replica() {
286 let dead = MockStore::dead();
287 let live = MockStore::alive();
288 let d = Dispatcher::new(vec![as_store(&dead), as_store(&live)]);
289 let out = d.get(&RecordKey::new("ns", "col", "k")).await;
291 assert!(
292 out.is_ok(),
293 "should have failed over to the live replica: {out:?}"
294 );
295 assert_eq!(dead.calls(), 1, "dead replica was tried");
296 assert_eq!(live.calls(), 1, "then the live replica served it");
297 }
298
299 #[tokio::test]
300 async fn all_dead_returns_error() {
301 let a = MockStore::dead();
302 let b = MockStore::dead();
303 let d = Dispatcher::new(vec![as_store(&a), as_store(&b)]);
304 assert!(d.get(&RecordKey::new("ns", "col", "k")).await.is_err());
305 assert_eq!(a.calls() + b.calls(), 2, "both replicas were attempted");
306 }
307
308 #[tokio::test]
309 async fn live_replica_short_circuits() {
310 let a = MockStore::alive();
311 let b = MockStore::alive();
312 let c = MockStore::alive();
313 let d = Dispatcher::new(vec![as_store(&a), as_store(&b), as_store(&c)]);
314 let _ = d.get(&RecordKey::new("ns", "col", "k")).await.unwrap();
315 assert_eq!(
316 a.calls() + b.calls() + c.calls(),
317 1,
318 "exactly one replica served the op"
319 );
320 }
321
322 #[tokio::test]
323 async fn round_robin_distributes_load() {
324 let a = MockStore::alive();
325 let b = MockStore::alive();
326 let c = MockStore::alive();
327 let d = Dispatcher::new(vec![as_store(&a), as_store(&b), as_store(&c)]);
328 for _ in 0..3 {
329 d.get(&RecordKey::new("ns", "col", "k")).await.unwrap();
330 }
331 assert_eq!(
332 (a.calls(), b.calls(), c.calls()),
333 (1, 1, 1),
334 "round-robin over 3 replicas"
335 );
336 }
337
338 #[tokio::test]
339 async fn put_fails_over_too() {
340 let dead = MockStore::dead();
341 let live = MockStore::alive();
342 let d = Dispatcher::new(vec![as_store(&dead), as_store(&live)]);
343 let out = d.put(rec(), None).await;
344 assert!(
345 matches!(out, Ok(PutResult::Committed(_))),
346 "put failed over: {out:?}"
347 );
348 }
349
350 #[tokio::test]
351 async fn delete_fails_over_too() {
352 let dead = MockStore::dead();
353 let live = MockStore::alive();
354 let d = Dispatcher::new(vec![as_store(&dead), as_store(&live)]);
355 let out = d.delete(&RecordKey::new("ns", "col", "k"), None).await;
356 assert!(
357 matches!(out, Ok(DeleteResult::Deleted)),
358 "delete failed over: {out:?}"
359 );
360 assert_eq!(dead.calls(), 1, "dead replica was tried");
361 assert_eq!(live.calls(), 1, "then the live replica served it");
362 }
363
364 #[tokio::test]
365 async fn get_raw_fails_over_too() {
366 let dead = MockStore::dead();
367 let live = MockStore::alive();
368 let d = Dispatcher::new(vec![as_store(&dead), as_store(&live)]);
369 let out = d.get_raw(&RecordKey::new("ns", "col", "k")).await;
370 assert!(matches!(out, Ok(None)), "get_raw failed over: {out:?}");
371 assert_eq!(dead.calls() + live.calls(), 2);
372 }
373
374 #[tokio::test]
375 async fn replicas_exposes_every_handle_in_order() {
376 let a = MockStore::alive();
377 let b = MockStore::dead();
378 let d = Dispatcher::new(vec![as_store(&a), as_store(&b)]);
379 assert_eq!(d.replicas().len(), 2);
380 assert!(
382 d.replicas()[0]
383 .get_raw(&RecordKey::new("ns", "col", "k"))
384 .await
385 .is_ok()
386 );
387 assert!(
388 d.replicas()[1]
389 .get_raw(&RecordKey::new("ns", "col", "k"))
390 .await
391 .is_err()
392 );
393 }
394
395 #[tokio::test]
397 async fn conflict_is_returned_not_retried() {
398 let c0 = Answering::new(
399 || {
400 Ok(PutResult::Conflict(Box::new(Conflict {
401 key: RecordKey::new("ns", "col", "k"),
402 expected: None,
403 current: rec(),
404 })))
405 },
406 || Ok(DeleteResult::Deleted),
407 );
408 let c1 = Answering::new(
409 || {
410 Ok(PutResult::Conflict(Box::new(Conflict {
411 key: RecordKey::new("ns", "col", "k"),
412 expected: None,
413 current: rec(),
414 })))
415 },
416 || Ok(DeleteResult::Deleted),
417 );
418 let d = Dispatcher::new(vec![
419 c0.clone() as Arc<dyn Store>,
420 c1.clone() as Arc<dyn Store>,
421 ]);
422 let out = d.put(rec(), None).await;
423 assert!(
424 matches!(out, Ok(PutResult::Conflict(_))),
425 "conflict returned: {out:?}"
426 );
427 assert_eq!(
428 c0.calls.load(Ordering::SeqCst) + c1.calls.load(Ordering::SeqCst),
429 1,
430 "a conflict is a definitive answer — not retried on another replica"
431 );
432 }
433
434 #[tokio::test]
436 async fn delete_conflict_is_returned_not_retried() {
437 let c0 = Answering::new(
438 || Ok(PutResult::Committed(Revision::initial(b"x"))),
439 || {
440 Ok(DeleteResult::Conflict(Box::new(Conflict {
441 key: RecordKey::new("ns", "col", "k"),
442 expected: Some(Revision::initial(b"stale")),
443 current: rec(),
444 })))
445 },
446 );
447 let c1 = Answering::new(
448 || Ok(PutResult::Committed(Revision::initial(b"x"))),
449 || {
450 Ok(DeleteResult::Conflict(Box::new(Conflict {
451 key: RecordKey::new("ns", "col", "k"),
452 expected: Some(Revision::initial(b"stale")),
453 current: rec(),
454 })))
455 },
456 );
457 let d = Dispatcher::new(vec![
458 c0.clone() as Arc<dyn Store>,
459 c1.clone() as Arc<dyn Store>,
460 ]);
461 let out = d
462 .delete(
463 &RecordKey::new("ns", "col", "k"),
464 Some(Revision::initial(b"stale")),
465 )
466 .await;
467 assert!(
468 matches!(out, Ok(DeleteResult::Conflict(_))),
469 "conflict returned: {out:?}"
470 );
471 assert_eq!(
472 c0.calls.load(Ordering::SeqCst) + c1.calls.load(Ordering::SeqCst),
473 1,
474 "a delete conflict is not retried on another replica"
475 );
476 }
477
478 #[tokio::test]
480 async fn not_found_is_returned_not_retried() {
481 let c0 = Answering::new(
482 || Err(CoreError::NotFound(RecordKey::new("ns", "col", "k"))),
483 || Ok(DeleteResult::Deleted),
484 );
485 let c1 = Answering::new(
486 || Err(CoreError::NotFound(RecordKey::new("ns", "col", "k"))),
487 || Ok(DeleteResult::Deleted),
488 );
489 let d = Dispatcher::new(vec![
490 c0.clone() as Arc<dyn Store>,
491 c1.clone() as Arc<dyn Store>,
492 ]);
493 let out = d.put(rec(), None).await;
494 assert!(
495 matches!(out, Err(CoreError::NotFound(_))),
496 "not found returned: {out:?}"
497 );
498 assert_eq!(
499 c0.calls.load(Ordering::SeqCst) + c1.calls.load(Ordering::SeqCst),
500 1,
501 "a NotFound answer from a live replica is not retried on another replica"
502 );
503 }
504}