Skip to main content

ipfrs_storage/
storage_replication_manager.rs

1//! StorageReplicationManager — production-grade replication orchestration for IPFS blocks.
2//!
3//! Maintains a registry of [`ReplicaTarget`] nodes (identified by 16-byte [`ReplicaId`]s),
4//! a bounded pending-operations queue ([`ReplicationOp`]), a rolling replication log,
5//! and a configurable [`ReplicationPolicy`].
6//!
7//! # Design highlights
8//!
9//! * **Zero external PRNG dependency** — success/failure simulation uses an inline
10//!   xorshift64 seeded from a deterministic function of each operation's content-id.
11//! * **No `unwrap()`** — every fallible path uses `?`, `if let`, or `ok_or`.
12//! * **Bounded memory** — operation queue capped at 10 000; log capped at 500 entries.
13//!
14//! # Quick-start
15//!
16//! ```rust
17//! use ipfrs_storage::storage_replication_manager::{
18//!     StorageReplicationManager, ReplicaTarget, SrmReplicationConfig, ReplicationPolicy,
19//! };
20//!
21//! let cfg = SrmReplicationConfig {
22//!     policy: ReplicationPolicy::Asynchronous,
23//!     min_replicas: 2,
24//!     max_replicas: 5,
25//!     retry_limit: 3,
26//!     batch_size: 32,
27//! };
28//! let mut mgr = StorageReplicationManager::new(cfg);
29//!
30//! let id = [1u8; 16];
31//! mgr.add_replica(ReplicaTarget {
32//!     id,
33//!     endpoint: "10.0.0.1:4001".into(),
34//!     priority: 10,
35//!     is_healthy: true,
36//!     last_sync_ts: 0,
37//!     bytes_replicated: 0,
38//! }).unwrap();
39//!
40//! let cid = [0xABu8; 32];
41//! mgr.enqueue_put(cid, 4096);
42//! let result = mgr.process_batch();
43//! println!("successes: {}", result.success_count);
44//! ```
45
46use std::collections::{HashMap, VecDeque};
47
48// ── Constants ────────────────────────────────────────────────────────────────
49
50/// Maximum number of pending [`ReplicationOp`]s in the queue.
51pub const MAX_PENDING_OPS: usize = 10_000;
52/// Maximum number of entries retained in the replication log.
53pub const MAX_LOG_ENTRIES: usize = 500;
54
55// ── PRNG & hashing helpers ───────────────────────────────────────────────────
56
57/// Xorshift64 PRNG — no external dependencies, deterministic.
58#[inline]
59pub fn xorshift64(state: &mut u64) -> u64 {
60    let mut x = *state;
61    x ^= x << 13;
62    x ^= x >> 7;
63    x ^= x << 17;
64    *state = x;
65    x
66}
67
68/// FNV-1a 64-bit hash.
69#[inline]
70pub fn fnv1a_64(data: &[u8]) -> u64 {
71    let mut h: u64 = 14_695_981_039_346_656_037;
72    for &b in data {
73        h ^= b as u64;
74        h = h.wrapping_mul(1_099_511_628_211);
75    }
76    h
77}
78
79/// Derive a per-operation seed from a CID and a replica id.
80#[inline]
81fn op_seed(cid: &[u8; 32], replica_id: &ReplicaId) -> u64 {
82    let mut combined = [0u8; 48];
83    combined[..32].copy_from_slice(cid);
84    combined[32..].copy_from_slice(replica_id);
85    fnv1a_64(&combined)
86}
87
88// ── ReplicaId ────────────────────────────────────────────────────────────────
89
90/// 16-byte opaque identifier for a replication target.
91pub type ReplicaId = [u8; 16];
92
93// ── ReplicaTarget ────────────────────────────────────────────────────────────
94
95/// Metadata and runtime state for a single replication target node.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ReplicaTarget {
98    /// Unique identifier.
99    pub id: ReplicaId,
100    /// Network endpoint (e.g. `"10.0.0.2:4001"`).
101    pub endpoint: String,
102    /// Scheduling priority — lower value means higher priority (like UNIX nice).
103    pub priority: u32,
104    /// Whether the replica is currently reachable and accepting writes.
105    pub is_healthy: bool,
106    /// Unix-epoch millisecond timestamp of the last successful sync.
107    pub last_sync_ts: u64,
108    /// Cumulative bytes replicated to this target during its lifetime.
109    pub bytes_replicated: u64,
110}
111
112/// Type alias — `SrmReplicaTarget` is the public-facing name.
113pub type SrmReplicaTarget = ReplicaTarget;
114
115// ── ReplicationOp ────────────────────────────────────────────────────────────
116
117/// An individual replication operation waiting to be processed.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum ReplicationOp {
120    /// Replicate a newly added block to all configured targets.
121    Put {
122        /// Content-id (32-byte raw multihash digest).
123        cid: [u8; 32],
124        /// Size of the block in bytes.
125        data_len: usize,
126        /// Unix-epoch millisecond timestamp when the operation was enqueued.
127        ts: u64,
128    },
129    /// Propagate a block deletion to all configured targets.
130    Delete {
131        /// Content-id.
132        cid: [u8; 32],
133        /// Enqueue timestamp.
134        ts: u64,
135    },
136    /// Request a full incremental sync from all targets since a given timestamp.
137    Sync {
138        /// Only fetch operations that occurred after this timestamp.
139        since_ts: u64,
140    },
141}
142
143/// Type alias — `SrmReplicationOp` is the public-facing name.
144pub type SrmReplicationOp = ReplicationOp;
145
146// ── ReplicationPolicy ─────────────────────────────────────────────────────────
147
148/// Controls how and when replication is performed.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum ReplicationPolicy {
151    /// Wait for **all** healthy replicas to confirm before returning.
152    Synchronous,
153    /// Fire-and-forget — enqueue and return immediately.
154    Asynchronous,
155    /// Skip failed replicas silently; do not retry.
156    BestEffort,
157    /// Require acknowledgement from at least `n` replicas before proceeding.
158    QuorumWrite(usize),
159    /// Process replicas in descending priority order, stopping at the first failure.
160    PriorityFirst,
161}
162
163/// Type alias — `SrmReplicationPolicy` is the public-facing name.
164pub type SrmReplicationPolicy = ReplicationPolicy;
165
166// ── SrmReplicationConfig ──────────────────────────────────────────────────────
167
168/// Configuration for [`StorageReplicationManager`].
169#[derive(Debug, Clone)]
170pub struct SrmReplicationConfig {
171    /// Replication strategy.
172    pub policy: ReplicationPolicy,
173    /// Minimum number of healthy replicas required for normal operation.
174    pub min_replicas: usize,
175    /// Maximum number of replication targets that may be registered.
176    pub max_replicas: usize,
177    /// Number of retry attempts for a failed replication before giving up.
178    pub retry_limit: usize,
179    /// Number of pending operations to drain per [`StorageReplicationManager::process_batch`] call.
180    pub batch_size: usize,
181}
182
183impl Default for SrmReplicationConfig {
184    fn default() -> Self {
185        Self {
186            policy: ReplicationPolicy::Asynchronous,
187            min_replicas: 2,
188            max_replicas: 10,
189            retry_limit: 3,
190            batch_size: 64,
191        }
192    }
193}
194
195// ── ReplicationLogEntry ───────────────────────────────────────────────────────
196
197/// A single audit entry written after each replication attempt.
198#[derive(Debug, Clone)]
199pub struct ReplicationLogEntry {
200    /// Unix-epoch millisecond timestamp of the attempt.
201    pub ts: u64,
202    /// Human-readable operation kind: `"Put"`, `"Delete"`, or `"Sync"`.
203    pub op_kind: String,
204    /// Target replica that was attempted.
205    pub replica_id: ReplicaId,
206    /// Whether the attempt succeeded.
207    pub success: bool,
208    /// Bytes transferred (0 for Delete/Sync).
209    pub bytes: usize,
210    /// Error description if the attempt failed.
211    pub error: Option<String>,
212}
213
214// ── SrmBatchResult ────────────────────────────────────────────────────────────
215
216/// Summary returned by [`StorageReplicationManager::process_batch`].
217#[derive(Debug, Clone, Default)]
218pub struct SrmBatchResult {
219    /// Number of operations processed.
220    pub ops_processed: usize,
221    /// Number of per-replica replication attempts that succeeded.
222    pub success_count: usize,
223    /// Number of per-replica replication attempts that failed.
224    pub failure_count: usize,
225    /// Total bytes dispatched across all successful Put operations.
226    pub bytes_dispatched: usize,
227}
228
229// ── SrmReplicationStats ───────────────────────────────────────────────────────
230
231/// Snapshot of accumulated statistics for the manager.
232#[derive(Debug, Clone, Default)]
233pub struct SrmReplicationStats {
234    /// Total number of operations ever enqueued.
235    pub total_ops_enqueued: u64,
236    /// Total number of operations ever processed (drained from the queue).
237    pub total_ops_processed: u64,
238    /// Total per-replica attempts that succeeded.
239    pub total_successes: u64,
240    /// Total per-replica attempts that failed.
241    pub total_failures: u64,
242    /// Total bytes replicated across all targets.
243    pub total_bytes_replicated: u64,
244    /// Number of currently pending operations.
245    pub pending_ops: usize,
246    /// Number of healthy replicas at query time.
247    pub healthy_replicas: usize,
248    /// Number of unhealthy replicas at query time.
249    pub unhealthy_replicas: usize,
250    /// Number of entries in the rolling log.
251    pub log_entries: usize,
252    /// Number of times the operation queue reached its capacity limit.
253    pub queue_overflow_count: u64,
254}
255
256// ── Error type ────────────────────────────────────────────────────────────────
257
258/// Errors that may be returned by [`StorageReplicationManager`] methods.
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum SrmError {
261    /// A replica with the given id is already registered.
262    DuplicateReplica(ReplicaId),
263    /// No replica with the given id exists.
264    ReplicaNotFound(ReplicaId),
265    /// The replica registry is at capacity (`max_replicas`).
266    RegistryFull,
267    /// The pending-operations queue is full (`MAX_PENDING_OPS`).
268    QueueFull,
269}
270
271impl std::fmt::Display for SrmError {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            Self::DuplicateReplica(id) => write!(f, "replica already registered: {:?}", id),
275            Self::ReplicaNotFound(id) => write!(f, "replica not found: {:?}", id),
276            Self::RegistryFull => write!(f, "replica registry is full"),
277            Self::QueueFull => write!(f, "pending-operation queue is full"),
278        }
279    }
280}
281
282impl std::error::Error for SrmError {}
283
284// ── StorageReplicationManager ─────────────────────────────────────────────────
285
286/// Production-grade storage replication manager.
287///
288/// Maintains:
289/// - A registry of up to `max_replicas` [`ReplicaTarget`] nodes.
290/// - A bounded [`VecDeque`] of pending [`ReplicationOp`]s.
291/// - A rolling [`Vec`] of the last [`MAX_LOG_ENTRIES`] [`ReplicationLogEntry`] records.
292/// - Accumulated [`SrmReplicationStats`].
293///
294/// Call [`process_batch`](Self::process_batch) periodically to drain the queue and
295/// simulate replication outcomes using the inline xorshift64 PRNG.
296#[derive(Debug)]
297pub struct StorageReplicationManager {
298    /// Registered replication targets indexed by their [`ReplicaId`].
299    pub targets: HashMap<ReplicaId, ReplicaTarget>,
300    /// Bounded queue of pending replication operations.
301    pub pending_ops: VecDeque<ReplicationOp>,
302    /// Rolling log of recent replication attempts.
303    pub replication_log: Vec<ReplicationLogEntry>,
304    /// Manager configuration.
305    pub config: SrmReplicationConfig,
306    /// Accumulated statistics.
307    stats: SrmReplicationStats,
308    /// Internal xorshift64 PRNG state (seeded per-operation, mutated per-replica).
309    prng_state: u64,
310    /// Monotonically increasing fake clock (milliseconds) for log timestamps.
311    clock_ms: u64,
312}
313
314impl StorageReplicationManager {
315    // ── Construction ──────────────────────────────────────────────────────────
316
317    /// Create a new manager with the given configuration.
318    ///
319    /// The PRNG is seeded deterministically from the config values so that unit
320    /// tests are reproducible without an external random source.
321    pub fn new(config: SrmReplicationConfig) -> Self {
322        let seed = fnv1a_64(&[
323            config.min_replicas as u8,
324            config.max_replicas as u8,
325            config.retry_limit as u8,
326            config.batch_size as u8,
327        ]);
328        Self {
329            targets: HashMap::new(),
330            pending_ops: VecDeque::new(),
331            replication_log: Vec::new(),
332            config,
333            stats: SrmReplicationStats::default(),
334            prng_state: seed | 1, // ensure non-zero
335            clock_ms: 1_000,      // start at 1 second
336        }
337    }
338
339    /// Create a manager with default configuration.
340    pub fn with_defaults() -> Self {
341        Self::new(SrmReplicationConfig::default())
342    }
343
344    // ── Replica management ────────────────────────────────────────────────────
345
346    /// Register a new replication target.
347    ///
348    /// # Errors
349    /// - [`SrmError::RegistryFull`] if `max_replicas` is already reached.
350    /// - [`SrmError::DuplicateReplica`] if the id is already registered.
351    pub fn add_replica(&mut self, target: ReplicaTarget) -> Result<(), SrmError> {
352        if self.targets.len() >= self.config.max_replicas {
353            return Err(SrmError::RegistryFull);
354        }
355        if self.targets.contains_key(&target.id) {
356            return Err(SrmError::DuplicateReplica(target.id));
357        }
358        self.targets.insert(target.id, target);
359        Ok(())
360    }
361
362    /// Remove a replication target by id.
363    ///
364    /// # Errors
365    /// - [`SrmError::ReplicaNotFound`] if no target with `id` is registered.
366    pub fn remove_replica(&mut self, id: ReplicaId) -> Result<ReplicaTarget, SrmError> {
367        self.targets
368            .remove(&id)
369            .ok_or(SrmError::ReplicaNotFound(id))
370    }
371
372    /// Mark a replica as unhealthy.  Does nothing (ok) if the replica is already unhealthy.
373    ///
374    /// # Errors
375    /// - [`SrmError::ReplicaNotFound`] if no target with `id` is registered.
376    pub fn mark_unhealthy(&mut self, id: ReplicaId) -> Result<(), SrmError> {
377        let target = self
378            .targets
379            .get_mut(&id)
380            .ok_or(SrmError::ReplicaNotFound(id))?;
381        target.is_healthy = false;
382        Ok(())
383    }
384
385    /// Mark a previously unhealthy replica as healthy again.
386    ///
387    /// # Errors
388    /// - [`SrmError::ReplicaNotFound`] if no target with `id` is registered.
389    pub fn mark_healthy(&mut self, id: ReplicaId) -> Result<(), SrmError> {
390        let target = self
391            .targets
392            .get_mut(&id)
393            .ok_or(SrmError::ReplicaNotFound(id))?;
394        target.is_healthy = true;
395        Ok(())
396    }
397
398    /// Update the priority of a registered replica.
399    ///
400    /// # Errors
401    /// - [`SrmError::ReplicaNotFound`] if no target with `id` is registered.
402    pub fn set_priority(&mut self, id: ReplicaId, priority: u32) -> Result<(), SrmError> {
403        let target = self
404            .targets
405            .get_mut(&id)
406            .ok_or(SrmError::ReplicaNotFound(id))?;
407        target.priority = priority;
408        Ok(())
409    }
410
411    // ── Operation enqueueing ──────────────────────────────────────────────────
412
413    /// Enqueue a `Put` replication operation.
414    ///
415    /// Returns `false` (and increments `queue_overflow_count`) if the queue is full.
416    pub fn enqueue_put(&mut self, cid: [u8; 32], data_len: usize) -> bool {
417        self.try_enqueue(ReplicationOp::Put {
418            cid,
419            data_len,
420            ts: self.clock_ms,
421        })
422    }
423
424    /// Enqueue a `Delete` replication operation.
425    ///
426    /// Returns `false` if the queue is full.
427    pub fn enqueue_delete(&mut self, cid: [u8; 32]) -> bool {
428        self.try_enqueue(ReplicationOp::Delete {
429            cid,
430            ts: self.clock_ms,
431        })
432    }
433
434    /// Enqueue a full incremental `Sync` operation.
435    ///
436    /// Returns `false` if the queue is full.
437    pub fn enqueue_sync(&mut self, since_ts: u64) -> bool {
438        self.try_enqueue(ReplicationOp::Sync { since_ts })
439    }
440
441    /// Internal: push an op onto the queue, honouring the capacity limit.
442    fn try_enqueue(&mut self, op: ReplicationOp) -> bool {
443        if self.pending_ops.len() >= MAX_PENDING_OPS {
444            self.stats.queue_overflow_count += 1;
445            return false;
446        }
447        self.pending_ops.push_back(op);
448        self.stats.total_ops_enqueued += 1;
449        self.clock_ms = self.clock_ms.wrapping_add(1);
450        true
451    }
452
453    // ── Batch processing ──────────────────────────────────────────────────────
454
455    /// Drain up to `batch_size` pending operations and simulate replication.
456    ///
457    /// For each operation the manager iterates over all healthy replicas and
458    /// uses xorshift64 (seeded from the CID + replica-id) to decide success or
459    /// failure (success probability ≈ 7/8 for Put/Delete, 3/4 for Sync).
460    /// Results are appended to the rolling log (capped at [`MAX_LOG_ENTRIES`]).
461    /// Statistics are updated atomically per batch.
462    pub fn process_batch(&mut self) -> SrmBatchResult {
463        let limit = self.config.batch_size.min(self.pending_ops.len());
464        let mut result = SrmBatchResult::default();
465
466        // Collect target ids/priorities upfront to avoid borrow conflicts.
467        let mut ordered_ids: Vec<(u32, ReplicaId)> = self
468            .targets
469            .values()
470            .filter(|t| t.is_healthy)
471            .map(|t| (t.priority, t.id))
472            .collect();
473
474        match &self.config.policy {
475            ReplicationPolicy::PriorityFirst => {
476                // Lower priority value = higher precedence.
477                ordered_ids.sort_by_key(|(p, _)| *p);
478            }
479            _ => {
480                // Stable deterministic order otherwise.
481                ordered_ids.sort_by_key(|(_, id)| *id);
482            }
483        }
484
485        let replica_ids: Vec<ReplicaId> = ordered_ids.into_iter().map(|(_, id)| id).collect();
486
487        for _ in 0..limit {
488            let op = match self.pending_ops.pop_front() {
489                Some(o) => o,
490                None => break,
491            };
492            result.ops_processed += 1;
493            self.stats.total_ops_processed += 1;
494
495            let (op_kind, cid_ref, data_len_val) = match &op {
496                ReplicationOp::Put { cid, data_len, .. } => ("Put", *cid, *data_len),
497                ReplicationOp::Delete { cid, .. } => ("Delete", *cid, 0usize),
498                ReplicationOp::Sync { .. } => {
499                    // Sync uses a synthetic CID derived from its timestamp.
500                    let ts = if let ReplicationOp::Sync { since_ts } = &op {
501                        *since_ts
502                    } else {
503                        0
504                    };
505                    let mut cid = [0u8; 32];
506                    let ts_bytes = ts.to_le_bytes();
507                    cid[..8].copy_from_slice(&ts_bytes);
508                    ("Sync", cid, 0usize)
509                }
510            };
511
512            let mut quorum_satisfied = false;
513            let quorum_needed = if let ReplicationPolicy::QuorumWrite(n) = self.config.policy {
514                n
515            } else {
516                0
517            };
518            let mut quorum_count = 0usize;
519
520            for &replica_id in &replica_ids {
521                // Seed PRNG deterministically per (op, replica) pair.
522                let mut seed = op_seed(&cid_ref, &replica_id);
523                seed ^= self.prng_state;
524                // Advance shared state.
525                self.prng_state = xorshift64(&mut self.prng_state);
526
527                let rnd = xorshift64(&mut seed);
528
529                // Success probability: 7/8 for Put/Delete, 3/4 for Sync.
530                let success = match op_kind {
531                    "Sync" => (rnd & 0x3) != 0,
532                    _ => (rnd & 0x7) != 0,
533                };
534
535                let bytes_for_entry = if success && op_kind == "Put" {
536                    data_len_val
537                } else {
538                    0
539                };
540
541                let log_entry = ReplicationLogEntry {
542                    ts: self.clock_ms,
543                    op_kind: op_kind.to_string(),
544                    replica_id,
545                    success,
546                    bytes: bytes_for_entry,
547                    error: if success {
548                        None
549                    } else {
550                        Some(format!(
551                            "simulated network failure for replica {:?}",
552                            replica_id
553                        ))
554                    },
555                };
556
557                if success {
558                    result.success_count += 1;
559                    result.bytes_dispatched += bytes_for_entry;
560                    self.stats.total_successes += 1;
561                    self.stats.total_bytes_replicated += bytes_for_entry as u64;
562                    quorum_count += 1;
563
564                    // Update replica state.
565                    if let Some(target) = self.targets.get_mut(&replica_id) {
566                        target.last_sync_ts = self.clock_ms;
567                        target.bytes_replicated += bytes_for_entry as u64;
568                    }
569                } else {
570                    result.failure_count += 1;
571                    self.stats.total_failures += 1;
572
573                    // Optionally mark replica unhealthy on consistent failures.
574                    // (policy: BestEffort never marks; others mark after simulated failure)
575                    if matches!(self.config.policy, ReplicationPolicy::Synchronous) {
576                        if let Some(target) = self.targets.get_mut(&replica_id) {
577                            target.is_healthy = false;
578                        }
579                    }
580                }
581
582                // Append to rolling log.
583                self.append_log(log_entry);
584
585                // Quorum: once satisfied we continue to log remaining replicas.
586                if quorum_count >= quorum_needed && quorum_needed > 0 {
587                    quorum_satisfied = true;
588                }
589
590                // PriorityFirst: stop at first failure.
591                if matches!(self.config.policy, ReplicationPolicy::PriorityFirst) && !success {
592                    break;
593                }
594            }
595
596            // Avoid unused-variable warning in non-quorum paths.
597            let _ = quorum_satisfied;
598
599            self.clock_ms = self.clock_ms.wrapping_add(1);
600        }
601
602        result
603    }
604
605    /// Internal: append an entry to the rolling log, evicting oldest if necessary.
606    fn append_log(&mut self, entry: ReplicationLogEntry) {
607        if self.replication_log.len() >= MAX_LOG_ENTRIES {
608            self.replication_log.remove(0);
609        }
610        self.replication_log.push(entry);
611    }
612
613    // ── Quorum & recovery ─────────────────────────────────────────────────────
614
615    /// Return `true` if at least `required` replicas are currently healthy.
616    pub fn check_quorum(&self, required: usize) -> bool {
617        self.healthy_replica_count() >= required
618    }
619
620    /// Return the ids of all unhealthy replicas that need re-synchronisation.
621    pub fn recovery_plan(&self) -> Vec<ReplicaId> {
622        self.targets
623            .values()
624            .filter(|t| !t.is_healthy)
625            .map(|t| t.id)
626            .collect()
627    }
628
629    // ── Statistics ────────────────────────────────────────────────────────────
630
631    /// Return a snapshot of current statistics.
632    pub fn replication_stats(&self) -> SrmReplicationStats {
633        let (healthy, unhealthy) = self.replica_health_counts();
634        SrmReplicationStats {
635            total_ops_enqueued: self.stats.total_ops_enqueued,
636            total_ops_processed: self.stats.total_ops_processed,
637            total_successes: self.stats.total_successes,
638            total_failures: self.stats.total_failures,
639            total_bytes_replicated: self.stats.total_bytes_replicated,
640            pending_ops: self.pending_ops.len(),
641            healthy_replicas: healthy,
642            unhealthy_replicas: unhealthy,
643            log_entries: self.replication_log.len(),
644            queue_overflow_count: self.stats.queue_overflow_count,
645        }
646    }
647
648    // ── Query helpers ─────────────────────────────────────────────────────────
649
650    /// Return the number of healthy replicas.
651    pub fn healthy_replica_count(&self) -> usize {
652        self.targets.values().filter(|t| t.is_healthy).count()
653    }
654
655    /// Return the number of registered replicas.
656    pub fn replica_count(&self) -> usize {
657        self.targets.len()
658    }
659
660    /// Return `(healthy, unhealthy)` counts.
661    pub fn replica_health_counts(&self) -> (usize, usize) {
662        let healthy = self.targets.values().filter(|t| t.is_healthy).count();
663        let unhealthy = self.targets.len() - healthy;
664        (healthy, unhealthy)
665    }
666
667    /// Return a reference to a replica by id.
668    pub fn get_replica(&self, id: &ReplicaId) -> Option<&ReplicaTarget> {
669        self.targets.get(id)
670    }
671
672    /// Return a mutable reference to a replica by id.
673    pub fn get_replica_mut(&mut self, id: &ReplicaId) -> Option<&mut ReplicaTarget> {
674        self.targets.get_mut(id)
675    }
676
677    /// Return the number of pending operations.
678    pub fn pending_count(&self) -> usize {
679        self.pending_ops.len()
680    }
681
682    /// Return entries from the replication log whose `op_kind` matches `kind`.
683    pub fn log_entries_for_kind(&self, kind: &str) -> Vec<&ReplicationLogEntry> {
684        self.replication_log
685            .iter()
686            .filter(|e| e.op_kind == kind)
687            .collect()
688    }
689
690    /// Return only failed log entries.
691    pub fn failed_log_entries(&self) -> Vec<&ReplicationLogEntry> {
692        self.replication_log.iter().filter(|e| !e.success).collect()
693    }
694
695    /// Drain the pending queue and discard all operations (useful for shutdown).
696    pub fn drain_pending(&mut self) -> usize {
697        let n = self.pending_ops.len();
698        self.pending_ops.clear();
699        n
700    }
701
702    /// Clear the replication log.
703    pub fn clear_log(&mut self) {
704        self.replication_log.clear();
705    }
706
707    /// Reset statistics (but not configuration or targets).
708    pub fn reset_stats(&mut self) {
709        self.stats = SrmReplicationStats::default();
710    }
711
712    /// Return the current fake clock value (milliseconds).
713    pub fn clock(&self) -> u64 {
714        self.clock_ms
715    }
716
717    /// Return total bytes replicated across all registered targets.
718    pub fn total_bytes_across_targets(&self) -> u64 {
719        self.targets.values().map(|t| t.bytes_replicated).sum()
720    }
721
722    /// Return the replica with the most bytes replicated, if any.
723    pub fn most_active_replica(&self) -> Option<&ReplicaTarget> {
724        self.targets.values().max_by_key(|t| t.bytes_replicated)
725    }
726
727    /// Return the replica with the fewest bytes replicated (least-loaded), if any.
728    pub fn least_active_replica(&self) -> Option<&ReplicaTarget> {
729        self.targets.values().min_by_key(|t| t.bytes_replicated)
730    }
731
732    /// True if a quorum of at least `min_replicas` healthy targets is available.
733    pub fn has_minimum_quorum(&self) -> bool {
734        self.check_quorum(self.config.min_replicas)
735    }
736
737    /// Return the list of replica ids sorted by priority (ascending = highest priority first).
738    pub fn replicas_by_priority(&self) -> Vec<ReplicaId> {
739        let mut pairs: Vec<(u32, ReplicaId)> =
740            self.targets.values().map(|t| (t.priority, t.id)).collect();
741        pairs.sort_by_key(|(p, _)| *p);
742        pairs.into_iter().map(|(_, id)| id).collect()
743    }
744
745    /// Return all healthy replica ids.
746    pub fn healthy_replica_ids(&self) -> Vec<ReplicaId> {
747        self.targets
748            .values()
749            .filter(|t| t.is_healthy)
750            .map(|t| t.id)
751            .collect()
752    }
753
754    /// Return all unhealthy replica ids.
755    pub fn unhealthy_replica_ids(&self) -> Vec<ReplicaId> {
756        self.targets
757            .values()
758            .filter(|t| !t.is_healthy)
759            .map(|t| t.id)
760            .collect()
761    }
762
763    /// Return the most-recent log entry, if any.
764    pub fn last_log_entry(&self) -> Option<&ReplicationLogEntry> {
765        self.replication_log.last()
766    }
767
768    /// Count log entries that succeeded.
769    pub fn log_success_count(&self) -> usize {
770        self.replication_log.iter().filter(|e| e.success).count()
771    }
772
773    /// Count log entries that failed.
774    pub fn log_failure_count(&self) -> usize {
775        self.replication_log.iter().filter(|e| !e.success).count()
776    }
777
778    /// Compute the success rate across all log entries (0.0 if log is empty).
779    pub fn log_success_rate(&self) -> f64 {
780        let total = self.replication_log.len();
781        if total == 0 {
782            return 0.0;
783        }
784        self.log_success_count() as f64 / total as f64
785    }
786
787    /// Sum of bytes in all log entries.
788    pub fn log_total_bytes(&self) -> usize {
789        self.replication_log.iter().map(|e| e.bytes).sum()
790    }
791
792    /// Return all log entries for a specific replica.
793    pub fn log_entries_for_replica(&self, id: &ReplicaId) -> Vec<&ReplicationLogEntry> {
794        self.replication_log
795            .iter()
796            .filter(|e| &e.replica_id == id)
797            .collect()
798    }
799}
800
801// ── Type aliases (public-facing Srm* names) ───────────────────────────────────
802
803/// Public alias — `SrmStorageReplicationManager` maps to [`StorageReplicationManager`].
804pub type SrmStorageReplicationManager = StorageReplicationManager;
805
806// ── Exports re-exported by lib.rs ─────────────────────────────────────────────
807
808// (These are consumed by the `pub use` declarations in lib.rs.)
809
810// ── Tests ─────────────────────────────────────────────────────────────────────
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815
816    // ── Helpers ───────────────────────────────────────────────────────────────
817
818    fn make_config(policy: ReplicationPolicy) -> SrmReplicationConfig {
819        SrmReplicationConfig {
820            policy,
821            min_replicas: 2,
822            max_replicas: 8,
823            retry_limit: 2,
824            batch_size: 16,
825        }
826    }
827
828    fn make_target(id_byte: u8) -> ReplicaTarget {
829        ReplicaTarget {
830            id: [id_byte; 16],
831            endpoint: format!("10.0.0.{}:4001", id_byte),
832            priority: id_byte as u32 * 10,
833            is_healthy: true,
834            last_sync_ts: 0,
835            bytes_replicated: 0,
836        }
837    }
838
839    fn make_mgr_with_replicas(n: u8) -> StorageReplicationManager {
840        let mut mgr = StorageReplicationManager::new(make_config(ReplicationPolicy::Asynchronous));
841        for i in 1..=n {
842            mgr.add_replica(make_target(i)).unwrap();
843        }
844        mgr
845    }
846
847    // ── xorshift64 ────────────────────────────────────────────────────────────
848
849    #[test]
850    fn test_xorshift64_non_zero() {
851        let mut s = 12345u64;
852        let v = xorshift64(&mut s);
853        assert_ne!(v, 0);
854    }
855
856    #[test]
857    fn test_xorshift64_state_mutated() {
858        let mut s = 99u64;
859        let initial = s;
860        xorshift64(&mut s);
861        assert_ne!(s, initial);
862    }
863
864    #[test]
865    fn test_xorshift64_deterministic() {
866        let mut s1 = 42u64;
867        let mut s2 = 42u64;
868        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
869    }
870
871    #[test]
872    fn test_xorshift64_sequence_no_repeat() {
873        let mut s = 7u64;
874        let a = xorshift64(&mut s);
875        let b = xorshift64(&mut s);
876        assert_ne!(a, b);
877    }
878
879    // ── fnv1a_64 ──────────────────────────────────────────────────────────────
880
881    #[test]
882    fn test_fnv1a_empty() {
883        let h = fnv1a_64(&[]);
884        assert_eq!(h, 14_695_981_039_346_656_037u64);
885    }
886
887    #[test]
888    fn test_fnv1a_known() {
889        // "hello" — known FNV-1a value
890        let h = fnv1a_64(b"hello");
891        assert_ne!(h, 0);
892        assert_ne!(h, 14_695_981_039_346_656_037u64);
893    }
894
895    #[test]
896    fn test_fnv1a_different_inputs_differ() {
897        assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"xyz"));
898    }
899
900    // ── Manager construction ──────────────────────────────────────────────────
901
902    #[test]
903    fn test_new_empty_manager() {
904        let mgr = StorageReplicationManager::with_defaults();
905        assert_eq!(mgr.replica_count(), 0);
906        assert_eq!(mgr.pending_count(), 0);
907    }
908
909    #[test]
910    fn test_default_config_fields() {
911        let cfg = SrmReplicationConfig::default();
912        assert_eq!(cfg.min_replicas, 2);
913        assert_eq!(cfg.max_replicas, 10);
914        assert_eq!(cfg.batch_size, 64);
915    }
916
917    #[test]
918    fn test_new_seeds_prng_nonzero() {
919        let mgr = StorageReplicationManager::with_defaults();
920        assert_ne!(mgr.prng_state, 0);
921    }
922
923    // ── add_replica ───────────────────────────────────────────────────────────
924
925    #[test]
926    fn test_add_replica_success() {
927        let mut mgr = StorageReplicationManager::with_defaults();
928        assert!(mgr.add_replica(make_target(1)).is_ok());
929        assert_eq!(mgr.replica_count(), 1);
930    }
931
932    #[test]
933    fn test_add_replica_duplicate_error() {
934        let mut mgr = StorageReplicationManager::with_defaults();
935        mgr.add_replica(make_target(1)).unwrap();
936        let err = mgr.add_replica(make_target(1));
937        assert!(matches!(err, Err(SrmError::DuplicateReplica(_))));
938    }
939
940    #[test]
941    fn test_add_replica_registry_full() {
942        let mut mgr = StorageReplicationManager::new(SrmReplicationConfig {
943            max_replicas: 2,
944            ..Default::default()
945        });
946        mgr.add_replica(make_target(1)).unwrap();
947        mgr.add_replica(make_target(2)).unwrap();
948        let err = mgr.add_replica(make_target(3));
949        assert!(matches!(err, Err(SrmError::RegistryFull)));
950    }
951
952    #[test]
953    fn test_add_multiple_replicas() {
954        let mgr = make_mgr_with_replicas(5);
955        assert_eq!(mgr.replica_count(), 5);
956    }
957
958    // ── remove_replica ────────────────────────────────────────────────────────
959
960    #[test]
961    fn test_remove_replica_success() {
962        let mut mgr = make_mgr_with_replicas(3);
963        let id = [1u8; 16];
964        let removed = mgr.remove_replica(id).unwrap();
965        assert_eq!(removed.id, id);
966        assert_eq!(mgr.replica_count(), 2);
967    }
968
969    #[test]
970    fn test_remove_replica_not_found() {
971        let mut mgr = StorageReplicationManager::with_defaults();
972        let err = mgr.remove_replica([99u8; 16]);
973        assert!(matches!(err, Err(SrmError::ReplicaNotFound(_))));
974    }
975
976    // ── mark_unhealthy / mark_healthy ─────────────────────────────────────────
977
978    #[test]
979    fn test_mark_unhealthy() {
980        let mut mgr = make_mgr_with_replicas(2);
981        let id = [1u8; 16];
982        mgr.mark_unhealthy(id).unwrap();
983        assert!(!mgr.get_replica(&id).unwrap().is_healthy);
984    }
985
986    #[test]
987    fn test_mark_healthy_restores() {
988        let mut mgr = make_mgr_with_replicas(2);
989        let id = [1u8; 16];
990        mgr.mark_unhealthy(id).unwrap();
991        mgr.mark_healthy(id).unwrap();
992        assert!(mgr.get_replica(&id).unwrap().is_healthy);
993    }
994
995    #[test]
996    fn test_mark_unhealthy_not_found() {
997        let mut mgr = StorageReplicationManager::with_defaults();
998        assert!(matches!(
999            mgr.mark_unhealthy([0u8; 16]),
1000            Err(SrmError::ReplicaNotFound(_))
1001        ));
1002    }
1003
1004    // ── set_priority ──────────────────────────────────────────────────────────
1005
1006    #[test]
1007    fn test_set_priority() {
1008        let mut mgr = make_mgr_with_replicas(1);
1009        let id = [1u8; 16];
1010        mgr.set_priority(id, 999).unwrap();
1011        assert_eq!(mgr.get_replica(&id).unwrap().priority, 999);
1012    }
1013
1014    #[test]
1015    fn test_set_priority_not_found() {
1016        let mut mgr = StorageReplicationManager::with_defaults();
1017        assert!(mgr.set_priority([0u8; 16], 5).is_err());
1018    }
1019
1020    // ── enqueue_put / enqueue_delete / enqueue_sync ───────────────────────────
1021
1022    #[test]
1023    fn test_enqueue_put_increments_pending() {
1024        let mut mgr = make_mgr_with_replicas(1);
1025        let pushed = mgr.enqueue_put([0u8; 32], 512);
1026        assert!(pushed);
1027        assert_eq!(mgr.pending_count(), 1);
1028    }
1029
1030    #[test]
1031    fn test_enqueue_delete() {
1032        let mut mgr = make_mgr_with_replicas(1);
1033        assert!(mgr.enqueue_delete([1u8; 32]));
1034        assert_eq!(mgr.pending_count(), 1);
1035    }
1036
1037    #[test]
1038    fn test_enqueue_sync() {
1039        let mut mgr = make_mgr_with_replicas(1);
1040        assert!(mgr.enqueue_sync(0));
1041        assert_eq!(mgr.pending_count(), 1);
1042    }
1043
1044    #[test]
1045    fn test_enqueue_increments_stats() {
1046        let mut mgr = make_mgr_with_replicas(1);
1047        mgr.enqueue_put([0u8; 32], 100);
1048        mgr.enqueue_delete([1u8; 32]);
1049        let stats = mgr.replication_stats();
1050        assert_eq!(stats.total_ops_enqueued, 2);
1051    }
1052
1053    #[test]
1054    fn test_enqueue_overflow_returns_false() {
1055        let mut mgr = StorageReplicationManager::new(SrmReplicationConfig {
1056            batch_size: 1,
1057            max_replicas: 10,
1058            ..Default::default()
1059        });
1060        // Fill the queue.
1061        for _ in 0..MAX_PENDING_OPS {
1062            mgr.enqueue_put([0u8; 32], 1);
1063        }
1064        let result = mgr.enqueue_put([1u8; 32], 1);
1065        assert!(!result);
1066        let stats = mgr.replication_stats();
1067        assert_eq!(stats.queue_overflow_count, 1);
1068    }
1069
1070    // ── process_batch ─────────────────────────────────────────────────────────
1071
1072    #[test]
1073    fn test_process_batch_drains_ops() {
1074        let mut mgr = make_mgr_with_replicas(3);
1075        for i in 0..10u8 {
1076            mgr.enqueue_put([i; 32], 128);
1077        }
1078        let result = mgr.process_batch();
1079        assert!(result.ops_processed <= 16);
1080        assert!(mgr.pending_count() <= 10);
1081    }
1082
1083    #[test]
1084    fn test_process_batch_empty_queue() {
1085        let mut mgr = make_mgr_with_replicas(2);
1086        let result = mgr.process_batch();
1087        assert_eq!(result.ops_processed, 0);
1088        assert_eq!(result.success_count, 0);
1089    }
1090
1091    #[test]
1092    fn test_process_batch_no_replicas() {
1093        let mut mgr = StorageReplicationManager::with_defaults();
1094        mgr.enqueue_put([0u8; 32], 512);
1095        let result = mgr.process_batch();
1096        // Op is drained but no replica attempts occur.
1097        assert_eq!(result.ops_processed, 1);
1098        assert_eq!(result.success_count, 0);
1099        assert_eq!(result.failure_count, 0);
1100    }
1101
1102    #[test]
1103    fn test_process_batch_updates_log() {
1104        let mut mgr = make_mgr_with_replicas(2);
1105        mgr.enqueue_put([0u8; 32], 256);
1106        mgr.process_batch();
1107        assert!(!mgr.replication_log.is_empty());
1108    }
1109
1110    #[test]
1111    fn test_process_batch_bytes_dispatched() {
1112        let mut mgr = make_mgr_with_replicas(3);
1113        mgr.enqueue_put([0xAAu8; 32], 1024);
1114        let result = mgr.process_batch();
1115        // At least some bytes should be dispatched (success rate ≈ 7/8).
1116        // bytes_dispatched is usize; just check it's been set.
1117        let _ = result.bytes_dispatched;
1118    }
1119
1120    #[test]
1121    fn test_process_batch_stats_updated() {
1122        let mut mgr = make_mgr_with_replicas(2);
1123        mgr.enqueue_put([0u8; 32], 100);
1124        mgr.process_batch();
1125        let stats = mgr.replication_stats();
1126        assert_eq!(stats.total_ops_processed, 1);
1127    }
1128
1129    // ── process_batch with Delete/Sync ────────────────────────────────────────
1130
1131    #[test]
1132    fn test_process_delete_op() {
1133        let mut mgr = make_mgr_with_replicas(2);
1134        mgr.enqueue_delete([0xBBu8; 32]);
1135        let result = mgr.process_batch();
1136        assert_eq!(result.ops_processed, 1);
1137    }
1138
1139    #[test]
1140    fn test_process_sync_op() {
1141        let mut mgr = make_mgr_with_replicas(2);
1142        mgr.enqueue_sync(500);
1143        let result = mgr.process_batch();
1144        assert_eq!(result.ops_processed, 1);
1145    }
1146
1147    // ── Log rolling ───────────────────────────────────────────────────────────
1148
1149    #[test]
1150    fn test_log_capped_at_max() {
1151        // Each op against 1 replica adds 1 log entry.
1152        let mut mgr = StorageReplicationManager::new(SrmReplicationConfig {
1153            batch_size: 1000,
1154            max_replicas: 10,
1155            ..Default::default()
1156        });
1157        mgr.add_replica(make_target(1)).unwrap();
1158
1159        for i in 0..600u16 {
1160            mgr.enqueue_put([(i & 0xFF) as u8; 32], 10);
1161        }
1162        mgr.process_batch();
1163
1164        assert!(mgr.replication_log.len() <= MAX_LOG_ENTRIES);
1165    }
1166
1167    #[test]
1168    fn test_log_entry_fields() {
1169        let mut mgr = make_mgr_with_replicas(1);
1170        mgr.enqueue_put([7u8; 32], 2048);
1171        mgr.process_batch();
1172        let entry = mgr.last_log_entry().unwrap();
1173        assert_eq!(entry.op_kind, "Put");
1174        assert_eq!(entry.replica_id, [1u8; 16]);
1175    }
1176
1177    // ── check_quorum ──────────────────────────────────────────────────────────
1178
1179    #[test]
1180    fn test_quorum_met() {
1181        let mgr = make_mgr_with_replicas(3);
1182        assert!(mgr.check_quorum(2));
1183        assert!(mgr.check_quorum(3));
1184    }
1185
1186    #[test]
1187    fn test_quorum_not_met() {
1188        let mgr = make_mgr_with_replicas(1);
1189        assert!(!mgr.check_quorum(2));
1190    }
1191
1192    #[test]
1193    fn test_quorum_zero_always_met() {
1194        let mgr = StorageReplicationManager::with_defaults();
1195        assert!(mgr.check_quorum(0));
1196    }
1197
1198    #[test]
1199    fn test_has_minimum_quorum() {
1200        let mgr = make_mgr_with_replicas(3);
1201        assert!(mgr.has_minimum_quorum()); // min_replicas = 2, we have 3
1202    }
1203
1204    // ── recovery_plan ─────────────────────────────────────────────────────────
1205
1206    #[test]
1207    fn test_recovery_plan_empty_when_all_healthy() {
1208        let mgr = make_mgr_with_replicas(3);
1209        assert!(mgr.recovery_plan().is_empty());
1210    }
1211
1212    #[test]
1213    fn test_recovery_plan_lists_unhealthy() {
1214        let mut mgr = make_mgr_with_replicas(3);
1215        mgr.mark_unhealthy([1u8; 16]).unwrap();
1216        mgr.mark_unhealthy([2u8; 16]).unwrap();
1217        let plan = mgr.recovery_plan();
1218        assert_eq!(plan.len(), 2);
1219    }
1220
1221    #[test]
1222    fn test_recovery_plan_excludes_healthy() {
1223        let mut mgr = make_mgr_with_replicas(3);
1224        mgr.mark_unhealthy([1u8; 16]).unwrap();
1225        let plan = mgr.recovery_plan();
1226        assert!(!plan.contains(&[2u8; 16]));
1227        assert!(!plan.contains(&[3u8; 16]));
1228    }
1229
1230    // ── replication_stats ─────────────────────────────────────────────────────
1231
1232    #[test]
1233    fn test_stats_initial_zeros() {
1234        let mgr = StorageReplicationManager::with_defaults();
1235        let s = mgr.replication_stats();
1236        assert_eq!(s.total_ops_enqueued, 0);
1237        assert_eq!(s.total_ops_processed, 0);
1238        assert_eq!(s.total_bytes_replicated, 0);
1239    }
1240
1241    #[test]
1242    fn test_stats_healthy_unhealthy_counts() {
1243        let mut mgr = make_mgr_with_replicas(4);
1244        mgr.mark_unhealthy([1u8; 16]).unwrap();
1245        let s = mgr.replication_stats();
1246        assert_eq!(s.healthy_replicas, 3);
1247        assert_eq!(s.unhealthy_replicas, 1);
1248    }
1249
1250    #[test]
1251    fn test_stats_log_entries_count() {
1252        let mut mgr = make_mgr_with_replicas(1);
1253        mgr.enqueue_put([0u8; 32], 100);
1254        mgr.process_batch();
1255        let s = mgr.replication_stats();
1256        assert!(s.log_entries > 0);
1257    }
1258
1259    // ── Priority ordering ─────────────────────────────────────────────────────
1260
1261    #[test]
1262    fn test_replicas_by_priority_sorted() {
1263        let mut mgr = StorageReplicationManager::new(make_config(ReplicationPolicy::PriorityFirst));
1264        mgr.add_replica(ReplicaTarget {
1265            priority: 30,
1266            ..make_target(1)
1267        })
1268        .unwrap();
1269        mgr.add_replica(ReplicaTarget {
1270            priority: 10,
1271            ..make_target(2)
1272        })
1273        .unwrap();
1274        mgr.add_replica(ReplicaTarget {
1275            priority: 20,
1276            ..make_target(3)
1277        })
1278        .unwrap();
1279        let ids = mgr.replicas_by_priority();
1280        // Priority 10 = [2;16], 20 = [3;16], 30 = [1;16]
1281        assert_eq!(ids[0], [2u8; 16]);
1282        assert_eq!(ids[2], [1u8; 16]);
1283    }
1284
1285    // ── Healthy / unhealthy id helpers ────────────────────────────────────────
1286
1287    #[test]
1288    fn test_healthy_replica_ids() {
1289        let mut mgr = make_mgr_with_replicas(3);
1290        mgr.mark_unhealthy([1u8; 16]).unwrap();
1291        let ids = mgr.healthy_replica_ids();
1292        assert_eq!(ids.len(), 2);
1293        assert!(!ids.contains(&[1u8; 16]));
1294    }
1295
1296    #[test]
1297    fn test_unhealthy_replica_ids() {
1298        let mut mgr = make_mgr_with_replicas(3);
1299        mgr.mark_unhealthy([2u8; 16]).unwrap();
1300        let ids = mgr.unhealthy_replica_ids();
1301        assert_eq!(ids.len(), 1);
1302        assert!(ids.contains(&[2u8; 16]));
1303    }
1304
1305    // ── most/least active ─────────────────────────────────────────────────────
1306
1307    #[test]
1308    fn test_most_active_replica_none_when_empty() {
1309        let mgr = StorageReplicationManager::with_defaults();
1310        assert!(mgr.most_active_replica().is_none());
1311    }
1312
1313    #[test]
1314    fn test_least_active_replica_none_when_empty() {
1315        let mgr = StorageReplicationManager::with_defaults();
1316        assert!(mgr.least_active_replica().is_none());
1317    }
1318
1319    #[test]
1320    fn test_total_bytes_across_targets_zero_initially() {
1321        let mgr = make_mgr_with_replicas(3);
1322        assert_eq!(mgr.total_bytes_across_targets(), 0);
1323    }
1324
1325    // ── drain_pending / clear_log / reset_stats ───────────────────────────────
1326
1327    #[test]
1328    fn test_drain_pending() {
1329        let mut mgr = make_mgr_with_replicas(1);
1330        mgr.enqueue_put([0u8; 32], 100);
1331        mgr.enqueue_delete([1u8; 32]);
1332        let n = mgr.drain_pending();
1333        assert_eq!(n, 2);
1334        assert_eq!(mgr.pending_count(), 0);
1335    }
1336
1337    #[test]
1338    fn test_clear_log() {
1339        let mut mgr = make_mgr_with_replicas(1);
1340        mgr.enqueue_put([0u8; 32], 100);
1341        mgr.process_batch();
1342        assert!(!mgr.replication_log.is_empty());
1343        mgr.clear_log();
1344        assert!(mgr.replication_log.is_empty());
1345    }
1346
1347    #[test]
1348    fn test_reset_stats() {
1349        let mut mgr = make_mgr_with_replicas(1);
1350        mgr.enqueue_put([0u8; 32], 100);
1351        mgr.process_batch();
1352        mgr.reset_stats();
1353        let s = mgr.replication_stats();
1354        assert_eq!(s.total_ops_enqueued, 0);
1355        assert_eq!(s.total_ops_processed, 0);
1356    }
1357
1358    // ── log query helpers ─────────────────────────────────────────────────────
1359
1360    #[test]
1361    fn test_log_entries_for_kind() {
1362        let mut mgr = make_mgr_with_replicas(2);
1363        mgr.enqueue_put([0u8; 32], 100);
1364        mgr.enqueue_delete([1u8; 32]);
1365        mgr.process_batch();
1366        let puts = mgr.log_entries_for_kind("Put");
1367        let deletes = mgr.log_entries_for_kind("Delete");
1368        assert!(!puts.is_empty());
1369        assert!(!deletes.is_empty());
1370    }
1371
1372    #[test]
1373    fn test_failed_log_entries_type() {
1374        let mgr = make_mgr_with_replicas(1);
1375        let failed = mgr.failed_log_entries();
1376        for e in &failed {
1377            assert!(!e.success);
1378        }
1379    }
1380
1381    #[test]
1382    fn test_log_success_rate_empty() {
1383        let mgr = StorageReplicationManager::with_defaults();
1384        assert_eq!(mgr.log_success_rate(), 0.0);
1385    }
1386
1387    #[test]
1388    fn test_log_success_rate_after_batch() {
1389        let mut mgr = make_mgr_with_replicas(4);
1390        for i in 0..8u8 {
1391            mgr.enqueue_put([i; 32], 256);
1392        }
1393        mgr.process_batch();
1394        let rate = mgr.log_success_rate();
1395        assert!((0.0..=1.0).contains(&rate));
1396    }
1397
1398    #[test]
1399    fn test_log_total_bytes_nonnegative() {
1400        let mut mgr = make_mgr_with_replicas(3);
1401        mgr.enqueue_put([0u8; 32], 4096);
1402        mgr.process_batch();
1403        // Total bytes must be >= 0 (trivially true for usize, but confirms no panic).
1404        let _ = mgr.log_total_bytes();
1405    }
1406
1407    #[test]
1408    fn test_log_entries_for_replica() {
1409        let mut mgr = make_mgr_with_replicas(2);
1410        mgr.enqueue_put([0u8; 32], 512);
1411        mgr.process_batch();
1412        let id = [1u8; 16];
1413        let entries = mgr.log_entries_for_replica(&id);
1414        for e in &entries {
1415            assert_eq!(e.replica_id, id);
1416        }
1417    }
1418
1419    // ── Policy variants ───────────────────────────────────────────────────────
1420
1421    #[test]
1422    fn test_policy_best_effort() {
1423        let mut mgr = StorageReplicationManager::new(make_config(ReplicationPolicy::BestEffort));
1424        mgr.add_replica(make_target(1)).unwrap();
1425        mgr.add_replica(make_target(2)).unwrap();
1426        mgr.enqueue_put([0u8; 32], 100);
1427        let result = mgr.process_batch();
1428        assert_eq!(result.ops_processed, 1);
1429    }
1430
1431    #[test]
1432    fn test_policy_quorum_write() {
1433        let mut mgr =
1434            StorageReplicationManager::new(make_config(ReplicationPolicy::QuorumWrite(2)));
1435        for i in 1..=4 {
1436            mgr.add_replica(make_target(i)).unwrap();
1437        }
1438        mgr.enqueue_put([0u8; 32], 512);
1439        let result = mgr.process_batch();
1440        assert_eq!(result.ops_processed, 1);
1441    }
1442
1443    #[test]
1444    fn test_policy_synchronous() {
1445        let mut mgr = StorageReplicationManager::new(make_config(ReplicationPolicy::Synchronous));
1446        mgr.add_replica(make_target(1)).unwrap();
1447        mgr.enqueue_put([0u8; 32], 256);
1448        let result = mgr.process_batch();
1449        assert_eq!(result.ops_processed, 1);
1450    }
1451
1452    #[test]
1453    fn test_policy_priority_first() {
1454        let mut mgr = StorageReplicationManager::new(make_config(ReplicationPolicy::PriorityFirst));
1455        mgr.add_replica(ReplicaTarget {
1456            priority: 5,
1457            ..make_target(1)
1458        })
1459        .unwrap();
1460        mgr.add_replica(ReplicaTarget {
1461            priority: 1,
1462            ..make_target(2)
1463        })
1464        .unwrap();
1465        mgr.enqueue_put([0u8; 32], 256);
1466        let result = mgr.process_batch();
1467        assert_eq!(result.ops_processed, 1);
1468    }
1469
1470    // ── Clock advances ────────────────────────────────────────────────────────
1471
1472    #[test]
1473    fn test_clock_advances_on_enqueue() {
1474        let mut mgr = StorageReplicationManager::with_defaults();
1475        let t0 = mgr.clock();
1476        mgr.enqueue_put([0u8; 32], 1);
1477        assert!(mgr.clock() > t0);
1478    }
1479
1480    #[test]
1481    fn test_clock_advances_on_process() {
1482        let mut mgr = make_mgr_with_replicas(1);
1483        mgr.enqueue_put([0u8; 32], 1);
1484        let t0 = mgr.clock();
1485        mgr.process_batch();
1486        assert!(mgr.clock() > t0);
1487    }
1488
1489    // ── Replica health counts ─────────────────────────────────────────────────
1490
1491    #[test]
1492    fn test_replica_health_counts() {
1493        let mut mgr = make_mgr_with_replicas(4);
1494        mgr.mark_unhealthy([1u8; 16]).unwrap();
1495        mgr.mark_unhealthy([2u8; 16]).unwrap();
1496        let (h, u) = mgr.replica_health_counts();
1497        assert_eq!(h, 2);
1498        assert_eq!(u, 2);
1499    }
1500
1501    // ── Snapshot stats pending_ops matches ───────────────────────────────────
1502
1503    #[test]
1504    fn test_stats_pending_ops_matches_queue_len() {
1505        let mut mgr = make_mgr_with_replicas(2);
1506        mgr.enqueue_put([0u8; 32], 50);
1507        mgr.enqueue_put([1u8; 32], 60);
1508        let s = mgr.replication_stats();
1509        assert_eq!(s.pending_ops, mgr.pending_count());
1510    }
1511
1512    // ── SrmError display ──────────────────────────────────────────────────────
1513
1514    #[test]
1515    fn test_error_display_duplicate() {
1516        let e = SrmError::DuplicateReplica([0u8; 16]);
1517        let s = e.to_string();
1518        assert!(s.contains("already registered"));
1519    }
1520
1521    #[test]
1522    fn test_error_display_not_found() {
1523        let e = SrmError::ReplicaNotFound([0u8; 16]);
1524        let s = e.to_string();
1525        assert!(s.contains("not found"));
1526    }
1527
1528    #[test]
1529    fn test_error_display_registry_full() {
1530        let s = SrmError::RegistryFull.to_string();
1531        assert!(s.contains("full"));
1532    }
1533
1534    #[test]
1535    fn test_error_display_queue_full() {
1536        let s = SrmError::QueueFull.to_string();
1537        assert!(s.contains("full"));
1538    }
1539
1540    // ── Type alias accessibility ──────────────────────────────────────────────
1541
1542    #[test]
1543    fn test_srm_aliases_accessible() {
1544        let _: SrmReplicaTarget = make_target(1);
1545        let _: SrmReplicationPolicy = ReplicationPolicy::Asynchronous;
1546        let _: SrmReplicationConfig = SrmReplicationConfig::default();
1547        let _: SrmStorageReplicationManager = StorageReplicationManager::with_defaults();
1548    }
1549
1550    // ── Multiple process_batch rounds ─────────────────────────────────────────
1551
1552    #[test]
1553    fn test_multiple_batches_accumulate_stats() {
1554        let mut mgr = make_mgr_with_replicas(2);
1555        for i in 0..32u8 {
1556            mgr.enqueue_put([i; 32], 128);
1557        }
1558        mgr.process_batch(); // processes up to 16
1559        mgr.process_batch(); // processes remaining 16
1560        let s = mgr.replication_stats();
1561        assert_eq!(s.total_ops_processed, 32);
1562        assert_eq!(s.pending_ops, 0);
1563    }
1564
1565    // ── op_seed determinism ───────────────────────────────────────────────────
1566
1567    #[test]
1568    fn test_op_seed_deterministic() {
1569        let cid = [0xCCu8; 32];
1570        let rid = [0xDDu8; 16];
1571        let s1 = op_seed(&cid, &rid);
1572        let s2 = op_seed(&cid, &rid);
1573        assert_eq!(s1, s2);
1574    }
1575
1576    #[test]
1577    fn test_op_seed_differs_by_cid() {
1578        let rid = [0u8; 16];
1579        let s1 = op_seed(&[1u8; 32], &rid);
1580        let s2 = op_seed(&[2u8; 32], &rid);
1581        assert_ne!(s1, s2);
1582    }
1583}