1use crate::error::LaserError;
2use crate::iggy::prelude::Client;
3use crate::kv::{
4 AGDX_KV_CAS_FENCED_CODE, AGDX_KV_GET_CODE, AGDX_KV_LEASE_CODE, AGDX_KV_LEASE_RENEW_CODE,
5 AGDX_KV_RELEASE_CODE, KvCasFenced, KvEntry, KvGet, KvLease, KvLeaseRenew, KvRelease, Lease,
6};
7use crate::laser::Laser;
8use laser_wire::framing::encode_named;
9use laser_wire::kv::{KvOutcome, KvReply};
10use laser_wire::mutation::{MANAGED_REQUEST_VERSION, ManagedRequestEnvelope};
11use laser_wire::validate::Validate;
12use std::future::Future;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::time::Duration;
16use tokio::sync::Mutex;
17
18pub const DEFAULT_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(10);
22
23#[trait_variant::make(ManagedKvTransport: Send)]
28pub trait LocalManagedKvTransport {
29 fn ready(&self) -> impl Future<Output = Result<(), LaserError>> {
33 async { Ok(()) }
34 }
35 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError>;
37 async fn reset(&self);
43
44 fn close(&self) -> impl Future<Output = ()>
46 where
47 Self: Sync,
48 {
49 async { self.reset().await }
50 }
51}
52
53#[async_trait::async_trait]
76pub trait DynManagedKvTransport: Send + Sync {
77 async fn ready(&self) -> Result<(), LaserError>;
79 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError>;
81 async fn reset(&self);
84
85 async fn close(&self) {
86 self.reset().await;
87 }
88}
89
90pub type SharedKvTransport = Arc<dyn DynManagedKvTransport>;
94
95#[async_trait::async_trait]
96impl<T: ManagedKvTransport + Send + Sync> DynManagedKvTransport for T {
97 async fn ready(&self) -> Result<(), LaserError> {
98 ManagedKvTransport::ready(self).await
99 }
100
101 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
102 ManagedKvTransport::send(self, code, frame).await
103 }
104
105 async fn reset(&self) {
106 ManagedKvTransport::reset(self).await;
107 }
108
109 async fn close(&self) {
110 ManagedKvTransport::close(self).await;
111 }
112}
113
114impl ManagedKvTransport for SharedKvTransport {
118 async fn ready(&self) -> Result<(), LaserError> {
119 DynManagedKvTransport::ready(&**self).await
120 }
121
122 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
123 DynManagedKvTransport::send(&**self, code, frame).await
124 }
125
126 async fn reset(&self) {
127 DynManagedKvTransport::reset(&**self).await;
128 }
129
130 async fn close(&self) {
131 DynManagedKvTransport::close(&**self).await;
132 }
133}
134
135pub struct DedicatedKvTransport {
146 connection_string: String,
147 slot: Mutex<Option<Laser>>,
148 closed: AtomicBool,
149 retirement: Mutex<()>,
150}
151
152impl DedicatedKvTransport {
153 pub fn new(connection_string: impl Into<String>) -> Self {
155 Self {
156 connection_string: connection_string.into(),
157 slot: Mutex::new(None),
158 closed: AtomicBool::new(false),
159 retirement: Mutex::new(()),
160 }
161 }
162
163 pub async fn close(&self) {
165 self.closed.store(true, Ordering::Release);
166 ManagedKvTransport::reset(self).await;
167 }
168
169 async fn client(&self) -> Result<Laser, LaserError> {
170 let _retirement = self.retirement.lock().await;
171 let mut slot = self.slot.lock().await;
172 if self.closed.load(Ordering::Acquire) {
173 return Err(crate::iggy::prelude::IggyError::ClientShutdown.into());
174 }
175 let (laser, reused) = match slot.as_ref() {
176 Some(laser) => (laser.clone(), true),
177 None => {
178 let laser = Laser::connect(&self.connection_string).await?;
179 *slot = Some(laser.clone());
180 (laser, false)
181 }
182 };
183 let mut capabilities = laser.capabilities().await;
184 if reused && !capabilities.kv.fenced_leases {
185 capabilities = laser.refresh_capabilities().await;
186 }
187 if !capabilities.kv.fenced_leases {
188 return Err(LaserError::unsupported_feature(
189 "kv",
190 "coordination",
191 "the fenced-lease contract is not advertised by this deployment",
192 ));
193 }
194 Ok(laser)
195 }
196}
197
198impl ManagedKvTransport for DedicatedKvTransport {
199 async fn ready(&self) -> Result<(), LaserError> {
200 self.client().await.map(|_| ())
201 }
202
203 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
204 let laser = self.slot.lock().await.as_ref().cloned().ok_or_else(|| {
205 LaserError::HandlerConfig("coordination transport is not ready".to_owned())
206 })?;
207 laser
208 .send_raw_preframed(code, frame)
209 .await
210 .map_err(LaserError::from)
211 }
212
213 async fn reset(&self) {
214 let _retirement = self.retirement.lock().await;
215 let retired = self.slot.lock().await.take();
216 if let Some(laser) = retired {
217 let _ = laser.client().shutdown().await;
218 }
219 }
220
221 async fn close(&self) {
222 DedicatedKvTransport::close(self).await;
223 }
224}
225
226#[derive(Debug)]
232pub struct PreparedMutation {
233 client_id: u128,
234 code: u32,
235 operation_id: u128,
236 frame: Vec<u8>,
237 ambiguous_recovery: AmbiguousMutationRecovery,
238}
239
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum AmbiguousMutationRecovery {
245 WaitForLeaseExpiry(Duration),
248 RepeatPrepared,
250 ReconcileTargetPrecondition,
252}
253
254impl PreparedMutation {
255 pub fn operation_id(&self) -> u128 {
257 self.operation_id
258 }
259
260 pub fn ambiguous_recovery(&self) -> AmbiguousMutationRecovery {
262 self.ambiguous_recovery
263 }
264
265 fn build(
266 client_id: u128,
267 code: u32,
268 request: &impl serde::Serialize,
269 ambiguous_recovery: AmbiguousMutationRecovery,
270 ) -> Result<Self, LaserError> {
271 let payload = encode_named(request)
272 .map_err(|error| LaserError::Codec(format!("encode request: {error}")))?;
273 let operation_id = u128::from(ulid::Ulid::generate());
274 let frame = encode_named(&ManagedRequestEnvelope {
275 v: MANAGED_REQUEST_VERSION,
276 operation_id,
277 payload,
278 })
279 .map_err(|error| LaserError::Codec(format!("encode envelope: {error}")))?;
280 Ok(Self {
281 client_id,
282 code,
283 operation_id,
284 frame,
285 ambiguous_recovery,
286 })
287 }
288}
289
290pub struct FencedLeaseClient<T> {
306 client_id: u128,
307 transport: T,
308 attempt_timeout: Duration,
309 closed: AtomicBool,
310 retirement: Mutex<()>,
311}
312
313impl FencedLeaseClient<DedicatedKvTransport> {
314 pub fn connect_dedicated(connection_string: impl Into<String>) -> Self {
317 Self::new(DedicatedKvTransport::new(connection_string))
318 }
319}
320
321impl<T: ManagedKvTransport + Sync> FencedLeaseClient<T> {
322 pub fn new(transport: T) -> Self {
324 Self {
325 client_id: u128::from(ulid::Ulid::generate()),
326 transport,
327 attempt_timeout: DEFAULT_ATTEMPT_TIMEOUT,
328 closed: AtomicBool::new(false),
329 retirement: Mutex::new(()),
330 }
331 }
332
333 #[must_use]
336 pub fn with_attempt_timeout(mut self, timeout: Duration) -> Self {
337 self.attempt_timeout = timeout;
338 self
339 }
340
341 pub async fn close(&self) {
343 let _retirement = self.retirement.lock().await;
344 if !self.closed.swap(true, Ordering::AcqRel) {
345 self.transport.close().await;
346 }
347 }
348
349 pub fn prepare_acquire(&self, request: &KvLease) -> Result<PreparedMutation, LaserError> {
352 request.validate()?;
353 PreparedMutation::build(
354 self.client_id,
355 AGDX_KV_LEASE_CODE,
356 request,
357 AmbiguousMutationRecovery::WaitForLeaseExpiry(Duration::from_micros(
358 request.lease_ttl_micros,
359 )),
360 )
361 }
362
363 pub fn prepare_renew(&self, request: &KvLeaseRenew) -> Result<PreparedMutation, LaserError> {
365 request.validate()?;
366 PreparedMutation::build(
367 self.client_id,
368 AGDX_KV_LEASE_RENEW_CODE,
369 request,
370 AmbiguousMutationRecovery::RepeatPrepared,
371 )
372 }
373
374 pub fn prepare_release(&self, request: &KvRelease) -> Result<PreparedMutation, LaserError> {
376 request.validate()?;
377 PreparedMutation::build(
378 self.client_id,
379 AGDX_KV_RELEASE_CODE,
380 request,
381 AmbiguousMutationRecovery::RepeatPrepared,
382 )
383 }
384
385 pub fn prepare_cas_fenced(
387 &self,
388 request: &KvCasFenced,
389 ) -> Result<PreparedMutation, LaserError> {
390 request.validate()?;
391 PreparedMutation::build(
392 self.client_id,
393 AGDX_KV_CAS_FENCED_CODE,
394 request,
395 AmbiguousMutationRecovery::ReconcileTargetPrecondition,
396 )
397 }
398
399 pub async fn acquire(&self, op: &PreparedMutation) -> Result<Lease, LaserError> {
401 match self
402 .execute_with_timeout(op, AGDX_KV_LEASE_CODE, "acquire", self.attempt_timeout)
403 .await?
404 {
405 KvOutcome::Leased {
406 lease_token,
407 granted_ttl_micros,
408 position,
409 } => Ok(Lease {
410 token: lease_token,
411 granted_ttl: Duration::from_micros(granted_ttl_micros),
412 position,
413 }),
414 other => Err(unexpected("acquire", &other)),
415 }
416 }
417
418 pub async fn renew(&self, op: &PreparedMutation) -> Result<Lease, LaserError> {
421 self.renew_with_timeout(op, self.attempt_timeout).await
422 }
423
424 pub(crate) async fn renew_with_timeout(
425 &self,
426 op: &PreparedMutation,
427 timeout: Duration,
428 ) -> Result<Lease, LaserError> {
429 match self
430 .execute_with_timeout(op, AGDX_KV_LEASE_RENEW_CODE, "renew", timeout)
431 .await?
432 {
433 KvOutcome::Renewed {
434 lease_token,
435 granted_ttl_micros,
436 position,
437 } => Ok(Lease {
438 token: lease_token,
439 granted_ttl: Duration::from_micros(granted_ttl_micros),
440 position,
441 }),
442 other => Err(unexpected("renew", &other)),
443 }
444 }
445
446 pub async fn release(&self, op: &PreparedMutation) -> Result<bool, LaserError> {
449 match self
450 .execute_with_timeout(op, AGDX_KV_RELEASE_CODE, "release", self.attempt_timeout)
451 .await?
452 {
453 KvOutcome::Released(released) => Ok(released),
454 other => Err(unexpected("release", &other)),
455 }
456 }
457
458 pub async fn cas_fenced(&self, op: &PreparedMutation) -> Result<u64, LaserError> {
461 match self
462 .execute_with_timeout(
463 op,
464 AGDX_KV_CAS_FENCED_CODE,
465 "cas_fenced",
466 self.attempt_timeout,
467 )
468 .await?
469 {
470 KvOutcome::Committed { version } => Ok(version),
471 other => Err(unexpected("cas_fenced", &other)),
472 }
473 }
474
475 pub async fn get(&self, request: &KvGet) -> Result<Option<KvEntry>, LaserError> {
483 Self::ensure_attempt_timeout(self.attempt_timeout)?;
484 self.ready().await?;
485 let frame = encode_named(request)
486 .map_err(|error| LaserError::Codec(format!("encode request: {error}")))?;
487 let send = self.transport.send(AGDX_KV_GET_CODE, frame);
488 let reply = match tokio::time::timeout(self.attempt_timeout, send).await {
489 Err(_elapsed) => {
490 self.transport.reset().await;
491 return Err(LaserError::Timeout("kv coordination get"));
492 }
493 Ok(Err(error)) => {
494 self.transport.reset().await;
495 return Err(error);
496 }
497 Ok(Ok(bytes)) => bytes,
498 };
499 match decode_reply(&reply)? {
500 KvOutcome::Value(entry) => Ok(entry),
501 other => Err(unexpected("get", &other)),
502 }
503 }
504
505 async fn execute_with_timeout(
506 &self,
507 op: &PreparedMutation,
508 expected_code: u32,
509 what: &'static str,
510 attempt_timeout: Duration,
511 ) -> Result<KvOutcome, LaserError> {
512 Self::ensure_attempt_timeout(attempt_timeout)?;
513 if op.client_id != self.client_id {
514 return Err(LaserError::Invalid(
515 "prepared mutation belongs to a different fenced-lease client".to_owned(),
516 ));
517 }
518 if op.code != expected_code {
521 return Err(LaserError::Invalid(format!(
522 "prepared mutation carries code {} but was executed as {what}",
523 op.code
524 )));
525 }
526 self.ready_with_timeout(attempt_timeout).await?;
527 let send = self.transport.send(op.code, op.frame.clone());
528 let reply = match tokio::time::timeout(attempt_timeout, send).await {
529 Err(_elapsed) => {
530 self.transport.reset().await;
531 return Err(LaserError::AmbiguousMutation(format!(
532 "{what} attempt exceeded {:?} and requires operation-specific recovery",
533 attempt_timeout,
534 )));
535 }
536 Ok(Err(error)) if !error.is_retryable() || error.is_permission_denied() => {
537 self.transport.reset().await;
538 return Err(error);
539 }
540 Ok(Err(error)) => {
543 self.transport.reset().await;
544 return Err(LaserError::AmbiguousMutation(format!(
545 "{what} transport failed mid-request: {error}"
546 )));
547 }
548 Ok(Ok(bytes)) => bytes,
549 };
550 decode_reply(&reply)
551 }
552
553 fn ensure_attempt_timeout(timeout: Duration) -> Result<(), LaserError> {
554 if timeout.is_zero() {
555 Err(LaserError::Invalid(
556 "coordination attempt timeout must be greater than zero".to_owned(),
557 ))
558 } else {
559 Ok(())
560 }
561 }
562
563 async fn ready(&self) -> Result<(), LaserError> {
564 Self::ensure_attempt_timeout(self.attempt_timeout)?;
565 self.ready_with_timeout(self.attempt_timeout).await
566 }
567
568 async fn ready_with_timeout(&self, timeout: Duration) -> Result<(), LaserError> {
569 if self.closed.load(Ordering::Acquire) {
570 return Err(crate::iggy::prelude::IggyError::ClientShutdown.into());
571 }
572 match tokio::time::timeout(timeout, self.transport.ready()).await {
573 Err(_elapsed) => {
574 self.transport.reset().await;
575 Err(LaserError::Timeout("kv coordination connect"))
576 }
577 Ok(Err(error @ LaserError::Unsupported { .. })) => Err(error),
580 Ok(Err(error)) => {
581 self.transport.reset().await;
582 Err(error)
583 }
584 Ok(Ok(())) => Ok(()),
585 }
586 }
587}
588
589fn decode_reply(payload: &[u8]) -> Result<KvOutcome, LaserError> {
590 match crate::error::decode_managed_reply::<KvReply>(payload)? {
591 KvReply::Ok(outcome) => Ok(outcome),
592 KvReply::Err(error) => Err(error.into()),
593 _ => Err(LaserError::Protocol(
596 "kv coordination: unknown reply variant".to_owned(),
597 )),
598 }
599}
600
601fn unexpected(what: &str, outcome: &KvOutcome) -> LaserError {
602 LaserError::Protocol(format!(
603 "kv coordination {what}: unexpected outcome {outcome:?}"
604 ))
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::kv::{CasExpect, KV_LEASE_OP_VERSION, KV_OP_VERSION, KvError};
611 use laser_wire::framing::decode_named;
612 use laser_wire::mutation::MutationPosition;
613 use std::sync::Mutex as StdMutex;
614 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
615
616 struct StubTransport {
617 replies: StdMutex<Vec<Result<Vec<u8>, LaserError>>>,
618 sent: StdMutex<Vec<(u32, Vec<u8>)>>,
619 resets: AtomicUsize,
620 delay: Option<Duration>,
621 }
622
623 impl StubTransport {
624 fn answering(replies: Vec<Result<Vec<u8>, LaserError>>) -> Self {
625 Self {
626 replies: StdMutex::new(replies),
627 sent: StdMutex::new(Vec::new()),
628 resets: AtomicUsize::new(0),
629 delay: None,
630 }
631 }
632
633 fn hanging() -> Self {
634 Self {
635 replies: StdMutex::new(Vec::new()),
636 sent: StdMutex::new(Vec::new()),
637 resets: AtomicUsize::new(0),
638 delay: Some(Duration::from_secs(60)),
639 }
640 }
641 }
642
643 impl ManagedKvTransport for &StubTransport {
644 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
645 self.sent.lock().expect("sent lock").push((code, frame));
646 if let Some(delay) = self.delay {
647 tokio::time::sleep(delay).await;
648 }
649 self.replies.lock().expect("replies lock").remove(0)
650 }
651
652 async fn reset(&self) {
653 self.resets.fetch_add(1, Ordering::SeqCst);
654 }
655 }
656
657 #[derive(Default)]
658 struct UnreadyTransport {
659 resets: AtomicUsize,
660 }
661
662 impl ManagedKvTransport for &UnreadyTransport {
663 async fn ready(&self) -> Result<(), LaserError> {
664 Err(LaserError::unsupported_feature(
665 "kv",
666 "coordination",
667 "fenced leases are unavailable",
668 ))
669 }
670
671 async fn send(&self, _code: u32, _frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
672 panic!("a permanent setup failure must stop before send")
673 }
674
675 async fn reset(&self) {
676 self.resets.fetch_add(1, Ordering::SeqCst);
677 }
678 }
679
680 struct SlowResetTransport {
681 reset_finished: AtomicBool,
682 }
683
684 impl ManagedKvTransport for &SlowResetTransport {
685 async fn send(&self, _code: u32, _frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
686 std::future::pending().await
687 }
688
689 async fn reset(&self) {
690 tokio::time::sleep(Duration::from_millis(25)).await;
691 self.reset_finished.store(true, Ordering::SeqCst);
692 }
693 }
694
695 struct SlowReadyTransport {
696 ready_delay: Duration,
697 send_delay: Duration,
698 }
699
700 impl ManagedKvTransport for &SlowReadyTransport {
701 async fn ready(&self) -> Result<(), LaserError> {
702 tokio::time::sleep(self.ready_delay).await;
703 Ok(())
704 }
705
706 async fn send(&self, _code: u32, _frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
707 tokio::time::sleep(self.send_delay).await;
708 ok_reply(KvOutcome::Leased {
709 lease_token: 9,
710 granted_ttl_micros: 30_000_000,
711 position: position(),
712 })
713 }
714
715 async fn reset(&self) {}
716 }
717
718 fn ok_reply(outcome: KvOutcome) -> Result<Vec<u8>, LaserError> {
719 Ok(encode_named(&KvReply::Ok(outcome)).expect("reply encodes"))
720 }
721
722 fn err_reply(error: KvError) -> Result<Vec<u8>, LaserError> {
723 Ok(encode_named(&KvReply::Err(error)).expect("reply encodes"))
724 }
725
726 fn lease_request() -> KvLease {
727 KvLease {
728 v: KV_LEASE_OP_VERSION,
729 namespace: "connectors.coordination".to_owned(),
730 key: b"source-owner".to_vec(),
731 lease_ttl_micros: 30_000_000,
732 holder_id: "node-1".to_owned(),
733 subject_user_id: Some(42),
734 }
735 }
736
737 fn position() -> MutationPosition {
738 MutationPosition {
739 topic_generation: 1,
740 partition: 0,
741 offset: 512,
742 }
743 }
744
745 #[tokio::test]
746 async fn given_a_prepared_renewal_when_retried_then_should_reuse_the_exact_frame_and_id() {
747 let stub = StubTransport::answering(vec![
748 Err(LaserError::Timeout("boom")),
749 ok_reply(KvOutcome::Renewed {
750 lease_token: 7,
751 granted_ttl_micros: 30_000_000,
752 position: position(),
753 }),
754 ]);
755 let client = FencedLeaseClient::new(&stub);
756 let request = KvLeaseRenew {
757 v: KV_LEASE_OP_VERSION,
758 namespace: "connectors.coordination".to_owned(),
759 key: b"source-owner".to_vec(),
760 holder_id: "node-1".to_owned(),
761 subject_user_id: Some(42),
762 lease_token: 7,
763 lease_ttl_micros: 30_000_000,
764 };
765 let op = client.prepare_renew(&request).expect("prepares");
766 assert_eq!(
767 op.ambiguous_recovery(),
768 AmbiguousMutationRecovery::RepeatPrepared
769 );
770
771 let first = client.renew(&op).await;
772 assert!(
773 matches!(first, Err(LaserError::AmbiguousMutation(_))),
774 "a transport failure is ambiguous, got {first:?}"
775 );
776 let lease = client.renew(&op).await.expect("retry succeeds");
777 assert_eq!(lease.token, 7);
778 assert_eq!(lease.position, position());
779
780 let sent = stub.sent.lock().expect("sent lock");
781 assert_eq!(sent.len(), 2);
782 assert_eq!(sent[0], sent[1], "the retry reuses the exact frame");
783 let envelope: ManagedRequestEnvelope =
784 decode_named(&sent[0].1).expect("the frame is a managed envelope");
785 assert_eq!(envelope.operation_id, op.operation_id());
786 assert_ne!(envelope.operation_id, 0);
787 let inner: KvLeaseRenew =
788 decode_named(&envelope.payload).expect("the payload is the renewal");
789 assert_eq!(inner.holder_id, "node-1");
790 assert_eq!(stub.resets.load(Ordering::SeqCst), 1, "reset after failure");
791 }
792
793 #[tokio::test]
797 async fn given_a_boxed_transport_when_used_then_should_drive_the_typed_client() {
798 struct OwnedStub {
799 sent: StdMutex<Vec<(u32, Vec<u8>)>>,
800 }
801
802 impl ManagedKvTransport for OwnedStub {
803 async fn send(&self, code: u32, frame: Vec<u8>) -> Result<Vec<u8>, LaserError> {
804 self.sent.lock().expect("sent lock").push((code, frame));
805 ok_reply(KvOutcome::Leased {
806 lease_token: 9,
807 granted_ttl_micros: 30_000_000,
808 position: position(),
809 })
810 }
811
812 async fn reset(&self) {}
813 }
814
815 let stub = Arc::new(OwnedStub {
816 sent: StdMutex::new(Vec::new()),
817 });
818 let transport: SharedKvTransport = Arc::clone(&stub) as SharedKvTransport;
819 let client = FencedLeaseClient::new(transport);
820 let op = client.prepare_acquire(&lease_request()).expect("prepares");
821 let lease = client.acquire(&op).await.expect("acquires");
822
823 assert_eq!(lease.token, 9);
824 assert_eq!(lease.granted_ttl, Duration::from_micros(30_000_000));
825 let sent = stub.sent.lock().expect("sent lock");
828 assert_eq!(sent[0].0, AGDX_KV_LEASE_CODE);
829 let envelope: ManagedRequestEnvelope =
830 decode_named(&sent[0].1).expect("the frame is a managed envelope");
831 assert_eq!(envelope.operation_id, op.operation_id());
832 }
833
834 #[tokio::test]
835 async fn given_two_prepared_mutations_when_built_then_should_mint_distinct_operation_ids() {
836 let stub = StubTransport::answering(Vec::new());
837 let client = FencedLeaseClient::new(&stub);
838 let first = client.prepare_acquire(&lease_request()).expect("prepares");
839 let second = client.prepare_acquire(&lease_request()).expect("prepares");
840 assert_ne!(first.operation_id(), second.operation_id());
841 }
842
843 #[tokio::test]
844 async fn given_a_hung_transport_when_executing_then_should_reset_and_report_ambiguous() {
845 let stub = StubTransport::hanging();
846 let client = FencedLeaseClient::new(&stub).with_attempt_timeout(Duration::from_millis(100));
847 let op = client.prepare_acquire(&lease_request()).expect("prepares");
848 assert_eq!(
849 op.ambiguous_recovery(),
850 AmbiguousMutationRecovery::WaitForLeaseExpiry(Duration::from_secs(30))
851 );
852 let outcome = client.acquire(&op).await;
853 assert!(matches!(outcome, Err(LaserError::AmbiguousMutation(_))));
854 assert_eq!(stub.resets.load(Ordering::SeqCst), 1);
855 }
856
857 #[tokio::test]
858 async fn given_slow_readiness_when_send_fits_the_bound_then_should_not_report_ambiguous() {
859 let transport = SlowReadyTransport {
860 ready_delay: Duration::from_millis(125),
861 send_delay: Duration::from_millis(125),
862 };
863 let client =
864 FencedLeaseClient::new(&transport).with_attempt_timeout(Duration::from_millis(200));
865 let op = client.prepare_acquire(&lease_request()).expect("prepares");
866
867 let lease = client.acquire(&op).await.expect("acquires after readiness");
868
869 assert_eq!(lease.token, 9);
870 }
871
872 #[test]
873 fn given_default_client_when_built_then_should_allow_ten_second_attempts() {
874 let stub = StubTransport::answering(Vec::new());
875 let client = FencedLeaseClient::new(&stub);
876
877 assert_eq!(client.attempt_timeout, Duration::from_secs(10));
878 }
879
880 #[tokio::test]
881 async fn given_a_timed_out_mutation_when_reset_is_slow_then_should_wait_for_retirement() {
882 let transport = SlowResetTransport {
883 reset_finished: AtomicBool::new(false),
884 };
885 let client =
886 FencedLeaseClient::new(&transport).with_attempt_timeout(Duration::from_millis(1));
887 let op = client.prepare_acquire(&lease_request()).expect("prepares");
888 let started = tokio::time::Instant::now();
889
890 let outcome = client.acquire(&op).await;
891
892 assert!(matches!(outcome, Err(LaserError::AmbiguousMutation(_))));
893 assert!(transport.reset_finished.load(Ordering::SeqCst));
894 assert!(started.elapsed() >= Duration::from_millis(25));
895 }
896
897 #[tokio::test]
898 async fn given_a_closed_coordination_client_when_readied_then_should_not_reconnect() {
899 let client = FencedLeaseClient::connect_dedicated("not a connection string");
900 client.close().await;
901 client.close().await;
902 ManagedKvTransport::reset(&client.transport).await;
903 assert!(matches!(
904 ManagedKvTransport::ready(&client.transport).await,
905 Err(LaserError::Iggy(
906 crate::iggy::prelude::IggyError::ClientShutdown
907 ))
908 ));
909 }
910
911 #[tokio::test]
912 async fn given_a_permanent_setup_failure_when_executing_then_should_preserve_the_error() {
913 let unready = UnreadyTransport::default();
914 let client = FencedLeaseClient::new(&unready);
915 let op = client.prepare_acquire(&lease_request()).expect("prepares");
916 let outcome = client.acquire(&op).await;
917 assert!(matches!(outcome, Err(LaserError::Unsupported { .. })));
918 }
919
920 #[tokio::test]
921 async fn given_an_unsupported_deployment_when_called_repeatedly_then_should_keep_the_connection()
922 {
923 let unready = UnreadyTransport::default();
924 let client = FencedLeaseClient::new(&unready);
925 for _ in 0..3 {
926 let op = client.prepare_acquire(&lease_request()).expect("prepares");
927 assert!(matches!(
928 client.acquire(&op).await,
929 Err(LaserError::Unsupported { .. })
930 ));
931 }
932 assert_eq!(unready.resets.load(Ordering::SeqCst), 0);
933 }
934
935 #[tokio::test]
936 async fn given_a_permanent_send_failure_when_executing_then_should_preserve_the_error() {
937 let stub = StubTransport::answering(vec![Err(LaserError::unsupported_feature(
938 "kv",
939 "coordination",
940 "fenced leases are unavailable",
941 ))]);
942 let client = FencedLeaseClient::new(&stub);
943 let op = client.prepare_acquire(&lease_request()).expect("prepares");
944 let outcome = client.acquire(&op).await;
945 assert!(matches!(outcome, Err(LaserError::Unsupported { .. })));
946 assert_eq!(stub.resets.load(Ordering::SeqCst), 1);
947 }
948
949 #[tokio::test]
950 async fn given_an_authorization_send_failure_when_executing_then_should_preserve_the_error() {
951 let stub = StubTransport::answering(vec![Err(LaserError::Iggy(
952 crate::iggy::prelude::IggyError::Unauthorized,
953 ))]);
954 let client = FencedLeaseClient::new(&stub);
955 let op = client.prepare_acquire(&lease_request()).expect("prepares");
956 let outcome = client.acquire(&op).await;
957 assert!(matches!(
958 outcome,
959 Err(LaserError::Iggy(
960 crate::iggy::prelude::IggyError::Unauthorized
961 ))
962 ));
963 assert_eq!(stub.resets.load(Ordering::SeqCst), 1);
964 }
965
966 #[tokio::test]
967 async fn given_a_zero_attempt_timeout_when_executing_then_should_reject_without_sending() {
968 let stub = StubTransport::answering(Vec::new());
969 let client = FencedLeaseClient::new(&stub).with_attempt_timeout(Duration::ZERO);
970 let op = client.prepare_acquire(&lease_request()).expect("prepares");
971 let outcome = client.acquire(&op).await;
972 assert!(matches!(outcome, Err(LaserError::Invalid(_))));
973 assert!(stub.sent.lock().expect("sent lock").is_empty());
974 }
975
976 #[tokio::test]
977 async fn given_a_prepared_mutation_from_another_client_when_executed_then_should_reject() {
978 let first_stub = StubTransport::answering(Vec::new());
979 let second_stub = StubTransport::answering(Vec::new());
980 let first = FencedLeaseClient::new(&first_stub);
981 let second = FencedLeaseClient::new(&second_stub);
982 let op = first.prepare_acquire(&lease_request()).expect("prepares");
983 let outcome = second.acquire(&op).await;
984 assert!(matches!(outcome, Err(LaserError::Invalid(_))));
985 assert!(second_stub.sent.lock().expect("sent lock").is_empty());
986 }
987
988 #[tokio::test]
989 async fn given_an_unexpected_outcome_when_executing_then_should_fail_closed() {
990 let stub = StubTransport::answering(vec![ok_reply(KvOutcome::Leased {
992 lease_token: 7,
993 granted_ttl_micros: 30_000_000,
994 position: position(),
995 })]);
996 let client = FencedLeaseClient::new(&stub);
997 let release = KvRelease {
998 v: KV_LEASE_OP_VERSION,
999 namespace: "connectors.coordination".to_owned(),
1000 key: b"source-owner".to_vec(),
1001 lease_token: 7,
1002 holder_id: "node-1".to_owned(),
1003 };
1004 let op = client.prepare_release(&release).expect("prepares");
1005 let outcome = client.release(&op).await;
1006 assert!(matches!(outcome, Err(LaserError::Protocol(_))));
1007 }
1008
1009 #[tokio::test]
1010 async fn given_a_mismatched_prepared_mutation_when_executed_then_should_reject() {
1011 let stub = StubTransport::answering(Vec::new());
1012 let client = FencedLeaseClient::new(&stub);
1013 let op = client.prepare_acquire(&lease_request()).expect("prepares");
1014 let outcome = client.release(&op).await;
1015 assert!(matches!(outcome, Err(LaserError::Invalid(_))));
1016 assert!(stub.sent.lock().expect("sent lock").is_empty());
1017 }
1018
1019 #[tokio::test]
1020 async fn given_a_renewal_when_executed_then_should_return_the_same_token() {
1021 let stub = StubTransport::answering(vec![ok_reply(KvOutcome::Renewed {
1022 lease_token: 7,
1023 granted_ttl_micros: 25_000_000,
1024 position: position(),
1025 })]);
1026 let client = FencedLeaseClient::new(&stub);
1027 let renew = KvLeaseRenew {
1028 v: KV_LEASE_OP_VERSION,
1029 namespace: "connectors.coordination".to_owned(),
1030 key: b"source-owner".to_vec(),
1031 holder_id: "node-1".to_owned(),
1032 subject_user_id: Some(42),
1033 lease_token: 7,
1034 lease_ttl_micros: 30_000_000,
1035 };
1036 let op = client.prepare_renew(&renew).expect("prepares");
1037 let lease = client.renew(&op).await.expect("renews");
1038 assert_eq!(lease.token, 7);
1039 assert_eq!(lease.granted_ttl, Duration::from_micros(25_000_000));
1040 }
1041
1042 #[tokio::test]
1043 async fn given_a_lease_lost_reply_when_executing_then_should_surface_the_typed_error() {
1044 let stub = StubTransport::answering(vec![err_reply(KvError::LeaseLost)]);
1045 let client = FencedLeaseClient::new(&stub);
1046 let cas = KvCasFenced {
1047 v: KV_LEASE_OP_VERSION,
1048 namespace: "connectors.state".to_owned(),
1049 key: b"source_state/pg".to_vec(),
1050 value: b"cursor".to_vec(),
1051 expires_at_micros: None,
1052 expect: CasExpect::Match(4),
1053 fence_namespace: "connectors.coordination".to_owned(),
1054 fence_key: b"source-owner".to_vec(),
1055 fence_token: 7,
1056 };
1057 let op = client.prepare_cas_fenced(&cas).expect("prepares");
1058 let outcome = client.cas_fenced(&op).await;
1059 assert!(matches!(outcome, Err(LaserError::Kv(KvError::LeaseLost))));
1060 assert_eq!(
1061 stub.resets.load(Ordering::SeqCst),
1062 0,
1063 "a typed reply is not ambiguous"
1064 );
1065 }
1066
1067 #[tokio::test]
1068 async fn given_an_invalid_request_when_preparing_then_should_reject_before_any_send() {
1069 let stub = StubTransport::answering(Vec::new());
1070 let client = FencedLeaseClient::new(&stub);
1071 let stale_version = KvLease {
1072 v: KV_LEASE_OP_VERSION + 1,
1073 ..lease_request()
1074 };
1075 assert!(client.prepare_acquire(&stale_version).is_err());
1076 let no_holder = KvLease {
1077 holder_id: String::new(),
1078 ..lease_request()
1079 };
1080 assert!(client.prepare_acquire(&no_holder).is_err());
1081 assert!(stub.sent.lock().expect("sent lock").is_empty());
1082 }
1083
1084 #[tokio::test]
1085 async fn given_a_barriered_get_when_answered_stale_then_should_surface_stale_not_absent() {
1086 let stub = StubTransport::answering(vec![err_reply(KvError::Stale {
1087 required: position(),
1088 })]);
1089 let client = FencedLeaseClient::new(&stub);
1090 let request = KvGet {
1091 v: KV_OP_VERSION,
1092 namespace: "connectors.state".to_owned(),
1093 key: b"source_state/pg".to_vec(),
1094 if_none_match: None,
1095 min_position: Some(position()),
1096 };
1097 let outcome = client.get(&request).await;
1098 assert!(matches!(
1099 outcome,
1100 Err(LaserError::Kv(KvError::Stale { .. }))
1101 ));
1102 let sent = stub.sent.lock().expect("sent lock");
1104 assert_eq!(sent[0].0, AGDX_KV_GET_CODE);
1105 let direct: KvGet = decode_named(&sent[0].1).expect("the frame is the bare get");
1106 assert_eq!(direct.min_position, Some(position()));
1107 }
1108}