Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
cc61e9d
perf(sessions): read projection queue heads once per window
ScriptedAlchemy Nov 8, 2026
e1874e3
style(sessions): format queue-head window regression
ScriptedAlchemy Nov 8, 2026
7f8147b
perf(sessions): avoid idle projection writer transactions
ScriptedAlchemy Nov 8, 2026
d265e31
perf(sessions): skip terminal retries during restart re-arm
ScriptedAlchemy Nov 8, 2026
7322c88
style(sessions): format projection queue regression
ScriptedAlchemy Nov 8, 2026
35da211
fix(sessions): avoid idle retry-rearm writer transactions
ScriptedAlchemy Nov 8, 2026
5466c90
perf(sessions): avoid terminal-only projection writer leases
ScriptedAlchemy Nov 8, 2026
1c08ac0
perf(sessions): skip deferred-only projection writer polls
ScriptedAlchemy Nov 8, 2026
6f32ff2
perf(sessions): honor deferred sequence head without writer
ScriptedAlchemy Nov 8, 2026
71195bb
perf(sessions): avoid writer leases during projection rebuilds
ScriptedAlchemy Nov 8, 2026
348d92f
style(sessions): apply hosted Bazel rustfmt corrections
ScriptedAlchemy Nov 8, 2026
f7e4a6e
fix(sessions): preserve queue index plans under QPSG
ScriptedAlchemy Nov 8, 2026
aadc35a
style(sessions): format projection index import
ScriptedAlchemy Nov 8, 2026
c1d1f67
style(sessions): satisfy Bazel rustfmt import ordering
ScriptedAlchemy Nov 8, 2026
5fa0250
fix(sessions): clone queued observation id in restart assertion
ScriptedAlchemy Nov 8, 2026
b8e1c38
fix(projection): register strict queue index authority
ScriptedAlchemy Nov 8, 2026
73b7422
fix(sessions): validate queued-head partial index predicate
ScriptedAlchemy Nov 8, 2026
4700f30
style(sessions): normalize projection regression spacing
ScriptedAlchemy Nov 8, 2026
ba5f862
fix(storage): validate the complete active queue index definition
ScriptedAlchemy Nov 8, 2026
16a4266
test(automation): bind scheduler fixtures to backend identities
ScriptedAlchemy Nov 8, 2026
935c8c7
perf(sessions): avoid rearming already-due projection retries
ScriptedAlchemy Nov 8, 2026
ca73d62
style(sessions): apply hosted Rust 2024 formatting
ScriptedAlchemy Nov 8, 2026
35c00f3
fix(sessions): document stable QPSG retry cutoff plans
ScriptedAlchemy Nov 8, 2026
5632168
perf(sessions): update exact rows from each retry window
ScriptedAlchemy Nov 9, 2026
7708ab1
test(sessions): measure settled retry writer leases
ScriptedAlchemy Nov 9, 2026
e30bcbc
fix(sessions): preserve qualified projection audit ownership
ScriptedAlchemy Nov 9, 2026
84bf091
test(sessions): distinguish mount-local projection retry deadlines
ScriptedAlchemy Nov 9, 2026
1df969b
perf(sessions): move sparse retry scans off SQLite writer
ScriptedAlchemy Nov 9, 2026
fbec982
test(sessions): cover interrupted retry recovery across restart
ScriptedAlchemy Nov 9, 2026
b6d7a9b
fix(sessions): bound retry recovery by observation sequence
ScriptedAlchemy Nov 9, 2026
c19037a
style(sessions): format retry recovery restart assertion
ScriptedAlchemy Nov 9, 2026
46a16e6
fix(sessions): freeze retry eligibility across rearm windows
ScriptedAlchemy Nov 9, 2026
7dd565d
simplify(sessions): use the frozen retry snapshot as the scan bound
ScriptedAlchemy Nov 9, 2026
ebe7739
fix(sessions): release retry snapshot before batched writes
ScriptedAlchemy Nov 9, 2026
f0b5e5c
fix(sessions): rearm retry backlogs beyond staging memory
ScriptedAlchemy Nov 9, 2026
a144ac6
fix(sessions): refuse unbounded retry staging disk spill
ScriptedAlchemy Nov 9, 2026
09a2762
fix(sessions): rearm earlier mounts one retry page at a time
ScriptedAlchemy Nov 9, 2026
6269188
fix(sessions): refuse incompatible projection retry queues
ScriptedAlchemy Nov 9, 2026
b7cd1af
fix(sessions): preserve the shipped retry queue on recovery
ScriptedAlchemy Nov 9, 2026
1c0af1e
perf(sessions): batch durable source presence reoffers
ScriptedAlchemy Nov 9, 2026
49edb8a
perf(sessions): avoid rearming retries that matured mid-recovery
ScriptedAlchemy Nov 9, 2026
d494ae8
style(sessions): format source and retry recovery regressions
ScriptedAlchemy Nov 9, 2026
5aa7854
test(sessions): measure SQLite statement pages on the writer
ScriptedAlchemy Nov 9, 2026
89bd943
style(sessions): format the matured retry guard
ScriptedAlchemy Nov 9, 2026
c3ddf73
fix(storage): count prepared reads and writes inside SQL transactions
ScriptedAlchemy Nov 9, 2026
ad5e8af
Merge master into fix/4756-bounded-projection-head-read
ScriptedAlchemy Nov 9, 2026
ddbcad6
perf(sessions): scope rewrite queue updates to source records
ScriptedAlchemy Nov 9, 2026
3e6aa1f
style(sessions): format the rewrite queue regression
ScriptedAlchemy Nov 9, 2026
81e71e5
perf(sessions): skip settled source rewrite retirements
ScriptedAlchemy Nov 9, 2026
6aeabb8
test(sessions): preserve queued revivals during rewrite cleanup
ScriptedAlchemy Nov 9, 2026
50d2822
perf(sessions): skip absent source-record retirement work
ScriptedAlchemy Nov 9, 2026
7125670
fix(sessions): resume source retirement in bounded writer pages
ScriptedAlchemy Nov 9, 2026
6aab511
style(sessions): format bounded rewrite regression counters
ScriptedAlchemy Nov 9, 2026
57ff967
perf(sessions): seek rewrite pages past settled survivors
ScriptedAlchemy Nov 9, 2026
a169d54
fix(sessions): preserve typed rollback result for rewrite seek
ScriptedAlchemy Nov 9, 2026
6a0abf5
perf(sessions): avoid second scan for single rewrite page
ScriptedAlchemy Nov 9, 2026
54e8f80
test(sessions): bound rewrite fixture setup and scope writer
ScriptedAlchemy Nov 9, 2026
656ae03
perf(sessions): skip repeated retained-prefix rewrite updates
ScriptedAlchemy Nov 9, 2026
9573a03
perf(sessions): avoid unchanged pending rewrite updates
ScriptedAlchemy Nov 9, 2026
cd99010
test(sessions): batch retry fixture writes without reducing coverage
ScriptedAlchemy Nov 9, 2026
6974281
perf(sessions): avoid empty source-rewrite tail transactions
ScriptedAlchemy Nov 9, 2026
010fcce
fix(sessions): discard source-rewrite lookahead without must-use warning
ScriptedAlchemy Nov 9, 2026
c47732c
perf(sessions): coalesce temporal resets in source retirement pages
ScriptedAlchemy Nov 10, 2026
721fdc8
test(sessions): bound projected restart fixture batches
ScriptedAlchemy Nov 10, 2026
0099fe6
style(sessions): apply pinned formatting to restart regression
ScriptedAlchemy Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/tracedecay-global-db/src/observation_projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(super) use apply::{
derive_projection_with_alias, retire_source_record, verify_workflow_effects,
};
pub use rebuild::{project_observation, project_queued_observations, rebuild_projection};
pub(crate) use schema::OBSERVATION_PROJECTION_PERFORMANCE_INDEX_SQL;
pub(crate) use schema::{ACTIVE_QUEUE_INDEX_SQL, OBSERVATION_PROJECTION_PERFORMANCE_INDEX_SQL};
pub(super) use schema::{
ensure_observation_projection_performance_indexes, ensure_observation_projection_schema,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,8 @@ pub(in super::super) async fn retire_source_record(
};
supersede_projected_message(conn, &message).await?;
}
request_session_temporal_reset(conn, observation_id).await?;
// Source-rewrite completion coalesces temporal reset requests for the
// retired page after all output ownership has been reconciled.
Ok(true)
}

