tatara_process/requeue.rs
1//! Requeue-action primitives — the small typed layer over the
2//! `kube::runtime::controller::Action::requeue(std::time::Duration
3//! ::from_secs(<secs>))` two-link chain every phase-machine handler,
4//! error-policy sink, and Signals ingest arm in the workspace exits
5//! through.
6//!
7//! `kube_runtime` exposes only ONE requeue constructor —
8//! `Action::requeue(std::time::Duration)` — but every consumer in
9//! this workspace already carries its retry / heartbeat / short-
10//! retry budget as a whole-second `u64` (the module-level
11//! `TICK_RETRY` / `HEARTBEAT` / `SHORT_RETRY` constants, the
12//! per-call literals `1`, `5`, `10`, `15`, `30`, `60`, `120`, and
13//! the `ctx.config.heartbeat_seconds` slot). This module owns the
14//! one-line chain from that `u64` to the returned `Action`, so a
15//! future normalization (an injectable-jitter overlay, a bounded-
16//! rate limiter, a per-controller floor / ceiling clamp, a
17//! deterministic-clock test hook) lands at ONE substrate primitive
18//! and every downstream retry sink inherits the upgrade mechanically.
19
20use kube::runtime::controller::Action;
21use std::time::Duration;
22
23/// A `kube::runtime::controller::Action` that re-enqueues the
24/// current object `secs` seconds from now — the one-line
25/// `Action::requeue(Duration::from_secs(secs))` two-link chain
26/// lifted to ONE typed owner past the ★★ PRIME-DIRECTIVE ≥ 2
27/// duplication threshold.
28///
29/// Pre-lift the SAME chain was hand-authored at 35 workspace-wide
30/// consumer sites across 5 files in the two ACTIVE reconciler
31/// crates:
32///
33/// * `tatara-reconciler::phase_machine` — 22 sites feeding the
34/// module-level `TICK_RETRY` / `HEARTBEAT` / `SHORT_RETRY`
35/// constants at every FSM handler's tail (the `pending` /
36/// `forking` / `execing` / `running` / `attested` / `reconverging`
37/// / `releasing` / `exiting` / `failed` / `zombie` / `reaped`
38/// arms).
39/// * `tatara-reconciler::controller` — 5 sites feeding literal `1`
40/// (deletion-preemption + signal-arm re-poll), the
41/// `ctx.config.heartbeat_seconds` slot (suspend), and `30`
42/// (reconcile-error sink + `error_policy`).
43/// * `tatara-reconciler::table_controller` — 2 sites feeding `30`
44/// at the ProcessTable heartbeat + error policy.
45/// * `tatara-pool-reconciler::controller_pool` — 3 sites feeding
46/// the reconcile-interval slot + literal `15` (the two `15`
47/// sites — the pool reconciler's `error_policy` sink + the
48/// Allocation reconciler's peer sink — subsequently reached
49/// [`error_backoff`] on the named-intent axis; count preserved
50/// here for the pre-lift audit).
51/// * `tatara-pool-reconciler::controller_allocation` — 3 sites
52/// feeding literal `5` (bind-retry), the reconcile-interval
53/// slot, and literal `15`.
54///
55/// All 35 sites walked the SAME two-link chain — take a whole-
56/// second `u64` (a literal, a named constant, or a config slot),
57/// wrap it in a `std::time::Duration` via `from_secs`, then hand
58/// the duration to `Action::requeue`. Differing only in the
59/// second-count operand. Post-lift each callsite reads
60/// `tatara_process::requeue::after_secs(N)` and the wrap +
61/// requeue chain lives at ONE substrate owner.
62///
63/// Return-form axis: `kube::runtime::controller::Action` — the
64/// exact type every kube-runtime `reconcile` fn returns as
65/// `Ok(...)` and every `error_policy` returns bare. The `u64`
66/// `secs` parameter matches `Duration::from_secs`'s own signature
67/// so the migration is byte-identical: every pre-lift site fed a
68/// `u64` (either a literal, a named `pub const N: u64 = ...;`, or
69/// a config field typed as `u64`) directly into `Duration::
70/// from_secs`, and the same feed continues to work at
71/// `after_secs`.
72///
73/// A future normalization — an injectable jitter overlay that
74/// randomizes ±10% of `secs` to avoid a thundering herd of
75/// synchronized reconcile ticks, a per-controller
76/// floor/ceiling clamp so a mis-configured heartbeat can't drive
77/// the API server, an injectable deterministic clock so
78/// integration tests can advance requeue budgets without waiting
79/// wall-clock time, a per-fleet rate limiter that spreads bursts
80/// across a sliding window, a `tracing`-annotated span carrying
81/// the requeue reason for post-hoc audit — lands at THIS ONE
82/// substrate primitive and every downstream retry sink across
83/// the two active reconciler crates inherits the upgrade
84/// mechanically. No per-site edit at any of the 35 listed
85/// callers or at future consumers (a new phase handler, a new
86/// controller crate, a per-Kind retry sink).
87///
88/// Sibling to the timed-decision primitives in [`crate::time`]
89/// on the "second-count → typed timed value" axis: `seconds_ago(N)
90/// -> DateTime<Utc>` seeds a wall-clock anchor `N` seconds in the
91/// past for `elapsed_since` consumers; `after_secs(N) -> Action`
92/// seeds a kube-runtime requeue `N` seconds in the future. Both
93/// carry the same "`u64` seconds is the workspace's canonical
94/// short-time unit" invariant so a switch to a finer-grained unit
95/// (a millisecond-precision retry budget for tight probes) would
96/// land at both primitives together rather than as scattered per-
97/// site conversions.
98#[must_use]
99pub fn after_secs(secs: u64) -> Action {
100 Action::requeue(Duration::from_secs(secs))
101}
102
103/// Second-count budget for the "fast re-poll after a state change"
104/// requeue intent (1s). The wire-form value binds the
105/// `Ok(after_secs(TICK_RETRY))` / `Ok(after_secs(1))` chains every
106/// FSM handler tail + `controller.rs` signal-consumed / deletion-
107/// preempt arm exit through — pre-lift restated as the `TICK_RETRY`
108/// local const in `tatara-reconciler::phase_machine` and as a bare
109/// `1` literal in `tatara-reconciler::controller`, past the ★★
110/// PRIME-DIRECTIVE ≥ 2 duplication threshold across two files that
111/// each named the same intent independently. Post-lift both bind
112/// through this ONE workspace-wide constant so a future budget
113/// normalization (a bounded-rate limiter's floor, a jitter overlay's
114/// pivot) lands at ONE owner.
115pub const TICK_SECONDS: u64 = 1;
116
117/// Second-count budget for the "short retry after transient
118/// failure" requeue intent (5s). Bound by the `SHORT_RETRY` const in
119/// `tatara-reconciler::phase_machine` (execing evaluation / running
120/// evaluation / attested probe / reconverging), and by the bare `5`
121/// literal in `tatara-pool-reconciler::controller_allocation`'s
122/// bind-retry arm, past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
123/// threshold across two files.
124pub const SHORT_RETRY_SECONDS: u64 = 5;
125
126/// Second-count budget for the "steady-state heartbeat" requeue
127/// intent (30s). Bound by the `HEARTBEAT` const in
128/// `tatara-reconciler::phase_machine` (7 handler-tail sites), by the
129/// bare `30` literal in `tatara-reconciler::controller`'s
130/// reconcile-error sink + `error_policy`, and by the bare `30`
131/// literal in `tatara-reconciler::table_controller`'s ProcessTable
132/// heartbeat + `error_policy`, past the ★★ PRIME-DIRECTIVE ≥ 2
133/// duplication threshold across three files that each named the
134/// same intent independently.
135pub const HEARTBEAT_SECONDS: u64 = 30;
136
137/// Second-count budget for the "back off after a reconcile error"
138/// requeue intent (15s). Bound by the bare `15` literal in
139/// `tatara-pool-reconciler::controller_pool::error_policy` (Pool
140/// reconcile-failure backoff) and by the bare `15` literal in
141/// `tatara-pool-reconciler::controller_allocation::error_policy`
142/// (Allocation reconcile-failure backoff), past the ★★
143/// PRIME-DIRECTIVE ≥ 2 duplication threshold across two files that
144/// each named the same intent independently.
145///
146/// Sits between [`SHORT_RETRY_SECONDS`] (5s — a per-branch bounded
147/// retry inside a reconcile pass) and [`HEARTBEAT_SECONDS`] (30s —
148/// the steady-state re-observation cadence). The pool-reconciler
149/// deliberately chose a *shorter* error-backoff than its own
150/// `heartbeat_seconds` slot (default 30) because a Pool /
151/// EphemeralAllocation reconcile failure signals a mis-computed
152/// desired-count or a stale claim — either of which the operator
153/// wants observed again ahead of the next heartbeat tick — whereas
154/// the two `tatara-reconciler` error_policy sinks fall through to
155/// [`heartbeat`] directly because a Process-reconcile failure holds
156/// its own retry budget in the FSM handlers themselves.
157pub const ERROR_BACKOFF_SECONDS: u64 = 15;
158
159/// The "fast re-poll" requeue action — `after_secs(TICK_SECONDS)`.
160///
161/// The ONE substrate owner of the "immediate re-poll after a signal
162/// consumption or state-change latch" requeue intent every FSM
163/// handler tail and top-level dispatcher exits through. Pre-lift the
164/// intent was hand-authored at 12 workspace-wide sites past the ★★
165/// PRIME-DIRECTIVE ≥ 2 duplication threshold across two files, each
166/// naming the same intent independently:
167///
168/// * `tatara-reconciler::phase_machine` — 10 sites fed the local
169/// `const TICK_RETRY: u64 = 1;` binding at pending / forking-
170/// heartbeat / attested-heartbeat / exiting-tail / zombie-poll /
171/// reaped handler tails.
172/// * `tatara-reconciler::controller` — 2 sites fed a bare `1`
173/// literal at the deletion-preempt requeue and the signal-
174/// consumed requeue (both semantically "re-poll immediately").
175///
176/// Post-lift each callsite reads `tatara_process::requeue::tick()`
177/// and the intent → second-count → requeue chain lives at ONE
178/// substrate owner. Peer to the [`short_retry`] and [`heartbeat`]
179/// helpers on the "named requeue intent" axis; sibling to the raw-
180/// second-count [`after_secs`] on the "typed requeue constructor"
181/// axis (the semantic helpers here compose through it).
182///
183/// A future normalization on the tick intent alone (e.g. a
184/// millisecond-precision path for tight controller inner loops,
185/// bounded by a workspace-wide floor) lands at THIS ONE substrate
186/// primitive and every downstream tick site inherits the upgrade
187/// mechanically. No per-site edit at any of the 12 listed callers
188/// or at future consumers (a new phase handler tail, a new signal
189/// arm, a new top-level dispatcher).
190///
191/// Theory anchor: THEORY.md §VI.1 (generation over composition —
192/// the intent recurred at 12 hand-authored production sites past
193/// the PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
194/// ONE owner here). THEORY.md §II.1 invariant 5 (composition
195/// preserves proofs — the intent → second-count mapping lives at
196/// ONE typed algebra projection; a regression that drifted the
197/// second-count would fail at the byte-shape pin below rather than
198/// as silent operator-visible cadence skew across the 12
199/// downstream requeue sites).
200#[must_use]
201pub fn tick() -> Action {
202 after_secs(TICK_SECONDS)
203}
204
205/// The "short retry after transient failure" requeue action —
206/// `after_secs(SHORT_RETRY_SECONDS)`.
207///
208/// The ONE substrate owner of the "short back-off after a
209/// transient error / not-yet-ready state" requeue intent. Pre-lift
210/// bound at 5 workspace-wide sites past the ★★ PRIME-DIRECTIVE
211/// ≥ 2 duplication threshold across two files:
212///
213/// * `tatara-reconciler::phase_machine` — 4 sites fed the local
214/// `const SHORT_RETRY: u64 = 5;` at handler branches that
215/// re-check a slow-converging condition (execing evaluator /
216/// running evaluator retry / attested-attestation retry /
217/// reconverging spec-hash check).
218/// * `tatara-pool-reconciler::controller_allocation` — 1 site fed
219/// a bare `5` literal at the Bind-arm patch-failure retry.
220///
221/// Post-lift each callsite reads
222/// `tatara_process::requeue::short_retry()`. Peer to [`tick`] and
223/// [`heartbeat`] on the "named requeue intent" axis; each future
224/// consumer that observes a transient error and wants a short
225/// bounded retry lands as ONE new callsite here instead of another
226/// hand-authored `after_secs(5)` chain.
227#[must_use]
228pub fn short_retry() -> Action {
229 after_secs(SHORT_RETRY_SECONDS)
230}
231
232/// The "steady-state heartbeat" requeue action —
233/// `after_secs(HEARTBEAT_SECONDS)`.
234///
235/// The ONE substrate owner of the "default periodic reconcile
236/// heartbeat" requeue intent. Pre-lift bound at 11 workspace-wide
237/// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold
238/// across three files:
239///
240/// * `tatara-reconciler::phase_machine` — 7 sites fed the local
241/// `const HEARTBEAT: u64 = 30;` at handler tails that maintain
242/// steady-state re-observation (forking / execing / running /
243/// attested / reconverging / releasing / failed).
244/// * `tatara-reconciler::controller` — 2 sites fed a bare `30`
245/// literal at the reconcile-error sink and the `error_policy`
246/// return (both semantically "back off to the default
247/// heartbeat after an error").
248/// * `tatara-reconciler::table_controller` — 2 sites fed a bare
249/// `30` literal at the ProcessTable heartbeat return and its
250/// `error_policy` return.
251///
252/// Post-lift each callsite reads
253/// `tatara_process::requeue::heartbeat()`. Peer to [`tick`] and
254/// [`short_retry`] on the "named requeue intent" axis; each future
255/// controller that wants the workspace-canonical heartbeat cadence
256/// lands as ONE new callsite here instead of another hand-authored
257/// `after_secs(30)` chain, and a future workspace-wide heartbeat
258/// re-tuning (a config-slot override, a per-controller adaptive
259/// cadence) lands at THIS primitive rather than as a scatter-
260/// gather sweep across every reconciler.
261#[must_use]
262pub fn heartbeat() -> Action {
263 after_secs(HEARTBEAT_SECONDS)
264}
265
266/// The "back off after a reconcile error" requeue action —
267/// `after_secs(ERROR_BACKOFF_SECONDS)`.
268///
269/// The ONE substrate owner of the "re-enqueue after an
270/// `error_policy` sink caught a reconcile failure" requeue intent.
271/// Pre-lift bound at 2 workspace-wide `error_policy` return-sites
272/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across
273/// one crate:
274///
275/// * `tatara-pool-reconciler::controller_pool::error_policy` — the
276/// `EphemeralPool` reconciler's failure sink; a bare `15`
277/// literal fed [`after_secs`].
278/// * `tatara-pool-reconciler::controller_allocation::error_policy`
279/// — the `EphemeralAllocation` reconciler's failure sink; a bare
280/// `15` literal fed [`after_secs`].
281///
282/// Post-lift each callsite reads
283/// `tatara_process::requeue::error_backoff()`. Peer to [`tick`],
284/// [`short_retry`], and [`heartbeat`] on the "named requeue intent"
285/// axis; the pool-reconciler's `error_policy` deliberately chose a
286/// backoff shorter than its own `heartbeat_seconds` slot (see
287/// [`ERROR_BACKOFF_SECONDS`] for the "pool-failure signals stale
288/// desired-count / claim" rationale). `tatara-reconciler`'s two
289/// `error_policy` sinks fall through to [`heartbeat`] directly
290/// because a Process-reconcile failure holds its own retry budget
291/// in the FSM handlers themselves — so the two intents are named
292/// separately on purpose rather than folded.
293///
294/// A future normalization on the error-backoff intent alone (an
295/// exponential-backoff overlay bounded by `HEARTBEAT_SECONDS`, a
296/// per-reconciler injectable ceiling for a mis-configured pool, a
297/// jitter overlay to avoid a thundering herd of pool + allocation
298/// error retries) lands at THIS ONE substrate primitive and both
299/// downstream `error_policy` sites inherit the upgrade
300/// mechanically. No per-site edit at either of the 2 listed
301/// callers or at future consumers (a new pool-adjacent controller,
302/// a new failure-sink axis).
303///
304/// Theory anchor: THEORY.md §VI.1 (generation over composition —
305/// the intent recurred at 2 hand-authored production sites past
306/// the PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
307/// ONE owner here). THEORY.md §II.1 invariant 5 (composition
308/// preserves proofs — the intent → second-count mapping lives at
309/// ONE typed algebra projection; a regression that drifted the
310/// second-count would fail at the byte-shape pin below rather than
311/// as silent operator-visible cadence skew across both downstream
312/// `error_policy` return-sites).
313#[must_use]
314pub fn error_backoff() -> Action {
315 after_secs(ERROR_BACKOFF_SECONDS)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 // ─── after_secs substrate pins ─────────────────────────────────────
323 //
324 // Bind [`after_secs`] at fail-before-pass-after granularity so a
325 // regression that swapped the unit (`from_millis` instead of
326 // `from_secs`), flipped the requeue constructor (`Action::await_change`
327 // instead of `Action::requeue`), dropped the return through a stale
328 // hardcoded fallback, or reshaped the return form (an owned `Duration`
329 // instead of the `Action`) surfaces HERE rather than as silent
330 // operator-visible drift at the 35 downstream consumer sites across
331 // both active reconciler crates.
332
333 #[test]
334 fn after_secs_returns_action_carrying_the_second_count_duration() {
335 // Primary shape asserted end-to-end: the returned Action carries
336 // the exact `Duration::from_secs(secs)` value on its requeue slot.
337 // `Action` doesn't expose its inner Duration as a public getter,
338 // so verify the shape via `Debug` — `Action::requeue(Duration::
339 // from_secs(30))` formats with `30s` somewhere in the debug output;
340 // a regression that unit-swapped to `from_millis` would show `30ms`.
341 let debug = format!("{:?}", after_secs(30));
342 assert!(
343 debug.contains("30s") || debug.contains("30 s") || debug.contains("30.0s"),
344 "after_secs(30) debug {debug:?} must mention 30s — a regression that swapped the unit would drift here"
345 );
346 }
347
348 #[test]
349 fn after_secs_zero_returns_action_that_requeues_immediately() {
350 // Boundary corner: `secs = 0` yields `Action::requeue(Duration::
351 // ZERO)`, the "requeue this reconciliation immediately" shape
352 // kube-runtime treats as a tight re-tick. A regression that
353 // clamped the zero arm to a minimum (a "no requeue faster than
354 // 1s" clamp) would silently delay every tight-loop consumer that
355 // legitimately wants an immediate re-tick — the signal-consumed
356 // arm in `controller.rs`, a pending-phase check that just
357 // observed a wire update.
358 let debug = format!("{:?}", after_secs(0));
359 assert!(
360 debug.contains("0s") || debug.contains("0 s") || debug.contains("0ns"),
361 "after_secs(0) debug {debug:?} must reflect a zero requeue duration — a regression that clamped to a minimum would drift here"
362 );
363 }
364
365 #[test]
366 fn after_secs_matches_hand_authored_pre_lift_chain_shape() {
367 // Byte-identical parity with the pre-lift `Action::requeue(
368 // Duration::from_secs(N))` block that all 35 hand-authored
369 // callsites restated verbatim, swept across the representative
370 // second-counts every pre-lift consumer used:
371 //
372 // 1 → deletion-preempt + signal re-poll (`controller.rs`)
373 // 5 → bind-retry (`controller_allocation.rs`)
374 // 15 → cluster fallback (`controller_pool` / `controller_allocation`)
375 // 30 → reconcile-error + table-controller (`controller.rs`,
376 // `table_controller.rs`)
377 // 60 → `TICK_RETRY` / `HEARTBEAT` scale (`phase_machine.rs`)
378 // 3600 → hour-scale sanity check
379 //
380 // `Action` doesn't derive `PartialEq`, so parity is asserted via
381 // `Debug` output — both blocks build the exact same requeue
382 // Action, so their debug reps must string-equal.
383 for secs in [1_u64, 5, 15, 30, 60, 3_600] {
384 let composed = format!("{:?}", after_secs(secs));
385 let hand_authored = format!("{:?}", Action::requeue(Duration::from_secs(secs)));
386 assert_eq!(
387 composed, hand_authored,
388 "after_secs({secs}) debug {composed:?} must match hand-authored Action::requeue(Duration::from_secs({secs})) debug {hand_authored:?} — a regression that reshaped either link would drift here"
389 );
390 }
391 }
392
393 #[test]
394 fn after_secs_composes_at_reconcile_return_position() {
395 // The canonical consumer shape end-to-end: a `reconcile` fn
396 // returns `Ok(after_secs(N))` where the caller expects a
397 // `Result<Action, _>` back. A regression that returned a
398 // different type (an owned `Duration`, a `Result` wrapper,
399 // an `Option<Action>`) would fail to type-check at every
400 // consumer. Pin the return shape by explicitly annotating
401 // the `Ok` arm so a regression that widened the return
402 // surfaces HERE rather than as a workspace-wide type error.
403 let out: Result<Action, ()> = Ok(after_secs(5));
404 assert!(out.is_ok());
405 }
406
407 #[test]
408 fn after_secs_composes_at_error_policy_return_position() {
409 // Peer to the reconcile-return shape: `error_policy` returns
410 // bare `Action` (no `Result` wrapper). Pin that shape too so a
411 // regression that changed the return to `Result<Action, _>`
412 // would surface HERE rather than as a workspace-wide type
413 // error at every `Controller::run(...).error_policy(...)`
414 // callsite.
415 let _out: Action = after_secs(30);
416 }
417
418 #[test]
419 fn after_secs_accepts_config_slot_typed_as_u64() {
420 // The `ctx.config.heartbeat_seconds` slot and every
421 // `TICK_RETRY` / `HEARTBEAT` / `SHORT_RETRY` module-level
422 // `pub const N: u64` binding is typed as `u64`; the primitive
423 // must accept those without a cast. A regression that
424 // narrowed the parameter to `u32` or widened it to `i64`
425 // would break either the `phase_machine.rs` const-fed
426 // callsites or the `controller.rs` config-fed slot at the
427 // migration boundary.
428 let heartbeat_seconds: u64 = 60;
429 let _out: Action = after_secs(heartbeat_seconds);
430 }
431
432 // ─── Named requeue-intent helpers ───────────────────────────────
433 //
434 // Fail-before-pass-after pins for [`tick`], [`short_retry`], and
435 // [`heartbeat`]. Each helper binds ONE named requeue intent to
436 // its second-count budget through [`after_secs`]. The pins here
437 // catch three regression axes at the substrate boundary:
438 //
439 // 1. The intent → second-count binding drifts (e.g. `heartbeat`
440 // shifts from 30s to 60s under a partial re-tuning), silently
441 // changing the reconcile cadence across every downstream
442 // consumer.
443 // 2. A helper is retargeted onto a different underlying primitive
444 // (`Action::await_change` instead of `Action::requeue`) or
445 // unit (`from_millis` instead of `from_secs`), silently
446 // inverting or scale-shifting the requeue semantics.
447 // 3. The public second-count constant and the helper drift out
448 // of lockstep (the helper still returns 30s but the const
449 // reports 60s, misleading any consumer that reads the const
450 // to build its own tuned requeue).
451
452 #[test]
453 fn tick_binds_to_one_second_and_matches_pre_lift_hand_authored_chain() {
454 // The `TICK_RETRY = 1` local const the pre-lift 10
455 // `phase_machine.rs` sites + the bare `1` literal the pre-
456 // lift 2 `controller.rs` sites walked, both now bound at ONE
457 // owner here. A drift would silently change the tick cadence
458 // across all 12 downstream sites.
459 assert_eq!(TICK_SECONDS, 1);
460 let composed = format!("{:?}", tick());
461 let hand_authored = format!("{:?}", after_secs(1));
462 assert_eq!(
463 composed, hand_authored,
464 "tick() must byte-shape-match after_secs(1); the intent → second-count binding drifted",
465 );
466 }
467
468 #[test]
469 fn short_retry_binds_to_five_seconds_and_matches_pre_lift_hand_authored_chain() {
470 // The `SHORT_RETRY = 5` local const the pre-lift 4
471 // `phase_machine.rs` sites + the bare `5` literal the pre-
472 // lift 1 `controller_allocation.rs` bind-retry site walked,
473 // both now bound at ONE owner here.
474 assert_eq!(SHORT_RETRY_SECONDS, 5);
475 let composed = format!("{:?}", short_retry());
476 let hand_authored = format!("{:?}", after_secs(5));
477 assert_eq!(
478 composed, hand_authored,
479 "short_retry() must byte-shape-match after_secs(5); the intent → second-count binding drifted",
480 );
481 }
482
483 #[test]
484 fn heartbeat_binds_to_thirty_seconds_and_matches_pre_lift_hand_authored_chain() {
485 // The `HEARTBEAT = 30` local const the pre-lift 7
486 // `phase_machine.rs` sites + the bare `30` literal the pre-
487 // lift 4 `controller.rs` / `table_controller.rs` sites
488 // walked, all now bound at ONE owner here. A drift would
489 // silently change the heartbeat cadence across all 11
490 // downstream sites, potentially exceeding a per-cluster
491 // reconcile-rate budget or delaying steady-state
492 // re-observation past a control-loop deadline.
493 assert_eq!(HEARTBEAT_SECONDS, 30);
494 let composed = format!("{:?}", heartbeat());
495 let hand_authored = format!("{:?}", after_secs(30));
496 assert_eq!(
497 composed, hand_authored,
498 "heartbeat() must byte-shape-match after_secs(30); the intent → second-count binding drifted",
499 );
500 }
501
502 #[test]
503 fn error_backoff_binds_to_fifteen_seconds_and_matches_pre_lift_hand_authored_chain() {
504 // The bare `15` literal both `tatara-pool-reconciler::
505 // controller_pool::error_policy` and `tatara-pool-
506 // reconciler::controller_allocation::error_policy` fed
507 // into `after_secs` pre-lift, now bound at ONE owner
508 // here. A drift would silently change the pool + allocation
509 // error-backoff cadence across both downstream sites,
510 // either racing the API server on a genuinely stuck pool
511 // (if shortened) or delaying observation of a stale
512 // desired-count / claim past the operator's expectation
513 // (if lengthened).
514 assert_eq!(ERROR_BACKOFF_SECONDS, 15);
515 let composed = format!("{:?}", error_backoff());
516 let hand_authored = format!("{:?}", after_secs(15));
517 assert_eq!(
518 composed, hand_authored,
519 "error_backoff() must byte-shape-match after_secs(15); the intent → second-count binding drifted",
520 );
521 }
522
523 #[test]
524 fn named_requeue_intents_compose_at_reconcile_return_position() {
525 // Every helper returns `Action` and composes at the
526 // canonical `Result<Action, _>` return position every
527 // `reconcile` fn signature expects. A regression that
528 // widened any helper's return (an `Option<Action>` sink,
529 // a `Result` wrapper) would fail to type-check here rather
530 // than at the 28 downstream migration sites.
531 let _tick_ok: Result<Action, ()> = Ok(tick());
532 let _short_retry_ok: Result<Action, ()> = Ok(short_retry());
533 let _heartbeat_ok: Result<Action, ()> = Ok(heartbeat());
534 let _error_backoff_ok: Result<Action, ()> = Ok(error_backoff());
535 }
536
537 #[test]
538 fn named_requeue_intents_compose_at_error_policy_return_position() {
539 // Peer to the reconcile-return shape: `error_policy` returns
540 // bare `Action` (no `Result` wrapper). `heartbeat()` is the
541 // canonical error-policy return for both `controller.rs` and
542 // `table_controller.rs`; `error_backoff()` is the canonical
543 // error-policy return for both `controller_pool.rs` and
544 // `controller_allocation.rs`. Pin the shape so a regression
545 // that added a wrapper surfaces HERE rather than at the four
546 // `error_policy` sites.
547 let _tick: Action = tick();
548 let _short_retry: Action = short_retry();
549 let _heartbeat: Action = heartbeat();
550 let _error_backoff: Action = error_backoff();
551 }
552
553 #[test]
554 fn named_requeue_intents_project_distinct_second_counts() {
555 // Cross-intent coherence pin: the four named intents must
556 // resolve to four distinct second-counts. A regression that
557 // collapsed two intents onto the same constant (e.g.
558 // `TICK_SECONDS == SHORT_RETRY_SECONDS` after an over-eager
559 // "unify budgets" refactor, or `ERROR_BACKOFF_SECONDS ==
560 // HEARTBEAT_SECONDS` after folding the pool-reconciler's
561 // error sink into the process-reconciler's) would fold a
562 // semantic distinction into a numeric one, and the
563 // downstream sites would silently lose the intent name's
564 // meaning even though the helpers still compile.
565 assert_ne!(TICK_SECONDS, SHORT_RETRY_SECONDS);
566 assert_ne!(TICK_SECONDS, ERROR_BACKOFF_SECONDS);
567 assert_ne!(TICK_SECONDS, HEARTBEAT_SECONDS);
568 assert_ne!(SHORT_RETRY_SECONDS, ERROR_BACKOFF_SECONDS);
569 assert_ne!(SHORT_RETRY_SECONDS, HEARTBEAT_SECONDS);
570 assert_ne!(ERROR_BACKOFF_SECONDS, HEARTBEAT_SECONDS);
571 assert!(
572 TICK_SECONDS < SHORT_RETRY_SECONDS
573 && SHORT_RETRY_SECONDS < ERROR_BACKOFF_SECONDS
574 && ERROR_BACKOFF_SECONDS < HEARTBEAT_SECONDS,
575 "named requeue intents must project onto strictly increasing second-counts (tick < short_retry < error_backoff < heartbeat); a regression that flipped the ordering would silently invert the retry-cadence hierarchy",
576 );
577 }
578}