Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions crates/tracedecay/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ const MAX_CACHED_PROJECT_SERVERS: usize = 8;
const MAX_TRACKED_PROJECT_OPEN_TASKS: usize = MAX_CACHED_PROJECT_SERVERS;
const MAX_CACHED_PROJECT_OPEN_FAILURES: usize = 64;
const PROJECT_OPEN_REQUEST_DEADLINE: Duration = Duration::from_millis(500);

/// Instant a foreground request stops waiting for an open it has already claimed.
///
/// Measured from the claim, not from connection arrival. Route enrollment and
/// git discovery have their own bounds. Charging them to this deadline made a
/// restart's first request answer warming for a store whose open had already
/// recorded `reset_required`: the connection spent the budget before it joined
/// the watch, so the publication wait returned immediately and never read the
/// refusal.
fn project_open_publication_deadline(claimed_at: tokio::time::Instant) -> tokio::time::Instant {
claimed_at + PROJECT_OPEN_REQUEST_DEADLINE
}
/// One budget for every blocking repository probe a route resolution runs.
///
/// Route resolution reads the repository's topology, enrollment marker, and
Expand Down
3 changes: 2 additions & 1 deletion crates/tracedecay/src/daemon/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,9 +680,10 @@ impl DaemonEngine {
// warm-up runs. The open task remains tracked and continues in the
// background after this bounded wait expires.
let mut retry_init = handshake.allow_init;
let publication_deadline = tokio::time::Instant::now() + PROJECT_OPEN_REQUEST_DEADLINE;
loop {
let claim = Box::pin(self.begin_project_open(handshake.clone(), None)).await?;
let publication_deadline =
project_open_publication_deadline(tokio::time::Instant::now());
let result = match claim {
ProjectOpenTaskClaim::InFlight(state) => {
let recorded = state.clone();
Expand Down
17 changes: 9 additions & 8 deletions crates/tracedecay/src/daemon/project_open_orchestration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ pub(super) async fn wait_for_project_open_publication<Publication, Output>(
where
Publication: std::future::Future<Output = Result<Output>>,
{
// The bound is a plain deadline: a waiter resumed after it elapsed still
// needs one more await, `route_bound_project_server`, before its
// publication loop can read the route's terminal state, so an elapsed
// deadline preempts a failure that was already recorded and the caller
// would see warming for a route that is no longer opening. Callers repair
// that with `prefer_recorded_open_failure` against the claim's own watch
// channel instead of weakening the bound.
// The bound is a plain deadline, measured from the open claim. A waiter
// resumed after it elapsed may still be inside `route_bound_project_server`
// and would otherwise answer warming for a refusal already on the watch.
// Callers repair that with `prefer_recorded_open_failure` against the
// claim's own watch channel instead of weakening the bound.
hotpath::future!(
tokio::time::timeout_at(deadline, publication),
label = "daemon.project.open.publication_wait"
Expand Down Expand Up @@ -483,7 +481,6 @@ pub(super) async fn portable_project_server_for_request(
// warm-up runs. The open task remains tracked and continues in the
// background after this bounded wait expires.
let mut retry_init = handshake.allow_init;
let publication_deadline = tokio::time::Instant::now() + PROJECT_OPEN_REQUEST_DEADLINE;
loop {
let claim = Box::pin(begin_portable_project_open(
lifecycle.clone(),
Expand All @@ -499,6 +496,10 @@ pub(super) async fn portable_project_server_for_request(
project_open_attempts.clone(),
))
.await;
// The bound starts here, after the open is claimed. Starting it at
// connection arrival let route enrollment spend it, and the request
// then answered warming for a refusal already on the watch.
let publication_deadline = project_open_publication_deadline(tokio::time::Instant::now());
let result = match claim {
ProjectOpenTaskClaim::InFlight(state) => {
let recorded = state.clone();
Expand Down
18 changes: 7 additions & 11 deletions crates/tracedecay/src/daemon/tests/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2249,10 +2249,9 @@ async fn explicit_init_retries_after_joining_an_ordinary_missing_database_open()
let _database_scope =
enter_test_daemon_database_scope(&profile_root, "registered missing-db init retry");
let engine = test_daemon_engine_for_profile(&profile_root);
// Each request arms its publication bound on its first poll, before route
// enrollment resolves. Opening and migrating the profile database on that
// path would spend the whole bound before either request subscribes to the
// open below, so the runtime is warmed the way daemon bootstrap warms it.
// Profile open sits before the publication bound (the bound starts when
// the open is claimed). Warm it so the 150 ms join window below is spent
// on the controlled open, not on the first profile migration.
prewarm_test_profile_runtime(&engine.store_administration).await;
let ordinary_handshake = DaemonHandshake {
project_path: Some(project.clone()),
Expand All @@ -2264,13 +2263,10 @@ async fn explicit_init_retries_after_joining_an_ordinary_missing_database_open()
allow_init: true,
..ordinary_handshake.clone()
};
// The bound also covers everything a request does *before* it can join an
// open, route enrollment, git discovery, the registered layout, so a
// first-touch request can spend the whole bound before it ever subscribes
// to the open below and would then mint its own. One ordinary request
// ahead of the fixture resolves that path for this exact route, and its
// refusal is the same missing-index failure the joined waiter classifies
// later.
// One ordinary request ahead of the fixture resolves this exact route,
// and its refusal is the same missing-index failure the joined waiter
// classifies later. The publication bound no longer includes that
// enrollment, so the request can subscribe before the bound is spent.
// It is retried the way a client retries the warming hint, so the route is
// left with no open of its own before the fixture takes it over.
let warmup_give_up = tokio::time::Instant::now() + std::time::Duration::from_secs(30);
Expand Down
Loading