Expand Down
938 changes: 876 additions & 62 deletions crates/tracedecay-global-db/src/observation_projection/rearm_tests.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
use crate::observation_projection::state::{
RetryRearmWindows, apply_rearm_projection_retry_candidates,
};
use crate::observation_projection::{RearmedProjectionRetries, rearm_queued_projection_retries};
use crate::tests::harness::{RegisteredGlobalDbHarness, writer_telemetry};
use tracedecay_runtime_core::db::engine::params;

use super::{FAR_FUTURE_MICROS, queue_state, seed_queue};

async fn retry_deadlines(harness: &RegisteredGlobalDbHarness) -> Vec<(i64, i64)> {
let snapshot = harness.registered.read_snapshot().await.unwrap();
let mut rows = snapshot
.query(
"SELECT observation_sequence, next_retry_at_micros
FROM projection_queue ORDER BY observation_sequence",
(),
)
.await
.unwrap();
let mut deadlines = Vec::new();
while let Some(row) = rows.next().await.unwrap() {
deadlines.push((row.get(0).unwrap(), row.get(1).unwrap()));
}
deadlines
}

#[asupersync::test]
async fn retry_rearm_resumes_committed_windows_after_failure_and_restart() {
let harness = RegisteredGlobalDbHarness::open("rearm-interrupted-recovery").await;
seed_queue(&harness, 9, 0).await;

// Two windows commit before the third fails. The failing window must
// roll back as a unit rather than lose its first observation's retry.
let transaction = harness.registered.begin_write_transaction().await.unwrap();
transaction
.execute(
"CREATE TRIGGER reject_retry_window
BEFORE UPDATE OF next_retry_at_micros ON projection_queue
WHEN OLD.observation_sequence = 6 AND NEW.next_retry_at_micros = 0
BEGIN SELECT RAISE(ABORT, 'injected retry window failure'); END",
(),
)
.await
.unwrap();
transaction.commit().await.unwrap();
assert!(
rearm_queued_projection_retries(harness.registered.runtime_database(), 2)
.await
.is_err(),
"the injected failure must interrupt recovery"
);

let interrupted = (1..=9_i64)
.map(|sequence| (sequence, if sequence <= 4 { 0 } else { FAR_FUTURE_MICROS }))
.collect::<Vec<_>>();
assert_eq!(retry_deadlines(&harness).await, interrupted);
assert_eq!(queue_state(&harness).await, (9, 5, 9));

let transaction = harness.registered.begin_write_transaction().await.unwrap();
transaction
.execute("DROP TRIGGER reject_retry_window", ())
.await
.unwrap();
transaction.commit().await.unwrap();
let (harness, _convergence) = harness.restart_for_daemon().await;
assert_eq!(retry_deadlines(&harness).await, interrupted);

// Make a replay of the committed prefix fail, even if it would write
// identical values. Row counts alone cannot detect redundant updates.
let transaction = harness.registered.begin_write_transaction().await.unwrap();
transaction
.execute(
"CREATE TRIGGER reject_committed_retry_replay
BEFORE UPDATE OF next_retry_at_micros ON projection_queue
WHEN OLD.observation_sequence <= 4
BEGIN SELECT RAISE(ABORT, 'committed retry prefix replayed'); END",
(),
)
.await
.unwrap();
transaction.commit().await.unwrap();

let before = writer_telemetry(&harness.registered).transactions;
assert_eq!(
rearm_queued_projection_retries(harness.registered.runtime_database(), 2)
.await
.unwrap(),
RearmedProjectionRetries {
rows: 5,
batches: 3,
},
"restart must resume only the five remaining rows in bounded windows"
);
let after = writer_telemetry(&harness.registered).transactions;
assert_eq!(
after.commands - before.commands,
3,
"the final productive window must not be followed by an empty writer lease"
);
assert_eq!(queue_state(&harness).await, (9, 0, 9));
assert_eq!(
retry_deadlines(&harness).await,
(1..=9_i64)
.map(|sequence| (sequence, 0))
.collect::<Vec<_>>()
);
for _ in 0..2 {
assert_eq!(
rearm_queued_projection_retries(harness.registered.runtime_database(), 2)
.await
.unwrap(),
RearmedProjectionRetries::default()
);
assert_eq!(writer_telemetry(&harness.registered).transactions, after);
}

let transaction = harness.registered.begin_write_transaction().await.unwrap();
transaction
.execute("DROP TRIGGER reject_committed_retry_replay", ())
.await
.unwrap();
transaction.commit().await.unwrap();
}

