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
41 changes: 33 additions & 8 deletions crates/tracedecay/src/daemon/connection_serving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,10 +1277,17 @@ fn serve_broker_socket_client_inner(
&first_request,
|| {
Box::pin(async {
Ok(engine
.cached_project_server(&handshake)
.await?
.is_some_and(|server| server.doctor_report_ready()))
if let Some(server) = engine.cached_project_server(&handshake).await? {
return Ok(server.doctor_report_ready());
}
// Only the project owner publishes the report, so a
// Doctor asking about an unopened project admits
// and opens it the way an initialize would.
Box::pin(
engine.schedule_project_server_warmup(handshake.clone(), None),
)
.await?;
Ok(false)
})
},
))
Expand Down Expand Up @@ -1444,7 +1451,7 @@ fn serve_broker_socket_client_inner(
{
Box::pin(engine.schedule_project_server_warmup(
handshake.clone(),
request.clone(),
Some(request.clone()),
))
.await
.err()
Expand Down Expand Up @@ -1795,14 +1802,32 @@ pub(super) async fn serve_windows_broker_client_with_class_and_invocation(
|| async {
let (canonical_project_path, _) =
project_route_for_handshake(&handshake, store_administration.owner_home()?)?;
Ok(Box::pin(portable_cached_project_server(
if let Some(server) = Box::pin(portable_cached_project_server(
&store_administration,
&canonical_project_path,
&handshake,
ProjectServerRequirement::Core,
))
.await?
.is_some_and(|server| server.doctor_report_ready()))
{
return Ok(server.doctor_report_ready());
}
// Only the project owner publishes the report, so a Doctor asking
// about an unopened project admits and opens it the way an
// initialize would.
Box::pin(schedule_portable_project_server_warmup(
lifecycle.clone(),
store_administration.clone(),
Arc::clone(&project_open_gates),
invocation.clone(),
http_application_registry.clone(),
handshake.clone(),
None,
#[cfg(test)]
project_open_attempts.clone(),
))
.await?;
Ok(false)
},
))
.await?
Expand Down Expand Up @@ -1983,7 +2008,7 @@ pub(super) async fn serve_windows_broker_client_with_class_and_invocation(
invocation.clone(),
http_application_registry.clone(),
handshake.clone(),
request.clone(),
Some(request.clone()),
#[cfg(test)]
project_open_attempts.clone(),
))
Expand Down
6 changes: 3 additions & 3 deletions crates/tracedecay/src/daemon/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ impl DaemonEngine {
pub(super) async fn schedule_project_server_warmup(
&self,
handshake: DaemonHandshake,
initialize_request: JsonRpcRequest,
initialize_request: Option<JsonRpcRequest>,
) -> Result<()> {
self.schedule_project_server_warmup_inner(handshake, initialize_request)
.await
Expand All @@ -649,15 +649,15 @@ impl DaemonEngine {
fn schedule_project_server_warmup_inner(
&self,
handshake: DaemonHandshake,
initialize_request: JsonRpcRequest,
initialize_request: Option<JsonRpcRequest>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
// Erase the deeply nested future before it reaches the measured
// wrapper so every profiling feature can compute its layout.
Box::pin(async move {
if self.cached_project_server(&handshake).await?.is_some() {
return Ok(());
}
match Box::pin(self.begin_project_open(handshake, Some(initialize_request))).await? {
match Box::pin(self.begin_project_open(handshake, initialize_request)).await? {
ProjectOpenTaskClaim::InFlight(_) => Ok(()),
ProjectOpenTaskClaim::Failed(failure) => Err(failure.to_error()),
ProjectOpenTaskClaim::Saturated => Err(project_open_task_capacity_error()),
Expand Down
4 changes: 2 additions & 2 deletions crates/tracedecay/src/daemon/project_open_orchestration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ pub(super) async fn schedule_portable_project_server_warmup(
invocation: DaemonInvocationState,
http_application_registry: http_application::DaemonHttpApplicationRegistry,
handshake: DaemonHandshake,
initialize_request: JsonRpcRequest,
initialize_request: Option<JsonRpcRequest>,
#[cfg(test)] project_open_attempts: Option<Arc<AtomicUsize>>,
) -> Result<()> {
let (canonical_project_path, route) =
Expand All @@ -448,7 +448,7 @@ pub(super) async fn schedule_portable_project_server_warmup(
handshake,
canonical_project_path,
route,
Some(initialize_request),
initialize_request,
#[cfg(test)]
project_open_attempts,
))
Expand Down
6 changes: 3 additions & 3 deletions crates/tracedecay/src/daemon/tests/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3126,7 +3126,7 @@ async fn project_server_warmup_drops_lifecycle_activity_on_draining() {
client_identity,
..test_handshake_defaults()
};
let initialize_request = serde_json::from_value(json!({
let initialize_request: tracedecay_mcp::JsonRpcRequest = serde_json::from_value(json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
Expand All @@ -3148,7 +3148,7 @@ async fn project_server_warmup_drops_lifecycle_activity_on_draining() {
});
writer_held.notified().await;

Box::pin(engine.schedule_project_server_warmup(handshake, initialize_request))
Box::pin(engine.schedule_project_server_warmup(handshake, Some(initialize_request)))
.await
.expect("schedule project warmup");
engine.lifecycle.begin_draining();
Expand Down Expand Up @@ -3257,7 +3257,7 @@ async fn portable_project_warmup_rejects_after_shutdown_snapshot() {
super::super::DaemonInvocationState::default(),
super::super::http_application::DaemonHttpApplicationRegistry::default(),
handshake,
initialize_request,
Some(initialize_request),
Some(Arc::clone(&attempts)),
))
.await
Expand Down
19 changes: 15 additions & 4 deletions crates/tracedecay/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,10 +591,19 @@ async fn daemon_project_status(
Some(deadline),
)
.await?;
let warmed_up = tokio::time::Instant::now() >= warmup_deadline;
match daemon_runtime_status(runtime)? {
Some(status) => return Ok(Some(status)),
None if tokio::time::Instant::now() >= warmup_deadline => return Ok(None),
None => tokio::time::sleep(RUNTIME_TELEMETRY_POLL).await,
// The first Doctor call starts the project open, so a warming
// owner is polled like missing telemetry until the report lands.
Some(status)
if warmed_up
|| canonical_daemon_doctor_report(&status)?
!= CanonicalDoctorReport::Mounting =>
{
return Ok(Some(status));
}
None if warmed_up => return Ok(None),
Some(_) | None => tokio::time::sleep(RUNTIME_TELEMETRY_POLL).await,
}
}
}
Expand Down Expand Up @@ -1089,7 +1098,9 @@ async fn check_project_index_paths(
}
}
}
Err(error) => dc.fail(&format!("{key} could not be read: {error}")),
// A reset-required or still-mounting store is already reported
// as its own state; a read that could not run is not an issue.
Err(error) => dc.warn(&format!("{key} could not be read: {error}")),
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ fn refused_session_stores_serve_code_until_their_scoped_reset(refusal: &SessionS
!doctor_text.contains("Stalled"),
"doctor must not report project open stalled on a session store:\n{doctor_text}"
);
let (exit, report, stderr) = mounted_doctor_report(&home_path, &project_path);
let (exit, report, stderr) = run_doctor_json(&home_path, &project_path);
assert_eq!(
(exit, &report["daemon_findings"]["state"], &report["issues"]),
(Some(75), &json!("observed"), &json!(0)),
Expand Down Expand Up @@ -715,36 +715,49 @@ fn seed_cursor_identity_collision_refusal(db_path: &Path, project_id: &str) {
.expect("seed the refusal");
}

/// `tracedecay doctor --json` exit code, document, and stderr once the
/// canonical Doctor report of the project has mounted.
fn mounted_doctor_report(home: &Path, project: &Path) -> (Option<i32>, Value, String) {
let started = Instant::now();
loop {
let doctor = tracedecay_command_with_home(home)
.args(["doctor", "--json"])
.current_dir(project)
.stdin(Stdio::null())
.output()
.expect("run doctor");
let stderr = String::from_utf8_lossy(&doctor.stderr).into_owned();
let report: Value = serde_json::from_slice(&doctor.stdout).unwrap_or_else(|error| {
panic!("doctor --json printed no document ({error}):\n{stderr}")
});
if report["daemon_findings"]["state"] != "mounting" {
return (doctor.status.code(), report, stderr);
}
assert!(
started.elapsed() < SERVE_TIMEOUT,
"the canonical Doctor report never mounted within {SERVE_TIMEOUT:?}:\n{stderr}"
);
std::thread::sleep(Duration::from_millis(500));
}
/// One `tracedecay doctor --json` run: exit code, document, and stderr.
fn run_doctor_json(home: &Path, project: &Path) -> (Option<i32>, Value, String) {
let doctor = tracedecay_command_with_home(home)
.args(["doctor", "--json"])
.current_dir(project)
.stdin(Stdio::null())
.output()
.expect("run doctor");
let stderr = String::from_utf8_lossy(&doctor.stderr).into_owned();
let report: Value = serde_json::from_slice(&doctor.stdout)
.unwrap_or_else(|error| panic!("doctor --json printed no document ({error}):\n{stderr}"));
(doctor.status.code(), report, stderr)
}

/// A restarted daemon has no project open until something asks for one. A
/// lone `doctor` must open the registered project itself and report the
/// canonical findings, not a mount that never starts.
#[test]
fn doctor_alone_on_a_fresh_daemon_opens_the_project_and_reports_findings() {
let home = tempfile::TempDir::new().expect("isolated home");
let home_path = canonical_existing_path(home.path());
let project = tempfile::TempDir::new().expect("project");
let project_path = canonical_existing_path(project.path());

let mut daemon = spawn_tracedecay_daemon_with(&home_path, |_| {});
super::initialize_project(&home_path, &project_path, "cold-doctor");
daemon
.kill_and_wait()
.expect("stop the daemon that registered the project");

let mut daemon = spawn_tracedecay_daemon_with(&home_path, |_| {});
let (_, report, stderr) = run_doctor_json(&home_path, &project_path);
assert_eq!(
report["daemon_findings"]["state"], "observed",
"the first doctor on a fresh daemon must reach the canonical report:\n{stderr}"
);

let _ = daemon.kill_and_wait();
}

/// `tracedecay doctor --json` exit code and its ingest-coverage finding, once
/// the project runtime that owns the canonical report has mounted.
/// `tracedecay doctor --json` exit code and its ingest-coverage finding.
fn doctor_ingest_coverage(home: &Path, project: &Path) -> (Option<i32>, Value, String) {
let (exit, report, stderr) = mounted_doctor_report(home, project);
let (exit, report, stderr) = run_doctor_json(home, project);
let finding = report["daemon_findings"]["payload"]["entries"]
.as_array()
.into_iter()
Expand Down
Loading