fix(worker): bind protocol lifetime to controller endpoints and join startup observers
T-700 prerequisite: expose real method-channel closure and endpoint identity without introducing WorkerRef routing tokens or catalog liveness mirrors. Delay command observer creation until fallible head/recovery/runtime writes finish; abort and join it on socket startup failure. Validation: cargo test -p worker (909 tests including integration targets); focused controller startup ordering regression; git diff --check HEAD. Full Runtime/Server validation and MR publication follow after the dependent T-700 implementation is clean.
This commit is contained in:
@@ -64,6 +64,20 @@ pub struct WorkerHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WorkerHandle {
|
impl WorkerHandle {
|
||||||
|
/// Transport closure belongs to this Controller endpoint, not a WorkerRef.
|
||||||
|
pub fn protocol_is_closed(&self) -> bool {
|
||||||
|
self.method_tx.is_closed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn protocol_closed(&self) {
|
||||||
|
self.method_tx.closed().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare actual endpoints when an execution-bound transport is dispatched.
|
||||||
|
pub fn same_controller(&self, other: &Self) -> bool {
|
||||||
|
self.method_tx.same_channel(&other.method_tx)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send(&self, method: Method) -> Result<(), mpsc::error::SendError<Method>> {
|
pub async fn send(&self, method: Method) -> Result<(), mpsc::error::SendError<Method>> {
|
||||||
// Reject known-busy sends before channel dispatch, including maintenance
|
// Reject known-busy sends before channel dispatch, including maintenance
|
||||||
// that cannot receive methods until it finishes. The controller checks
|
// that cannot receive methods until it finishes. The controller checks
|
||||||
@@ -873,9 +887,6 @@ impl WorkerController {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let command_observer = fs_for_view
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|session| wire_workdir_command_events(session, &in_flight));
|
|
||||||
|
|
||||||
// Intake role Workers self-terminate only after a successful
|
// Intake role Workers self-terminate only after a successful
|
||||||
// TicketIntakeReady turn has fully settled back to Idle. The request
|
// TicketIntakeReady turn has fully settled back to Idle. The request
|
||||||
@@ -929,8 +940,8 @@ impl WorkerController {
|
|||||||
protocol::ContextTokenSource::Measured,
|
protocol::ContextTokenSource::Measured,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if let Some(fs_for_view) = fs_for_view {
|
if let Some(fs_for_view) = fs_for_view.as_ref() {
|
||||||
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view));
|
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view.clone()));
|
||||||
}
|
}
|
||||||
runtime_dir.write_manifest(&manifest_toml).await?;
|
runtime_dir.write_manifest(&manifest_toml).await?;
|
||||||
runtime_dir.write_status(&shared_state).await?;
|
runtime_dir.write_status(&shared_state).await?;
|
||||||
@@ -952,8 +963,23 @@ impl WorkerController {
|
|||||||
pending_activations,
|
pending_activations,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Do not start an observer before fallible head/recovery/runtime-file
|
||||||
|
// initialization. The only remaining fallible step is socket startup,
|
||||||
|
// whose error path must abort AND join this task before returning.
|
||||||
|
let command_observer = fs_for_view
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|session| wire_workdir_command_events(session, &in_flight));
|
||||||
let socket_server = match transport {
|
let socket_server = match transport {
|
||||||
WorkerControllerTransport::UnixSocket => Some(SocketServer::start(&handle).await?),
|
WorkerControllerTransport::UnixSocket => match SocketServer::start(&handle).await {
|
||||||
|
Ok(server) => Some(server),
|
||||||
|
Err(error) => {
|
||||||
|
if let Some(observer) = command_observer {
|
||||||
|
observer.abort();
|
||||||
|
let _ = observer.await;
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
WorkerControllerTransport::InProcess => None,
|
WorkerControllerTransport::InProcess => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3892,6 +3918,21 @@ mod tests {
|
|||||||
assert!(feature_install < materialize_gate);
|
assert!(feature_install < materialize_gate);
|
||||||
assert!(materialize_gate < materialize);
|
assert!(materialize_gate < materialize);
|
||||||
assert!(materialize < exposure);
|
assert!(materialize < exposure);
|
||||||
|
let observer = startup.find("let command_observer = fs_for_view").unwrap();
|
||||||
|
let recover = startup.find(".recover_unfinished_compaction()").unwrap();
|
||||||
|
let files = startup
|
||||||
|
.find("runtime_dir.write_status(&shared_state).await?")
|
||||||
|
.unwrap();
|
||||||
|
let loop_start = startup.find("tokio::spawn(controller_loop(").unwrap();
|
||||||
|
assert!(recover < observer && files < observer && observer < loop_start);
|
||||||
|
let socket_startup = &startup[observer..loop_start];
|
||||||
|
let abort = socket_startup.find("observer.abort()").unwrap();
|
||||||
|
let join = socket_startup.find("observer.await").unwrap();
|
||||||
|
let failure = socket_startup.find("return Err(error)").unwrap();
|
||||||
|
assert!(
|
||||||
|
abort < join && join < failure,
|
||||||
|
"startup cannot detach its observer on error"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
startup[materialize_gate..materialize]
|
startup[materialize_gate..materialize]
|
||||||
.contains("needs_initial_session_head_materialization")
|
.contains("needs_initial_session_head_materialization")
|
||||||
|
|||||||
Reference in New Issue
Block a user