/// A recovery can spend a long time between its original clock read and later
/// candidate pages. Deadlines that matured meanwhile must not be rewritten:
/// they are already eligible for ordinary projection without restart rearm.
#[asupersync::test]
async fn retry_rearm_skips_deadlines_that_mature_during_recovery() {
let harness = RegisteredGlobalDbHarness::open("rearm-midwalk-deadlines").await;
seed_queue(&harness, 4, 0).await;
let database = harness.registered.runtime_database();

// Model a recovery that started before deadlines at 1_500_000 micros.
// The actual runtime clock is later; this does not rely on sleeping.
let mut windows = RetryRearmWindows::new(database, 4, 1_000_000)
.await
.unwrap();
let transaction = harness.registered.begin_write_transaction().await.unwrap();
assert_eq!(
transaction
.execute(
"UPDATE projection_queue SET next_retry_at_micros = 1500000
WHERE observation_sequence IN (1, 2)",
(),
)
.await
.unwrap(),
2
);
transaction.commit().await.unwrap();

let before = writer_telemetry(&harness.registered).transactions;
let candidates = windows
.next()
.await
.unwrap()
.expect("two still-deferred observations remain");
let selected: Vec<(i64, String, i64, i64)> = serde_json::from_str(&candidates).unwrap();
assert_eq!(selected.len(), 2, "matured deadlines need no write window");
assert!(selected.iter().all(|item| item.2 == FAR_FUTURE_MICROS));
assert_eq!(
apply_rearm_projection_retry_candidates(
database,
&candidates,
tracedecay_contracts::clock::now_micros().0,
&windows.mount,
)
.await
.unwrap(),
2
);
assert!(windows.next().await.unwrap().is_none());
assert_eq!(
retry_deadlines(&harness).await,
vec![(1, 1_500_000), (2, 1_500_000), (3, 0), (4, 0)]
);
let after = writer_telemetry(&harness.registered).transactions;
assert_eq!(after.commands - before.commands, 1);

// If *all* rows mature after the recovery cursor was established,
// the reader must finish without even acquiring an empty writer lease.
let transaction = harness.registered.begin_write_transaction().await.unwrap();
transaction
.execute(
"UPDATE projection_queue SET next_retry_at_micros = ?1
WHERE observation_sequence IN (3, 4)",
params![FAR_FUTURE_MICROS],
)
.await
.unwrap();
transaction.commit().await.unwrap();
let mut expired = RetryRearmWindows::new(database, 4, 1_000_000)
.await
.unwrap();
let transaction = harness.registered.begin_write_transaction().await.unwrap();
transaction
.execute(
"UPDATE projection_queue SET next_retry_at_micros = 1500000
WHERE observation_sequence IN (3, 4)",
(),
)
.await
.unwrap();
transaction.commit().await.unwrap();

let before_idle = writer_telemetry(&harness.registered).transactions;
assert!(expired.next().await.unwrap().is_none());
assert_eq!(
writer_telemetry(&harness.registered).transactions,
before_idle
);
assert_eq!(
rearm_queued_projection_retries(database, 4).await.unwrap(),
RearmedProjectionRetries::default()
);
assert_eq!(
writer_telemetry(&harness.registered).transactions,
before_idle
);
assert_eq!(
retry_deadlines(&harness).await,
vec![
(1, 1_500_000),
(2, 1_500_000),
(3, 1_500_000),
(4, 1_500_000)
]
);
}
Loading
Loading