tsoracle_core/allocator.rs
1//
2// ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3// ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4// ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6// tsoracle — Distributed Timestamp Oracle
7// https://www.tsoracle.rs
8//
9// Copyright (c) 2026 Prisma Risk
10//
11// Licensed under the Apache License, Version 2.0 (the "License");
12// you may not use this file except in compliance with the License.
13// You may obtain a copy of the License at
14//
15// https://www.apache.org/licenses/LICENSE-2.0
16//
17// Unless required by applicable law or agreed to in writing, software
18// distributed under the License is distributed on an "AS IS" BASIS,
19// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20// See the License for the specific language governing permissions and
21// limitations under the License.
22//
23
24// #[PerformanceCriticalPath]
25//! The window allocator state machine. Sync, no I/O.
26
27use crate::{Epoch, LOGICAL_MAX, PHYSICAL_MS_MAX, Timestamp};
28
29/// A `u64` physical-millisecond value proven `<= PHYSICAL_MS_MAX` at
30/// construction.
31///
32/// The 46-bit physical field of [`Timestamp`] cannot represent any value above
33/// [`PHYSICAL_MS_MAX`]; every allocator entry point used to re-check that
34/// bound on bare `u64` parameters (`fence_floor`, `committed_ceiling`,
35/// `now_ms`, `persisted_high_water`) — three different methods, each carrying
36/// its own `if value > PHYSICAL_MS_MAX { ... }` line. `PhysicalMs` collapses
37/// those per-method runtime checks into one construction-site check, so:
38///
39/// * a method signature taking `PhysicalMs` is compile-time proof that the
40/// 46-bit bound has already been validated for that argument; and
41/// * an accidental swap of `now_ms` and `committed_ceiling` at a call site no
42/// longer type-checks against bare `u64` clocks, durations, or counters.
43///
44/// Constructed via [`try_new`](Self::try_new) (or the equivalent
45/// [`TryFrom<u64>`] impl). The inner value can be recovered with
46/// [`get`](Self::get) for arithmetic; the result must be re-wrapped through
47/// `try_new` before crossing back into a `PhysicalMs`-typed boundary.
48#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct PhysicalMs(u64);
50
51impl PhysicalMs {
52 /// The largest in-range value, `2^46 - 1` (equal to [`PHYSICAL_MS_MAX`]).
53 pub const MAX: PhysicalMs = PhysicalMs(PHYSICAL_MS_MAX);
54 /// The zero value. Available as `const`, matching `Duration::ZERO` style.
55 pub const ZERO: PhysicalMs = PhysicalMs(0);
56
57 /// Validate `value <= PHYSICAL_MS_MAX` and wrap. Returns
58 /// [`CoreError::PhysicalMsOutOfRange`] otherwise.
59 ///
60 /// Declared `const fn` so [`MAX`](Self::MAX) and any other compile-time
61 /// `PhysicalMs` constant can be built without unsafe direct-field
62 /// construction outside this module.
63 pub const fn try_new(value: u64) -> Result<Self, CoreError> {
64 if value > PHYSICAL_MS_MAX {
65 return Err(CoreError::PhysicalMsOutOfRange(value));
66 }
67 Ok(PhysicalMs(value))
68 }
69
70 /// Recover the underlying `u64`. `Copy`, so the receiver remains usable.
71 pub const fn get(self) -> u64 {
72 self.0
73 }
74}
75
76impl TryFrom<u64> for PhysicalMs {
77 type Error = CoreError;
78 fn try_from(value: u64) -> Result<Self, Self::Error> {
79 Self::try_new(value)
80 }
81}
82
83impl core::fmt::Display for PhysicalMs {
84 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85 core::fmt::Display::fmt(&self.0, f)
86 }
87}
88
89/// A contiguous block of `count` timestamps starting at
90/// `(physical_ms, logical_start)`, all sharing one leadership `epoch`.
91///
92/// Fields are private and the only public constructor is
93/// [`try_new`](Self::try_new), which validates that every timestamp the grant
94/// covers fits the packed 46-bit physical / 18-bit logical layout. A value of
95/// this type is therefore proof that [`first`](Self::first) and
96/// [`last`](Self::last) can pack without panicking — the in-range invariant is
97/// guaranteed by the type, not by the constructors that happen to build it.
98/// The crate-internal back door `new_unchecked` skips
99/// the validation but documents the same invariant as a caller obligation.
100#[derive(Copy, Clone, Debug, PartialEq, Eq)]
101pub struct WindowGrant {
102 physical_ms: u64,
103 logical_start: u32,
104 count: u32,
105 epoch: Epoch,
106}
107
108impl WindowGrant {
109 /// Construct a grant, checking that every timestamp it covers packs
110 /// cleanly. This is the only *validating* constructor; the
111 /// crate-internal `new_unchecked` skips the
112 /// checks but requires the caller to have established the invariant
113 /// some other way. Either way, a constructed value witnesses that
114 /// `first`/`last` are infallible.
115 ///
116 /// Rejects `count == 0` (a grant covers at least one timestamp, and
117 /// `last`'s `logical_start + count - 1` would underflow). The range check
118 /// defers to [`Timestamp::try_pack`] on the *last* logical the grant emits:
119 /// it is the single source of truth for the bit layout, and since
120 /// `logical_start <= last_logical <= LOGICAL_MAX` validating the last
121 /// boundary validates the first by implication.
122 pub fn try_new(
123 physical_ms: u64,
124 logical_start: u32,
125 count: u32,
126 epoch: Epoch,
127 ) -> Result<Self, CoreError> {
128 if count == 0 {
129 return Err(CoreError::InvalidCount(0));
130 }
131 let last_logical =
132 logical_start
133 .checked_add(count - 1)
134 .ok_or(CoreError::LogicalRangeOutOfRange {
135 logical_start,
136 count,
137 })?;
138 Timestamp::try_pack(physical_ms, last_logical).map_err(|err| match err {
139 crate::TimestampError::PhysicalMsOutOfRange { physical_ms, .. } => {
140 CoreError::PhysicalMsOutOfRange(physical_ms)
141 }
142 crate::TimestampError::LogicalOutOfRange { .. } => CoreError::LogicalRangeOutOfRange {
143 logical_start,
144 count,
145 },
146 })?;
147 Ok(WindowGrant {
148 physical_ms,
149 logical_start,
150 count,
151 epoch,
152 })
153 }
154
155 /// Construct a grant without re-checking the packing invariant. The caller
156 /// must guarantee `count >= 1`, `physical_ms <= PHYSICAL_MS_MAX`, and
157 /// `logical_start + count - 1 <= LOGICAL_MAX` — i.e. every timestamp the
158 /// grant covers packs cleanly. Intended for `Allocator::try_grant`, whose
159 /// `project_grant` helper has already established these bounds; routing
160 /// that path through `try_new` would repeat four checked operations on the
161 /// hot path. The `debug_assert!`s catch a future drift between
162 /// `project_grant` and this constructor in test builds without paying any
163 /// cost in release.
164 pub(crate) fn new_unchecked(
165 physical_ms: u64,
166 logical_start: u32,
167 count: u32,
168 epoch: Epoch,
169 ) -> Self {
170 debug_assert!(count != 0);
171 debug_assert!(physical_ms <= PHYSICAL_MS_MAX);
172 debug_assert!((logical_start as u64) + (count as u64) <= (LOGICAL_MAX as u64) + 1);
173 WindowGrant {
174 physical_ms,
175 logical_start,
176 count,
177 epoch,
178 }
179 }
180
181 pub fn physical_ms(&self) -> u64 {
182 self.physical_ms
183 }
184 pub fn logical_start(&self) -> u32 {
185 self.logical_start
186 }
187 pub fn count(&self) -> u32 {
188 self.count
189 }
190 pub fn epoch(&self) -> Epoch {
191 self.epoch
192 }
193
194 /// The first timestamp in the grant. Infallible: [`try_new`](Self::try_new)
195 /// validated `(physical_ms, logical_start)` is in range, so `pack` cannot
196 /// trip its `assert!`.
197 pub fn first(&self) -> Timestamp {
198 Timestamp::pack(self.physical_ms, self.logical_start)
199 }
200 /// The last timestamp in the grant. Infallible: [`try_new`](Self::try_new)
201 /// validated `(physical_ms, logical_start + count - 1)` is in range (and
202 /// `count >= 1`, so the subtraction cannot underflow), so `pack` cannot
203 /// trip its `assert!`.
204 pub fn last(&self) -> Timestamp {
205 Timestamp::pack(self.physical_ms, self.logical_start + self.count - 1)
206 }
207}
208
209#[derive(Debug, thiserror::Error, PartialEq, Eq)]
210pub enum CoreError {
211 #[error("not leader")]
212 NotLeader,
213 #[error("window exhausted; caller must extend before retrying")]
214 WindowExhausted,
215 #[error("invalid count: {0}")]
216 InvalidCount(u32),
217 #[error("physical_ms {0} exceeds 46-bit maximum")]
218 PhysicalMsOutOfRange(u64),
219 #[error("logical range [{logical_start}, +{count}) exceeds the 18-bit logical field")]
220 LogicalRangeOutOfRange { logical_start: u32, count: u32 },
221 #[error(
222 "invalid leadership window: fence_floor {fence_floor} exceeds committed_ceiling {committed_ceiling}"
223 )]
224 InvalidLeadershipWindow {
225 fence_floor: u64,
226 committed_ceiling: u64,
227 },
228 #[error(
229 "window extension overflow: max(floor {floor}, now_ms {now_ms}) + ahead_ms {ahead_ms} exceeds u64::MAX"
230 )]
231 WindowExtensionOverflow {
232 floor: u64,
233 now_ms: u64,
234 ahead_ms: u64,
235 },
236 #[error("sequence key must not be empty")]
237 SeqKeyEmpty,
238 #[error("sequence key length {len} exceeds maximum {max} bytes")]
239 SeqKeyTooLong { len: usize, max: usize },
240 #[error("sequence count must be >= 1")]
241 SeqCountZero,
242 #[error("sequence count {count} exceeds maximum {max}")]
243 SeqCountTooLarge { count: u32, max: u32 },
244 #[error("sequence block [{start}, {start}+{count}) overflows u64")]
245 SeqBlockOverflow { start: u64, count: u32 },
246}
247
248/// The result of a `try_commit_window_extension` that passed range validation.
249///
250/// A commit either raises the durable bound or is dropped for one of three
251/// benign, expected reasons. Collapsing both into `Ok(())` left a caller that
252/// just paid for a `persist_high_water` round-trip unable to tell "I raised the
253/// bound" from "I silently dropped your durably-persisted value." This type
254/// preserves the distinction so the server can log/metric the dropped commits —
255/// a leading indicator of epoch churn or persist reordering.
256#[derive(Copy, Clone, Debug, PartialEq, Eq)]
257pub enum CommitOutcome {
258 /// The durable bound advanced to `high_water`.
259 Applied { high_water: u64 },
260 /// The bound did not move; see [`IgnoreReason`] for why.
261 Ignored(IgnoreReason),
262}
263
264/// Why a [`CommitOutcome::Ignored`] commit left the durable bound unchanged.
265///
266/// All three are benign and expected under normal failover: the epoch-fencing
267/// design of `try_commit_window_extension` deliberately drops late commits from
268/// a superseded epoch rather than erroring, and the monotonic bound rejects a
269/// value that does not advance. They are kept apart so a caller can distinguish
270/// epoch churn ([`NotLeader`](Self::NotLeader) / [`EpochMismatch`](Self::EpochMismatch))
271/// from persist reordering ([`NotAdvanced`](Self::NotAdvanced)).
272#[derive(Copy, Clone, Debug, PartialEq, Eq)]
273pub enum IgnoreReason {
274 /// The allocator is no longer a leader, so the commit has no window to raise.
275 NotLeader,
276 /// The allocator leads a different epoch than the commit targeted; the
277 /// commit is a late persist from a superseded epoch, fenced out.
278 EpochMismatch { expected: Epoch, current: Epoch },
279 /// The allocator still leads the targeted epoch, but the persisted value did
280 /// not exceed the current bound, so the monotonic bound rejects it.
281 NotAdvanced { persisted: u64, committed: u64 },
282}
283
284#[derive(Debug)]
285enum State {
286 NotLeader,
287 Leader {
288 epoch: Epoch,
289 /// Persisted upper bound: the allocator will not issue any timestamp with
290 /// `physical_ms` greater than this without a fresh `try_commit_window_extension`.
291 committed_high_water: u64,
292 /// Next `physical_ms` we are willing to issue at. Initialized to
293 /// `fence_floor` on leadership gain, then advances monotonically — never
294 /// retreats below the fence even when `now_ms` is a past value.
295 next_physical_ms: u64,
296 /// Next logical counter within `next_physical_ms`.
297 next_logical: u32,
298 },
299}
300
301pub struct Allocator {
302 state: State,
303}
304
305impl Allocator {
306 pub fn new() -> Self {
307 Allocator {
308 state: State::NotLeader,
309 }
310 }
311
312 /// Seed the allocator once the failover fence has durably persisted both
313 /// the floor and the pre-extended ceiling.
314 ///
315 /// `fence_floor` is the first `physical_ms` the new leader may issue —
316 /// the server sets it to `prior_high_water + 1` so the new leader's
317 /// timestamps are strictly above any the prior leader could have issued.
318 ///
319 /// `committed_ceiling` is the pre-extended upper bound the server has
320 /// already persisted (typically `fence_floor + window_ms`). It must
321 /// satisfy `committed_ceiling >= fence_floor` so the allocator can serve
322 /// `try_grant` immediately without an additional extension round-trip.
323 pub fn become_leader(
324 &mut self,
325 fence_floor: PhysicalMs,
326 committed_ceiling: PhysicalMs,
327 epoch: Epoch,
328 ) -> Result<(), CoreError> {
329 if committed_ceiling < fence_floor {
330 return Err(CoreError::InvalidLeadershipWindow {
331 fence_floor: fence_floor.get(),
332 committed_ceiling: committed_ceiling.get(),
333 });
334 }
335 self.state = State::Leader {
336 epoch,
337 committed_high_water: committed_ceiling.get(),
338 next_physical_ms: fence_floor.get(),
339 next_logical: 0,
340 };
341 Ok(())
342 }
343
344 pub fn step_down(&mut self) {
345 self.state = State::NotLeader;
346 }
347
348 pub fn is_leader(&self) -> bool {
349 matches!(self.state, State::Leader { .. })
350 }
351
352 pub fn epoch(&self) -> Option<Epoch> {
353 match self.state {
354 State::Leader { epoch, .. } => Some(epoch),
355 State::NotLeader => None,
356 }
357 }
358
359 /// Current committed high-water in physical-millisecond units, or `None`
360 /// when not the leader. The high-water is the upper bound the allocator
361 /// will not exceed without a fresh `try_commit_window_extension`.
362 pub fn committed_high_water(&self) -> Option<u64> {
363 match self.state {
364 State::Leader {
365 committed_high_water,
366 ..
367 } => Some(committed_high_water),
368 State::NotLeader => None,
369 }
370 }
371
372 /// Shared count guard for `try_grant` and `would_grant`, kept here so the
373 /// two entry points cannot drift. The server's extension single-flight
374 /// relies on `would_grant(now_ms, count) == true` implying the retry
375 /// `try_grant(now_ms, count)` succeeds (see `service::extend_window`), so
376 /// the set of rejected counts must be identical on both paths — splitting
377 /// this check across both methods would let a future edit to one degrade
378 /// the recheck into a spurious consensus round-trip (or worse).
379 ///
380 /// `count == 0` reuses the oversized case's `InvalidCount(count)`; since
381 /// `count` is `0` there, the surfaced value matches the prior explicit
382 /// `InvalidCount(0)`.
383 fn validate_count(count: u32) -> Result<(), CoreError> {
384 if count == 0 || count > LOGICAL_MAX + 1 {
385 return Err(CoreError::InvalidCount(count));
386 }
387 Ok(())
388 }
389
390 /// Single source of truth for the window-advance simulation and its bounds
391 /// checks, shared by `try_grant` and `would_grant`. Pure: it takes the
392 /// relevant state fields by value and mutates nothing, so a failed
393 /// projection cannot leave allocator state advanced.
394 ///
395 /// On success returns the `(physical_ms, logical_start)` the grant would
396 /// occupy. The two failure variants are kept distinct so `try_grant` can
397 /// surface the precise error its callers (and tests) expect;
398 /// `would_grant` collapses both to `false` via `.is_ok()`.
399 fn project_grant(
400 next_physical_ms: u64,
401 next_logical: u32,
402 committed_high_water: u64,
403 now_ms: u64,
404 count: u32,
405 ) -> Result<(u64, u32), CoreError> {
406 let mut physical_ms = next_physical_ms;
407 let mut logical = next_logical;
408
409 // Advance physical_ms toward wall clock if ahead. next_physical_ms is
410 // already at or above fence_floor, so a low now_ms simply leaves it there.
411 if now_ms > physical_ms {
412 physical_ms = now_ms;
413 logical = 0;
414 }
415
416 // If the current physical_ms cannot fit the request in its remaining
417 // logical range, advance to the next physical_ms.
418 if logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
419 physical_ms += 1;
420 logical = 0;
421 }
422
423 if physical_ms > PHYSICAL_MS_MAX {
424 return Err(CoreError::PhysicalMsOutOfRange(physical_ms));
425 }
426
427 // The fence: never issue a timestamp at a physical_ms above the committed
428 // high-water. If we are at or past the bound, the caller must extend.
429 if physical_ms > committed_high_water {
430 return Err(CoreError::WindowExhausted);
431 }
432
433 Ok((physical_ms, logical))
434 }
435
436 /// Normalize the post-grant cursor into a packable `(physical_ms, logical)`
437 /// pair. `project_grant` admits a grant that exactly fills the millisecond,
438 /// so `logical_start + count` can reach `LOGICAL_MAX + 1` — a logical the
439 /// packed layout cannot hold. Roll that exact-fill case to the next
440 /// millisecond at logical 0: this is precisely the position the next
441 /// `project_grant` call would compute, so behavior is unchanged; the stored
442 /// state just no longer depends on the implicit "next call always wraps or
443 /// resets" invariant. (The returned `physical_ms` may equal
444 /// `PHYSICAL_MS_MAX + 1`, which is never packed directly and is rejected as
445 /// out-of-range by the next grant's `project_grant`.)
446 ///
447 /// Caller contract: `logical_start + count <= LOGICAL_MAX + 1`, which
448 /// `project_grant`'s logical-range bound already enforces for `try_grant`.
449 fn advance_cursor(physical_ms: u64, logical_start: u32, count: u32) -> (u64, u32) {
450 let next_logical = logical_start + count;
451 if next_logical > LOGICAL_MAX {
452 (physical_ms + 1, 0)
453 } else {
454 (physical_ms, next_logical)
455 }
456 }
457
458 /// Hot path. Issue `count` timestamps from the in-memory window.
459 ///
460 /// Returns `WindowExhausted` when the in-memory remainder cannot cover the request;
461 /// the caller (typically the server) then runs prepare → persist → commit and retries.
462 ///
463 /// State is written back only on success: a failed grant (out-of-range or
464 /// exhausted window) leaves `next_physical_ms`/`next_logical` untouched.
465 pub fn try_grant(&mut self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
466 Self::validate_count(count)?;
467 let State::Leader {
468 epoch,
469 committed_high_water,
470 next_physical_ms,
471 next_logical,
472 } = &mut self.state
473 else {
474 return Err(CoreError::NotLeader);
475 };
476
477 let (physical_ms, logical_start) = Self::project_grant(
478 *next_physical_ms,
479 *next_logical,
480 *committed_high_water,
481 now_ms,
482 count,
483 )?;
484
485 let grant = WindowGrant::new_unchecked(physical_ms, logical_start, count, *epoch);
486 (*next_physical_ms, *next_logical) =
487 Self::advance_cursor(physical_ms, logical_start, count);
488 Ok(grant)
489 }
490
491 /// Non-mutating predicate: would `try_grant(now_ms, count)` succeed right
492 /// now? Used by the server's extension single-flight to decide whether a
493 /// peer extender has already added enough room, avoiding a redundant
494 /// `persist_high_water` round-trip. Delegates to the same `project_grant`
495 /// helper `try_grant` uses, so the exhaustion check cannot drift — a
496 /// coarser predicate would risk false positives (skip the extension, then
497 /// fail the outer retry) for requests whose `count` straddles the window edge.
498 pub fn would_grant(&self, now_ms: u64, count: u32) -> bool {
499 if Self::validate_count(count).is_err() {
500 return false;
501 }
502 let State::Leader {
503 committed_high_water,
504 next_physical_ms,
505 next_logical,
506 ..
507 } = &self.state
508 else {
509 return false;
510 };
511
512 Self::project_grant(
513 *next_physical_ms,
514 *next_logical,
515 *committed_high_water,
516 now_ms,
517 count,
518 )
519 .is_ok()
520 }
521
522 /// Compute the high-water value the caller should durably persist before
523 /// calling `try_commit_window_extension`. Does not mutate.
524 ///
525 /// Returns `max(committed_high_water + 1, now_ms) + ahead_ms`. The +1 on
526 /// `committed_high_water` guarantees forward progress when wall clock is
527 /// behind the persisted bound (rare, but possible after a clock-step-back).
528 ///
529 /// Returns `Err(CoreError::NotLeader)` off-leader, matching every other
530 /// mutating method. A `0` sentinel here would be indistinguishable from a
531 /// legitimately prepared bound, letting a caller that skipped `is_leader()`
532 /// proceed as if preparation had succeeded.
533 pub fn try_prepare_window_extension(
534 &self,
535 now_ms: PhysicalMs,
536 ahead_ms: u64,
537 ) -> Result<PhysicalMs, CoreError> {
538 match &self.state {
539 State::NotLeader => Err(CoreError::NotLeader),
540 State::Leader {
541 committed_high_water,
542 ..
543 } => {
544 debug_assert!(
545 *committed_high_water <= PHYSICAL_MS_MAX,
546 "committed_high_water > PHYSICAL_MS_MAX: \
547 try_on_leadership_gained / try_commit_window_extension invariant",
548 );
549 let floor = *committed_high_water + 1;
550 let now_ms = now_ms.get();
551 let requested = core::cmp::max(floor, now_ms).checked_add(ahead_ms).ok_or(
552 CoreError::WindowExtensionOverflow {
553 floor,
554 now_ms,
555 ahead_ms,
556 },
557 )?;
558 // Re-wrap via `PhysicalMs::try_new`: the only remaining bound
559 // check on this path is on the *derived* sum, no longer on
560 // each input parameter.
561 PhysicalMs::try_new(requested)
562 }
563 }
564 }
565
566 /// Apply a durably-persisted window extension. `persisted_high_water` is
567 /// the value returned by `ConsensusDriver::persist_high_water`, which is
568 /// monotonic — it may equal or exceed the value passed to prepare.
569 ///
570 /// The `expected_epoch` argument fences out late-arriving commits from a
571 /// prior leader epoch: if the allocator is no longer at this epoch (either
572 /// it has lost leadership or a new leader took over), the commit is
573 /// dropped. Combined with the server's drain barrier, this guarantees a
574 /// late persist from epoch N cannot raise the durable bound observed by
575 /// epoch N+M.
576 ///
577 /// Returns [`CommitOutcome`]: `Applied` when the bound advanced, or
578 /// `Ignored` (with the reason) for the three benign drop cases. The 46-bit
579 /// physical-ceiling invariant on `persisted_high_water` is now enforced by
580 /// the [`PhysicalMs`] parameter type itself ([`PhysicalMs::try_new`]);
581 /// the `Result<_, CoreError>` shape is retained for source compatibility
582 /// with [`become_leader`](Self::become_leader) / [`try_prepare_window_extension`](Self::try_prepare_window_extension),
583 /// so callers can stay uniform under `?`, but no current code path here
584 /// produces `Err`.
585 pub fn try_commit_window_extension(
586 &mut self,
587 persisted_high_water: PhysicalMs,
588 expected_epoch: Epoch,
589 ) -> Result<CommitOutcome, CoreError> {
590 let persisted_high_water = persisted_high_water.get();
591 let State::Leader {
592 epoch,
593 committed_high_water,
594 ..
595 } = &mut self.state
596 else {
597 return Ok(CommitOutcome::Ignored(IgnoreReason::NotLeader));
598 };
599 // Epoch fencing takes precedence over the monotonic check: a late
600 // persist from a superseded epoch must report EpochMismatch even when
601 // its value also fails to advance, so churn is not masked as reordering.
602 if *epoch != expected_epoch {
603 return Ok(CommitOutcome::Ignored(IgnoreReason::EpochMismatch {
604 expected: expected_epoch,
605 current: *epoch,
606 }));
607 }
608 if persisted_high_water <= *committed_high_water {
609 return Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
610 persisted: persisted_high_water,
611 committed: *committed_high_water,
612 }));
613 }
614 *committed_high_water = persisted_high_water;
615 Ok(CommitOutcome::Applied {
616 high_water: persisted_high_water,
617 })
618 }
619}
620
621impl Default for Allocator {
622 fn default() -> Self {
623 Self::new()
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 // Tiny helper to keep the bound-validated literals readable. Every
632 // pre-newtype `become_leader(1_000, 5_000, …)` call carried an
633 // implicit "these literals are within the 46-bit field" precondition;
634 // wrapping each in `PhysicalMs::try_new(_).unwrap()` would have buried
635 // every test in unwrap noise without adding coverage (the literals are
636 // tiny constants under static review). `pms()` keeps the precondition
637 // explicit at the type level while reading the same as the original.
638 fn pms(value: u64) -> PhysicalMs {
639 PhysicalMs::try_new(value).expect("test literal exceeds PHYSICAL_MS_MAX")
640 }
641
642 #[test]
643 fn new_allocator_is_not_leader() {
644 let allocator = Allocator::new();
645 assert!(!allocator.is_leader());
646 assert_eq!(allocator.epoch(), None);
647 }
648
649 #[test]
650 fn become_leader_sets_epoch() {
651 let mut allocator = Allocator::new();
652 allocator
653 .become_leader(pms(1000), pms(5000), Epoch(5))
654 .unwrap();
655 assert!(allocator.is_leader());
656 assert_eq!(allocator.epoch(), Some(Epoch(5)));
657 }
658
659 #[test]
660 fn become_leader_rejects_inverted_window() {
661 // The per-argument PHYSICAL_MS_MAX checks are now enforced one layer
662 // out at `PhysicalMs::try_new` (see the `physical_ms` test block below),
663 // so this method's only remaining error is the cross-argument
664 // `committed_ceiling < fence_floor` invariant.
665 let mut allocator = Allocator::new();
666 assert_eq!(
667 allocator.become_leader(pms(5000), pms(4000), Epoch(5)),
668 Err(CoreError::InvalidLeadershipWindow {
669 fence_floor: 5000,
670 committed_ceiling: 4000
671 })
672 );
673 }
674
675 #[test]
676 fn step_down_clears_state() {
677 let mut allocator = Allocator::new();
678 allocator
679 .become_leader(pms(1000), pms(5000), Epoch(5))
680 .unwrap();
681 allocator.step_down();
682 assert!(!allocator.is_leader());
683 assert_eq!(allocator.epoch(), None);
684 }
685
686 #[test]
687 fn committed_high_water_tracks_leader_state_and_extensions() {
688 let mut allocator = Allocator::new();
689 assert_eq!(allocator.committed_high_water(), None);
690
691 allocator
692 .become_leader(pms(1_000), pms(5_000), Epoch(1))
693 .unwrap();
694 assert_eq!(allocator.committed_high_water(), Some(5_000));
695
696 let target = allocator
697 .try_prepare_window_extension(pms(2_000), 3_000)
698 .unwrap();
699 allocator
700 .try_commit_window_extension(target, Epoch(1))
701 .unwrap();
702 assert_eq!(allocator.committed_high_water(), Some(target.get()));
703
704 allocator.step_down();
705 assert_eq!(allocator.committed_high_water(), None);
706 }
707
708 #[test]
709 fn try_grant_not_leader() {
710 let mut allocator = Allocator::new();
711 assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
712 }
713
714 #[test]
715 fn try_grant_zero_count() {
716 let mut allocator = Allocator::new();
717 allocator
718 .become_leader(pms(1000), pms(5000), Epoch(1))
719 .unwrap();
720 assert_eq!(
721 allocator.try_grant(1000, 0),
722 Err(CoreError::InvalidCount(0))
723 );
724 }
725
726 #[test]
727 fn try_grant_oversized_count() {
728 let mut allocator = Allocator::new();
729 allocator
730 .become_leader(pms(1000), pms(5000), Epoch(1))
731 .unwrap();
732 let oversized = LOGICAL_MAX + 2;
733 assert_eq!(
734 allocator.try_grant(1000, oversized),
735 Err(CoreError::InvalidCount(oversized))
736 );
737 }
738
739 #[test]
740 fn try_grant_above_committed_is_window_exhausted() {
741 // Advancing `now_ms` past `committed_high_water` correctly returns
742 // WindowExhausted; the server then extends.
743 let mut allocator = Allocator::new();
744 // fence_floor=5_000, ceiling=5_000 (tight window, no pre-extended gap).
745 allocator
746 .become_leader(pms(5_000), pms(5_000), Epoch(1))
747 .unwrap();
748 // now_ms below floor: clamps to floor=5_000, which equals the ceiling → succeeds.
749 allocator.try_grant(4_999, 1).unwrap();
750 // now_ms above ceiling: window exhausted.
751 assert_eq!(
752 allocator.try_grant(5_001, 1),
753 Err(CoreError::WindowExhausted)
754 );
755 }
756
757 #[test]
758 fn failed_try_grant_does_not_advance_state() {
759 // A grant that fails the exhaustion check must leave the allocator's
760 // advance state untouched, so a later grant at a lower `now_ms` is not
761 // pinned to the failed attempt's wall clock.
762 let mut allocator = Allocator::new();
763 // Tight initial window: fence_floor == ceiling == 1_000.
764 allocator
765 .become_leader(pms(1_000), pms(1_000), Epoch(1))
766 .unwrap();
767 // now_ms far past the ceiling exhausts the window.
768 assert_eq!(
769 allocator.try_grant(5_000, 1),
770 Err(CoreError::WindowExhausted)
771 );
772 // Extend the durable bound to exactly 2_000.
773 let target = allocator
774 .try_prepare_window_extension(pms(2_000), 0)
775 .unwrap();
776 assert_eq!(target, pms(2_000)); // max(committed+1=1_001, now=2_000) + 0
777 allocator
778 .try_commit_window_extension(target, Epoch(1))
779 .unwrap();
780 // The failed grant must not have pinned next_physical_ms at 5_000: a
781 // grant at now_ms=2_000 advances cleanly to physical_ms=2_000 (<= the
782 // committed 2_000). If state had advanced on the failure, next_physical_ms
783 // would still be 5_000 and this would exhaust the window again.
784 let grant = allocator.try_grant(2_000, 1).unwrap();
785 assert_eq!(grant.physical_ms, 2_000);
786 assert_eq!(grant.logical_start, 0);
787 }
788
789 #[test]
790 fn try_grant_after_gain_serves_immediately() {
791 // The fence has already persisted a pre-extended window, so the allocator
792 // can serve immediately. Grants start at fence_floor regardless of now_ms.
793 let mut allocator = Allocator::new();
794 allocator
795 .become_leader(pms(5_000), pms(10_000), Epoch(1))
796 .unwrap();
797 let grant = allocator.try_grant(1_000, 1).unwrap();
798 // now_ms=1_000 < fence_floor=5_000, so next_physical_ms stays at 5_000.
799 assert_eq!(grant.physical_ms, 5_000);
800 assert_eq!(grant.logical_start, 0);
801 assert_eq!(grant.epoch, Epoch(1));
802 }
803
804 #[test]
805 fn prepare_window_extension_not_leader() {
806 // Off-leader prepare must error like every other mutating method, not
807 // return a `0` that a caller could mistake for a prepared bound.
808 let allocator = Allocator::new();
809 assert_eq!(
810 allocator.try_prepare_window_extension(pms(1000), 3000),
811 Err(CoreError::NotLeader)
812 );
813 }
814
815 #[test]
816 fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
817 let mut allocator = Allocator::new();
818 allocator
819 .become_leader(pms(1000), pms(1000), Epoch(1))
820 .unwrap();
821 let target = allocator
822 .try_prepare_window_extension(pms(2000), 3000)
823 .unwrap();
824 assert_eq!(target, pms(5000)); // max(1001, 2000) + 3000
825 }
826
827 #[test]
828 fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
829 let mut allocator = Allocator::new();
830 allocator
831 .become_leader(pms(10_000), pms(10_000), Epoch(1))
832 .unwrap();
833 let target = allocator
834 .try_prepare_window_extension(pms(500), 3000)
835 .unwrap();
836 // floor = 10_001, clock = 500. max = 10_001. + 3000 = 13_001.
837 assert_eq!(target, pms(13_001));
838 }
839
840 #[test]
841 fn prepare_window_extension_rejects_out_of_range_target() {
842 let mut allocator = Allocator::new();
843 allocator
844 .become_leader(PhysicalMs::MAX, PhysicalMs::MAX, Epoch(1))
845 .unwrap();
846 assert_eq!(
847 allocator.try_prepare_window_extension(PhysicalMs::MAX, 1),
848 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
849 );
850 }
851
852 #[test]
853 fn prepare_window_extension_overflow_names_all_operands() {
854 // Pre-newtype, the canonical overflow scenario was a saturated clock
855 // (SystemClock::now_ms saturates to u64::MAX) plus any non-zero
856 // ahead_ms. The PhysicalMs newtype now rejects that scenario at the
857 // boundary wrap (PhysicalMs::try_new(u64::MAX) → PhysicalMsOutOfRange),
858 // surfacing an earlier, more precise error.
859 //
860 // The internal overflow path is still reachable, but only via a
861 // pathologically large `ahead_ms` (a duration, not a physical-ms, so
862 // it stays an unbounded u64). The error must still name all three
863 // real operands so the log points at the offending duration, not a
864 // phantom "someone passed an absurd physical_ms".
865 let mut allocator = Allocator::new();
866 allocator
867 .become_leader(pms(1_000), pms(1_000), Epoch(1))
868 .unwrap();
869 assert_eq!(
870 allocator.try_prepare_window_extension(pms(1_000), u64::MAX),
871 Err(CoreError::WindowExtensionOverflow {
872 floor: 1_001,
873 now_ms: 1_000,
874 ahead_ms: u64::MAX,
875 })
876 );
877 }
878
879 #[test]
880 fn commit_then_try_grant_succeeds() {
881 let mut allocator = Allocator::new();
882 allocator
883 .become_leader(pms(1000), pms(1000), Epoch(7))
884 .unwrap();
885 let target = allocator
886 .try_prepare_window_extension(pms(1000), 3000)
887 .unwrap();
888 assert_eq!(
889 allocator.try_commit_window_extension(target, Epoch(7)),
890 Ok(CommitOutcome::Applied {
891 high_water: target.get()
892 })
893 );
894 let grant = allocator.try_grant(1000, 5).unwrap();
895 assert_eq!(grant.count, 5);
896 assert_eq!(grant.logical_start, 0);
897 assert_eq!(grant.epoch, Epoch(7));
898 // physical_ms should be at most the persisted high-water.
899 assert!(grant.physical_ms <= target.get());
900 }
901
902 #[test]
903 fn commit_with_lower_value_is_ignored() {
904 let mut allocator = Allocator::new();
905 allocator
906 .become_leader(pms(1000), pms(1000), Epoch(1))
907 .unwrap();
908 assert_eq!(
909 allocator.try_commit_window_extension(pms(5000), Epoch(1)),
910 Ok(CommitOutcome::Applied { high_water: 5000 })
911 );
912 // A non-advancing commit reports the values so the caller can tell a
913 // monotonic-bound regression (persist reordering) from epoch churn.
914 assert_eq!(
915 allocator.try_commit_window_extension(pms(3000), Epoch(1)),
916 Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
917 persisted: 3000,
918 committed: 5000,
919 }))
920 );
921 // try_grant up to physical_ms=5000 should still work.
922 let grant = allocator.try_grant(4500, 1).unwrap();
923 assert_eq!(grant.physical_ms, 4500);
924 }
925
926 #[test]
927 fn commit_with_equal_value_is_ignored_not_applied() {
928 // persist_high_water is monotonic and may *equal* the prepared bound; an
929 // equal value moves nothing, so it is Ignored(NotAdvanced), not Applied.
930 let mut allocator = Allocator::new();
931 allocator
932 .become_leader(pms(1000), pms(5000), Epoch(1))
933 .unwrap();
934 assert_eq!(
935 allocator.try_commit_window_extension(pms(5000), Epoch(1)),
936 Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
937 persisted: 5000,
938 committed: 5000,
939 }))
940 );
941 }
942
943 // (`commit_rejects_out_of_range_high_water` migrated to the `physical_ms`
944 // test block: the bound check is now at `PhysicalMs::try_new`, so the
945 // bad value can no longer reach `try_commit_window_extension`.)
946
947 #[test]
948 fn try_grant_rejects_out_of_range_clock() {
949 let mut allocator = Allocator::new();
950 allocator
951 .become_leader(pms(1000), PhysicalMs::MAX, Epoch(1))
952 .unwrap();
953 assert_eq!(
954 allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
955 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
956 );
957 }
958
959 #[test]
960 fn commit_at_wrong_epoch_is_silently_dropped() {
961 let mut allocator = Allocator::new();
962 // fence_floor=1000, ceiling=1000: tight initial window.
963 allocator
964 .become_leader(pms(1000), pms(1000), Epoch(5))
965 .unwrap();
966 // A late persist from epoch 4 (the prior leader) — fenced out. The
967 // outcome names both epochs so the caller can metric epoch churn.
968 assert_eq!(
969 allocator.try_commit_window_extension(pms(9_999), Epoch(4)),
970 Ok(CommitOutcome::Ignored(IgnoreReason::EpochMismatch {
971 expected: Epoch(4),
972 current: Epoch(5),
973 }))
974 );
975 // The allocator's bound did not move; a grant at now=900 clamps to
976 // floor=1000, and a request with now=1_100 exhausts the window.
977 allocator.try_grant(900, 1).unwrap();
978 assert_eq!(
979 allocator.try_grant(1_100, 1),
980 Err(CoreError::WindowExhausted)
981 );
982 }
983
984 #[test]
985 fn commit_after_leadership_lost_is_ignored() {
986 let mut allocator = Allocator::new();
987 allocator
988 .become_leader(pms(1000), pms(5000), Epoch(1))
989 .unwrap();
990 allocator.step_down();
991 assert_eq!(
992 allocator.try_commit_window_extension(pms(9_999), Epoch(1)),
993 Ok(CommitOutcome::Ignored(IgnoreReason::NotLeader))
994 );
995 assert!(!allocator.is_leader());
996 }
997
998 #[test]
999 fn would_grant_matches_try_grant_outcome() {
1000 let mut allocator = Allocator::new();
1001 // Not leader: never grants.
1002 assert!(!allocator.would_grant(1_000, 1));
1003 // Invalid counts: never grants.
1004 allocator
1005 .become_leader(pms(1_000), pms(5_000), Epoch(1))
1006 .unwrap();
1007 assert!(!allocator.would_grant(1_000, 0));
1008 assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
1009 // Within-window: matches try_grant. now_ms below floor still grants
1010 // (clamped to floor=1_000, ceiling=5_000).
1011 assert!(allocator.would_grant(0, 1));
1012 // now_ms above ceiling: predicate refuses (would exhaust).
1013 assert!(!allocator.would_grant(5_001, 1));
1014 // Mid-window now_ms advances the predicate's internal physical_ms.
1015 assert!(allocator.would_grant(2_500, 1));
1016 }
1017
1018 #[test]
1019 fn would_grant_predicts_logical_wrap_advance() {
1020 // When (logical + count) overflows the per-ms logical range, the
1021 // predicate (like try_grant) advances physical_ms by 1. If that
1022 // advance leaves the window, would_grant must return false.
1023 let mut allocator = Allocator::new();
1024 allocator
1025 .become_leader(pms(1_000), pms(1_000), Epoch(1))
1026 .unwrap();
1027 // count >= LOGICAL_MAX + 1 forces the advance branch on a fresh
1028 // window: logical(0) + count(LOGICAL_MAX+1) doesn't overflow on its
1029 // own, but anything one bigger does. Use LOGICAL_MAX + 1 to land at
1030 // the edge, then any non-zero issue advances physical_ms.
1031 allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
1032 // Next grant of size 1 would advance to physical_ms = 1_001, which
1033 // exceeds the committed ceiling of 1_000.
1034 assert!(!allocator.would_grant(1_000, 1));
1035 }
1036
1037 #[test]
1038 fn would_grant_returns_false_when_advance_exceeds_physical_max() {
1039 // Construct an allocator at PHYSICAL_MS_MAX so the +1 advance
1040 // crosses the 46-bit ceiling and the predicate refuses.
1041 let mut allocator = Allocator::new();
1042 allocator
1043 .become_leader(PhysicalMs::MAX, PhysicalMs::MAX, Epoch(1))
1044 .unwrap();
1045 // Fill the logical range so the next would_grant call has to
1046 // advance physical_ms.
1047 allocator
1048 .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
1049 .unwrap();
1050 assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
1051 }
1052
1053 #[test]
1054 fn default_constructs_not_leader_allocator() {
1055 let allocator = Allocator::default();
1056 assert!(!allocator.is_leader());
1057 assert_eq!(allocator.epoch(), None);
1058 }
1059
1060 #[test]
1061 fn logical_wraps_to_next_physical_ms() {
1062 let mut allocator = Allocator::new();
1063 // fence_floor=0, ceiling=0; extend to 10 before granting.
1064 allocator
1065 .become_leader(PhysicalMs::ZERO, PhysicalMs::ZERO, Epoch(1))
1066 .unwrap();
1067 allocator
1068 .try_commit_window_extension(pms(10), Epoch(1))
1069 .unwrap();
1070 // Issue LOGICAL_MAX+1 logicals at physical_ms=1, then one more should bump to 2.
1071 let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
1072 assert_eq!(first.physical_ms, 1);
1073 assert_eq!(first.logical_start, 0);
1074 let second = allocator.try_grant(1, 1).unwrap();
1075 assert_eq!(second.physical_ms, 2);
1076 assert_eq!(second.logical_start, 0);
1077 }
1078
1079 #[test]
1080 fn exact_fill_grant_normalizes_stored_state_to_packable() {
1081 // A grant that consumes a millisecond's entire logical range
1082 // (logical_start + count == LOGICAL_MAX + 1) must not leave the
1083 // LOGICAL_MAX+1 sentinel in next_logical: that value cannot be packed
1084 // (Timestamp::pack asserts logical <= LOGICAL_MAX), so the stored state
1085 // would only be safe by the implicit "next call always wraps" invariant.
1086 // The write-back normalizes it to the already-rolled position
1087 // (physical_ms + 1, 0), so stored state is always directly packable.
1088 let mut allocator = Allocator::new();
1089 allocator
1090 .become_leader(PhysicalMs::ZERO, PhysicalMs::ZERO, Epoch(1))
1091 .unwrap();
1092 allocator
1093 .try_commit_window_extension(pms(10), Epoch(1))
1094 .unwrap();
1095 // Fill physical_ms=1 exactly: logical [0, LOGICAL_MAX].
1096 let grant = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
1097 assert_eq!(grant.physical_ms, 1);
1098 assert_eq!(grant.logical_start, 0);
1099
1100 let State::Leader {
1101 next_physical_ms,
1102 next_logical,
1103 ..
1104 } = allocator.state
1105 else {
1106 panic!("expected leader state after a successful grant");
1107 };
1108 // The stored cursor rolled to the next millisecond at logical 0 …
1109 assert_eq!(next_physical_ms, 2);
1110 assert_eq!(next_logical, 0);
1111 // … and is, by construction, a packable timestamp.
1112 assert!(Timestamp::try_pack(next_physical_ms, next_logical).is_ok());
1113 }
1114
1115 #[test]
1116 fn try_new_accepts_valid_grant_and_packs_boundaries() {
1117 // A checked grant exposes its fields through accessors and its
1118 // boundary timestamps pack without panicking — first() at
1119 // logical_start, last() at logical_start + count - 1.
1120 let grant = WindowGrant::try_new(1_000, 5, 3, Epoch(7)).unwrap();
1121 assert_eq!(grant.physical_ms(), 1_000);
1122 assert_eq!(grant.logical_start(), 5);
1123 assert_eq!(grant.count(), 3);
1124 assert_eq!(grant.epoch(), Epoch(7));
1125 assert_eq!(grant.first(), Timestamp::pack(1_000, 5));
1126 assert_eq!(grant.last(), Timestamp::pack(1_000, 7));
1127 }
1128
1129 #[test]
1130 fn try_new_accepts_max_logical_boundary() {
1131 // logical_start + count - 1 == LOGICAL_MAX is the widest in-range grant.
1132 let grant = WindowGrant::try_new(1_000, 0, LOGICAL_MAX + 1, Epoch(1)).unwrap();
1133 assert_eq!(grant.last(), Timestamp::pack(1_000, LOGICAL_MAX));
1134 }
1135
1136 #[test]
1137 fn try_new_rejects_zero_count() {
1138 // count == 0 would underflow logical_start + count - 1 in last().
1139 assert_eq!(
1140 WindowGrant::try_new(1_000, 0, 0, Epoch(1)),
1141 Err(CoreError::InvalidCount(0))
1142 );
1143 }
1144
1145 #[test]
1146 fn try_new_rejects_out_of_range_physical_ms() {
1147 assert_eq!(
1148 WindowGrant::try_new(PHYSICAL_MS_MAX + 1, 0, 1, Epoch(1)),
1149 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
1150 );
1151 }
1152
1153 #[test]
1154 fn try_new_rejects_logical_range_overflow() {
1155 // last logical (logical_start + count - 1) exceeds the 18-bit field.
1156 assert_eq!(
1157 WindowGrant::try_new(1_000, LOGICAL_MAX, 2, Epoch(1)),
1158 Err(CoreError::LogicalRangeOutOfRange {
1159 logical_start: LOGICAL_MAX,
1160 count: 2
1161 })
1162 );
1163 }
1164
1165 #[test]
1166 fn try_new_rejects_logical_count_u32_overflow() {
1167 // logical_start + (count - 1) overflows u32 before any range check.
1168 assert_eq!(
1169 WindowGrant::try_new(1_000, u32::MAX, 2, Epoch(1)),
1170 Err(CoreError::LogicalRangeOutOfRange {
1171 logical_start: u32::MAX,
1172 count: 2
1173 })
1174 );
1175 }
1176
1177 // ----------------------------------------------------------------
1178 // PhysicalMs newtype: the construction-site bound check that the
1179 // three Allocator entry points used to re-implement inline. Every
1180 // assertion below was previously expressed as a runtime check
1181 // *inside* become_leader, try_prepare_window_extension,
1182 // or try_commit_window_extension; they now belong to the type.
1183 // ----------------------------------------------------------------
1184
1185 #[test]
1186 fn physical_ms_accepts_zero() {
1187 assert_eq!(PhysicalMs::try_new(0).unwrap().get(), 0);
1188 }
1189
1190 #[test]
1191 fn physical_ms_accepts_max() {
1192 assert_eq!(
1193 PhysicalMs::try_new(PHYSICAL_MS_MAX).unwrap().get(),
1194 PHYSICAL_MS_MAX,
1195 );
1196 }
1197
1198 #[test]
1199 fn physical_ms_rejects_one_past_max() {
1200 // The previously-inline checks in become_leader and
1201 // try_commit_window_extension lived at this exact boundary;
1202 // they now live here.
1203 assert_eq!(
1204 PhysicalMs::try_new(PHYSICAL_MS_MAX + 1),
1205 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1)),
1206 );
1207 }
1208
1209 #[test]
1210 fn physical_ms_rejects_u64_max() {
1211 // The saturated-clock scenario the old `try_prepare_window_extension`
1212 // overflow test probed by passing u64::MAX is now caught one layer
1213 // out, at construction.
1214 assert_eq!(
1215 PhysicalMs::try_new(u64::MAX),
1216 Err(CoreError::PhysicalMsOutOfRange(u64::MAX)),
1217 );
1218 }
1219
1220 #[test]
1221 fn physical_ms_max_const_matches_try_new() {
1222 assert_eq!(
1223 PhysicalMs::MAX,
1224 PhysicalMs::try_new(PHYSICAL_MS_MAX).unwrap(),
1225 );
1226 }
1227
1228 #[test]
1229 fn physical_ms_zero_const_matches_try_new_and_default() {
1230 // ZERO, Default::default(), and try_new(0) must all coincide so
1231 // callers can use any spelling without semantic difference.
1232 assert_eq!(PhysicalMs::ZERO, PhysicalMs::try_new(0).unwrap());
1233 assert_eq!(PhysicalMs::default(), PhysicalMs::ZERO);
1234 }
1235
1236 #[test]
1237 fn physical_ms_try_from_matches_try_new() {
1238 // TryFrom<u64> is required for generic conversion code; it must
1239 // produce identical Ok/Err to the inherent try_new on every input.
1240 let good: u64 = 1_234_567;
1241 let from_inherent = PhysicalMs::try_new(good).unwrap();
1242 let from_trait: PhysicalMs = good.try_into().unwrap();
1243 assert_eq!(from_inherent, from_trait);
1244
1245 let bad = PHYSICAL_MS_MAX + 1;
1246 let bad_inherent = PhysicalMs::try_new(bad);
1247 let bad_trait: Result<PhysicalMs, CoreError> = bad.try_into();
1248 assert_eq!(bad_inherent, bad_trait);
1249 }
1250
1251 #[test]
1252 fn physical_ms_display_passes_through_inner_value() {
1253 // Display is a thin passthrough — it must format identically to the
1254 // underlying u64 so log lines and error messages read the same after
1255 // the refactor.
1256 let v: u64 = 4_242_424_242;
1257 assert_eq!(
1258 format!("{}", PhysicalMs::try_new(v).unwrap()),
1259 format!("{v}"),
1260 );
1261 }
1262
1263 #[test]
1264 fn physical_ms_ordering_follows_inner_value() {
1265 // The wrapper exposes Ord/PartialOrd so the allocator can compare
1266 // bounds (e.g. committed_ceiling < fence_floor) without stripping to
1267 // u64 — this test pins that the derived ordering matches the inner.
1268 let a = PhysicalMs::try_new(5).unwrap();
1269 let b = PhysicalMs::try_new(10).unwrap();
1270 assert!(a < b);
1271 assert!(b > a);
1272 assert!(a <= a);
1273 }
1274
1275 #[test]
1276 fn physical_ms_is_copy_and_eq() {
1277 // Compile-time witness: if a future edit accidentally drops `Copy`
1278 // or `Eq`, several allocator call sites that consume the value
1279 // twice or compare it inside `assert_eq!` would silently break.
1280 fn assert_copy_eq<T: Copy + Eq>() {}
1281 assert_copy_eq::<PhysicalMs>();
1282 }
1283}