1use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
25
26use crate::api::{RedDBError, RedDBOptions, RedDBResult};
27use crate::cluster::{
28 admit_durable_write, CollectionId, DurableWriteReject, LeasedOwner, NodeIdentity,
29 OwnershipEpoch, OwnershipLease, PlacementMetadata, RangeBounds, RangeId, RangeOwnership,
30 ShardKeyMode, ShardOwnershipCatalog, SupervisorTerm,
31};
32use crate::replication::cdc::RangeAuthority;
33use crate::replication::flow_control::{Admission, FlowController};
34use crate::replication::ReplicationRole;
35use crate::telemetry::operator_event::OperatorEvent;
36
37pub const FLOW_CONTROL_SOFT_TARGET_ENV: &str = "RED_REPLICATION_FLOW_CONTROL_SOFT_TARGET_LSN";
40
41const RESERVED_GLOBAL_SYSTEM_COLLECTION: &str = "system.global";
42const RESERVED_GLOBAL_SYSTEM_RANGE_ID: u64 = 0;
43const RESERVED_GLOBAL_SYSTEM_RANGE_KEY: &[u8] = b"reserved-global-system-range";
44const DEFAULT_PRIMARY_REPLICA_OWNER: &str = "CN=primary-replica-primary,O=reddb";
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum WriteKind {
50 Dml,
52 Ddl,
54 IndexBuild,
56 Maintenance,
58 Backup,
60 Serverless,
63}
64
65impl WriteKind {
66 fn label(self) -> &'static str {
67 match self {
68 WriteKind::Dml => "DML",
69 WriteKind::Ddl => "DDL",
70 WriteKind::IndexBuild => "index build",
71 WriteKind::Maintenance => "maintenance",
72 WriteKind::Backup => "backup trigger",
73 WriteKind::Serverless => "serverless lifecycle",
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[repr(u8)]
87pub enum LeaseGateState {
88 NotRequired = 0,
89 Held = 1,
90 NotHeld = 2,
91}
92
93impl LeaseGateState {
94 fn from_u8(raw: u8) -> Self {
95 match raw {
96 1 => Self::Held,
97 2 => Self::NotHeld,
98 _ => Self::NotRequired,
99 }
100 }
101
102 pub fn label(self) -> &'static str {
103 match self {
104 Self::NotRequired => "not_required",
105 Self::Held => "held",
106 Self::NotHeld => "not_held",
107 }
108 }
109}
110
111#[derive(Debug)]
120pub struct WriteGate {
121 read_only: AtomicBool,
126 role: ReplicationRole,
127 lease: AtomicU8,
128 auto_paused: AtomicBool,
133 last_archive_at_ms: AtomicU64,
139 pause_threshold_secs: AtomicU64,
143 flow: FlowController,
149 ownership: parking_lot::RwLock<Option<OwnershipAdmissionGate>>,
153}
154
155impl WriteGate {
156 pub fn from_options(options: &RedDBOptions) -> Self {
157 let soft_target = std::env::var(FLOW_CONTROL_SOFT_TARGET_ENV)
158 .ok()
159 .and_then(|raw| raw.trim().parse::<u64>().ok())
160 .unwrap_or(0);
161 let ownership = match options.replication.role {
162 ReplicationRole::Primary | ReplicationRole::Replica { .. } => {
163 Some(OwnershipAdmissionGate::primary_replica(
164 DEFAULT_PRIMARY_REPLICA_OWNER,
165 options.replication.term,
166 ))
167 }
168 ReplicationRole::Standalone => None,
169 };
170 Self {
171 read_only: AtomicBool::new(options.read_only),
172 role: options.replication.role.clone(),
173 lease: AtomicU8::new(LeaseGateState::NotRequired as u8),
174 auto_paused: AtomicBool::new(false),
175 last_archive_at_ms: AtomicU64::new(0),
176 pause_threshold_secs: AtomicU64::new(0),
177 flow: FlowController::new(soft_target, options.replication.quorum.clone()),
178 ownership: parking_lot::RwLock::new(ownership),
179 }
180 }
181
182 pub fn check(&self, kind: WriteKind) -> RedDBResult<()> {
195 self.check_consent(kind).map(|_| ())
196 }
197
198 pub fn check_consent(&self, kind: WriteKind) -> RedDBResult<crate::application::WriteConsent> {
205 if matches!(self.role, ReplicationRole::Replica { .. }) {
206 return Err(RedDBError::ReadOnly(format!(
207 "instance is a replica — {} rejected on public surface",
208 kind.label()
209 )));
210 }
211 if matches!(self.lease_state(), LeaseGateState::NotHeld) {
212 return Err(RedDBError::ReadOnly(format!(
213 "writer lease not held — {} rejected (serverless fence)",
214 kind.label()
215 )));
216 }
217 self.check_ownership(kind)?;
218 if self.read_only.load(Ordering::Acquire) {
219 return Err(RedDBError::ReadOnly(format!(
220 "instance is configured read_only — {} rejected",
221 kind.label()
222 )));
223 }
224 if self.auto_paused.load(Ordering::Acquire) {
225 return Err(RedDBError::ReadOnly(format!(
226 "instance is paused — WAL archive lag exceeded threshold — {} rejected",
227 kind.label()
228 )));
229 }
230 if matches!(self.flow.try_admit(), Admission::Throttled) {
235 return Err(RedDBError::ReadOnly(format!(
236 "write admission throttled — in-quorum replica lag exceeded soft target ({} records) — {} rejected",
237 self.flow.soft_target_lsn(),
238 kind.label()
239 )));
240 }
241 Ok(crate::application::WriteConsent {
242 kind,
243 _seal: crate::application::WriteConsentSeal::new(),
244 })
245 }
246
247 pub fn is_read_only(&self) -> bool {
248 self.read_only.load(Ordering::Acquire)
249 || self.auto_paused.load(Ordering::Acquire)
250 || matches!(self.role, ReplicationRole::Replica { .. })
251 || matches!(self.lease_state(), LeaseGateState::NotHeld)
252 || self
253 .ownership
254 .read()
255 .as_ref()
256 .is_some_and(|gate| gate.is_fenced())
257 }
258
259 pub fn is_manual_read_only(&self) -> bool {
264 self.read_only.load(Ordering::Acquire)
265 }
266
267 pub fn is_auto_paused(&self) -> bool {
271 self.auto_paused.load(Ordering::Acquire)
272 }
273
274 pub fn role(&self) -> &ReplicationRole {
275 &self.role
276 }
277
278 pub fn flow_control(&self) -> &FlowController {
282 &self.flow
283 }
284
285 pub fn is_flow_throttled(&self) -> bool {
289 self.flow.is_throttled()
290 }
291
292 pub fn set_read_only(&self, enabled: bool) -> bool {
300 self.read_only.swap(enabled, Ordering::AcqRel)
301 }
302
303 pub fn lease_state(&self) -> LeaseGateState {
306 LeaseGateState::from_u8(self.lease.load(Ordering::Acquire))
307 }
308
309 pub fn configure_archive_lag_pause(&self, threshold_secs: u64, baseline_ms: u64) {
321 self.pause_threshold_secs
322 .store(threshold_secs, Ordering::Release);
323 self.last_archive_at_ms
324 .store(baseline_ms, Ordering::Release);
325 }
326
327 pub fn record_archive_success(&self, now_ms: u64) {
331 self.last_archive_at_ms.store(now_ms, Ordering::Release);
332 }
333
334 pub fn archive_pause_threshold_secs(&self) -> u64 {
337 self.pause_threshold_secs.load(Ordering::Acquire)
338 }
339
340 pub fn last_archive_at_ms(&self) -> u64 {
342 self.last_archive_at_ms.load(Ordering::Acquire)
343 }
344
345 pub fn evaluate_archive_lag(&self, now_ms: u64) -> bool {
359 let threshold = self.pause_threshold_secs.load(Ordering::Acquire);
360 if threshold == 0 {
361 return self.auto_paused.load(Ordering::Acquire);
362 }
363 if self.read_only.load(Ordering::Acquire) {
364 return self.auto_paused.load(Ordering::Acquire);
365 }
366 let last_ms = self.last_archive_at_ms.load(Ordering::Acquire);
367 let lag_secs = now_ms.saturating_sub(last_ms) / 1000;
368 let should_pause = lag_secs > threshold;
369 self.auto_paused.store(should_pause, Ordering::Release);
370 should_pause
371 }
372
373 pub(crate) fn set_lease_state(&self, state: LeaseGateState) -> LeaseGateState {
381 LeaseGateState::from_u8(self.lease.swap(state as u8, Ordering::AcqRel))
382 }
383
384 fn check_ownership(&self, kind: WriteKind) -> RedDBResult<()> {
385 let Some(gate) = self.ownership.read().as_ref().cloned() else {
386 return Ok(());
387 };
388 match gate.admit() {
389 Ok(()) => Ok(()),
390 Err(reject) => {
391 let detail = OwnershipFenceDetail::from_reject(&reject);
392 OperatorEvent::OwnershipFenced {
393 reason: detail.reason.clone(),
394 ownership_epoch: detail.current_epoch,
395 range_identity: detail.range_identity.clone(),
396 }
397 .emit_global();
398 Err(RedDBError::ReadOnly(format!(
399 "ownership_fenced reason={} current_epoch={} range={} — {} rejected below routing",
400 detail.reason,
401 detail.current_epoch,
402 detail.range_identity,
403 kind.label()
404 )))
405 }
406 }
407 }
408
409 pub fn promote_primary_replica_owner(
410 &self,
411 new_owner_subject: &str,
412 new_term: u64,
413 ) -> RedDBResult<()> {
414 let new_owner = node_identity(new_owner_subject)?;
415 let mut guard = self.ownership.write();
416 let Some(gate) = guard.as_mut() else {
417 return Ok(());
418 };
419 gate.promote_to(new_owner, new_term)
420 }
421
422 pub fn primary_replica_range_authority(&self) -> Option<RangeAuthority> {
423 self.ownership
424 .read()
425 .as_ref()
426 .map(OwnershipAdmissionGate::range_authority)
427 }
428}
429
430#[derive(Debug, Clone)]
431struct OwnershipAdmissionGate {
432 catalog: ShardOwnershipCatalog,
433 holder: LeasedOwner,
434 node: NodeIdentity,
435 collection: CollectionId,
436 range_id: RangeId,
437 key: Vec<u8>,
438 current_term: SupervisorTerm,
439 now_ms: u64,
440}
441
442impl OwnershipAdmissionGate {
443 fn primary_replica(owner_subject: &str, term: u64) -> Self {
444 let owner = node_identity(owner_subject).expect("static primary-replica owner identity");
445 let collection =
446 CollectionId::new(RESERVED_GLOBAL_SYSTEM_COLLECTION).expect("static collection id");
447 let range_id = RangeId::new(RESERVED_GLOBAL_SYSTEM_RANGE_ID);
448 let range = RangeOwnership::establish(
449 collection.clone(),
450 range_id,
451 ShardKeyMode::Ordered,
452 RangeBounds::full(),
453 owner.clone(),
454 Vec::<NodeIdentity>::new(),
455 PlacementMetadata::with_replication_factor(1),
456 );
457 let current_term = SupervisorTerm::new(term);
458 let lease = OwnershipLease::grant(
459 current_term,
460 collection.clone(),
461 range_id,
462 owner.clone(),
463 OwnershipEpoch::initial(),
464 0,
465 u64::MAX,
466 );
467 let mut catalog = ShardOwnershipCatalog::new();
468 catalog
469 .apply_update(range)
470 .expect("initial reserved global system range is valid");
471 Self {
472 catalog,
473 holder: LeasedOwner::with_lease(lease),
474 node: owner,
475 collection,
476 range_id,
477 key: RESERVED_GLOBAL_SYSTEM_RANGE_KEY.to_vec(),
478 current_term,
479 now_ms: 0,
480 }
481 }
482
483 fn admit(&self) -> Result<(), DurableWriteReject> {
484 admit_durable_write(
485 &self.catalog,
486 &self.holder,
487 &self.node,
488 &self.collection,
489 &self.key,
490 self.current_term,
491 self.now_ms,
492 )
493 .map(|_| ())
494 }
495
496 fn is_fenced(&self) -> bool {
497 self.admit().is_err()
498 }
499
500 fn range_authority(&self) -> RangeAuthority {
501 let epoch = self
502 .catalog
503 .range(&self.collection, self.range_id)
504 .map(|range| range.epoch().value())
505 .unwrap_or(0);
506 RangeAuthority {
507 range_id: self.range_id.value(),
508 min_term: self.current_term.value(),
509 min_ownership_epoch: epoch,
510 }
511 }
512
513 fn promote_to(&mut self, new_owner: NodeIdentity, new_term: u64) -> RedDBResult<()> {
514 let current = self
515 .catalog
516 .range(&self.collection, self.range_id)
517 .ok_or_else(|| RedDBError::Internal("reserved global system range missing".into()))?
518 .clone();
519 self.catalog
520 .apply_update(current.transfer_to(new_owner, [self.node.clone()]))
521 .map_err(|err| RedDBError::Catalog(err.to_string()))?;
522 self.current_term = SupervisorTerm::new(new_term);
523 Ok(())
524 }
525}
526
527struct OwnershipFenceDetail {
528 reason: String,
529 current_epoch: u64,
530 range_identity: String,
531}
532
533impl OwnershipFenceDetail {
534 fn from_reject(reject: &DurableWriteReject) -> Self {
535 match reject {
536 DurableWriteReject::NoRange { collection } => Self {
537 reason: "no_range".to_string(),
538 current_epoch: 0,
539 range_identity: collection.to_string(),
540 },
541 DurableWriteReject::StaleOwnership {
542 collection,
543 range_id,
544 current_epoch,
545 ..
546 } => Self {
547 reason: "stale_ownership".to_string(),
548 current_epoch: current_epoch.value(),
549 range_identity: format!("{collection}/{range_id}"),
550 },
551 DurableWriteReject::NotOwner {
552 collection,
553 range_id,
554 epoch,
555 ..
556 } => Self {
557 reason: "not_owner".to_string(),
558 current_epoch: epoch.value(),
559 range_identity: format!("{collection}/{range_id}"),
560 },
561 DurableWriteReject::Fenced {
562 collection,
563 range_id,
564 reason,
565 } => Self {
566 reason: fence_reason_label(reason).to_string(),
567 current_epoch: 0,
568 range_identity: format!("{collection}/{range_id}"),
569 },
570 }
571 }
572}
573
574fn fence_reason_label(reason: &crate::cluster::FenceReason) -> &'static str {
575 match reason {
576 crate::cluster::FenceReason::Unleased => "unleased",
577 crate::cluster::FenceReason::Revoked => "revoked",
578 crate::cluster::FenceReason::TermSuperseded { .. } => "term_superseded",
579 crate::cluster::FenceReason::EpochSuperseded { .. } => "epoch_superseded",
580 crate::cluster::FenceReason::Expired { .. } => "expired",
581 }
582}
583
584fn node_identity(subject: &str) -> RedDBResult<NodeIdentity> {
585 NodeIdentity::from_certificate_subject(subject)
586 .map_err(|err| RedDBError::InvalidConfig(err.to_string()))
587}
588
589#[cfg(test)]
590impl WriteGate {
591 fn install_primary_replica_ownership_gate_for_test(&self, owner_subject: &str, term: u64) {
592 *self.ownership.write() =
593 Some(OwnershipAdmissionGate::primary_replica(owner_subject, term));
594 }
595
596 fn promote_primary_replica_owner_for_test(&self, new_owner_subject: &str, new_term: u64) {
597 self.promote_primary_replica_owner(new_owner_subject, new_term)
598 .expect("test promotion succeeds");
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 fn gate(read_only: bool, role: ReplicationRole) -> WriteGate {
607 WriteGate {
608 read_only: AtomicBool::new(read_only),
609 role,
610 lease: AtomicU8::new(LeaseGateState::NotRequired as u8),
611 auto_paused: AtomicBool::new(false),
612 last_archive_at_ms: AtomicU64::new(0),
613 pause_threshold_secs: AtomicU64::new(0),
614 flow: FlowController::disabled(),
615 ownership: parking_lot::RwLock::new(None),
616 }
617 }
618
619 #[test]
620 fn standalone_allows_writes() {
621 let g = gate(false, ReplicationRole::Standalone);
622 assert!(g.check(WriteKind::Dml).is_ok());
623 assert!(g.check(WriteKind::Ddl).is_ok());
624 assert!(!g.is_read_only());
625 }
626
627 #[test]
628 fn primary_allows_writes() {
629 let g = gate(false, ReplicationRole::Primary);
630 assert!(g.check(WriteKind::Dml).is_ok());
631 assert!(!g.is_read_only());
632 }
633
634 #[test]
635 fn replica_rejects_every_kind() {
636 let g = gate(
637 true,
638 ReplicationRole::Replica {
639 primary_addr: "http://primary:55055".into(),
640 },
641 );
642 for kind in [
643 WriteKind::Dml,
644 WriteKind::Ddl,
645 WriteKind::IndexBuild,
646 WriteKind::Maintenance,
647 WriteKind::Backup,
648 WriteKind::Serverless,
649 ] {
650 let err = g.check(kind).unwrap_err();
651 match err {
652 RedDBError::ReadOnly(msg) => assert!(msg.contains("replica")),
653 other => panic!("expected ReadOnly, got {other:?}"),
654 }
655 }
656 assert!(g.is_read_only());
657 }
658
659 #[test]
660 fn read_only_flag_rejects_writes_on_standalone() {
661 let g = gate(true, ReplicationRole::Standalone);
662 let err = g.check(WriteKind::Dml).unwrap_err();
663 match err {
664 RedDBError::ReadOnly(msg) => assert!(msg.contains("read_only")),
665 other => panic!("expected ReadOnly, got {other:?}"),
666 }
667 }
668
669 #[test]
670 fn lease_not_held_rejects_writes_on_primary() {
671 let g = gate(false, ReplicationRole::Primary);
672 g.set_lease_state(LeaseGateState::NotHeld);
673 let err = g.check(WriteKind::Dml).unwrap_err();
674 match err {
675 RedDBError::ReadOnly(msg) => assert!(msg.contains("lease")),
676 other => panic!("expected ReadOnly, got {other:?}"),
677 }
678 assert!(g.is_read_only());
679 }
680
681 #[test]
682 fn lease_held_allows_writes_on_primary() {
683 let g = gate(false, ReplicationRole::Primary);
684 g.set_lease_state(LeaseGateState::Held);
685 assert!(g.check(WriteKind::Dml).is_ok());
686 assert!(!g.is_read_only());
687 }
688
689 #[test]
690 fn lease_state_transitions_return_previous() {
691 let g = gate(false, ReplicationRole::Primary);
692 assert_eq!(
693 g.set_lease_state(LeaseGateState::Held),
694 LeaseGateState::NotRequired
695 );
696 assert_eq!(
697 g.set_lease_state(LeaseGateState::NotHeld),
698 LeaseGateState::Held
699 );
700 assert_eq!(g.lease_state(), LeaseGateState::NotHeld);
701 }
702
703 #[test]
704 fn lease_loss_overrides_writable_read_only_flag() {
705 let g = gate(false, ReplicationRole::Primary);
707 g.set_lease_state(LeaseGateState::NotHeld);
708 let err = g.check(WriteKind::Ddl).unwrap_err();
709 match err {
710 RedDBError::ReadOnly(msg) => assert!(msg.contains("lease")),
711 other => panic!("expected ReadOnly, got {other:?}"),
712 }
713 }
714
715 #[test]
721 fn archive_lag_disabled_threshold_is_noop() {
722 let g = gate(false, ReplicationRole::Standalone);
723 g.configure_archive_lag_pause(0, 1_000);
724 assert!(!g.evaluate_archive_lag(10_000_000_000));
726 assert!(!g.is_auto_paused());
727 assert!(g.check(WriteKind::Dml).is_ok());
728 }
729
730 #[test]
731 fn archive_lag_triggers_auto_pause_past_threshold() {
732 let g = gate(false, ReplicationRole::Standalone);
733 g.configure_archive_lag_pause(60, 1_000_000);
735 assert!(!g.evaluate_archive_lag(1_000_000 + 30_000));
737 assert!(g.check(WriteKind::Dml).is_ok());
738
739 assert!(g.evaluate_archive_lag(1_000_000 + 120_000));
741 assert!(g.is_auto_paused());
742 let err = g.check(WriteKind::Dml).unwrap_err();
743 match err {
744 RedDBError::ReadOnly(msg) => assert!(msg.contains("WAL archive lag"), "{msg}"),
745 other => panic!("expected ReadOnly, got {other:?}"),
746 }
747 assert!(g.is_read_only());
748 }
749
750 #[test]
751 fn archive_lag_auto_resume_after_recovery() {
752 let g = gate(false, ReplicationRole::Standalone);
753 g.configure_archive_lag_pause(60, 1_000_000);
754 assert!(g.evaluate_archive_lag(1_000_000 + 120_000));
756 assert!(g.is_auto_paused());
757 g.record_archive_success(1_000_000 + 130_000);
759 assert!(!g.evaluate_archive_lag(1_000_000 + 130_000));
760 assert!(!g.is_auto_paused());
761 assert!(g.check(WriteKind::Dml).is_ok());
762 }
763
764 #[test]
765 fn manual_read_only_blocks_auto_pause_writes_and_is_sticky() {
766 let g = gate(true, ReplicationRole::Standalone);
770 g.configure_archive_lag_pause(60, 1_000_000);
771
772 assert!(!g.evaluate_archive_lag(1_000_000 + 120_000));
774 assert!(!g.is_auto_paused());
775 assert!(g.is_manual_read_only());
776 let err = g.check(WriteKind::Dml).unwrap_err();
778 match err {
779 RedDBError::ReadOnly(msg) => {
780 assert!(msg.contains("read_only"), "{msg}");
781 assert!(!msg.contains("WAL archive lag"), "{msg}");
782 }
783 other => panic!("expected ReadOnly, got {other:?}"),
784 }
785
786 g.record_archive_success(1_000_000 + 130_000);
789 assert!(!g.evaluate_archive_lag(1_000_000 + 130_000));
790 assert!(g.is_manual_read_only(), "manual must stay set");
791 assert!(!g.is_auto_paused());
792 assert!(g.check(WriteKind::Dml).is_err());
793 }
794
795 #[test]
796 fn manual_clearing_resumes_auto_evaluation() {
797 let g = gate(true, ReplicationRole::Standalone);
800 g.configure_archive_lag_pause(60, 1_000_000);
801 assert!(!g.evaluate_archive_lag(1_000_000 + 120_000));
803 g.set_read_only(false);
805 assert!(g.evaluate_archive_lag(1_000_000 + 120_000));
807 assert!(g.is_auto_paused());
808 }
809
810 #[test]
811 fn archive_lag_pause_state_independent_from_manual_flag() {
812 let g = gate(false, ReplicationRole::Standalone);
813 g.configure_archive_lag_pause(60, 1_000_000);
814 assert!(g.evaluate_archive_lag(1_000_000 + 120_000));
815 let prev = g.set_read_only(true);
817 assert!(!prev);
818 assert!(g.is_manual_read_only());
819 assert!(g.is_auto_paused());
820 g.set_read_only(false);
822 assert!(g.is_auto_paused());
823 assert!(g.check(WriteKind::Dml).is_err());
824 }
825
826 fn gate_with_flow(soft_target: u64, quorum: crate::replication::QuorumConfig) -> WriteGate {
831 WriteGate {
832 read_only: AtomicBool::new(false),
833 role: ReplicationRole::Primary,
834 lease: AtomicU8::new(LeaseGateState::NotRequired as u8),
835 auto_paused: AtomicBool::new(false),
836 last_archive_at_ms: AtomicU64::new(0),
837 pause_threshold_secs: AtomicU64::new(0),
838 flow: FlowController::new(soft_target, quorum),
839 ownership: parking_lot::RwLock::new(None),
840 }
841 }
842
843 fn flow_replica(
844 id: &str,
845 region: Option<&str>,
846 last_acked_lsn: u64,
847 ) -> crate::replication::primary::ReplicaState {
848 crate::replication::primary::ReplicaState {
849 id: id.to_string(),
850 last_acked_lsn,
851 last_sent_lsn: last_acked_lsn,
852 last_durable_lsn: last_acked_lsn,
853 apply_error_count: 0,
854 divergence_count: 0,
855 connected_at_unix_ms: 0,
856 last_seen_at_unix_ms: 0,
857 region: region.map(String::from),
858 rebootstrapping: false,
859 }
860 }
861
862 #[test]
863 fn flow_throttle_rejects_writes_when_in_quorum_replica_lags_and_releases() {
864 let g = gate_with_flow(100, crate::replication::QuorumConfig::sync(1));
865 assert!(g.check(WriteKind::Dml).is_ok());
867 assert!(!g.is_flow_throttled());
868
869 g.flow_control()
871 .observe(&[flow_replica("r1", Some("us"), 350)], 500);
872 assert!(g.is_flow_throttled());
873 let err = g.check(WriteKind::Dml).unwrap_err();
874 match err {
875 RedDBError::ReadOnly(msg) => assert!(msg.contains("throttled"), "{msg}"),
876 other => panic!("expected ReadOnly throttle, got {other:?}"),
877 }
878 assert!(!g.is_read_only());
880
881 g.flow_control()
883 .observe(&[flow_replica("r1", Some("us"), 450)], 500);
884 assert!(!g.is_flow_throttled());
885 assert!(g.check(WriteKind::Dml).is_ok());
886 }
887
888 #[test]
889 fn flow_throttle_excludes_async_read_replica() {
890 let g = gate_with_flow(100, crate::replication::QuorumConfig::regions(["us"]));
893 g.flow_control().observe(
894 &[
895 flow_replica("in-quorum-us", Some("us"), 500),
896 flow_replica("async-ap", Some("ap"), 0),
897 ],
898 500,
899 );
900 assert!(!g.is_flow_throttled());
901 assert!(g.check(WriteKind::Dml).is_ok());
902 }
903
904 #[test]
905 fn flow_throttle_disabled_by_default() {
906 let g = gate(false, ReplicationRole::Primary);
907 g.flow_control()
909 .observe(&[flow_replica("r1", Some("us"), 0)], 1_000_000);
910 assert!(!g.is_flow_throttled());
911 assert!(g.check(WriteKind::Dml).is_ok());
912 }
913
914 #[test]
915 fn replica_role_overrides_missing_read_only_flag() {
916 let g = gate(
917 false,
918 ReplicationRole::Replica {
919 primary_addr: "http://primary:55055".into(),
920 },
921 );
922 let err = g.check(WriteKind::Dml).unwrap_err();
923 match err {
924 RedDBError::ReadOnly(msg) => assert!(msg.contains("replica")),
925 other => panic!("expected ReadOnly, got {other:?}"),
926 }
927 }
928
929 #[test]
930 fn replica_role_carries_apply_authority_but_stays_public_read_only() {
931 let options = RedDBOptions::in_memory().with_replication(
932 crate::replication::ReplicationConfig::replica("http://primary:55055").with_term(8),
933 );
934 let g = WriteGate::from_options(&options);
935
936 let authority = g
937 .primary_replica_range_authority()
938 .expect("replica apply authority");
939 assert_eq!(authority.range_id, RESERVED_GLOBAL_SYSTEM_RANGE_ID);
940 assert_eq!(authority.min_term, 8);
941 assert_eq!(authority.min_ownership_epoch, 1);
942
943 let err = g.check(WriteKind::Dml).unwrap_err();
944 match err {
945 RedDBError::ReadOnly(msg) => assert!(msg.contains("replica"), "{msg}"),
946 other => panic!("expected replica read-only, got {other:?}"),
947 }
948 }
949
950 #[test]
951 fn ownership_admission_fences_deposed_primary() {
952 let g = gate(false, ReplicationRole::Primary);
953 g.install_primary_replica_ownership_gate_for_test("CN=node-a,O=reddb", 7);
954
955 assert!(g.check(WriteKind::Dml).is_ok());
956
957 g.promote_primary_replica_owner_for_test("CN=node-b,O=reddb", 8);
958 let err = g.check(WriteKind::Dml).unwrap_err();
959 match err {
960 RedDBError::ReadOnly(msg) => {
961 assert!(msg.contains("ownership_fenced"), "{msg}");
962 assert!(msg.contains("reason=stale_ownership"), "{msg}");
963 assert!(msg.contains("current_epoch=2"), "{msg}");
964 assert!(msg.contains("range=system.global/0"), "{msg}");
965 }
966 other => panic!("expected ownership fencing ReadOnly, got {other:?}"),
967 }
968 }
969
970 #[test]
971 fn primary_replica_authority_reports_current_term_epoch_and_range() {
972 let g = gate(false, ReplicationRole::Primary);
973 g.install_primary_replica_ownership_gate_for_test("CN=node-a,O=reddb", 7);
974 g.promote_primary_replica_owner_for_test("CN=node-b,O=reddb", 8);
975
976 let authority = g
977 .primary_replica_range_authority()
978 .expect("primary-replica authority");
979 assert_eq!(authority.range_id, RESERVED_GLOBAL_SYSTEM_RANGE_ID);
980 assert_eq!(authority.min_term, 8);
981 assert_eq!(authority.min_ownership_epoch, 2);
982 }
983}