review-wave-6 — the wave-6 gate passes, engine-mem.md flipped to stable
The born-pinned gate audited the mem engine against the contract and ADR-024: the conformance read clean in mechanism substance (queue state machine, scheduler collapse, outbox, streams, locks, clock, validation, tx overlay, wake substrate — incl. a full guard-order and panic-hygiene audit); the suite's third column read row-by-row and 25/25 green under the per-test current-thread flavor (24 engine-shared rows + mem's payload_too_large non-occurrence arm + the factory-shape test); three-way arithmetic equivalence green on all columns; the wasm32 acceptance items verified (compile-clean, tokio sync+time, no blocking, flavor enforced); the rider's two arms verified (Finding 1 discharged; Findings 2-3 stay parked for wave 7); workspace gates, taskgraph validate green. Three edge-domain fixes landed inline: the stream cursor read's i64::MAX overflow (Bound::Excluded), trim_to's i64::MAX horizon endpoint (Bound::Included), and the scheduler register path now rejecting a parseable interval whose first boundary overflows with InvalidSpec — the one engine-vs-engine divergence the read found, aligned into the class-1 window — plus a pinning test and scaffold- residue doc/comment cleanups (clock allows, tx per-stream wake comment, wakes/stream module docs). Recorded findings with dispositions in the task's Notes for the wave-7 pre-release pass: the multi-thread publish-to-wake ordering tolerance, the heartbeat/ save_offset/EventReceiver-try_recv ADR-023 §2 classification gaps, and the mem arithmetic being a suite-pinned transplant. Verified: cargo build; cargo test --workspace green (mem 101 lib + 25 suite, sqlite 191+25, pg 124+25+9 server-less, core 25, harness 3); clippy --all-targets -D warnings clean; fmt clean; taskgraph validate (61 tasks). Gate record: tasks/review-wave-6.md (Notes + Summary), implementation.md wave-6 row + review-rounds entry.
This commit is contained in:
1 parent
76c06e42a9
commit
7e8bfde12d
8 files changed
+282
-37
No files matched your search
@@ -15,9 +15,6 @@ use std::sync::Mutex;
|
||||
#[cfg(test)]
|
||||
use crate::state::Guard;
|
||||
|
||||
// Mechanism task code stamps every timestamp through this accessor —
|
||||
// scaffolding reads as dead until the wave-6 mechanism tasks wire it.
|
||||
#[allow(dead_code)]
|
||||
fn process_unix_now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -47,7 +44,6 @@ impl Clock {
|
||||
}
|
||||
|
||||
/// Unix-seconds "now" — the one timestamp source of the engine.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn unix_now(&self) -> i64 {
|
||||
#[cfg(test)]
|
||||
let frozen = { *self.frozen.guard() };
|
||||
|
||||
@@ -165,7 +165,14 @@ pub(crate) fn schedule(
|
||||
visibility_timeout_s: stamps.visibility_timeout_s,
|
||||
backoff_base_s: stamps.backoff_base_s,
|
||||
dead_letter_retention_s: stamps.dead_letter_retention_s,
|
||||
next_fire_at: now.saturating_add(interval),
|
||||
// A parseable interval whose first boundary overflows the
|
||||
// clock registers nothing: the SQLite register path
|
||||
// (substrate/queue_ops.rs) rejects the same spec with
|
||||
// InvalidSpec — one register-time semantics, engines
|
||||
// aligned (the tick's i64::MAX sentinel stays identical).
|
||||
next_fire_at: now
|
||||
.checked_add(interval)
|
||||
.ok_or_else(|| invalid_spec(&spec))?,
|
||||
};
|
||||
{
|
||||
let mut sched = state.schedules.guard();
|
||||
@@ -453,6 +460,28 @@ mod tests {
|
||||
assert_eq!(row.expires_s, Some(120));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn register_rejects_a_parseable_interval_whose_first_boundary_overflows() {
|
||||
let store = store();
|
||||
// Parseable (n · 1s fits i64) but now + interval overflows the
|
||||
// clock — the register path rejects with InvalidSpec, no row
|
||||
// stored (aligned with the SQLite register semantics; distinct
|
||||
// from the unparsable-too-large spec the parser refuses).
|
||||
let err = store
|
||||
.schedule(
|
||||
"overflow",
|
||||
"@every 9223372036854775807s",
|
||||
"q",
|
||||
serde_json::json!(1),
|
||||
ScheduleOpts::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::InvalidSpec { .. }), "{err:?}");
|
||||
let sched = store.state.schedules.guard();
|
||||
assert!(sched.rows.is_empty(), "nothing registered");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn register_rejects_bad_names_and_specs_pre_storage() {
|
||||
let store = store();
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
//! FIFO by monotone offset; keyed publish rides the overlay).
|
||||
//!
|
||||
//! - Publishes append to the per-stream log under its section guard:
|
||||
//! engine-assigned offsets (the engine-wide allocator — pg-bigserial
|
||||
//! shaped, monotone, immutable, never renumbered; a fresh stream's
|
||||
//! first event does not carry offset 1, the ADR-015 §4 note), keys
|
||||
//! engine-assigned offsets (the engine-wide allocator — monotone,
|
||||
//! immutable, never renumbered; the concrete numbering is engine-
|
||||
//! internal and never compared cross-engine — ADR-015 §4: mem's
|
||||
//! fresh-engine count starts at 1, the SQL engines' counters may
|
||||
//! not), keys
|
||||
//! are carried metadata (a present key must be non-empty —
|
||||
//! `InvalidName`, ADR-015 §1), and the commit boundary fires one
|
||||
//! wake per event on the stream's engine-internal wake channel.
|
||||
@@ -30,6 +32,7 @@
|
||||
//! (ADR-021 §5).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::Bound;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
@@ -72,7 +75,7 @@ pub(crate) fn read_events(s: &StreamState, stream: &str, from: i64, limit: i64)
|
||||
s.logs
|
||||
.get(stream)
|
||||
.map(|log| {
|
||||
log.range((from + 1)..)
|
||||
log.range((Bound::Excluded(from), Bound::Unbounded))
|
||||
.take(limit.max(0) as usize)
|
||||
.map(|(_, row)| row.clone())
|
||||
.collect()
|
||||
@@ -107,8 +110,10 @@ pub(crate) fn trim_events(s: &mut StreamState, stream: &str, horizon: i64) -> i6
|
||||
let Some(log) = s.logs.get_mut(stream) else {
|
||||
return 0;
|
||||
};
|
||||
let horizon_next = horizon.saturating_add(1);
|
||||
let removed: Vec<i64> = log.range(..horizon_next).map(|(k, _)| *k).collect();
|
||||
let removed: Vec<i64> = log
|
||||
.range((Bound::Unbounded, Bound::Included(horizon)))
|
||||
.map(|(k, _)| *k)
|
||||
.collect();
|
||||
let count = removed.len();
|
||||
for offset in removed {
|
||||
log.remove(&offset);
|
||||
|
||||
@@ -443,8 +443,10 @@ fn merge_and_wake(state: Arc<State>, wakes: Arc<WakeBus>, mut overlay: Overlay)
|
||||
}
|
||||
}
|
||||
// Wake-at-commit: staged notifies in stage order (duplicates ride —
|
||||
// per-notify), plus one wake per staged event on its stream's
|
||||
// engine-internal feed (per event — never coalesced).
|
||||
// per-notify), plus one wake per staged stream on its engine-internal
|
||||
// feed (receivers drain by cursor, so one wake per stream is enough —
|
||||
// the feed is plumbing, not contract surface; never coalesced across
|
||||
// commits).
|
||||
let mut fired: VecDeque<String> = VecDeque::with_capacity(overlay.notifies.len());
|
||||
fired.extend(overlay.notifies.iter().cloned());
|
||||
for stream in overlay.events.keys() {
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//! commit order; coalescing is not applicable. The substrate realizes
|
||||
//! the shape structurally:
|
||||
//!
|
||||
//! - **Commit-ordered source**: a mechanism (notify now — streams and
|
||||
//! queue wakes arrive with their tasks) fires its commit's wakes
|
||||
//! - **Commit-ordered source**: a mechanism (notify, stream publishes,
|
||||
//! the tx commit-merge's staged notifies and publishes) fires its
|
||||
//! commit's wakes
|
||||
//! through [`WakeBus::fire_commit_wakes`], under one guard: the
|
||||
//! commit sequence bumps and the per-channel fanout completes as a
|
||||
//! single operation, so every subscriber's receipt order is exactly
|
||||
|
||||
Reference in new issue
Block a user