general review 004 (pre-release, whole workspace): one HIGH slot-poisoning fix, two contract-observable divergence fixes, robustness + doc corrections — all gates re-run green
- sqlite scheduler tick: a failed COMMIT (or ROLLBACK) left the connection released with a stuck-open transaction, wedging the writer slot forever — the one commit site missing the drop-+-reopen-replenish arm; tick now takes the conn by value with a poisoned disposition (drop + replenish), the commit-fault seam reaches the tick, and failed_tick_commit_replenishes_the_writer_slot pins the arm - pg save_offset_tx stored GREATEST($3,0) where the wave-8 gate's negative-seed fix (raw seed, ADR-023 §2 2026-10-11 annotation) covered only the auto-commit form; mem's merged in-tx read clamped a staged negative seed against a phantom zero; both fixed and the tx-form totality pinned by a new stamped suite leg (in_tx_reads_see_own_writes) green on all three columns vs the live harness server - pg: a failed forwarder register leaked the channel refcount forever (no receiver to drop it) — listen/spawn_bridge now unregister on the register error, mirroring the subscribe-failure arm; register doc corrected to put the failure disposition at the call site - pg forwarder: nine poisoned-mutex expect() panics converted to the mem crate's into_inner recovery idiom (no-expect-outside-tests rule) - core encode_payload: a deeply-nested Value aborted the process in serde_json's unguarded serializer recursion and the fuzz pin blessed storing bytes no decoder could read back; encode now rejects nesting past 127 containers (the empirically-verified decode limit) with typed Codec — decode-aligned boundary pins added to the fuzz invariant and value_type_tests, applied pre-publish within the open class-1 window - scheduler sleep deadlines: Instant + Duration overflow caps (MAX_SLEEP_S, tick-and-resleep) on all three engines for far-future boundaries; sqlite tick claim stamp now saturating_add - docs read true again: Error::Closed scope (stream try_recv arm), validation whitespace wording, sqlite lib.rs phantom durability knobs, pg lock.rs honest autocommit wording, mem tx/stream merge + wake- granularity wording; wakes.rs stale allow(dead_code) removed - tests: value_type_tests exercised its own map_err instead of the function under test (fixed); unwrap_or_default reference encodes panic with cause - docs/reviews/004-pre-release-general-review.md records nine fixed + sixteen parked findings (sqlite seam panic-safety armor, watcher post-death-subscribe hang + initial-stat self-kill, pg forwarder TOCTOU/hang/diagnosability, pg BIGINT server-side stamp overflow, mem growth/outbox notes, pg identifier-truncation doc note, smaller) and two false positives caught on verification; implementation.md review-rounds entry + core-contract backlog rows updated - gates: build, workspace tests (sqlite 194, core 27, suite legs new), full live pg column 127+25+9, clippy -D warnings, fmt, fuzz replay 6/6, wasm32 check, doc gate -D warnings — all green
This commit is contained in:
1 parent
5385f6bee3
commit
029f885aa1
24 files changed
+738
-103
No files matched your search
@@ -54,9 +54,11 @@ pub enum Error {
|
||||
LeadershipLost,
|
||||
/// An operation against a closed receiver/source.
|
||||
///
|
||||
/// The `try_recv`/`recv_timeout` form of wake close (never
|
||||
/// returned by `recv_timeout`'s timeout expiry, which yields
|
||||
/// `Ok(None)`); never a timeout signal.
|
||||
/// The closed arms of the wake receivers' non-`recv` forms
|
||||
/// (`try_recv`/`recv_timeout` — never
|
||||
/// `recv_timeout`'s timeout expiry, which yields `Ok(None)`) and
|
||||
/// the event receiver's `try_recv` on a `Closed`-caused stream
|
||||
/// receiver; never a timeout signal.
|
||||
#[error("the receiver or source is closed")]
|
||||
Closed,
|
||||
/// Payload serialization or deserialization failure.
|
||||
|
||||
+50
-3
@@ -23,13 +23,60 @@ use crate::Error;
|
||||
/// Fallible per ADR-023 §1: serialization failure is
|
||||
/// [`Error::Codec`](crate::Error::Codec), typed at this helper — no
|
||||
/// silent fallback form exists (an empty-byte store would decode to
|
||||
/// `Null`, silent data corruption). Unreachable for `serde_json::Value`
|
||||
/// in practice (its variants all serialize), pinned against an
|
||||
/// encoding posture change.
|
||||
/// `Null`, silent data corruption). Failures here are two-fold: a
|
||||
/// `Value` nested deeper than the depth limit (`PAYLOAD_DEPTH_LIMIT`)
|
||||
/// is rejected — the stored bytes could never be read back, and the
|
||||
/// serializer's own unguarded recursion would otherwise abort deep
|
||||
/// enough — and genuine serialization failure maps through the same
|
||||
/// typed arm.
|
||||
pub fn encode_payload(value: &serde_json::Value) -> Result<Vec<u8>, Error> {
|
||||
if value_depth(value) > PAYLOAD_DEPTH_LIMIT {
|
||||
return Err(Error::Codec(format!(
|
||||
"payload nesting exceeds the depth limit ({PAYLOAD_DEPTH_LIMIT})"
|
||||
)));
|
||||
}
|
||||
serde_json::to_vec(value).map_err(|e| Error::Codec(e.to_string()))
|
||||
}
|
||||
|
||||
/// The payload nesting cap: aligned with serde_json's deserialization
|
||||
/// recursion limit (a nested-open at container depth 128 fails to
|
||||
/// decode; depths through 127 read back). A `Value` nested deeper than
|
||||
/// the cap produces stored bytes no decoder can read — silent data
|
||||
/// loss at read time — and deep enough the (unguarded) serializer
|
||||
/// itself would overflow the stack abortably; the encode side refuses
|
||||
/// it instead.
|
||||
const PAYLOAD_DEPTH_LIMIT: usize = 127;
|
||||
|
||||
/// The maximum number of containers on any path through `value`
|
||||
/// (iterative, not recursive — a recursive walk would itself overflow
|
||||
/// the stack on exactly the deep inputs this guards against).
|
||||
fn value_depth(value: &serde_json::Value) -> usize {
|
||||
let mut max = 0;
|
||||
let mut stack: Vec<(&serde_json::Value, usize)> = vec![(value, 1)];
|
||||
while let Some((current, depth)) = stack.pop() {
|
||||
match current {
|
||||
serde_json::Value::Array(items) => {
|
||||
if depth > max {
|
||||
max = depth;
|
||||
}
|
||||
for item in items {
|
||||
stack.push((item, depth + 1));
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
||||
if depth > max {
|
||||
max = depth;
|
||||
}
|
||||
for item in map.values() {
|
||||
stack.push((item, depth + 1));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
max
|
||||
}
|
||||
|
||||
/// Decode stored payload bytes into a concrete target type.
|
||||
///
|
||||
/// Used by [`Job::payload_as`](crate::Job::payload_as) and
|
||||
|
||||
@@ -26,17 +26,19 @@ pub enum NameKind {
|
||||
/// rejected.
|
||||
Shared,
|
||||
/// Consumer-local identifiers — stream-consumer names, `owner` on
|
||||
/// `try_lock`, `worker_id` on claims. Non-empty only: a leading
|
||||
/// `__alkstore_` is legal (they are not a shared engine namespace,
|
||||
/// so there is nothing to collide with).
|
||||
/// `try_lock`, `worker_id` on claims. Non-empty (whitespace-only
|
||||
/// rejected like any empty): a leading `__alkstore_` is legal
|
||||
/// (they are not a shared engine namespace, so there is nothing to
|
||||
/// collide with).
|
||||
Local,
|
||||
}
|
||||
|
||||
/// Validate a name per its [`NameKind`].
|
||||
///
|
||||
/// - non-empty (after no trimming — whitespace-only counts as
|
||||
/// non-empty content but is nonetheless rejected below) →
|
||||
/// [`Error::InvalidName`](crate::Error::InvalidName) when empty
|
||||
/// - non-empty → [`Error::InvalidName`](crate::Error::InvalidName)
|
||||
/// when empty; the emptiness check is the raw string's (`""`
|
||||
/// rejects) with whitespace-only strings rejected by the same arm
|
||||
/// (they are never a meaningful name on any engine — see below)
|
||||
/// - `NameKind::Shared` additionally rejects names carrying
|
||||
/// [`RESERVED_PREFIX`] with
|
||||
/// [`Error::ReservedName`](crate::Error::ReservedName)
|
||||
|
||||
@@ -87,7 +87,10 @@ fn sample_event(payload: Vec<u8>) -> StreamEvent {
|
||||
fn payload_as_round_trips_json_value_through_job() {
|
||||
let v = json!({"id": 9, "nested": {"ok": [true, null, 1.5]}, "s": "ünï"});
|
||||
let bytes = crate::encode_payload(&v).unwrap_or_else(|e| panic!("encode failed: {e}"));
|
||||
assert_eq!(bytes, serde_json::to_vec(&v).unwrap_or_default());
|
||||
assert_eq!(
|
||||
bytes,
|
||||
serde_json::to_vec(&v).unwrap_or_else(|e| panic!("reference encode failed: {e}"))
|
||||
);
|
||||
let job = sample_job(bytes);
|
||||
let decoded: Value = job
|
||||
.payload_as()
|
||||
@@ -128,16 +131,54 @@ fn payload_as_undecodable_target_yields_codec() {
|
||||
Err(crate::Error::Codec(msg)) => assert!(!msg.is_empty()),
|
||||
other => panic!("expected Codec, got {other:?}"),
|
||||
}
|
||||
let r2: crate::Result<bool> =
|
||||
serde_json::from_slice(b"not json").map_err(|e| crate::Error::Codec(e.to_string()));
|
||||
let r2: crate::Result<bool> = {
|
||||
let undecodable = sample_job(b"not json".to_vec());
|
||||
undecodable.payload_as()
|
||||
};
|
||||
assert!(matches!(r2, Err(crate::Error::Codec(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_payload_rejects_nesting_below_the_decode_limit_as_typed_codec() {
|
||||
let boundary = nest_value(Value::Null, 127);
|
||||
assert!(
|
||||
crate::encode_payload(&boundary).is_ok(),
|
||||
"the deepest decodable nesting encodes and reads back"
|
||||
);
|
||||
let past = nest_value(Value::Null, 128);
|
||||
match crate::encode_payload(&past) {
|
||||
Err(crate::Error::Codec(msg)) => assert!(!msg.is_empty()),
|
||||
other => panic!("expected Codec on past-limit nesting, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn nest_value(inner: Value, depth: usize) -> Value {
|
||||
let mut v = inner;
|
||||
for _ in 0..depth {
|
||||
v = Value::Array(vec![v]);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_payload_boundary_nesting_round_trips() {
|
||||
let v = nest_value(Value::from(0u8), 127);
|
||||
let bytes = crate::encode_payload(&v).unwrap_or_else(|e| panic!("encode failed: {e}"));
|
||||
let job = sample_job(bytes);
|
||||
let back: Value = job
|
||||
.payload_as()
|
||||
.unwrap_or_else(|e| panic!("boundary decode failed: {e}"));
|
||||
assert_eq!(back, v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_payload_success_carries_exact_serialization() {
|
||||
let v = json!({"k": [1, 2, 3]});
|
||||
let bytes = crate::encode_payload(&v).unwrap_or_else(|e| panic!("encode failed: {e}"));
|
||||
assert_eq!(bytes, serde_json::to_vec(&v).unwrap_or_default());
|
||||
assert_eq!(
|
||||
bytes,
|
||||
serde_json::to_vec(&v).unwrap_or_else(|e| panic!("reference encode failed: {e}"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user