From 86798e815025869054b14ba21552ca81a31efdd7 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 18:30:27 +0000 Subject: [PATCH 01/12] data plane: resume process SSE streams from the guest output journal A process stream that reattached never asked the guest for the output it retains. Without Last-Event-ID the backend cursor was unset, so a reattach after the process exited got only process_started and process_exited, and a reattach to a running process followed the live tail. With Last-Event-ID the stream resumed live behind resync_required(cursor_replay_unavailable). A backend reconnect after a reattach then passed a cursor counted from that attach's own frames, replaying stale output, and always sent resync_required(process_stream_reconnected). Process SSE ids now carry the guest journal position: (position + 1) * 1_000_000 + frames since output last advanced it. A reattach without Last-Event-ID replays from the start of the journal, a resume continues from the position its id carries, and a backend reconnect reopens at the position the client has reached. resync_required is sent only when the guest reports the requested output evicted (cursor_out_of_window, after which the stream follows the live tail), or when a reconnect has no known position. Ids stay increasing integers clients echo back; ids below the stride, as earlier data planes issued, keep the old live-tail resume. The events docs describe the new resume contract, and the M1 conformance and REST smokes assert it. --- crates/libs/api/src/routes/public_docs.rs | 2 +- .../src/routes/public_docs/schema_contract.rs | 4 +- .../libs/data/plane/server/src/native/sse.rs | 165 ++++++-- .../data/plane/server/src/native/tests.rs | 390 +++++++++++++++++- crates/libs/data/plane/server/src/tests.rs | 73 +++- docs/architecture.md | 3 +- docs/first-sandbox.md | 2 +- docs/troubleshooting.md | 2 +- scripts/tests/smoke-spaced-rest.sh | 27 +- space_smoke/m1_conformance.py | 35 +- 10 files changed, 594 insertions(+), 109 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index eb7a4941d9..49992c16e4 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. The stream resumes live; missed output is not replayed.\n- The first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`.\n- Keep output already received. If the process exited while disconnected, reconnecting still reports `process_exited`.\n- For file watches, re-list the watched path after `resync_required` before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Treat it as opaque: process stream ids are not consecutive.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the oldest retained output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with live output. Keep output already received.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/api/src/routes/public_docs/schema_contract.rs b/crates/libs/api/src/routes/public_docs/schema_contract.rs index 5152cac833..7c5f1104a1 100644 --- a/crates/libs/api/src/routes/public_docs/schema_contract.rs +++ b/crates/libs/api/src/routes/public_docs/schema_contract.rs @@ -88,7 +88,7 @@ fn openapi_identifier(value: &str) -> String { pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { let response = json!({ - "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 2\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":0,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 3\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", + "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 2000000\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 2000001\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", "content": { "text/event-stream": { "schema": { "type": "string" }, @@ -117,7 +117,7 @@ pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { "name": "Last-Event-ID", "in": "header", "required": false, - "description": "SSE `id` of the last frame received.\n\n- Resumes the live stream; missed output is not replayed.\n- The first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum return 400.", + "description": "SSE `id` of the last frame received.\n\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` (`cursor_out_of_window`) precedes live output.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum return 400.", "schema": { "type": "integer", "format": "int64", diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index 8b8fe037f5..d7b39a33b2 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -211,7 +211,11 @@ pub(crate) struct ProcessSseState { client: WorkerApiClient>, stream: tonic::Streaming, event_seq: u64, + /// The guest output journal position the client has received output up + /// to, or `None` once the stream follows the live tail from an unknown one. backend_output_cursor: Option, + /// The `seq` of the last output frame sent. + output_seq: u64, process_id: Option, process_bound: bool, binding: Option, @@ -233,11 +237,12 @@ pub(crate) struct ProcessReplayState { /// id and the `resync_required(cursor_replay_unavailable)` frame that carries /// it. `None` when the client did not send a cursor. /// -/// This is the public reconnection contract for both process and file-watch -/// sessions: the data plane resumes the *live* stream at `last + 1` and does -/// not replay what was missed, because it does not map SSE ids onto the guest -/// output journal. Keep this the single place that decides so the docs, the -/// conformance smoke, and both session kinds cannot drift. +/// This is the reconnection contract for file-watch sessions, and for a +/// process session resumed from an id that carries no output position (see +/// [`PROCESS_EVENT_ID_STRIDE`]): the data plane resumes the *live* stream at +/// `last + 1` and does not replay what was missed. Keep this the single place +/// that decides so the docs, the conformance smoke, and both session kinds +/// cannot drift. /// /// `last + 1` cannot overflow: the request boundary rejects cursors above /// [`auth::MAX_LAST_EVENT_ID`] (`i64::MAX`) with a 400, so a resumed stream @@ -254,26 +259,65 @@ pub(crate) fn resume_resync_frame( )) } +/// How far apart the SSE ids of successive guest output positions are. +/// +/// A process stream's id is `(position + 1) * PROCESS_EVENT_ID_STRIDE + n`: +/// `position` is the guest output journal position the client has received +/// output up to, and `n` counts the frames since output last advanced it. A +/// resume recovers the position from the id alone, so any replica can serve +/// it. Ids below the stride carry no position: they are the plain frame +/// counters earlier data planes issued, and a resume from one still follows +/// the live tail behind [`resume_resync_frame`]. +pub(crate) const PROCESS_EVENT_ID_STRIDE: u64 = 1_000_000; + +/// The guest output position a process stream's SSE id resumes from, or +/// `None` for an id that carries none. +pub(crate) fn process_event_output_position(event_id: u64) -> Option { + (event_id / PROCESS_EVENT_ID_STRIDE).checked_sub(1) +} + +/// The SSE id of a process stream's frame after `previous`, once the client +/// has received output up to `output_position`. +/// +/// While the position is unknown, ids stay among those of the last position +/// the stream knew. The guest has evicted that position, or the stream never +/// had one, so a resume from such an id reports the gap again instead of +/// replaying output the client already has. +pub(crate) fn next_process_event_id(previous: u64, output_position: Option) -> u64 { + let first = output_position + .and_then(|position| { + position + .checked_add(1)? + .checked_mul(PROCESS_EVENT_ID_STRIDE) + }) + .filter(|first| *first <= auth::MAX_LAST_EVENT_ID) + .unwrap_or(previous - previous % PROCESS_EVENT_ID_STRIDE); + let last = first + .saturating_add(PROCESS_EVENT_ID_STRIDE - 1) + .min(auth::MAX_LAST_EVENT_ID); + previous.saturating_add(1).clamp(first, last) +} + impl ProcessReplayState { + /// Where a process stream starts in the guest's output journal: a fresh + /// attach replays the retained output from the start, and a resume + /// continues from the position its `Last-Event-ID` carries. pub(crate) fn load(session_id: DataPlaneSessionId, last_event_id: Option) -> Self { - let Some((event_seq, frame)) = - resume_resync_frame(session_id.kind().as_str(), last_event_id) - else { - return Self { - event_seq: 0, - backend_output_cursor: None, - pending: VecDeque::new(), - }; + let mut replay = Self { + event_seq: last_event_id.unwrap_or_default(), + backend_output_cursor: last_event_id.map_or(Some(0), process_event_output_position), + pending: VecDeque::new(), }; - - let mut pending = VecDeque::new(); - pending.push_back(sse_event_from_frame(&frame, Some(event_seq))); - - Self { - event_seq, - backend_output_cursor: None, - pending, + if replay.backend_output_cursor.is_none() + && let Some((event_seq, frame)) = + resume_resync_frame(session_id.kind().as_str(), last_event_id) + { + replay.event_seq = event_seq; + replay + .pending + .push_back(sse_event_from_frame(&frame, Some(event_seq))); } + replay } } @@ -287,22 +331,18 @@ impl ProcessSseState { start_permit: Option, ) -> Result { let replay = ProcessReplayState::load(session_id, last_event_id); - let (client, stream) = open_native_process_stream( - &state, - session_id, - &mut attached, - replay.backend_output_cursor, - ) - .await?; + let backend_output_cursor = replay + .backend_output_cursor + .or_else(|| attached.backend_process_id.is_none().then_some(0)); + let (client, stream) = + open_native_process_stream(&state, session_id, &mut attached, backend_output_cursor) + .await?; let mut pending = VecDeque::new(); pending.push_back(sse_event_from_frame(&attached_frame(&attached), None)); pending.extend(replay.pending); let process_id = attached.backend_process_id.clone(); let process_bound = process_id.is_some(); let revocation_tick = revocation_check_interval(&state); - let backend_output_cursor = replay - .backend_output_cursor - .or_else(|| attached.backend_process_id.is_none().then_some(0)); let shutdown_token = state.stream_shutdown_token.child_token(); Ok(Self { @@ -313,6 +353,7 @@ impl ProcessSseState { stream, event_seq: replay.event_seq, backend_output_cursor, + output_seq: backend_output_cursor.unwrap_or_default(), process_id, process_bound, binding: None, @@ -326,7 +367,7 @@ impl ProcessSseState { } fn push_frame(&mut self, frame: DataPlaneServerFrame) { - self.event_seq = self.event_seq.saturating_add(1); + self.event_seq = next_process_event_id(self.event_seq, self.backend_output_cursor); self.pending .push_back(sse_event_from_frame(&frame, Some(self.event_seq))); } @@ -458,7 +499,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { )); } } - let mut output_seq = state.backend_output_cursor.unwrap_or_default(); + let mut output_seq = state.output_seq; let batch = process_event_frames(event, &mut output_seq); if batch .frames @@ -473,12 +514,13 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { for frame in batch.frames { let exited = matches!(frame, DataPlaneServerFrame::ProcessExited { .. }); - let advances_backend_cursor = - process_frame_advances_backend_cursor(&frame); - state.push_frame(frame); - if advances_backend_cursor { - state.backend_output_cursor = Some(output_seq); + if process_frame_advances_backend_cursor(&frame) { + state.output_seq = output_seq; + if let Some(cursor) = state.backend_output_cursor.as_mut() { + *cursor = output_seq; + } } + state.push_frame(frame); if exited { state.push_frame(DataPlaneServerFrame::Closed { reason: StreamCloseReason::BackendClosed, @@ -490,6 +532,32 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { return Some(event); } } + Err(status) if is_output_journal_gap(&status) => { + // The guest no longer retains the output this stream + // asked for. Report the gap, then follow the live tail. + let Some(process_id) = state.process_id.clone() else { + state.finish_with_error(StreamBridgeError::BackendStatus(status)); + return state.pending.pop_front(); + }; + state.backend_output_cursor = None; + state.push_frame(resync_required_frame( + state.session_id.kind().as_str(), + DataPlaneResyncReason::CursorOutOfWindow, + )); + match connect_process_stream( + &mut state.client, + &state.attached.sandbox_id, + &process_id, + None, + state.attached.space_api_route_token.as_ref(), + ) + .await + { + Ok(stream) => state.stream = stream, + Err(error) => state.finish_with_error(error), + } + return state.pending.pop_front(); + } Err(status) if status.code() == tonic::Code::Unavailable || is_refreshable_route_identity_rejection(&status) => @@ -559,10 +627,15 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { route_generation: refreshed.route_generation, current_node_id: refreshed.current_node_id.to_string(), }); - state.push_frame(resync_required_frame( - state.session_id.kind().as_str(), - DataPlaneResyncReason::ProcessStreamReconnected, - )); + // From a known position the reopened stream + // continues where this one stopped; only one + // that follows the live tail can skip output. + if state.backend_output_cursor.is_none() { + state.push_frame(resync_required_frame( + state.session_id.kind().as_str(), + DataPlaneResyncReason::ProcessStreamReconnected, + )); + } state.attached = refreshed; } Err(error) => state.finish_with_error(error), @@ -579,6 +652,14 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } } +/// The guest ends a process stream with `resource_exhausted` once the output +/// its cursor reaches has been evicted. The node's per-sandbox limiter uses the +/// same code, but it refuses a call before any stream exists. +fn is_output_journal_gap(status: &tonic::Status) -> bool { + status.code() == tonic::Code::ResourceExhausted + && pplx_space_common_grpc::UdsPerSandboxLimitExceeded::from_status(status).is_none() +} + pub(crate) struct FileWatchSseState { state: DataPlaneState, session_id: DataPlaneSessionId, diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index 8730392b31..e88b9ea41e 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -539,29 +539,30 @@ fn binding_fake_test_process() -> bool { ]) } -/// Opens the session's event stream and forwards its body text, chunk by -/// chunk, until the body ends. +/// Opens the session's event stream, through the fake cluster API at +/// `endpoint`, and forwards its body text, chunk by chunk, until the body ends. async fn open_process_events( - fake: &BindingFake, + endpoint: &str, session_id: DataPlaneSessionId, + last_event_id: Option, ) -> (DataPlaneState, tokio::sync::mpsc::UnboundedReceiver) { let mut state = crate::tests::test_data_plane_state().await; state.cluster_api = ClusterApiClients::service_only( - pplx_space_node_api_client::cluster::connect(fake.endpoint.clone(), "test") + pplx_space_node_api_client::cluster::connect(endpoint.to_string(), "test") .await .unwrap(), ); state.revocation_check_interval = Duration::from_secs(3600); let token = format!("{DATA_PLANE_TOKEN_PREFIX}{}", "c".repeat(64)); - let response = tower::ServiceExt::oneshot( - router(state.clone()), - Request::get(format!("/v1/sessions/{session_id}/events")) - .header(header::AUTHORIZATION, format!("Bearer {token}")) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + let mut request = Request::get(format!("/v1/sessions/{session_id}/events")) + .header(header::AUTHORIZATION, format!("Bearer {token}")); + if let Some(last_event_id) = last_event_id { + request = request.header("last-event-id", last_event_id.to_string()); + } + let response = + tower::ServiceExt::oneshot(router(state.clone()), request.body(Body::empty()).unwrap()) + .await + .unwrap(); assert_eq!(response.status(), StatusCode::OK); let mut body = response.into_body(); let (chunk_tx, chunks) = tokio::sync::mpsc::unbounded_channel(); @@ -607,7 +608,7 @@ async fn process_start_reaches_the_client_before_its_binding_commits() { } let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_binding_fake(session_id, false, FakeBind::Succeeds).await; - let (state, mut chunks) = open_process_events(&fake, session_id).await; + let (state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; let mut seen = String::new(); // The bind is still held open, yet the start already reached the client. @@ -642,7 +643,7 @@ async fn process_exit_is_reported_only_after_its_binding_commits() { } let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_binding_fake(session_id, true, FakeBind::Succeeds).await; - let (_state, mut chunks) = open_process_events(&fake, session_id).await; + let (_state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; let mut seen = String::new(); read_until(&mut chunks, &mut seen, "process_started").await; @@ -674,7 +675,7 @@ async fn failed_binding_ends_the_process_stream_with_a_retryable_error() { } let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_binding_fake(session_id, false, FakeBind::NotFound).await; - let (state, mut chunks) = open_process_events(&fake, session_id).await; + let (state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; let mut seen = String::new(); read_until(&mut chunks, &mut seen, "process_started").await; @@ -696,7 +697,7 @@ async fn binding_survives_a_client_that_disconnects_mid_bind() { } let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_binding_fake(session_id, false, FakeBind::Succeeds).await; - let (state, mut chunks) = open_process_events(&fake, session_id).await; + let (state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; let mut seen = String::new(); read_until(&mut chunks, &mut seen, "process_started").await; @@ -1709,3 +1710,358 @@ async fn websocket_resize_refusal_carries_a_fixed_message() { "internal detail leaked: {seen}" ); } + +/// One reply of a fake guest process stream. +enum FakeReply { + Event(spaced_process_event::Event), + /// Ends the stream with this status. + Status(tonic::Code), +} + +fn connected() -> FakeReply { + FakeReply::Event(spaced_process_event::Event::Connected( + pplx_space_sbx_mgr_client::operations::ProcessConnected {}, + )) +} + +fn stdout(line: &str) -> FakeReply { + FakeReply::Event(spaced_process_event::Event::Stdout( + pplx_space_sbx_mgr_client::operations::ProcessOutput { + data: line.as_bytes().to_vec(), + }, + )) +} + +fn exited() -> FakeReply { + FakeReply::Event(spaced_process_event::Event::Exit( + pplx_space_sbx_mgr_client::proto::SpacedProcessExit { + outcome: Some( + pplx_space_sbx_mgr_client::proto::spaced_process_exit::Outcome::Exited( + pplx_space_sbx_mgr_client::proto::SpacedProcessExited { exit_code: 0 }, + ), + ), + }, + )) +} + +/// What the guest replays for `output_cursor` from a process that wrote +/// `lines` and exited: the output from that position on, or, without a +/// cursor, only the start and the exit. +fn journal_replay(lines: &[&str], output_cursor: Option) -> Vec { + let Some(cursor) = output_cursor else { + return vec![connected(), exited()]; + }; + let mut replies = vec![connected()]; + replies.extend(lines.iter().skip(cursor as usize).map(|line| stdout(line))); + replies.push(exited()); + replies +} + +type FakeReplies = Arc) -> Vec + Send + Sync>; + +/// A fake cluster API and worker for a native process session whose process +/// is already bound. Each `SpacedConnectProcess` is answered by `replies`, +/// given how many connects came before it and its `output_cursor`; a stream +/// without a closing status stays open. +struct JournalFake { + endpoint: String, + output_cursors: Arc>>>, +} + +async fn spawn_journal_fake(session_id: DataPlaneSessionId, replies: FakeReplies) -> JournalFake { + use prost::Message as _; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let process_id = pplx_space_public_api::process_id_for_session(session_id).to_string(); + let mut route = crate::tests::route_response_with_token(session_id, None); + route.backend.as_mut().unwrap().endpoint = endpoint.clone(); + route.adapter_state_json = r#"{"command":["true"]}"#.to_string(); + route.backend_process_id = Some(process_id.clone()); + let route = route.encode_to_vec(); + let output_cursors = Arc::new(std::sync::Mutex::new(Vec::new())); + let fake = JournalFake { + endpoint, + output_cursors: Arc::clone(&output_cursors), + }; + let service = axum::Router::new().fallback(move |request: Request| { + let route = route.clone(); + let process_id = process_id.clone(); + let replies = Arc::clone(&replies); + let output_cursors = Arc::clone(&output_cursors); + async move { + let path = request.uri().path().to_string(); + if path.ends_with("/Check") { + return crate::tests::grpc_test_response(vec![8, 1]); + } + if path.ends_with("/ResolveDataPlaneSessionRoute") { + return crate::tests::grpc_test_response(route); + } + if path.ends_with("/RecordDataPlaneSessionReconnect") { + return crate::tests::grpc_test_response(Vec::new()); + } + assert!( + path.ends_with("/SpacedConnectProcess"), + "unexpected call {path}" + ); + let body = request.into_body().collect().await.unwrap().to_bytes(); + let output_cursor = SpacedConnectProcessRequest::decode(&body[5..]) + .unwrap() + .operation + .unwrap() + .output_cursor; + let connects = { + let mut output_cursors = output_cursors.lock().unwrap(); + output_cursors.push(output_cursor); + output_cursors.len() - 1 + }; + let mut frames = Vec::new(); + let mut status = None; + for reply in replies(connects, output_cursor) { + match reply { + FakeReply::Event(event) => { + let encoded = ProcessEvent { + process_id: process_id.clone(), + pid: Some(42), + event: Some(event), + } + .encode_to_vec(); + let mut framed = vec![0]; + framed.extend_from_slice(&(encoded.len() as u32).to_be_bytes()); + framed.extend(encoded); + frames.push(hyper::body::Frame::data(bytes::Bytes::from(framed))); + } + FakeReply::Status(code) => status = Some(code), + } + } + let ending = match status { + Some(code) => { + let mut trailers = HeaderMap::new(); + trailers.insert("grpc-status", HeaderValue::from(code as i32)); + futures_util::stream::once( + async move { hyper::body::Frame::trailers(trailers) }, + ) + .boxed() + } + None => futures_util::stream::pending().boxed(), + }; + let frames = futures_util::stream::iter(frames) + .chain(ending) + .map(Ok::<_, Infallible>); + Response::builder() + .header("content-type", "application/grpc") + .body(Body::new(http_body_util::StreamBody::new(frames))) + .unwrap() + } + }); + tokio::spawn(async move { + axum::serve(listener, service).await.unwrap(); + }); + fake +} + +/// The complete SSE events in `body`, as each one's id and JSON frame. +fn sse_frames(body: &str) -> Vec<(Option, serde_json::Value)> { + let mut events: Vec<&str> = body.split("\n\n").collect(); + events.pop(); + events + .into_iter() + .filter_map(|event| { + let mut id = None; + let mut data = None; + for line in event.lines() { + if let Some(value) = line.strip_prefix("id:") { + id = Some(value.trim().parse().unwrap()); + } else if let Some(value) = line.strip_prefix("data:") { + data = Some(serde_json::from_str(value.trim()).unwrap()); + } + } + Some((id, data?)) + }) + .collect() +} + +fn frames_of_type<'a>( + frames: &'a [(Option, serde_json::Value)], + frame_type: &'a str, +) -> impl Iterator, serde_json::Value)> { + frames + .iter() + .filter(move |(_, frame)| frame["type"] == frame_type) +} + +fn stdout_text(frames: &[(Option, serde_json::Value)]) -> String { + frames_of_type(frames, "stdout") + .map(|(_, frame)| { + let data = BASE64_STANDARD + .decode(frame["data_base64"].as_str().unwrap()) + .unwrap(); + String::from_utf8(data).unwrap() + }) + .collect() +} + +fn resync_reasons(frames: &[(Option, serde_json::Value)]) -> Vec { + frames_of_type(frames, "resync_required") + .map(|(_, frame)| frame["reason"].as_str().unwrap().to_string()) + .collect() +} + +/// Reads one process event stream to its end. +async fn process_events_to_close( + endpoint: &str, + session_id: DataPlaneSessionId, + last_event_id: Option, +) -> Vec<(Option, serde_json::Value)> { + let (_state, mut chunks) = open_process_events(endpoint, session_id, last_event_id).await; + let mut seen = String::new(); + tokio::time::timeout(Duration::from_secs(5), async { + while let Some(chunk) = chunks.recv().await { + seen.push_str(&chunk); + } + }) + .await + .unwrap_or_else(|_| panic!("stream ends within 5s; saw {seen}")); + let frames = sse_frames(&seen); + assert_eq!(frames.last().unwrap().1["type"], "closed", "{seen}"); + let ids: Vec = frames.iter().filter_map(|(id, _)| *id).collect(); + assert!(ids.is_sorted_by(|a, b| a < b), "ids increase: {ids:?}"); + frames +} + +const JOURNAL: [&str; 4] = ["one\n", "two\n", "three\n", "four\n"]; + +/// A client that drops its first stream, and so never sends `Last-Event-ID`, +/// still gets the process's whole retained output when it attaches again. +#[tokio::test] +async fn process_reattach_replays_retained_output_from_the_start() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + Arc::new(|_, output_cursor| journal_replay(&JOURNAL, output_cursor)), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + + assert_eq!(stdout_text(&frames), JOURNAL.concat()); + assert_eq!(resync_reasons(&frames), Vec::::new()); + assert_eq!(*fake.output_cursors.lock().unwrap(), [Some(0)]); +} + +/// `Last-Event-ID` resumes at the output after the last frame received: the +/// two streams together carry the output once, without a gap or a resync. +#[tokio::test] +async fn process_resume_continues_after_the_last_event_id() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + Arc::new(|_, output_cursor| journal_replay(&JOURNAL, output_cursor)), + ) + .await; + let first = process_events_to_close(&fake.endpoint, session_id, None).await; + let received: Vec<_> = first + .iter() + .take_while(|(_, frame)| frame["seq"] != 3) + .cloned() + .collect(); + let last_event_id = received.last().unwrap().0.unwrap(); + + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; + + assert_eq!(stdout_text(&received), "one\ntwo\n"); + assert_eq!(stdout_text(&resumed), "three\nfour\n"); + assert_eq!(resync_reasons(&resumed), Vec::::new()); + assert!( + resumed + .iter() + .filter_map(|(id, _)| *id) + .all(|id| id > last_event_id) + ); + assert_eq!(*fake.output_cursors.lock().unwrap(), [Some(0), Some(2)]); +} + +/// Output the guest evicted before the client asked for it is a real gap: +/// the stream reports it, follows the live tail, and a resume from what it +/// sent next reports the gap again rather than replaying output. +#[tokio::test] +async fn evicted_process_output_is_reported_before_the_live_tail() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + Arc::new(|_, output_cursor| match output_cursor { + Some(cursor) if cursor < 2 => vec![ + connected(), + FakeReply::Status(tonic::Code::ResourceExhausted), + ], + Some(cursor) => journal_replay(&JOURNAL, Some(cursor)), + None => vec![connected(), stdout("live\n"), exited()], + }), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), "live\n"); + assert!( + frames_of_type(&frames, "error").next().is_none(), + "{frames:?}" + ); + + let last_event_id = frames.last().unwrap().0.unwrap(); + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; + assert_eq!(resync_reasons(&resumed), ["cursor_out_of_window"]); + assert_eq!( + *fake.output_cursors.lock().unwrap(), + [Some(0), None, Some(0), None] + ); +} + +/// A backend reconnect after a resume reopens the guest stream at the output +/// position the client has reached; that replay is lossless, so the client is +/// not told to resync. +#[tokio::test] +async fn backend_reconnect_after_a_resume_continues_without_a_resync() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + Arc::new(|connects, output_cursor| match (connects, output_cursor) { + // The resume's stream drops after one line. + (1, Some(cursor)) => vec![ + connected(), + stdout(JOURNAL[cursor as usize]), + FakeReply::Status(tonic::Code::Unavailable), + ], + (_, output_cursor) => journal_replay(&JOURNAL, output_cursor), + }), + ) + .await; + let first = process_events_to_close(&fake.endpoint, session_id, None).await; + let after_one = first + .iter() + .find(|(_, frame)| frame["seq"] == 1) + .unwrap() + .0 + .unwrap(); + + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(after_one)).await; + + assert_eq!(stdout_text(&resumed), "two\nthree\nfour\n"); + assert_eq!(frames_of_type(&resumed, "backend_reconnected").count(), 1); + assert_eq!(resync_reasons(&resumed), Vec::::new()); + assert_eq!( + *fake.output_cursors.lock().unwrap(), + [Some(0), Some(1), Some(2)] + ); +} diff --git a/crates/libs/data/plane/server/src/tests.rs b/crates/libs/data/plane/server/src/tests.rs index 3824a3a9d0..ab57e84a60 100644 --- a/crates/libs/data/plane/server/src/tests.rs +++ b/crates/libs/data/plane/server/src/tests.rs @@ -1,6 +1,9 @@ use super::*; use crate::{ - native::{ProcessReplayState, first_frame_attach_token_from_recv}, + native::{ + PROCESS_EVENT_ID_STRIDE, ProcessReplayState, first_frame_attach_token_from_recv, + next_process_event_id, process_event_output_position, + }, runtime_route::sweep_expired_ecw_routes, }; use aws_lc_rs::rand::SystemRandom; @@ -3701,24 +3704,72 @@ async fn first_frame_attach_token_times_out_at_deadline() { assert_eq!(result, Err(DataPlaneErrorCode::AttachDeadlineExceeded)); } +/// A fresh attach replays the guest journal from its start, and a resume +/// continues from the output position its id carries. Only an id without a +/// position, as earlier data planes issued, still resumes the live tail +/// behind `resync_required`. #[test] -fn process_sse_resume_emits_resync_at_next_event_id() { +fn process_sse_resume_starts_at_the_journal_position_its_id_carries() { let session_id = DataPlaneSessionId::Process(Default::default()); - let replay = ProcessReplayState::load(session_id, Some(41)); - - assert_eq!(replay.event_seq, 42); - assert_eq!(replay.backend_output_cursor, None); - assert_eq!(replay.pending.len(), 1); let fresh = ProcessReplayState::load(session_id, None); assert_eq!(fresh.event_seq, 0); + assert_eq!(fresh.backend_output_cursor, Some(0)); assert!(fresh.pending.is_empty()); + + let resumed = ProcessReplayState::load(session_id, Some(4 * PROCESS_EVENT_ID_STRIDE + 2)); + assert_eq!(resumed.event_seq, 4 * PROCESS_EVENT_ID_STRIDE + 2); + assert_eq!(resumed.backend_output_cursor, Some(3)); + assert!(resumed.pending.is_empty()); + + let legacy = ProcessReplayState::load(session_id, Some(41)); + assert_eq!(legacy.event_seq, 42); + assert_eq!(legacy.backend_output_cursor, None); + assert_eq!(legacy.pending.len(), 1); +} + +/// Process SSE ids increase, and each decodes to the output position a +/// resume from it continues at. While the position is unknown the ids stay +/// with the last position known, never moving onto output the client lacks. +#[test] +fn process_event_ids_carry_the_output_position() { + let mut previous = 0; + let mut last_known = None; + for position in [ + Some(0), + Some(0), + Some(1), + Some(2), + Some(2), + None, + None, + Some(5), + ] { + let id = next_process_event_id(previous, position); + assert!(id > previous, "{id} after {previous}"); + last_known = position.or(last_known); + assert_eq!(process_event_output_position(id), last_known, "{id}"); + previous = id; + } + + // Ids without a position stay without one. + assert_eq!( + process_event_output_position(next_process_event_id(42, None)), + None + ); + // Frame counts saturate inside their position rather than spill into the next. + let last = 2 * PROCESS_EVENT_ID_STRIDE - 1; + assert_eq!(next_process_event_id(last, Some(0)), last); + assert_eq!(next_process_event_id(last, None), last); + // Every id stays a valid `Last-Event-ID`. + assert!(next_process_event_id(0, Some(u64::MAX)) <= MAX_LAST_EVENT_ID); + assert!(next_process_event_id(MAX_LAST_EVENT_ID, Some(u64::MAX)) <= MAX_LAST_EVENT_ID); } -/// The public reconnection contract, shared by process and file-watch -/// sessions: `Last-Event-ID` resumes the live stream at `last + 1` behind a -/// `resync_required(cursor_replay_unavailable)` frame; no cursor, no frame. -/// Missed output is never replayed, so the backend cursor stays unset. +/// The file-watch reconnection contract, which a process session keeps for an +/// id without an output position: `Last-Event-ID` resumes the live stream at +/// `last + 1` behind a `resync_required(cursor_replay_unavailable)` frame; no +/// cursor, no frame. #[test] fn last_event_id_resume_is_a_cursor_replay_unavailable_resync() { for kind in ["process", "file_watch"] { diff --git a/docs/architecture.md b/docs/architecture.md index ac6b450ae9..cc1b20fd40 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -261,7 +261,8 @@ they end or the pod's termination grace period kills the process. A second stop signal ends the drain the way the deadline does, but at once: a `Process.Start` still opening is cut the way SIGKILL would cut it. At the deadline: native event streams end, and a client that reconnects with `Last-Event-ID` resumes -the live stream but doesn't get the output it missed; native WebSockets close +a process's output after the last event it received while the guest still +retains it, and a file watch's live stream; native WebSockets close with 1012, including ones still opening their backend stream, and the client re-attaches the same session (a process or terminal session doesn't start its process again); ECW connect streams end with `unavailable` and need a new diff --git a/docs/first-sandbox.md b/docs/first-sandbox.md index ec069aefea..309d625437 100644 --- a/docs/first-sandbox.md +++ b/docs/first-sandbox.md @@ -85,7 +85,7 @@ Read `/openapi-m1.json` for exact request constraints and supported fields. This The example displays decoded stdout and stderr and saves the raw event stream in its run directory. Handle binary output separately rather than displaying it in a terminal. -Command output is a live stream, not durable log storage. Reconnecting with `Last-Event-ID` produces `resync_required`, not a replay of missed output. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. +Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID` resumes with the output after that event while it is retained, and otherwise produces `resync_required` before live output. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. ## Inspect and manage the sandbox diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index abe0e5c48c..d4140f5365 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -78,7 +78,7 @@ This prints sandbox listings and the saved creation/deletion operations, sandbox | Stream authentication fails | Use `attach.token`, not `SPACE_API_TOKEN`, and the correct session URL. Check expiry and attach promptly after creating the command. | | Example rejects an endpoint, events URL, or redirect | Use HTTPS except for loopback HTTP. Check the data-plane origin and session ID; correct the configuration rather than removing destination or redirect checks. | | HTTP stream succeeds but output is incomplete | Check for `process_exited`, its exit result, and any `error` or `resync_required` frame. A closed connection is not proof of successful execution. | -| Reconnect reports `resync_required` | Missed output is not replayed. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | +| Reconnect reports `resync_required` | Output from before the reconnect is no longer retained, so some of it was missed. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | | Command exits nonzero | Read stderr and the exit result from the saved event transcript. Check that the executable and working directory exist inside the sandbox, not just on the host. | | Single-process GET returns `not_found` | This endpoint inspects a running process, not historical command results. It returns `not_found` before attachment and after exit. | diff --git a/scripts/tests/smoke-spaced-rest.sh b/scripts/tests/smoke-spaced-rest.sh index 4a61ee1a47..bbebb8c47f 100755 --- a/scripts/tests/smoke-spaced-rest.sh +++ b/scripts/tests/smoke-spaced-rest.sh @@ -618,7 +618,7 @@ function parseEvents(buffer) { } // Open one SSE connection and feed events to `onEvent` until it returns true. -// Every identified frame must carry exactly the previous id plus one. +// Every identified frame must carry a greater id than the one before. async function readStream(resumeFrom, onEvent) { const headers = { Accept: 'text/event-stream', Authorization: `Bearer ${command.attach.token}` }; if (resumeFrom !== null) headers['Last-Event-ID'] = String(resumeFrom); @@ -637,7 +637,7 @@ async function readStream(resumeFrom, onEvent) { buffer = rest; for (const event of events) { if (event.id !== null) { - if (lastEventId !== null && event.id !== lastEventId + 1) eventIdsMonotonic = false; + if (lastEventId !== null && event.id <= lastEventId) eventIdsMonotonic = false; lastEventId = event.id; } if (event.event !== null && event.event !== event.data.type) eventIdsMonotonic = false; @@ -682,20 +682,19 @@ async function readStream(resumeFrom, onEvent) { } // Second connection: resume with Last-Event-ID. The contract is that the - // stream restarts with attached, then resync_required(cursor_replay_unavailable) - // at Last-Event-ID + 1, and the exited process still reports its exit. + // stream restarts with attached, resumes after Last-Event-ID without + // replaying output already received or asking to resync, and the exited + // process still reports its exit. const resumeFrom = lastEventId; - const resume = { attached_first: null, first_id: null, first_type: null, reason: null, exit_code: null }; + const resume = { attached_first: null, first_id: null, resync: null, stdout: '', exit_code: null }; let seen = 0; await readStream(resumeFrom, (event) => { const frame = event.data; seen += 1; if (seen === 1) resume.attached_first = frame.type === 'attached' && event.id === null; - if (event.id !== null && resume.first_id === null) { - resume.first_id = event.id; - resume.first_type = frame.type; - resume.reason = frame.reason === undefined ? null : frame.reason; - } + if (event.id !== null && resume.first_id === null) resume.first_id = event.id; + if (frame.type === 'resync_required' && resume.resync === null) resume.resync = frame.reason; + if (frame.type === 'stdout') resume.stdout += Buffer.from(frame.data_base64, 'base64').toString('utf8'); if (frame.type === 'process_exited') { resume.exit_code = frame.exit_code; return true; @@ -840,11 +839,11 @@ assert_eq "$(jq -r '.attached' <<<"$command_result")" "true" "command stream att assert_eq "$(jq -r '.stdout' <<<"$command_result")" "$RUN_ID" "command stdout" assert_eq "$(jq -r '.stderr' <<<"$command_result")" "command-err" "command stderr" assert_eq "$(jq -r '.exit_code' <<<"$command_result")" "6" "command exit code" -assert_eq "$(jq -r '.event_ids_monotonic' <<<"$command_result")" "true" "command stream ids increase by one" +assert_eq "$(jq -r '.event_ids_monotonic' <<<"$command_result")" "true" "command stream ids increase" assert_eq "$(jq -r '.resume.attached_first' <<<"$command_result")" "true" "resumed stream starts with attached" -assert_eq "$(jq -r '.resume.first_type' <<<"$command_result")" "resync_required" "resumed stream announces resync" -assert_eq "$(jq -r '.resume.reason' <<<"$command_result")" "cursor_replay_unavailable" "resync reason" -assert_eq "$(jq -r '.resume.first_id' <<<"$command_result")" "$(jq -r '.resume_from + 1' <<<"$command_result")" "resumed ids continue from Last-Event-ID" +assert_eq "$(jq -r '.resume.resync' <<<"$command_result")" "null" "lossless resume does not resync" +assert_eq "$(jq -r '.resume.stdout' <<<"$command_result")" "" "resume does not replay received output" +assert_eq "$(jq -r '.resume.first_id > .resume_from' <<<"$command_result")" "true" "resumed ids continue after Last-Event-ID" assert_eq "$(jq -r '.resume.exit_code' <<<"$command_result")" "6" "resumed stream reports the process exit" log "one-shot command event stream ok (Last-Event-ID resume verified)" diff --git a/space_smoke/m1_conformance.py b/space_smoke/m1_conformance.py index d3279ffdf6..8c6a4a03c7 100644 --- a/space_smoke/m1_conformance.py +++ b/space_smoke/m1_conformance.py @@ -623,9 +623,9 @@ def exercise_command(self, command: Any) -> None: f"match process_started {streamed_process_id!r}" ) - # Reconnect with Last-Event-ID. The documented guarantee: the live - # stream resumes at last+1 behind resync_required, missed output is - # not replayed, and the process is still controllable. + # Reconnect with Last-Event-ID. The documented guarantee: the stream + # resumes with the output after that event, without a resync while + # the output is retained, and the process is still controllable. resumed: list[dict[str, object]] = [] second = self.read_session_events( api, command.session_id, first.last_id, resumed, @@ -636,19 +636,14 @@ def exercise_command(self, command: Any) -> None: raise InvariantViolation("reconnected stream did not start with an attached frame") if not second.identified: raise InvariantViolation("reconnected stream delivered no identified events") - first_id, first_frame = second.identified[0] - if first_frame.get("type") != "resync_required": + first_id, _ = second.identified[0] + if first_id <= first.last_id: raise InvariantViolation( - f"reconnect did not start with resync_required: {first_frame}" - ) - if first_frame.get("reason") != "cursor_replay_unavailable": - raise InvariantViolation( - f"unexpected resync reason on Last-Event-ID resume: {first_frame}" - ) - if first_id != first.last_id + 1: - raise InvariantViolation( - f"resumed stream restarted ids at {first_id}, expected {first.last_id + 1}" + f"resumed stream restarted ids at {first_id}, after {first.last_id}" ) + resyncs = [frame for frame in resumed if frame.get("type") == "resync_required"] + if resyncs: + raise InvariantViolation(f"lossless Last-Event-ID resume asked to resync: {resyncs}") frames.extend(resumed) exits = [frame for frame in frames if frame.get("type") == "process_exited"] @@ -657,8 +652,10 @@ def exercise_command(self, command: Any) -> None: for frame in exits: assert_known("process_exited.status", frame.get("status"), KNOWN_PROCESS_EXIT_STATUSES) stdout = stdout_of(frames) - if MARKER not in stdout: - raise InvariantViolation(f"session event stream omitted command output: {stdout!r}") + if stdout.count(MARKER) != 1: + raise InvariantViolation( + f"session event streams did not carry the command output once: {stdout!r}" + ) def read_session_events( self, @@ -671,7 +668,7 @@ def read_session_events( ) -> SessionRead: """Read one SSE connection until `stop()` holds, asserting id monotonicity. - Every identified frame must carry exactly the previous id plus one; + Every identified frame must carry a greater id than the one before; `on_connected` runs once the stream reports the process (started or reconnected), so process control can be exercised while attached. """ @@ -689,9 +686,9 @@ def read_session_events( result.attached_first = frame.get("type") == "attached" and frame_id is None first = False if frame_id is not None: - if result.last_id is not None and frame_id != result.last_id + 1: + if result.last_id is not None and frame_id <= result.last_id: raise InvariantViolation( - f"SSE ids must increase by one: got {frame_id} after {result.last_id}" + f"SSE ids must increase: got {frame_id} after {result.last_id}" ) result.last_id = frame_id result.identified.append((frame_id, frame)) From 1799c298b296e65a22a4c2f81195e382a7304732 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 19:02:16 +0000 Subject: [PATCH 02/12] data plane: tag process SSE ids and refuse output cursors past the end Process SSE ids are now tagged with bit 62 and keep their low 16 bits zero, above a 12-bit frame counter and a 34-bit position field whose top bit marks blocks of ids without a position. Ids from earlier data planes, ids an earlier plane counted on from by +1, and ids rounded through a double all fail to decode, so a resume from one replays the retained output behind resync_required(cursor_replay_unavailable) instead of reading a wrong position. Ids never repeat: running out of frames at one position, a position the field cannot hold, or losing the position after an eviction moves the stream to a fresh block without a position, which sorts above every position. The guest journal now ends a stream whose cursor is past the output the process has written with OUT_OF_RANGE, marked process-output-cursor-ahead, instead of waiting for output to reach it and skipping everything before. The data plane answers it with resync_required(cursor_out_of_window) and replays the output from the start. An attach that starts the process counts its output from the start even when the client sent another stream's Last-Event-ID, and a stream reopened after a gap no longer reports the process start again. --- crates/libs/api/src/routes/public_docs.rs | 2 +- .../src/routes/public_docs/schema_contract.rs | 4 +- crates/libs/common/core/src/lib.rs | 5 + .../libs/data/plane/server/src/native/sse.rs | 286 ++++++++++++----- .../data/plane/server/src/native/tests.rs | 295 ++++++++++++++---- crates/libs/data/plane/server/src/tests.rs | 159 +++++++--- .../spaced/guest/execution/src/process.rs | 21 ++ .../guest/execution/src/process/tests.rs | 49 +++ docs/first-sandbox.md | 2 +- docs/troubleshooting.md | 2 +- scripts/tests/smoke-spaced-rest.sh | 13 +- 11 files changed, 635 insertions(+), 203 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index 49992c16e4..893829ad88 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Treat it as opaque: process stream ids are not consecutive.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the oldest retained output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with live output. Keep output already received.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Treat it as an opaque string and echo it exactly: process stream ids are not consecutive and exceed the integers a double represents exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process, and its ids continue above `Last-Event-ID`. Without `Last-Event-ID` it starts from the oldest retained output, which can repeat output already received.\n- When the output to resume from is no longer retained, or `Last-Event-ID` is past the process's output, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with live output, or replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- A stream that starts with `resync_required` may number its ids from below `Last-Event-ID`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/api/src/routes/public_docs/schema_contract.rs b/crates/libs/api/src/routes/public_docs/schema_contract.rs index 7c5f1104a1..f81c861386 100644 --- a/crates/libs/api/src/routes/public_docs/schema_contract.rs +++ b/crates/libs/api/src/routes/public_docs/schema_contract.rs @@ -88,7 +88,7 @@ fn openapi_identifier(value: &str) -> String { pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { let response = json!({ - "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 2000000\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 2000001\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", + "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4611686018695823360\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 4611686018695888896\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", "content": { "text/event-stream": { "schema": { "type": "string" }, @@ -117,7 +117,7 @@ pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { "name": "Last-Event-ID", "in": "header", "required": false, - "description": "SSE `id` of the last frame received.\n\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` (`cursor_out_of_window`) precedes live output.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum return 400.", + "description": "SSE `id` of the last frame received.\n\n- Echo the `id` exactly; do not parse it into a floating-point number.\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` precedes the output it continues with: `cursor_out_of_window` when the output is evicted or the `id` is past it, `cursor_replay_unavailable` when the `id` does not resume exactly.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum return 400.", "schema": { "type": "integer", "format": "int64", diff --git a/crates/libs/common/core/src/lib.rs b/crates/libs/common/core/src/lib.rs index c7013fc55e..c1cecd13ff 100644 --- a/crates/libs/common/core/src/lib.rs +++ b/crates/libs/common/core/src/lib.rs @@ -158,6 +158,11 @@ pub mod spaced { /// as opposed to the sandbox or its runtime being unknown to a hop. pub const GUEST_PATH_NOT_FOUND: &str = "guest-path-not-found"; + /// [`ERROR_REASON_METADATA_KEY`] value of an OUT_OF_RANGE that ends a + /// process output stream whose cursor is past the output the process has + /// written, as opposed to a message too large for a hop to decode. + pub const PROCESS_OUTPUT_CURSOR_AHEAD: &str = "process-output-cursor-ahead"; + /// [`ERROR_REASON_METADATA_KEY`] value of a request refused before any of /// its work started: an admission limit, or a guest or registry shutting /// down. This request did nothing, so the caller may retry it, after the diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index d7b39a33b2..408a3f664f 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -210,7 +210,7 @@ pub(crate) struct ProcessSseState { attached: AttachedSession, client: WorkerApiClient>, stream: tonic::Streaming, - event_seq: u64, + event_id: ProcessEventId, /// The guest output journal position the client has received output up /// to, or `None` once the stream follows the live tail from an unknown one. backend_output_cursor: Option, @@ -219,6 +219,9 @@ pub(crate) struct ProcessSseState { process_id: Option, process_bound: bool, binding: Option, + /// The stream was reopened after an output gap, so the start it reports + /// next repeats one the client already has. + reopened_after_gap: bool, revocation_tick: tokio::time::Interval, pending: VecDeque, finished: bool, @@ -228,8 +231,8 @@ pub(crate) struct ProcessSseState { } pub(crate) struct ProcessReplayState { - pub(crate) event_seq: u64, - pub(crate) backend_output_cursor: Option, + pub(crate) event_id: ProcessEventId, + pub(crate) backend_output_cursor: u64, pub(crate) pending: VecDeque, } @@ -237,12 +240,10 @@ pub(crate) struct ProcessReplayState { /// id and the `resync_required(cursor_replay_unavailable)` frame that carries /// it. `None` when the client did not send a cursor. /// -/// This is the reconnection contract for file-watch sessions, and for a -/// process session resumed from an id that carries no output position (see -/// [`PROCESS_EVENT_ID_STRIDE`]): the data plane resumes the *live* stream at -/// `last + 1` and does not replay what was missed. Keep this the single place -/// that decides so the docs, the conformance smoke, and both session kinds -/// cannot drift. +/// This is the reconnection contract for file-watch sessions: the data plane +/// resumes the *live* stream at `last + 1` and does not replay what was +/// missed. Keep this the single place that decides so the docs and the +/// conformance smoke cannot drift. /// /// `last + 1` cannot overflow: the request boundary rejects cursors above /// [`auth::MAX_LAST_EVENT_ID`] (`i64::MAX`) with a 400, so a resumed stream @@ -259,65 +260,142 @@ pub(crate) fn resume_resync_frame( )) } -/// How far apart the SSE ids of successive guest output positions are. +/// A process stream's SSE id, which a resume decodes to recover the guest +/// output position to continue from, so any replica can serve it. /// -/// A process stream's id is `(position + 1) * PROCESS_EVENT_ID_STRIDE + n`: -/// `position` is the guest output journal position the client has received -/// output up to, and `n` counts the frames since output last advanced it. A -/// resume recovers the position from the id alone, so any replica can serve -/// it. Ids below the stride carry no position: they are the plain frame -/// counters earlier data planes issued, and a resume from one still follows -/// the live tail behind [`resume_resync_frame`]. -pub(crate) const PROCESS_EVENT_ID_STRIDE: u64 = 1_000_000; - -/// The guest output position a process stream's SSE id resumes from, or -/// `None` for an id that carries none. -pub(crate) fn process_event_output_position(event_id: u64) -> Option { - (event_id / PROCESS_EVENT_ID_STRIDE).checked_sub(1) +/// On the wire, bit 62 tags the id: no frame counter an earlier data plane +/// issued reaches it. The low [`PROCESS_EVENT_ID_GUARD_BITS`] bits are zero, +/// so an id that a data plane without this encoding counted on from by `+1`, +/// or that a client rounded through a floating-point number, fails to decode +/// rather than decoding to the wrong position. Above them come a frame counter +/// and a position field, whose top bit marks a block of ids without a position. +/// Those blocks sort above every position, so a stream that loses its +/// position keeps its ids increasing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ProcessEventId { + /// The client has received the guest's output up to `position`. + Position { position: u64, frame: u64 }, + /// The id carries no position; a resume from it cannot be exact. + Unknown { block: u64, frame: u64 }, } -/// The SSE id of a process stream's frame after `previous`, once the client -/// has received output up to `output_position`. -/// -/// While the position is unknown, ids stay among those of the last position -/// the stream knew. The guest has evicted that position, or the stream never -/// had one, so a resume from such an id reports the gap again instead of -/// replaying output the client already has. -pub(crate) fn next_process_event_id(previous: u64, output_position: Option) -> u64 { - let first = output_position - .and_then(|position| { - position - .checked_add(1)? - .checked_mul(PROCESS_EVENT_ID_STRIDE) +const PROCESS_EVENT_ID_TAG: u64 = 1 << 62; +pub(crate) const PROCESS_EVENT_ID_GUARD_BITS: u32 = 16; +const PROCESS_EVENT_ID_FRAME_BITS: u32 = 12; +const PROCESS_EVENT_ID_FIELD_BITS: u32 = 34; +const PROCESS_EVENT_ID_FIELD_SHIFT: u32 = PROCESS_EVENT_ID_GUARD_BITS + PROCESS_EVENT_ID_FRAME_BITS; +pub(crate) const PROCESS_EVENT_ID_MAX_FRAME: u64 = (1 << PROCESS_EVENT_ID_FRAME_BITS) - 1; +const PROCESS_EVENT_ID_UNKNOWN: u64 = 1 << (PROCESS_EVENT_ID_FIELD_BITS - 1); +pub(crate) const PROCESS_EVENT_ID_MAX_POSITION: u64 = PROCESS_EVENT_ID_UNKNOWN - 1; + +impl ProcessEventId { + /// Precedes the first frame of a stream that starts at the beginning of + /// the guest's output. Never sent. + pub(crate) const START: Self = Self::Position { + position: 0, + frame: 0, + }; + + pub(crate) fn encode(self) -> u64 { + let (field, frame) = match self { + Self::Position { position, frame } => (position, frame), + Self::Unknown { block, frame } => (PROCESS_EVENT_ID_UNKNOWN | block, frame), + }; + PROCESS_EVENT_ID_TAG + | field << PROCESS_EVENT_ID_FIELD_SHIFT + | frame << PROCESS_EVENT_ID_GUARD_BITS + } + + /// `None` for an id this encoding did not produce. + pub(crate) fn decode(id: u64) -> Option { + if id >> 62 != 1 || id & ((1 << PROCESS_EVENT_ID_GUARD_BITS) - 1) != 0 { + return None; + } + let frame = (id >> PROCESS_EVENT_ID_GUARD_BITS) & PROCESS_EVENT_ID_MAX_FRAME; + let field = (id >> PROCESS_EVENT_ID_FIELD_SHIFT) & ((1 << PROCESS_EVENT_ID_FIELD_BITS) - 1); + Some(if field & PROCESS_EVENT_ID_UNKNOWN == 0 { + Self::Position { + position: field, + frame, + } + } else { + Self::Unknown { + block: field & !PROCESS_EVENT_ID_UNKNOWN, + frame, + } }) - .filter(|first| *first <= auth::MAX_LAST_EVENT_ID) - .unwrap_or(previous - previous % PROCESS_EVENT_ID_STRIDE); - let last = first - .saturating_add(PROCESS_EVENT_ID_STRIDE - 1) - .min(auth::MAX_LAST_EVENT_ID); - previous.saturating_add(1).clamp(first, last) + } + + /// The id of the next frame, once the client has received output up to + /// `position`. Always greater than this one. + pub(crate) fn next(self, position: Option) -> Self { + match self { + Self::Position { + position: last, + frame, + } => match position { + Some(position) if position == last && frame < PROCESS_EVENT_ID_MAX_FRAME => { + Self::Position { + position, + frame: frame + 1, + } + } + Some(position) if position > last && position <= PROCESS_EVENT_ID_MAX_POSITION => { + Self::Position { position, frame: 0 } + } + // The position is unknown, beyond the field, behind this one, + // or out of frames: no later id of this stream carries one, and + // a resume from them replays behind `resync_required`. + _ => Self::Unknown { block: 0, frame: 0 }, + }, + Self::Unknown { block, frame } if frame < PROCESS_EVENT_ID_MAX_FRAME => Self::Unknown { + block, + frame: frame + 1, + }, + // A stream would need 2^45 frames to exhaust the blocks. + Self::Unknown { block, .. } => Self::Unknown { + block: block + 1, + frame: 0, + }, + } + } } impl ProcessReplayState { /// Where a process stream starts in the guest's output journal: a fresh /// attach replays the retained output from the start, and a resume - /// continues from the position its `Last-Event-ID` carries. + /// continues from the position its `Last-Event-ID` carries. A resume from + /// an id without one also replays from the start, behind + /// `resync_required(cursor_replay_unavailable)`, as output the client + /// already has may repeat. pub(crate) fn load(session_id: DataPlaneSessionId, last_event_id: Option) -> Self { - let mut replay = Self { - event_seq: last_event_id.unwrap_or_default(), - backend_output_cursor: last_event_id.map_or(Some(0), process_event_output_position), - pending: VecDeque::new(), - }; - if replay.backend_output_cursor.is_none() - && let Some((event_seq, frame)) = - resume_resync_frame(session_id.kind().as_str(), last_event_id) - { - replay.event_seq = event_seq; - replay - .pending - .push_back(sse_event_from_frame(&frame, Some(event_seq))); + match last_event_id.map(ProcessEventId::decode) { + None => Self { + event_id: ProcessEventId::START, + backend_output_cursor: 0, + pending: VecDeque::new(), + }, + Some(Some(event_id @ ProcessEventId::Position { position, .. })) => Self { + event_id, + backend_output_cursor: position, + pending: VecDeque::new(), + }, + Some(_) => { + let event_id = ProcessEventId::START.next(Some(0)); + let frame = resync_required_frame( + session_id.kind().as_str(), + DataPlaneResyncReason::CursorReplayUnavailable, + ); + Self { + event_id, + backend_output_cursor: 0, + pending: VecDeque::from([sse_event_from_frame( + &frame, + Some(event_id.encode()), + )]), + } + } } - replay } } @@ -331,12 +409,13 @@ impl ProcessSseState { start_permit: Option, ) -> Result { let replay = ProcessReplayState::load(session_id, last_event_id); - let backend_output_cursor = replay - .backend_output_cursor - .or_else(|| attached.backend_process_id.is_none().then_some(0)); - let (client, stream) = - open_native_process_stream(&state, session_id, &mut attached, backend_output_cursor) - .await?; + let (client, stream) = open_native_process_stream( + &state, + session_id, + &mut attached, + Some(replay.backend_output_cursor), + ) + .await?; let mut pending = VecDeque::new(); pending.push_back(sse_event_from_frame(&attached_frame(&attached), None)); pending.extend(replay.pending); @@ -351,12 +430,13 @@ impl ProcessSseState { attached, client, stream, - event_seq: replay.event_seq, - backend_output_cursor, - output_seq: backend_output_cursor.unwrap_or_default(), + event_id: replay.event_id, + backend_output_cursor: Some(replay.backend_output_cursor), + output_seq: replay.backend_output_cursor, process_id, process_bound, binding: None, + reopened_after_gap: false, revocation_tick, pending, finished: false, @@ -367,9 +447,9 @@ impl ProcessSseState { } fn push_frame(&mut self, frame: DataPlaneServerFrame) { - self.event_seq = next_process_event_id(self.event_seq, self.backend_output_cursor); + self.event_id = self.event_id.next(self.backend_output_cursor); self.pending - .push_back(sse_event_from_frame(&frame, Some(self.event_seq))); + .push_back(sse_event_from_frame(&frame, Some(self.event_id.encode()))); } fn finish(&mut self, result: &'static str) { @@ -474,11 +554,13 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { }; match event { Ok(event) => { + let mut repeated_start = false; if matches!( event.event, Some(spaced_process_event::Event::Started(_)) | Some(spaced_process_event::Event::Connected(_)) ) { + repeated_start = std::mem::take(&mut state.reopened_after_gap); let event_process_id: ProcessId = match event.process_id.parse() { Ok(process_id) => process_id, Err(_) => { @@ -490,6 +572,22 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { }; state.process_id = Some(event_process_id.clone()); if !state.process_bound { + if matches!( + event.event, + Some(spaced_process_event::Event::Started(_)) + ) && state.backend_output_cursor != Some(0) + { + // This attach started the process, so its + // output begins here: the position the + // client resumed from was another's. + state.backend_output_cursor = Some(0); + state.output_seq = 0; + state.event_id = ProcessEventId::START; + state.push_frame(resync_required_frame( + state.session_id.kind().as_str(), + DataPlaneResyncReason::CursorOutOfWindow, + )); + } state.process_bound = true; state.binding = Some(PendingProcessBinding::spawn( &state.state, @@ -500,7 +598,12 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } } let mut output_seq = state.output_seq; - let batch = process_event_frames(event, &mut output_seq); + let mut batch = process_event_frames(event, &mut output_seq); + if repeated_start { + batch.frames.retain(|frame| { + !matches!(frame, DataPlaneServerFrame::ProcessStarted { .. }) + }); + } if batch .frames .iter() @@ -532,14 +635,20 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { return Some(event); } } - Err(status) if is_output_journal_gap(&status) => { - // The guest no longer retains the output this stream - // asked for. Report the gap, then follow the live tail. + Err(status) if is_output_window_miss(&status) => { + // The guest does not hold the output this stream asked + // for. Report the gap, then reopen: from the start of + // the output when the cursor was past its end, else at + // the live tail, as what was evicted is gone. let Some(process_id) = state.process_id.clone() else { state.finish_with_error(StreamBridgeError::BackendStatus(status)); return state.pending.pop_front(); }; - state.backend_output_cursor = None; + let reopen_at = (is_output_cursor_ahead(&status) + && state.backend_output_cursor != Some(0)) + .then_some(0); + state.backend_output_cursor = reopen_at; + state.output_seq = reopen_at.unwrap_or(state.output_seq); state.push_frame(resync_required_frame( state.session_id.kind().as_str(), DataPlaneResyncReason::CursorOutOfWindow, @@ -548,12 +657,15 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { &mut state.client, &state.attached.sandbox_id, &process_id, - None, + reopen_at, state.attached.space_api_route_token.as_ref(), ) .await { - Ok(stream) => state.stream = stream, + Ok(stream) => { + state.stream = stream; + state.reopened_after_gap = true; + } Err(error) => state.finish_with_error(error), } return state.pending.pop_front(); @@ -652,12 +764,24 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } } -/// The guest ends a process stream with `resource_exhausted` once the output -/// its cursor reaches has been evicted. The node's per-sandbox limiter uses the -/// same code, but it refuses a call before any stream exists. -fn is_output_journal_gap(status: &tonic::Status) -> bool { - status.code() == tonic::Code::ResourceExhausted - && pplx_space_common_grpc::UdsPerSandboxLimitExceeded::from_status(status).is_none() +/// Whether the guest ended a process stream because its cursor is outside the +/// retained output: `resource_exhausted` once that output has been evicted (the +/// node's per-sandbox limiter uses the same code, but it refuses a call before +/// any stream exists), or a cursor past the end of the output. +fn is_output_window_miss(status: &tonic::Status) -> bool { + is_output_cursor_ahead(status) + || (status.code() == tonic::Code::ResourceExhausted + && pplx_space_common_grpc::UdsPerSandboxLimitExceeded::from_status(status).is_none()) +} + +/// A guest older than this check waits for output to reach such a cursor. +fn is_output_cursor_ahead(status: &tonic::Status) -> bool { + status.code() == tonic::Code::OutOfRange + && status + .metadata() + .get(pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY) + .and_then(|value| value.to_str().ok()) + == Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD) } pub(crate) struct FileWatchSseState { diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index e88b9ea41e..a4c9d81c11 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -1716,6 +1716,21 @@ enum FakeReply { Event(spaced_process_event::Event), /// Ends the stream with this status. Status(tonic::Code), + /// Ends the stream as the guest does for a cursor past the output. + CursorAhead, +} + +/// How the data plane opened a fake guest process stream. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum FakeOpen { + Start, + Connect(Option), +} + +fn started() -> FakeReply { + FakeReply::Event(spaced_process_event::Event::Started( + pplx_space_sbx_mgr_client::operations::ProcessStarted { start_time: None }, + )) } fn connected() -> FakeReply { @@ -1744,31 +1759,41 @@ fn exited() -> FakeReply { )) } -/// What the guest replays for `output_cursor` from a process that wrote -/// `lines` and exited: the output from that position on, or, without a -/// cursor, only the start and the exit. -fn journal_replay(lines: &[&str], output_cursor: Option) -> Vec { - let Some(cursor) = output_cursor else { - return vec![connected(), exited()]; +/// What the guest sends for `open` from a process that wrote `lines` and +/// exited: a start or a cursor replays the output from that position on, a +/// cursor past the output fails, and a connect without a cursor gets only the +/// start and the exit. +fn journal_replay(lines: &[&str], open: FakeOpen) -> Vec { + let (mut replies, cursor) = match open { + FakeOpen::Start => (vec![started()], 0), + FakeOpen::Connect(None) => return vec![started(), exited()], + FakeOpen::Connect(Some(cursor)) if cursor as usize > lines.len() => { + return vec![connected(), FakeReply::CursorAhead]; + } + FakeOpen::Connect(Some(cursor)) => (vec![connected()], cursor), }; - let mut replies = vec![connected()]; replies.extend(lines.iter().skip(cursor as usize).map(|line| stdout(line))); replies.push(exited()); replies } -type FakeReplies = Arc) -> Vec + Send + Sync>; +type FakeReplies = Arc Vec + Send + Sync>; -/// A fake cluster API and worker for a native process session whose process -/// is already bound. Each `SpacedConnectProcess` is answered by `replies`, -/// given how many connects came before it and its `output_cursor`; a stream +/// A fake cluster API and worker for one native process session, bound to its +/// process unless `bound` is false. Each process stream the data plane opens +/// is answered by `replies`, given how many opens came before it; a stream /// without a closing status stays open. struct JournalFake { endpoint: String, - output_cursors: Arc>>>, + bound: Arc, + opens: Arc>>, } -async fn spawn_journal_fake(session_id: DataPlaneSessionId, replies: FakeReplies) -> JournalFake { +async fn spawn_journal_fake( + session_id: DataPlaneSessionId, + bound: bool, + replies: FakeReplies, +) -> JournalFake { use prost::Message as _; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let endpoint = format!("http://{}", listener.local_addr().unwrap()); @@ -1776,48 +1801,61 @@ async fn spawn_journal_fake(session_id: DataPlaneSessionId, replies: FakeReplies let mut route = crate::tests::route_response_with_token(session_id, None); route.backend.as_mut().unwrap().endpoint = endpoint.clone(); route.adapter_state_json = r#"{"command":["true"]}"#.to_string(); - route.backend_process_id = Some(process_id.clone()); - let route = route.encode_to_vec(); - let output_cursors = Arc::new(std::sync::Mutex::new(Vec::new())); + let bound = Arc::new(AtomicBool::new(bound)); + let opens = Arc::new(std::sync::Mutex::new(Vec::new())); let fake = JournalFake { endpoint, - output_cursors: Arc::clone(&output_cursors), + bound: Arc::clone(&bound), + opens: Arc::clone(&opens), }; let service = axum::Router::new().fallback(move |request: Request| { - let route = route.clone(); + let mut route = route.clone(); let process_id = process_id.clone(); let replies = Arc::clone(&replies); - let output_cursors = Arc::clone(&output_cursors); + let opens = Arc::clone(&opens); + let bound = Arc::clone(&bound); async move { let path = request.uri().path().to_string(); if path.ends_with("/Check") { return crate::tests::grpc_test_response(vec![8, 1]); } if path.ends_with("/ResolveDataPlaneSessionRoute") { - return crate::tests::grpc_test_response(route); + route.backend_process_id = bound.load(Ordering::SeqCst).then(|| process_id.clone()); + return crate::tests::grpc_test_response(route.encode_to_vec()); + } + if path.ends_with("/BindDataPlaneSessionProcess") { + bound.store(true, Ordering::SeqCst); } - if path.ends_with("/RecordDataPlaneSessionReconnect") { + if path.ends_with("/RecordDataPlaneSessionReconnect") + || path.ends_with("/BindDataPlaneSessionProcess") + { return crate::tests::grpc_test_response(Vec::new()); } - assert!( - path.ends_with("/SpacedConnectProcess"), - "unexpected call {path}" - ); - let body = request.into_body().collect().await.unwrap().to_bytes(); - let output_cursor = SpacedConnectProcessRequest::decode(&body[5..]) - .unwrap() - .operation - .unwrap() - .output_cursor; - let connects = { - let mut output_cursors = output_cursors.lock().unwrap(); - output_cursors.push(output_cursor); - output_cursors.len() - 1 + let open = if path.ends_with("/SpacedStartProcess") { + FakeOpen::Start + } else { + assert!( + path.ends_with("/SpacedConnectProcess"), + "unexpected call {path}" + ); + let body = request.into_body().collect().await.unwrap().to_bytes(); + FakeOpen::Connect( + SpacedConnectProcessRequest::decode(&body[5..]) + .unwrap() + .operation + .unwrap() + .output_cursor, + ) + }; + let opened = { + let mut opens = opens.lock().unwrap(); + opens.push(open); + opens.len() - 1 }; let mut frames = Vec::new(); - let mut status = None; - for reply in replies(connects, output_cursor) { - match reply { + let mut trailers = None; + for reply in replies(opened, open) { + let (code, reason) = match reply { FakeReply::Event(event) => { let encoded = ProcessEvent { process_id: process_id.clone(), @@ -1829,14 +1867,26 @@ async fn spawn_journal_fake(session_id: DataPlaneSessionId, replies: FakeReplies framed.extend_from_slice(&(encoded.len() as u32).to_be_bytes()); framed.extend(encoded); frames.push(hyper::body::Frame::data(bytes::Bytes::from(framed))); + continue; } - FakeReply::Status(code) => status = Some(code), + FakeReply::Status(code) => (code, None), + FakeReply::CursorAhead => ( + tonic::Code::OutOfRange, + Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD), + ), + }; + let mut status = HeaderMap::new(); + status.insert("grpc-status", HeaderValue::from(code as i32)); + if let Some(reason) = reason { + status.insert( + pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY, + HeaderValue::from_static(reason), + ); } + trailers = Some(status); } - let ending = match status { - Some(code) => { - let mut trailers = HeaderMap::new(); - trailers.insert("grpc-status", HeaderValue::from(code as i32)); + let ending = match trailers { + Some(trailers) => { futures_util::stream::once( async move { hyper::body::Frame::trailers(trailers) }, ) @@ -1930,6 +1980,10 @@ async fn process_events_to_close( const JOURNAL: [&str; 4] = ["one\n", "two\n", "three\n", "four\n"]; +fn connects(cursors: &[Option]) -> Vec { + cursors.iter().copied().map(FakeOpen::Connect).collect() +} + /// A client that drops its first stream, and so never sends `Last-Event-ID`, /// still gets the process's whole retained output when it attaches again. #[tokio::test] @@ -1940,7 +1994,8 @@ async fn process_reattach_replays_retained_output_from_the_start() { let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_journal_fake( session_id, - Arc::new(|_, output_cursor| journal_replay(&JOURNAL, output_cursor)), + true, + Arc::new(|_, open| journal_replay(&JOURNAL, open)), ) .await; @@ -1948,7 +2003,7 @@ async fn process_reattach_replays_retained_output_from_the_start() { assert_eq!(stdout_text(&frames), JOURNAL.concat()); assert_eq!(resync_reasons(&frames), Vec::::new()); - assert_eq!(*fake.output_cursors.lock().unwrap(), [Some(0)]); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0)])); } /// `Last-Event-ID` resumes at the output after the last frame received: the @@ -1961,7 +2016,8 @@ async fn process_resume_continues_after_the_last_event_id() { let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_journal_fake( session_id, - Arc::new(|_, output_cursor| journal_replay(&JOURNAL, output_cursor)), + true, + Arc::new(|_, open| journal_replay(&JOURNAL, open)), ) .await; let first = process_events_to_close(&fake.endpoint, session_id, None).await; @@ -1983,12 +2039,13 @@ async fn process_resume_continues_after_the_last_event_id() { .filter_map(|(id, _)| *id) .all(|id| id > last_event_id) ); - assert_eq!(*fake.output_cursors.lock().unwrap(), [Some(0), Some(2)]); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0), Some(2)])); } /// Output the guest evicted before the client asked for it is a real gap: -/// the stream reports it, follows the live tail, and a resume from what it -/// sent next reports the gap again rather than replaying output. +/// the stream reports it once, follows the live tail without reporting the +/// process start again, and a resume from what it sent next carries no +/// position, so it replays behind `resync_required` rather than silently. #[tokio::test] async fn evicted_process_output_is_reported_before_the_live_tail() { if !binding_fake_test_process() { @@ -1997,13 +2054,14 @@ async fn evicted_process_output_is_reported_before_the_live_tail() { let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_journal_fake( session_id, - Arc::new(|_, output_cursor| match output_cursor { - Some(cursor) if cursor < 2 => vec![ + true, + Arc::new(|_, open| match open { + FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![ connected(), FakeReply::Status(tonic::Code::ResourceExhausted), ], - Some(cursor) => journal_replay(&JOURNAL, Some(cursor)), - None => vec![connected(), stdout("live\n"), exited()], + FakeOpen::Connect(None) => vec![connected(), stdout("live\n"), exited()], + open => journal_replay(&JOURNAL, open), }), ) .await; @@ -2011,6 +2069,7 @@ async fn evicted_process_output_is_reported_before_the_live_tail() { let frames = process_events_to_close(&fake.endpoint, session_id, None).await; assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); assert_eq!(stdout_text(&frames), "live\n"); + assert_eq!(frames_of_type(&frames, "process_started").count(), 1); assert!( frames_of_type(&frames, "error").next().is_none(), "{frames:?}" @@ -2018,10 +2077,13 @@ async fn evicted_process_output_is_reported_before_the_live_tail() { let last_event_id = frames.last().unwrap().0.unwrap(); let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; - assert_eq!(resync_reasons(&resumed), ["cursor_out_of_window"]); assert_eq!( - *fake.output_cursors.lock().unwrap(), - [Some(0), None, Some(0), None] + resync_reasons(&resumed), + ["cursor_replay_unavailable", "cursor_out_of_window"] + ); + assert_eq!( + *fake.opens.lock().unwrap(), + connects(&[Some(0), None, Some(0), None]) ); } @@ -2036,14 +2098,15 @@ async fn backend_reconnect_after_a_resume_continues_without_a_resync() { let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); let fake = spawn_journal_fake( session_id, - Arc::new(|connects, output_cursor| match (connects, output_cursor) { + true, + Arc::new(|opened, open| match (opened, open) { // The resume's stream drops after one line. - (1, Some(cursor)) => vec![ + (1, FakeOpen::Connect(Some(cursor))) => vec![ connected(), stdout(JOURNAL[cursor as usize]), FakeReply::Status(tonic::Code::Unavailable), ], - (_, output_cursor) => journal_replay(&JOURNAL, output_cursor), + (_, open) => journal_replay(&JOURNAL, open), }), ) .await; @@ -2061,7 +2124,117 @@ async fn backend_reconnect_after_a_resume_continues_without_a_resync() { assert_eq!(frames_of_type(&resumed, "backend_reconnected").count(), 1); assert_eq!(resync_reasons(&resumed), Vec::::new()); assert_eq!( - *fake.output_cursors.lock().unwrap(), - [Some(0), Some(1), Some(2)] + *fake.opens.lock().unwrap(), + connects(&[Some(0), Some(1), Some(2)]) + ); +} + +/// A `Last-Event-ID` past the process's output (forged, or another +/// session's) cannot leave the stream waiting for output to reach it: the +/// guest refuses it, and the stream replays the output from the start behind +/// `resync_required`. +#[tokio::test] +async fn resume_past_the_output_replays_it_behind_a_resync() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| journal_replay(&JOURNAL, open)), + ) + .await; + let ahead = ProcessEventId::Position { + position: 10, + frame: 0, + } + .encode(); + + let frames = process_events_to_close(&fake.endpoint, session_id, Some(ahead)).await; + + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), JOURNAL.concat()); + assert_eq!(frames_of_type(&frames, "process_started").count(), 1); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(10), Some(0)])); +} + +/// An id without a position, such as one an earlier data plane issued, is +/// never read as one: the stream replays the retained output behind +/// `resync_required`. +#[tokio::test] +async fn resume_from_an_untagged_id_replays_behind_a_resync() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| journal_replay(&JOURNAL, open)), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, Some(1_500_000)).await; + + assert_eq!(resync_reasons(&frames), ["cursor_replay_unavailable"]); + assert_eq!(stdout_text(&frames), JOURNAL.concat()); + assert!( + frames + .iter() + .filter_map(|(id, _)| *id) + .all(|id| id > 1_500_000) + ); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0)])); +} + +/// The attach that starts a process counts its output from the start, even +/// when the client sent another stream's `Last-Event-ID`: the stream says the +/// id did not apply, and a later resume continues exactly. +#[tokio::test] +async fn starting_attach_ignores_a_foreign_last_event_id() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + false, + Arc::new(|_, open| match open { + // Still running: the stream stays open after two lines. + FakeOpen::Start => vec![started(), stdout(JOURNAL[0]), stdout(JOURNAL[1])], + open => journal_replay(&JOURNAL, open), + }), + ) + .await; + let foreign = ProcessEventId::Position { + position: 1, + frame: 0, + } + .encode(); + let (_state, mut chunks) = open_process_events(&fake.endpoint, session_id, Some(foreign)).await; + let mut seen = String::new(); + read_until(&mut chunks, &mut seen, &BASE64_STANDARD.encode(JOURNAL[1])).await; + read_until(&mut chunks, &mut seen, "\n\n").await; + drop(chunks); + tokio::time::timeout(Duration::from_secs(5), async { + while !fake.bound.load(Ordering::SeqCst) { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("the starting attach binds the process"); + let first = sse_frames(&seen); + assert_eq!(resync_reasons(&first), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&first), "one\ntwo\n"); + + let last_event_id = first.last().unwrap().0.unwrap(); + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; + + assert_eq!(stdout_text(&resumed), "three\nfour\n"); + assert_eq!(resync_reasons(&resumed), Vec::::new()); + assert_eq!( + *fake.opens.lock().unwrap(), + [FakeOpen::Start, FakeOpen::Connect(Some(2))] ); } diff --git a/crates/libs/data/plane/server/src/tests.rs b/crates/libs/data/plane/server/src/tests.rs index ab57e84a60..3b81d5aec4 100644 --- a/crates/libs/data/plane/server/src/tests.rs +++ b/crates/libs/data/plane/server/src/tests.rs @@ -1,8 +1,8 @@ use super::*; use crate::{ native::{ - PROCESS_EVENT_ID_STRIDE, ProcessReplayState, first_frame_attach_token_from_recv, - next_process_event_id, process_event_output_position, + PROCESS_EVENT_ID_GUARD_BITS, PROCESS_EVENT_ID_MAX_FRAME, PROCESS_EVENT_ID_MAX_POSITION, + ProcessEventId, ProcessReplayState, first_frame_attach_token_from_recv, }, runtime_route::sweep_expired_ecw_routes, }; @@ -3705,71 +3705,130 @@ async fn first_frame_attach_token_times_out_at_deadline() { } /// A fresh attach replays the guest journal from its start, and a resume -/// continues from the output position its id carries. Only an id without a -/// position, as earlier data planes issued, still resumes the live tail -/// behind `resync_required`. +/// continues from the output position its id carries. An id without one, +/// including every id an earlier data plane issued, also replays from the +/// start, behind `resync_required`. #[test] fn process_sse_resume_starts_at_the_journal_position_its_id_carries() { let session_id = DataPlaneSessionId::Process(Default::default()); let fresh = ProcessReplayState::load(session_id, None); - assert_eq!(fresh.event_seq, 0); - assert_eq!(fresh.backend_output_cursor, Some(0)); + assert_eq!(fresh.backend_output_cursor, 0); assert!(fresh.pending.is_empty()); - let resumed = ProcessReplayState::load(session_id, Some(4 * PROCESS_EVENT_ID_STRIDE + 2)); - assert_eq!(resumed.event_seq, 4 * PROCESS_EVENT_ID_STRIDE + 2); - assert_eq!(resumed.backend_output_cursor, Some(3)); + let carried = ProcessEventId::Position { + position: 3, + frame: 2, + }; + let resumed = ProcessReplayState::load(session_id, Some(carried.encode())); + assert_eq!(resumed.event_id, carried); + assert_eq!(resumed.backend_output_cursor, 3); assert!(resumed.pending.is_empty()); - let legacy = ProcessReplayState::load(session_id, Some(41)); - assert_eq!(legacy.event_seq, 42); - assert_eq!(legacy.backend_output_cursor, None); - assert_eq!(legacy.pending.len(), 1); + let unknown = ProcessEventId::Unknown { block: 4, frame: 1 }.encode(); + // Earlier data planes count frames from 1, so their ids sit on either side + // of any decimal boundary; one counted on from a tagged id fails its guard. + for last_event_id in [ + 0, + 41, + 999_999, + 1_000_000, + 1_500_000, + unknown, + carried.encode() + 1, + ] { + let replay = ProcessReplayState::load(session_id, Some(last_event_id)); + assert_eq!(replay.backend_output_cursor, 0, "{last_event_id}"); + assert_eq!(replay.pending.len(), 1, "{last_event_id}"); + } } -/// Process SSE ids increase, and each decodes to the output position a -/// resume from it continues at. While the position is unknown the ids stay -/// with the last position known, never moving onto output the client lacks. +/// Process SSE ids increase on every frame, each decodes to the position the +/// client has received output up to, and once a stream loses that position no +/// later id of it carries one, however many frames follow. #[test] -fn process_event_ids_carry_the_output_position() { - let mut previous = 0; - let mut last_known = None; - for position in [ - Some(0), - Some(0), - Some(1), - Some(2), - Some(2), - None, - None, - Some(5), - ] { - let id = next_process_event_id(previous, position); - assert!(id > previous, "{id} after {previous}"); - last_known = position.or(last_known); - assert_eq!(process_event_output_position(id), last_known, "{id}"); +fn process_event_ids_increase_and_carry_the_output_position() { + let mut previous = ProcessEventId::START; + let mut lost = false; + let positions = [Some(0), Some(0), Some(1), Some(2)] + .into_iter() + .chain(std::iter::repeat_n( + Some(2), + PROCESS_EVENT_ID_MAX_FRAME as usize + 1, + )) + .chain([Some(3), None, Some(4)]) + .chain(std::iter::repeat_n( + None, + PROCESS_EVENT_ID_MAX_FRAME as usize + 1, + )); + for position in positions { + let id = previous.next(position); + assert!(id.encode() > previous.encode(), "{id:?} after {previous:?}"); + assert!(id.encode() <= MAX_LAST_EVENT_ID); + assert_eq!(ProcessEventId::decode(id.encode()), Some(id)); + lost |= !matches!(id, ProcessEventId::Position { .. }); + match id { + ProcessEventId::Position { + position: carried, .. + } => { + assert!(!lost, "{id:?} after the position was lost"); + assert_eq!(Some(carried), position); + } + ProcessEventId::Unknown { .. } => assert!(lost), + } previous = id; } + assert!(lost, "running out of frames at one position loses it"); - // Ids without a position stay without one. - assert_eq!( - process_event_output_position(next_process_event_id(42, None)), - None - ); - // Frame counts saturate inside their position rather than spill into the next. - let last = 2 * PROCESS_EVENT_ID_STRIDE - 1; - assert_eq!(next_process_event_id(last, Some(0)), last); - assert_eq!(next_process_event_id(last, None), last); - // Every id stays a valid `Last-Event-ID`. - assert!(next_process_event_id(0, Some(u64::MAX)) <= MAX_LAST_EVENT_ID); - assert!(next_process_event_id(MAX_LAST_EVENT_ID, Some(u64::MAX)) <= MAX_LAST_EVENT_ID); + for (from, position) in [ + // A position the field cannot hold, and one behind the last. + ( + ProcessEventId::START, + Some(PROCESS_EVENT_ID_MAX_POSITION + 1), + ), + ( + ProcessEventId::Position { + position: 5, + frame: 0, + }, + Some(4), + ), + ] { + let id = from.next(position); + assert!(matches!(id, ProcessEventId::Unknown { .. }), "{id:?}"); + assert!(id.encode() > from.encode()); + } + + let highest = ProcessEventId::Unknown { + block: PROCESS_EVENT_ID_MAX_POSITION, + frame: PROCESS_EVENT_ID_MAX_FRAME, + }; + assert!(highest.encode() <= MAX_LAST_EVENT_ID); + + // An id off by less than the guard, as `+1` counting or rounding through + // a double leaves it, carries no position. + let id = ProcessEventId::Position { + position: 7, + frame: 3, + } + .encode(); + for mangled in [ + id + 1, + id - 1, + id + 96, + id - 512, + id + (1 << PROCESS_EVENT_ID_GUARD_BITS) - 1, + ] { + assert_eq!(ProcessEventId::decode(mangled), None, "{mangled}"); + } + // Unrounded, a double holds the id exactly. + let double: f64 = id.to_string().parse().unwrap(); + assert_eq!(format!("{double:.0}"), id.to_string()); } -/// The file-watch reconnection contract, which a process session keeps for an -/// id without an output position: `Last-Event-ID` resumes the live stream at -/// `last + 1` behind a `resync_required(cursor_replay_unavailable)` frame; no -/// cursor, no frame. +/// The file-watch reconnection contract: `Last-Event-ID` resumes the live +/// stream at `last + 1` behind a `resync_required(cursor_replay_unavailable)` +/// frame; no cursor, no frame. #[test] fn last_event_id_resume_is_a_cursor_replay_unavailable_resync() { for kind in ["process", "file_watch"] { diff --git a/crates/libs/spaced/guest/execution/src/process.rs b/crates/libs/spaced/guest/execution/src/process.rs index 1e5fc291eb..5d0163b9e3 100644 --- a/crates/libs/spaced/guest/execution/src/process.rs +++ b/crates/libs/spaced/guest/execution/src/process.rs @@ -81,6 +81,8 @@ const PROCESS_OUTPUT_SHARED_BYTES: usize = const PROCESS_OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_secs(1); const PROCESS_OUTPUT_LAGGED_MESSAGE: &str = "process output stream fell behind retained output buffer"; +const PROCESS_OUTPUT_CURSOR_AHEAD_MESSAGE: &str = + "process output cursor is past the output the process has written"; const PROCESS_CAPACITY_MESSAGE: &str = "too many retained processes"; // A start phase slower than this is logged with every phase's duration once // the spawn returns, so a slow start names its cause. The account lookup @@ -1211,6 +1213,12 @@ impl ProcessEventJournal { if cursor < state.first_sequence { return ProcessJournalRead::Lagged; } + // A cursor only ever advances over output it has read, so one past the + // end came from a caller, not from this journal: waiting for output to + // reach it would skip everything written before it. + if cursor > state.next_sequence { + return ProcessJournalRead::Ahead; + } if cursor < state.next_sequence { let offset = (cursor - state.first_sequence) as usize; return ProcessJournalRead::Event( @@ -1281,6 +1289,7 @@ enum ProcessJournalRead { Exit(ProcessEvent), Pending, Lagged, + Ahead, } #[derive(Clone, Copy)] @@ -1543,6 +1552,7 @@ fn process_event_stream_with_prefix( ProcessJournalRead::Lagged => { break Err(Status::resource_exhausted(PROCESS_OUTPUT_LAGGED_MESSAGE)); } + ProcessJournalRead::Ahead => break Err(process_output_cursor_ahead_status()), ProcessJournalRead::Pending => { if current.changed.changed().await.is_err() { break evicted_process_journal_result(current); @@ -1555,6 +1565,17 @@ fn process_event_stream_with_prefix( )) } +fn process_output_cursor_ahead_status() -> Status { + let mut status = Status::out_of_range(PROCESS_OUTPUT_CURSOR_AHEAD_MESSAGE); + status.metadata_mut().insert( + pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY, + tonic::metadata::MetadataValue::from_static( + pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD, + ), + ); + status +} + fn evicted_process_journal_result(cursor: &ProcessJournalCursor) -> Result { match cursor.changed.borrow().clone() { ProcessJournalChange::Exit { diff --git a/crates/libs/spaced/guest/execution/src/process/tests.rs b/crates/libs/spaced/guest/execution/src/process/tests.rs index d792107bed..87ded94632 100644 --- a/crates/libs/spaced/guest/execution/src/process/tests.rs +++ b/crates/libs/spaced/guest/execution/src/process/tests.rs @@ -904,6 +904,55 @@ async fn evicted_cursor_reports_lag_before_terminal_event() { ); } +/// A cursor past the written output fails at once, whether the process is +/// still running or has exited, rather than waiting for output to reach it and +/// skipping everything written before. The end of the output stays valid. +#[tokio::test] +async fn cursor_past_the_output_fails_instead_of_waiting() { + for completed in [false, true] { + let journal = Arc::new(ProcessEventJournal::new(output_slots())); + let process_id = ProcessId::new(); + journal.append_output(process_stdout_event( + &process_id, + Some(1), + b"written".to_vec(), + )); + let exit = process_stdout_event(&process_id, Some(1), b"terminal".to_vec()); + if completed { + journal.complete(exit.clone()); + } + + let mut ahead = process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(5)))); + let error = tokio::time::timeout(Duration::from_secs(1), ahead.next()) + .await + .expect("a cursor past the output must not wait") + .unwrap() + .unwrap_err(); + assert_eq!( + error.code(), + tonic::Code::OutOfRange, + "completed: {completed}" + ); + assert_eq!( + error + .metadata() + .get(pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY) + .and_then(|value| value.to_str().ok()), + Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD) + ); + assert!(ahead.next().await.is_none()); + + let mut at_end = + process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(1)))); + let next = tokio::time::timeout(Duration::from_millis(100), at_end.next()).await; + if completed { + assert_eq!(next.unwrap().unwrap().unwrap(), exit); + } else { + assert!(next.is_err(), "the end of a running process's output waits"); + } + } +} + #[tokio::test] async fn login_tty_pty_supports_output_input_and_resize() { let prepared = prepare_pty(Some(PtySize { rows: 24, cols: 80 })).expect("open test pty"); diff --git a/docs/first-sandbox.md b/docs/first-sandbox.md index 309d625437..1eb82abec6 100644 --- a/docs/first-sandbox.md +++ b/docs/first-sandbox.md @@ -85,7 +85,7 @@ Read `/openapi-m1.json` for exact request constraints and supported fields. This The example displays decoded stdout and stderr and saves the raw event stream in its run directory. Handle binary output separately rather than displaying it in a terminal. -Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID` resumes with the output after that event while it is retained, and otherwise produces `resync_required` before live output. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. +Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID`, set to the last event's `id` exactly as received, resumes with the output after that event while it is retained, and otherwise produces `resync_required` first. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. ## Inspect and manage the sandbox diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d4140f5365..3c24147d68 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -78,7 +78,7 @@ This prints sandbox listings and the saved creation/deletion operations, sandbox | Stream authentication fails | Use `attach.token`, not `SPACE_API_TOKEN`, and the correct session URL. Check expiry and attach promptly after creating the command. | | Example rejects an endpoint, events URL, or redirect | Use HTTPS except for loopback HTTP. Check the data-plane origin and session ID; correct the configuration rather than removing destination or redirect checks. | | HTTP stream succeeds but output is incomplete | Check for `process_exited`, its exit result, and any `error` or `resync_required` frame. A closed connection is not proof of successful execution. | -| Reconnect reports `resync_required` | Output from before the reconnect is no longer retained, so some of it was missed. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | +| Reconnect reports `resync_required` | The stream could not resume exactly: output from before the reconnect may be missing or repeated. Check that the client sends the last event `id` exactly as received. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | | Command exits nonzero | Read stderr and the exit result from the saved event transcript. Check that the executable and working directory exist inside the sandbox, not just on the host. | | Single-process GET returns `not_found` | This endpoint inspects a running process, not historical command results. It returns `not_found` before attachment and after exit. | diff --git a/scripts/tests/smoke-spaced-rest.sh b/scripts/tests/smoke-spaced-rest.sh index bbebb8c47f..09ea79f19d 100755 --- a/scripts/tests/smoke-spaced-rest.sh +++ b/scripts/tests/smoke-spaced-rest.sh @@ -612,7 +612,8 @@ function parseEvents(buffer) { else if (name === 'id') id = value; else if (name === 'data') data.push(value); } - if (data.length) events.push({ event, id: id === null ? null : Number(id), data: JSON.parse(data.join('\n')) }); + // Ids are opaque and exceed what a JavaScript number holds exactly. + if (data.length) events.push({ event, id, data: JSON.parse(data.join('\n')) }); } return { events, rest }; } @@ -621,7 +622,7 @@ function parseEvents(buffer) { // Every identified frame must carry a greater id than the one before. async function readStream(resumeFrom, onEvent) { const headers = { Accept: 'text/event-stream', Authorization: `Bearer ${command.attach.token}` }; - if (resumeFrom !== null) headers['Last-Event-ID'] = String(resumeFrom); + if (resumeFrom !== null) headers['Last-Event-ID'] = resumeFrom; const response = await fetch(eventsUrl(command), { headers }); if (!response.ok || !response.body) { throw new Error(`command event stream returned HTTP ${response.status}`); @@ -637,7 +638,7 @@ async function readStream(resumeFrom, onEvent) { buffer = rest; for (const event of events) { if (event.id !== null) { - if (lastEventId !== null && event.id <= lastEventId) eventIdsMonotonic = false; + if (lastEventId !== null && BigInt(event.id) <= BigInt(lastEventId)) eventIdsMonotonic = false; lastEventId = event.id; } if (event.event !== null && event.event !== event.data.type) eventIdsMonotonic = false; @@ -686,13 +687,13 @@ async function readStream(resumeFrom, onEvent) { // replaying output already received or asking to resync, and the exited // process still reports its exit. const resumeFrom = lastEventId; - const resume = { attached_first: null, first_id: null, resync: null, stdout: '', exit_code: null }; + const resume = { attached_first: null, ids_continue: null, resync: null, stdout: '', exit_code: null }; let seen = 0; await readStream(resumeFrom, (event) => { const frame = event.data; seen += 1; if (seen === 1) resume.attached_first = frame.type === 'attached' && event.id === null; - if (event.id !== null && resume.first_id === null) resume.first_id = event.id; + if (event.id !== null && resume.ids_continue === null) resume.ids_continue = BigInt(event.id) > BigInt(resumeFrom); if (frame.type === 'resync_required' && resume.resync === null) resume.resync = frame.reason; if (frame.type === 'stdout') resume.stdout += Buffer.from(frame.data_base64, 'base64').toString('utf8'); if (frame.type === 'process_exited') { @@ -843,7 +844,7 @@ assert_eq "$(jq -r '.event_ids_monotonic' <<<"$command_result")" "true" "command assert_eq "$(jq -r '.resume.attached_first' <<<"$command_result")" "true" "resumed stream starts with attached" assert_eq "$(jq -r '.resume.resync' <<<"$command_result")" "null" "lossless resume does not resync" assert_eq "$(jq -r '.resume.stdout' <<<"$command_result")" "" "resume does not replay received output" -assert_eq "$(jq -r '.resume.first_id > .resume_from' <<<"$command_result")" "true" "resumed ids continue after Last-Event-ID" +assert_eq "$(jq -r '.resume.ids_continue' <<<"$command_result")" "true" "resumed ids continue after Last-Event-ID" assert_eq "$(jq -r '.resume.exit_code' <<<"$command_result")" "6" "resumed stream reports the process exit" log "one-shot command event stream ok (Last-Event-ID resume verified)" From 9b5da65f6c4c3188d1d07f4e908830e4903211ee Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 19:27:54 +0000 Subject: [PATCH 03/12] data plane: keep process SSE ids above Last-Event-ID on every replay A resume from an id without a position, and an attach that started the process for a client holding another stream's id, replayed from the start of the guest output numbered from position 0, below ids the client had already seen. They now number those frames in a fresh block without a position above Last-Event-ID (positional ids from the start when the id is untagged, which sort above it already), so every id a resumed process stream sends is above Last-Event-ID. Both restarts and the cursor-ahead recovery share one path. A Last-Event-ID too close to the end of the id space to number a later frame above it is refused with 400. A backend reconnect after a route change no longer reports the process start a second time, as the gap reconnects already did not. --- crates/libs/api/src/routes/public_docs.rs | 2 +- .../src/routes/public_docs/schema_contract.rs | 2 +- .../libs/data/plane/server/src/native/sse.rs | 135 ++++++++++++------ .../data/plane/server/src/native/tests.rs | 107 ++++++++++++-- crates/libs/data/plane/server/src/tests.rs | 37 ++++- 5 files changed, 221 insertions(+), 62 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index 893829ad88..ac97cf20e4 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Treat it as an opaque string and echo it exactly: process stream ids are not consecutive and exceed the integers a double represents exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process, and its ids continue above `Last-Event-ID`. Without `Last-Event-ID` it starts from the oldest retained output, which can repeat output already received.\n- When the output to resume from is no longer retained, or `Last-Event-ID` is past the process's output, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with live output, or replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- A stream that starts with `resync_required` may number its ids from below `Last-Event-ID`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Treat it as an opaque string and echo it exactly: process stream ids are not consecutive and exceed the integers a double represents exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the oldest retained output, which can repeat output already received.\n- When the output to resume from is no longer retained, or `Last-Event-ID` is past the process's output, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with live output, or replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/api/src/routes/public_docs/schema_contract.rs b/crates/libs/api/src/routes/public_docs/schema_contract.rs index f81c861386..2017285128 100644 --- a/crates/libs/api/src/routes/public_docs/schema_contract.rs +++ b/crates/libs/api/src/routes/public_docs/schema_contract.rs @@ -117,7 +117,7 @@ pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { "name": "Last-Event-ID", "in": "header", "required": false, - "description": "SSE `id` of the last frame received.\n\n- Echo the `id` exactly; do not parse it into a floating-point number.\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` precedes the output it continues with: `cursor_out_of_window` when the output is evicted or the `id` is past it, `cursor_replay_unavailable` when the `id` does not resume exactly.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum return 400.", + "description": "SSE `id` of the last frame received.\n\n- Echo the `id` exactly; do not parse it into a floating-point number.\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` precedes the output it continues with: `cursor_out_of_window` when the output is evicted or the `id` is past it, `cursor_replay_unavailable` when the `id` does not resume exactly.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum, and process stream values too high to number a later frame above, return 400.", "schema": { "type": "integer", "format": "int64", diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index 408a3f664f..e05f3aeb82 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -94,6 +94,17 @@ pub(crate) async fn session_events( }; let stream_state = match attached.kind { SessionKind::Process => { + // Only a forged id leaves no later id to number the stream with. + let Some(replay) = ProcessReplayState::load(session_id, last_event_id) else { + return data_plane_http_error( + StatusCode::BAD_REQUEST, + DataPlaneHttpErrorCode::InvalidRequest, + "last-event-id is past every event id this stream can send", + false, + ResponseOrigin::Client, + ResponseKind::Validation, + ); + }; let start_permit = match state.native_sessions.try_acquire(session_id, attached.kind) { Ok(permit) => permit, Err(()) => { @@ -118,7 +129,7 @@ pub(crate) async fn session_events( state, session_id, attached, - last_event_id, + replay, stream_permit, start_permit, ), @@ -219,9 +230,9 @@ pub(crate) struct ProcessSseState { process_id: Option, process_bound: bool, binding: Option, - /// The stream was reopened after an output gap, so the start it reports - /// next repeats one the client already has. - reopened_after_gap: bool, + /// The backend stream was reopened, so the start it reports next repeats + /// one the client already has. + reopened: bool, revocation_tick: tokio::time::Interval, pending: VecDeque, finished: bool, @@ -287,6 +298,7 @@ const PROCESS_EVENT_ID_FIELD_SHIFT: u32 = PROCESS_EVENT_ID_GUARD_BITS + PROCESS_ pub(crate) const PROCESS_EVENT_ID_MAX_FRAME: u64 = (1 << PROCESS_EVENT_ID_FRAME_BITS) - 1; const PROCESS_EVENT_ID_UNKNOWN: u64 = 1 << (PROCESS_EVENT_ID_FIELD_BITS - 1); pub(crate) const PROCESS_EVENT_ID_MAX_POSITION: u64 = PROCESS_EVENT_ID_UNKNOWN - 1; +pub(crate) const PROCESS_EVENT_ID_MAX_RESUME_BLOCK: u64 = 1 << 32; impl ProcessEventId { /// Precedes the first frame of a stream that starts at the beginning of @@ -296,6 +308,27 @@ impl ProcessEventId { frame: 0, }; + /// Precedes the first frame of a stream that replays the output from its + /// start for a client whose `Last-Event-ID` carries no position, so that + /// every id it sends is above that one. Never sent. A tagged id gets a + /// fresh block without a position: positions from the start would sort + /// below it. `None` for an id fewer than 2^32 blocks from the end, which + /// only a forged one is, so a stream never runs out of blocks. + pub(crate) fn first_after(last_event_id: u64) -> Option { + if last_event_id < PROCESS_EVENT_ID_TAG { + return Some(Self::START); + } + let first_unknown = Self::Unknown { block: 0, frame: 0 }.encode(); + let block = if last_event_id < first_unknown { + 0 + } else { + let field = (last_event_id >> PROCESS_EVENT_ID_FIELD_SHIFT) + & ((1 << PROCESS_EVENT_ID_FIELD_BITS) - 1); + (field & !PROCESS_EVENT_ID_UNKNOWN) + 1 + }; + (block <= PROCESS_EVENT_ID_MAX_RESUME_BLOCK).then_some(Self::Unknown { block, frame: 0 }) + } + pub(crate) fn encode(self) -> u64 { let (field, frame) = match self { Self::Position { position, frame } => (position, frame), @@ -352,7 +385,8 @@ impl ProcessEventId { block, frame: frame + 1, }, - // A stream would need 2^45 frames to exhaust the blocks. + // A resumed stream starts at most at block 2^32 + // (`first_after`), and would need 2^44 frames to exhaust the rest. Self::Unknown { block, .. } => Self::Unknown { block: block + 1, frame: 0, @@ -367,35 +401,36 @@ impl ProcessReplayState { /// continues from the position its `Last-Event-ID` carries. A resume from /// an id without one also replays from the start, behind /// `resync_required(cursor_replay_unavailable)`, as output the client - /// already has may repeat. - pub(crate) fn load(session_id: DataPlaneSessionId, last_event_id: Option) -> Self { - match last_event_id.map(ProcessEventId::decode) { - None => Self { + /// already has may repeat. Every id the stream sends is above + /// `Last-Event-ID`; `None` when no id can be (see + /// [`ProcessEventId::first_after`]). + pub(crate) fn load(session_id: DataPlaneSessionId, last_event_id: Option) -> Option { + let Some(last_event_id) = last_event_id else { + return Some(Self { event_id: ProcessEventId::START, backend_output_cursor: 0, pending: VecDeque::new(), - }, - Some(Some(event_id @ ProcessEventId::Position { position, .. })) => Self { + }); + }; + if let Some(event_id @ ProcessEventId::Position { position, .. }) = + ProcessEventId::decode(last_event_id) + { + return Some(Self { event_id, backend_output_cursor: position, pending: VecDeque::new(), - }, - Some(_) => { - let event_id = ProcessEventId::START.next(Some(0)); - let frame = resync_required_frame( - session_id.kind().as_str(), - DataPlaneResyncReason::CursorReplayUnavailable, - ); - Self { - event_id, - backend_output_cursor: 0, - pending: VecDeque::from([sse_event_from_frame( - &frame, - Some(event_id.encode()), - )]), - } - } + }); } + let event_id = ProcessEventId::first_after(last_event_id)?.next(Some(0)); + let frame = resync_required_frame( + session_id.kind().as_str(), + DataPlaneResyncReason::CursorReplayUnavailable, + ); + Some(Self { + event_id, + backend_output_cursor: 0, + pending: VecDeque::from([sse_event_from_frame(&frame, Some(event_id.encode()))]), + }) } } @@ -404,11 +439,10 @@ impl ProcessSseState { state: DataPlaneState, session_id: DataPlaneSessionId, mut attached: AttachedSession, - last_event_id: Option, + replay: ProcessReplayState, stream_permit: AttachedStreamPermit, start_permit: Option, ) -> Result { - let replay = ProcessReplayState::load(session_id, last_event_id); let (client, stream) = open_native_process_stream( &state, session_id, @@ -436,7 +470,7 @@ impl ProcessSseState { process_id, process_bound, binding: None, - reopened_after_gap: false, + reopened: false, revocation_tick, pending, finished: false, @@ -446,6 +480,19 @@ impl ProcessSseState { }) } + /// Counts the client's output from the start of the guest's again, after + /// the position it resumed from turned out not to be in it. The ids sent + /// after this carry no position: they must stay above the ones already + /// sent, which positions from the start would sort below. + fn restart_from_output_start(&mut self) { + self.backend_output_cursor = Some(0); + self.output_seq = 0; + self.push_frame(resync_required_frame( + self.session_id.kind().as_str(), + DataPlaneResyncReason::CursorOutOfWindow, + )); + } + fn push_frame(&mut self, frame: DataPlaneServerFrame) { self.event_id = self.event_id.next(self.backend_output_cursor); self.pending @@ -560,7 +607,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { Some(spaced_process_event::Event::Started(_)) | Some(spaced_process_event::Event::Connected(_)) ) { - repeated_start = std::mem::take(&mut state.reopened_after_gap); + repeated_start = std::mem::take(&mut state.reopened); let event_process_id: ProcessId = match event.process_id.parse() { Ok(process_id) => process_id, Err(_) => { @@ -580,13 +627,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { // This attach started the process, so its // output begins here: the position the // client resumed from was another's. - state.backend_output_cursor = Some(0); - state.output_seq = 0; - state.event_id = ProcessEventId::START; - state.push_frame(resync_required_frame( - state.session_id.kind().as_str(), - DataPlaneResyncReason::CursorOutOfWindow, - )); + state.restart_from_output_start(); } state.process_bound = true; state.binding = Some(PendingProcessBinding::spawn( @@ -647,12 +688,15 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { let reopen_at = (is_output_cursor_ahead(&status) && state.backend_output_cursor != Some(0)) .then_some(0); - state.backend_output_cursor = reopen_at; - state.output_seq = reopen_at.unwrap_or(state.output_seq); - state.push_frame(resync_required_frame( - state.session_id.kind().as_str(), - DataPlaneResyncReason::CursorOutOfWindow, - )); + if reopen_at.is_some() { + state.restart_from_output_start(); + } else { + state.backend_output_cursor = None; + state.push_frame(resync_required_frame( + state.session_id.kind().as_str(), + DataPlaneResyncReason::CursorOutOfWindow, + )); + } match connect_process_stream( &mut state.client, &state.attached.sandbox_id, @@ -664,7 +708,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { { Ok(stream) => { state.stream = stream; - state.reopened_after_gap = true; + state.reopened = true; } Err(error) => state.finish_with_error(error), } @@ -735,6 +779,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { Ok((client, stream)) => { state.client = client; state.stream = stream; + state.reopened = true; state.push_frame(DataPlaneServerFrame::BackendReconnected { route_generation: refreshed.route_generation, current_node_id: refreshed.current_node_id.to_string(), diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index a4c9d81c11..4d9d78db15 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -1718,6 +1718,8 @@ enum FakeReply { Status(tonic::Code), /// Ends the stream as the guest does for a cursor past the output. CursorAhead, + /// Refuses the call outright, before any stream. + Refuse(tonic::Code), } /// How the data plane opened a fake guest process stream. @@ -1856,6 +1858,13 @@ async fn spawn_journal_fake( let mut trailers = None; for reply in replies(opened, open) { let (code, reason) = match reply { + FakeReply::Refuse(code) => { + return Response::builder() + .header("content-type", "application/grpc") + .header("grpc-status", code as i32) + .body(Body::empty()) + .unwrap(); + } FakeReply::Event(event) => { let encoded = ProcessEvent { process_id: process_id.clone(), @@ -1980,6 +1989,14 @@ async fn process_events_to_close( const JOURNAL: [&str; 4] = ["one\n", "two\n", "three\n", "four\n"]; +fn assert_ids_above(frames: &[(Option, serde_json::Value)], last_event_id: u64) { + let ids: Vec = frames.iter().filter_map(|(id, _)| *id).collect(); + assert!( + ids.iter().all(|id| *id > last_event_id), + "ids {ids:?} after Last-Event-ID {last_event_id}" + ); +} + fn connects(cursors: &[Option]) -> Vec { cursors.iter().copied().map(FakeOpen::Connect).collect() } @@ -2081,6 +2098,7 @@ async fn evicted_process_output_is_reported_before_the_live_tail() { resync_reasons(&resumed), ["cursor_replay_unavailable", "cursor_out_of_window"] ); + assert_ids_above(&resumed, last_event_id); assert_eq!( *fake.opens.lock().unwrap(), connects(&[Some(0), None, Some(0), None]) @@ -2122,6 +2140,7 @@ async fn backend_reconnect_after_a_resume_continues_without_a_resync() { assert_eq!(stdout_text(&resumed), "two\nthree\nfour\n"); assert_eq!(frames_of_type(&resumed, "backend_reconnected").count(), 1); + assert_eq!(frames_of_type(&resumed, "process_started").count(), 1); assert_eq!(resync_reasons(&resumed), Vec::::new()); assert_eq!( *fake.opens.lock().unwrap(), @@ -2153,6 +2172,7 @@ async fn resume_past_the_output_replays_it_behind_a_resync() { let frames = process_events_to_close(&fake.endpoint, session_id, Some(ahead)).await; + assert_ids_above(&frames, ahead); assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); assert_eq!(stdout_text(&frames), JOURNAL.concat()); assert_eq!(frames_of_type(&frames, "process_started").count(), 1); @@ -2179,18 +2199,14 @@ async fn resume_from_an_untagged_id_replays_behind_a_resync() { assert_eq!(resync_reasons(&frames), ["cursor_replay_unavailable"]); assert_eq!(stdout_text(&frames), JOURNAL.concat()); - assert!( - frames - .iter() - .filter_map(|(id, _)| *id) - .all(|id| id > 1_500_000) - ); + assert_ids_above(&frames, 1_500_000); assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0)])); } /// The attach that starts a process counts its output from the start, even /// when the client sent another stream's `Last-Event-ID`: the stream says the -/// id did not apply, and a later resume continues exactly. +/// id did not apply, numbers its frames above it, and a later resume replays +/// behind `resync_required` rather than at a wrong position. #[tokio::test] async fn starting_attach_ignores_a_foreign_last_event_id() { if !binding_fake_test_process() { @@ -2225,16 +2241,89 @@ async fn starting_attach_ignores_a_foreign_last_event_id() { .await .expect("the starting attach binds the process"); let first = sse_frames(&seen); + assert_ids_above(&first, foreign); assert_eq!(resync_reasons(&first), ["cursor_out_of_window"]); assert_eq!(stdout_text(&first), "one\ntwo\n"); let last_event_id = first.last().unwrap().0.unwrap(); let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; - assert_eq!(stdout_text(&resumed), "three\nfour\n"); - assert_eq!(resync_reasons(&resumed), Vec::::new()); + assert_ids_above(&resumed, last_event_id); + assert_eq!(resync_reasons(&resumed), ["cursor_replay_unavailable"]); + assert_eq!(stdout_text(&resumed), JOURNAL.concat()); + assert_eq!( + *fake.opens.lock().unwrap(), + [FakeOpen::Start, FakeOpen::Connect(Some(0))] + ); +} + +/// An attach that finds the process already started, its binding never +/// persisted, connects at the client's position: a completed process replays +/// exactly what the client lacks, with no resync. +#[tokio::test] +async fn already_started_process_is_resumed_exactly() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + false, + Arc::new(|_, open| match open { + FakeOpen::Start => vec![FakeReply::Refuse(tonic::Code::AlreadyExists)], + open => journal_replay(&JOURNAL, open), + }), + ) + .await; + let after_two = ProcessEventId::Position { + position: 2, + frame: 0, + } + .encode(); + + let frames = process_events_to_close(&fake.endpoint, session_id, Some(after_two)).await; + + assert_eq!(stdout_text(&frames), "three\nfour\n"); + assert_eq!(resync_reasons(&frames), Vec::::new()); + assert_ids_above(&frames, after_two); assert_eq!( *fake.opens.lock().unwrap(), [FakeOpen::Start, FakeOpen::Connect(Some(2))] ); } + +/// A `Last-Event-ID` too close to the end of the id space to number a later +/// frame above is refused before anything is opened. +#[tokio::test] +async fn last_event_id_without_room_after_it_is_rejected() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| journal_replay(&JOURNAL, open)), + ) + .await; + let mut state = crate::tests::test_data_plane_state().await; + state.cluster_api = ClusterApiClients::service_only( + pplx_space_node_api_client::cluster::connect(fake.endpoint.clone(), "test") + .await + .unwrap(), + ); + let token = format!("{DATA_PLANE_TOKEN_PREFIX}{}", "c".repeat(64)); + let response = tower::ServiceExt::oneshot( + router(state), + Request::get(format!("/v1/sessions/{session_id}/events")) + .header(header::AUTHORIZATION, format!("Bearer {token}")) + .header("last-event-id", MAX_LAST_EVENT_ID.to_string()) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert!(fake.opens.lock().unwrap().is_empty()); +} diff --git a/crates/libs/data/plane/server/src/tests.rs b/crates/libs/data/plane/server/src/tests.rs index 3b81d5aec4..72e37b5493 100644 --- a/crates/libs/data/plane/server/src/tests.rs +++ b/crates/libs/data/plane/server/src/tests.rs @@ -2,7 +2,8 @@ use super::*; use crate::{ native::{ PROCESS_EVENT_ID_GUARD_BITS, PROCESS_EVENT_ID_MAX_FRAME, PROCESS_EVENT_ID_MAX_POSITION, - ProcessEventId, ProcessReplayState, first_frame_attach_token_from_recv, + PROCESS_EVENT_ID_MAX_RESUME_BLOCK, ProcessEventId, ProcessReplayState, + first_frame_attach_token_from_recv, }, runtime_route::sweep_expired_ecw_routes, }; @@ -3712,7 +3713,7 @@ async fn first_frame_attach_token_times_out_at_deadline() { fn process_sse_resume_starts_at_the_journal_position_its_id_carries() { let session_id = DataPlaneSessionId::Process(Default::default()); - let fresh = ProcessReplayState::load(session_id, None); + let fresh = ProcessReplayState::load(session_id, None).unwrap(); assert_eq!(fresh.backend_output_cursor, 0); assert!(fresh.pending.is_empty()); @@ -3720,26 +3721,50 @@ fn process_sse_resume_starts_at_the_journal_position_its_id_carries() { position: 3, frame: 2, }; - let resumed = ProcessReplayState::load(session_id, Some(carried.encode())); + let resumed = ProcessReplayState::load(session_id, Some(carried.encode())).unwrap(); assert_eq!(resumed.event_id, carried); assert_eq!(resumed.backend_output_cursor, 3); assert!(resumed.pending.is_empty()); - let unknown = ProcessEventId::Unknown { block: 4, frame: 1 }.encode(); // Earlier data planes count frames from 1, so their ids sit on either side // of any decimal boundary; one counted on from a tagged id fails its guard. + // Whatever the id, every id the replay sends is above it. for last_event_id in [ 0, 41, 999_999, 1_000_000, 1_500_000, - unknown, carried.encode() + 1, + ProcessEventId::Unknown { block: 4, frame: 1 }.encode(), + ProcessEventId::Unknown { block: 4, frame: 1 }.encode() + 1, + ProcessEventId::Unknown { + block: PROCESS_EVENT_ID_MAX_RESUME_BLOCK - 1, + frame: PROCESS_EVENT_ID_MAX_FRAME, + } + .encode(), ] { - let replay = ProcessReplayState::load(session_id, Some(last_event_id)); + let replay = ProcessReplayState::load(session_id, Some(last_event_id)).unwrap(); assert_eq!(replay.backend_output_cursor, 0, "{last_event_id}"); assert_eq!(replay.pending.len(), 1, "{last_event_id}"); + assert!(replay.event_id.encode() > last_event_id, "{last_event_id}"); + let mut id = replay.event_id; + for position in [Some(0), Some(1), None, Some(2)] { + id = id.next(position); + assert!(id.encode() > last_event_id, "{last_event_id}"); + } + } + + // An id this close to the end leaves a stream too few ids to send. + for forged in [ + ProcessEventId::Unknown { + block: PROCESS_EVENT_ID_MAX_RESUME_BLOCK, + frame: 0, + } + .encode(), + MAX_LAST_EVENT_ID, + ] { + assert!(ProcessReplayState::load(session_id, Some(forged)).is_none()); } } From 16b687f701de774fb07e67475fbc263219551ffb Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 22:33:04 +0000 Subject: [PATCH 04/12] data plane: continue process SSE streams from the oldest retained output The guest now tags the RESOURCE_EXHAUSTED that ends a lagged process stream with `space-error-reason: process-output-lagged` and reports the oldest output it still retains in `space-process-output-first-sequence`. The data plane reopens there behind one `cursor_out_of_window` and keeps positional ids, so later resumes stay exact instead of replaying behind `resync_required`. A fresh attach to a process whose start was evicted now gets the retained output rather than only its exit. Only a tagged status, or a guest older than the tag sending its lag message, counts as evicted output; another RESOURCE_EXHAUSTED (a hop's refusal of an oversized chunk, an HTTP/2 reset) ends the stream with an error instead of jumping to the live tail. Reopens after a window miss are capped when no output follows, and one refused as a stale route goes through the route refresh path. Process SSE ids move their tag from bit 62 to bit 52: every id is below 2^53, so a JavaScript Number holds it exactly, with 16 guard bits, an 8-bit frame counter and a 28-bit position field. An id at or above 2^53 cannot have a later id and is refused with a 400. Each response now sends `process_started` once, even when its first backend stream fails before reporting the process. A gap reported once is not reported again before output follows it. The 400 is counted as an attach result, and `resync_required` is counted only once the stream opens. The node simulator fails a cursor past the output, as the guest does, and operations.proto documents both statuses. --- crates/libs/common/core/src/lib.rs | 18 + crates/libs/data/plane/server/Cargo.toml | 1 + .../libs/data/plane/server/src/native/auth.rs | 7 +- .../libs/data/plane/server/src/native/sse.rs | 518 +++++++++------ .../data/plane/server/src/native/sse/tests.rs | 215 +++++++ .../data/plane/server/src/native/tests.rs | 591 +++++++++++++----- crates/libs/data/plane/server/src/tests.rs | 192 +----- crates/libs/node/api/sim/src/processes.rs | 37 +- .../libs/node/api/sim/src/processes/tests.rs | 25 +- .../spaced/guest/execution/src/process.rs | 38 +- .../guest/execution/src/process/tests.rs | 57 +- proto/spaced/operations/v1/operations.proto | 11 + .../spaced/operations/v1/operations.pb.go | 12 + 13 files changed, 1169 insertions(+), 553 deletions(-) create mode 100644 crates/libs/data/plane/server/src/native/sse/tests.rs diff --git a/crates/libs/common/core/src/lib.rs b/crates/libs/common/core/src/lib.rs index c1cecd13ff..e4c07483a8 100644 --- a/crates/libs/common/core/src/lib.rs +++ b/crates/libs/common/core/src/lib.rs @@ -163,6 +163,24 @@ pub mod spaced { /// written, as opposed to a message too large for a hop to decode. pub const PROCESS_OUTPUT_CURSOR_AHEAD: &str = "process-output-cursor-ahead"; + /// [`ERROR_REASON_METADATA_KEY`] value of a RESOURCE_EXHAUSTED that ends a + /// process output stream whose cursor is before the oldest output the + /// guest still retains, as opposed to a hop's refusal or a transport reset + /// with the same code. [`PROCESS_OUTPUT_FIRST_SEQUENCE_METADATA_KEY`] says + /// where the retained output starts. + pub const PROCESS_OUTPUT_LAGGED: &str = "process-output-lagged"; + + /// Metadata key, on a [`PROCESS_OUTPUT_LAGGED`] status, of the cursor of + /// the oldest output the guest still retains, in decimal. Absent when the + /// guest no longer holds the process's output at all. + pub const PROCESS_OUTPUT_FIRST_SEQUENCE_METADATA_KEY: &str = + "space-process-output-first-sequence"; + + /// Message of the guest's [`PROCESS_OUTPUT_LAGGED`] status. Guests older + /// than that reason send it untagged, so it identifies their lag status. + pub const PROCESS_OUTPUT_LAGGED_MESSAGE: &str = + "process output stream fell behind retained output buffer"; + /// [`ERROR_REASON_METADATA_KEY`] value of a request refused before any of /// its work started: an admission limit, or a guest or registry shutting /// down. This request did nothing, so the caller may retry it, after the diff --git a/crates/libs/data/plane/server/Cargo.toml b/crates/libs/data/plane/server/Cargo.toml index 54cdd3ceb9..52f7a6435b 100644 --- a/crates/libs/data/plane/server/Cargo.toml +++ b/crates/libs/data/plane/server/Cargo.toml @@ -64,6 +64,7 @@ pplx-space-public-api = { workspace = true, features = ["serde"] } pplx-space-sbx-mgr-client = { workspace = true } [dev-dependencies] +pplx-space-metrics = { workspace = true, features = ["test-capture"] } tokio = { workspace = true, features = ["test-util"] } pplx-space-test-env = { workspace = true } aws-lc-rs = { workspace = true } diff --git a/crates/libs/data/plane/server/src/native/auth.rs b/crates/libs/data/plane/server/src/native/auth.rs index b39119ab76..1969f23a28 100644 --- a/crates/libs/data/plane/server/src/native/auth.rs +++ b/crates/libs/data/plane/server/src/native/auth.rs @@ -96,9 +96,10 @@ pub(crate) fn reject_query_tokens(query: Option<&str>) -> Result<(), AttachError } /// Largest `Last-Event-ID` a client may resume from. It is the OpenAPI -/// parameter's `int64` maximum, and it leaves room for the `last + 1` id the -/// resume contract promises, so the stream can never saturate onto an id the -/// client already acknowledged. +/// parameter's `int64` maximum, and it leaves room for the `last + 1` id a +/// file-watch resume sends, so that stream can never saturate onto an id the +/// client already acknowledged. Process streams bound their ids by their own +/// layout (see `ProcessEventId`) and refuse a higher id themselves. pub(crate) const MAX_LAST_EVENT_ID: u64 = i64::MAX as u64; pub(crate) fn last_event_id(headers: &HeaderMap) -> Result, &'static str> { diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index e05f3aeb82..f48b5db5e2 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -95,7 +95,11 @@ pub(crate) async fn session_events( let stream_state = match attached.kind { SessionKind::Process => { // Only a forged id leaves no later id to number the stream with. - let Some(replay) = ProcessReplayState::load(session_id, last_event_id) else { + let Some(replay) = ProcessReplayState::load(last_event_id) else { + pplx_space_metrics::data_plane::attach( + session_id.kind().as_str(), + "invalid_last_event_id", + ); return data_plane_http_error( StatusCode::BAD_REQUEST, DataPlaneHttpErrorCode::InvalidRequest, @@ -230,9 +234,15 @@ pub(crate) struct ProcessSseState { process_id: Option, process_bound: bool, binding: Option, - /// The backend stream was reopened, so the start it reports next repeats - /// one the client already has. - reopened: bool, + /// This response has sent `process_started`; a reopened backend stream + /// reports the start again, and the client already has it. + started_sent: bool, + /// `resync_required(cursor_out_of_window)` was sent and no output has + /// followed it, so another gap before any output is the same one. + gap_reported: bool, + /// Backend reopens after an output window miss since the last output or + /// exit, capped at [`PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS`]. + window_reopens_without_progress: u32, revocation_tick: tokio::time::Interval, pending: VecDeque, finished: bool, @@ -241,10 +251,12 @@ pub(crate) struct ProcessSseState { _stream_permit: AttachedStreamPermit, } -pub(crate) struct ProcessReplayState { - pub(crate) event_id: ProcessEventId, - pub(crate) backend_output_cursor: u64, - pub(crate) pending: VecDeque, +struct ProcessReplayState { + event_id: ProcessEventId, + backend_output_cursor: u64, + /// Sent with `event_id` before any live event. Counted only once the + /// stream opens, so a refused attach does not count a frame never sent. + resync: Option, } /// What a `Last-Event-ID` resume produces before any live event: the next SSE @@ -274,14 +286,15 @@ pub(crate) fn resume_resync_frame( /// A process stream's SSE id, which a resume decodes to recover the guest /// output position to continue from, so any replica can serve it. /// -/// On the wire, bit 62 tags the id: no frame counter an earlier data plane -/// issued reaches it. The low [`PROCESS_EVENT_ID_GUARD_BITS`] bits are zero, -/// so an id that a data plane without this encoding counted on from by `+1`, -/// or that a client rounded through a floating-point number, fails to decode -/// rather than decoding to the wrong position. Above them come a frame counter -/// and a position field, whose top bit marks a block of ids without a position. -/// Those blocks sort above every position, so a stream that loses its -/// position keeps its ids increasing. +/// On the wire, bit 52 tags the id: no frame counter an earlier data plane +/// issued reaches it, and every id stays below 2^53, so a client that parses +/// it into a double still holds it exactly. The low +/// [`PROCESS_EVENT_ID_GUARD_BITS`] bits are zero, so an id that a data plane +/// without this encoding counted on from by `+1` fails to decode rather than +/// decoding to the wrong position, unless it counted a multiple of 2^16 +/// frames. Above them come a frame counter and a position field, whose top +/// bit marks a block of ids without a position. Those blocks sort above every +/// position, so a stream that loses its position keeps its ids increasing. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ProcessEventId { /// The client has received the guest's output up to `position`. @@ -290,15 +303,31 @@ pub(crate) enum ProcessEventId { Unknown { block: u64, frame: u64 }, } -const PROCESS_EVENT_ID_TAG: u64 = 1 << 62; -pub(crate) const PROCESS_EVENT_ID_GUARD_BITS: u32 = 16; -const PROCESS_EVENT_ID_FRAME_BITS: u32 = 12; -const PROCESS_EVENT_ID_FIELD_BITS: u32 = 34; +const PROCESS_EVENT_ID_TAG_BIT: u32 = 52; +const PROCESS_EVENT_ID_TAG: u64 = 1 << PROCESS_EVENT_ID_TAG_BIT; +const PROCESS_EVENT_ID_GUARD_BITS: u32 = 16; +/// Frames at one position: those sent between two outputs, such as a +/// resume's `process_started` or a backend reconnect's two frames. The rest +/// of the 36 bits below the tag go to positions, up to 2^27 - 1 outputs. +const PROCESS_EVENT_ID_FRAME_BITS: u32 = 8; const PROCESS_EVENT_ID_FIELD_SHIFT: u32 = PROCESS_EVENT_ID_GUARD_BITS + PROCESS_EVENT_ID_FRAME_BITS; -pub(crate) const PROCESS_EVENT_ID_MAX_FRAME: u64 = (1 << PROCESS_EVENT_ID_FRAME_BITS) - 1; +const PROCESS_EVENT_ID_FIELD_BITS: u32 = PROCESS_EVENT_ID_TAG_BIT - PROCESS_EVENT_ID_FIELD_SHIFT; +const PROCESS_EVENT_ID_FIELD_MASK: u64 = (1 << PROCESS_EVENT_ID_FIELD_BITS) - 1; +const PROCESS_EVENT_ID_MAX_FRAME: u64 = (1 << PROCESS_EVENT_ID_FRAME_BITS) - 1; const PROCESS_EVENT_ID_UNKNOWN: u64 = 1 << (PROCESS_EVENT_ID_FIELD_BITS - 1); -pub(crate) const PROCESS_EVENT_ID_MAX_POSITION: u64 = PROCESS_EVENT_ID_UNKNOWN - 1; -pub(crate) const PROCESS_EVENT_ID_MAX_RESUME_BLOCK: u64 = 1 << 32; +const PROCESS_EVENT_ID_MAX_POSITION: u64 = PROCESS_EVENT_ID_UNKNOWN - 1; +/// The highest block a resumed stream starts in: half the blocks, so every +/// stream has at least 2^(field + frame - 2) ids left to send. +const PROCESS_EVENT_ID_MAX_RESUME_BLOCK: u64 = 1 << (PROCESS_EVENT_ID_FIELD_BITS - 2); + +const _: () = { + // The field holds the no-position marker and at least two resume blocks, + // and an id, whose bits all lie at or below the tag, is below 2^53. + assert!(PROCESS_EVENT_ID_FIELD_BITS > 2); + assert!(PROCESS_EVENT_ID_TAG_BIT < 53); + // A stream that starts in the highest resume block cannot run out. + assert!(PROCESS_EVENT_ID_MAX_RESUME_BLOCK < PROCESS_EVENT_ID_MAX_POSITION); +}; impl ProcessEventId { /// Precedes the first frame of a stream that starts at the beginning of @@ -312,18 +341,22 @@ impl ProcessEventId { /// start for a client whose `Last-Event-ID` carries no position, so that /// every id it sends is above that one. Never sent. A tagged id gets a /// fresh block without a position: positions from the start would sort - /// below it. `None` for an id fewer than 2^32 blocks from the end, which - /// only a forged one is, so a stream never runs out of blocks. - pub(crate) fn first_after(last_event_id: u64) -> Option { + /// below it. `None` for an id at or above 2^53, or in a block from + /// [`PROCESS_EVENT_ID_MAX_RESUME_BLOCK`] on, which only a forged one is, so + /// a stream never runs out of blocks. + fn first_after(last_event_id: u64) -> Option { if last_event_id < PROCESS_EVENT_ID_TAG { return Some(Self::START); } + if last_event_id >> PROCESS_EVENT_ID_TAG_BIT != 1 { + return None; + } let first_unknown = Self::Unknown { block: 0, frame: 0 }.encode(); let block = if last_event_id < first_unknown { 0 } else { - let field = (last_event_id >> PROCESS_EVENT_ID_FIELD_SHIFT) - & ((1 << PROCESS_EVENT_ID_FIELD_BITS) - 1); + let field = + (last_event_id >> PROCESS_EVENT_ID_FIELD_SHIFT) & PROCESS_EVENT_ID_FIELD_MASK; (field & !PROCESS_EVENT_ID_UNKNOWN) + 1 }; (block <= PROCESS_EVENT_ID_MAX_RESUME_BLOCK).then_some(Self::Unknown { block, frame: 0 }) @@ -331,21 +364,29 @@ impl ProcessEventId { pub(crate) fn encode(self) -> u64 { let (field, frame) = match self { - Self::Position { position, frame } => (position, frame), - Self::Unknown { block, frame } => (PROCESS_EVENT_ID_UNKNOWN | block, frame), + Self::Position { position, frame } => { + debug_assert!(position <= PROCESS_EVENT_ID_MAX_POSITION, "{self:?}"); + (position, frame) + } + Self::Unknown { block, frame } => { + debug_assert!(block <= PROCESS_EVENT_ID_MAX_POSITION, "{self:?}"); + (PROCESS_EVENT_ID_UNKNOWN | block, frame) + } }; + debug_assert!(frame <= PROCESS_EVENT_ID_MAX_FRAME, "{self:?}"); PROCESS_EVENT_ID_TAG | field << PROCESS_EVENT_ID_FIELD_SHIFT | frame << PROCESS_EVENT_ID_GUARD_BITS } /// `None` for an id this encoding did not produce. - pub(crate) fn decode(id: u64) -> Option { - if id >> 62 != 1 || id & ((1 << PROCESS_EVENT_ID_GUARD_BITS) - 1) != 0 { + fn decode(id: u64) -> Option { + if id >> PROCESS_EVENT_ID_TAG_BIT != 1 || id & ((1 << PROCESS_EVENT_ID_GUARD_BITS) - 1) != 0 + { return None; } let frame = (id >> PROCESS_EVENT_ID_GUARD_BITS) & PROCESS_EVENT_ID_MAX_FRAME; - let field = (id >> PROCESS_EVENT_ID_FIELD_SHIFT) & ((1 << PROCESS_EVENT_ID_FIELD_BITS) - 1); + let field = (id >> PROCESS_EVENT_ID_FIELD_SHIFT) & PROCESS_EVENT_ID_FIELD_MASK; Some(if field & PROCESS_EVENT_ID_UNKNOWN == 0 { Self::Position { position: field, @@ -361,7 +402,7 @@ impl ProcessEventId { /// The id of the next frame, once the client has received output up to /// `position`. Always greater than this one. - pub(crate) fn next(self, position: Option) -> Self { + fn next(self, position: Option) -> Self { match self { Self::Position { position: last, @@ -385,8 +426,9 @@ impl ProcessEventId { block, frame: frame + 1, }, - // A resumed stream starts at most at block 2^32 - // (`first_after`), and would need 2^44 frames to exhaust the rest. + // A resumed stream starts at most at block + // `PROCESS_EVENT_ID_MAX_RESUME_BLOCK` (`first_after`), and would + // need 2^34 frames to exhaust the rest. Self::Unknown { block, .. } => Self::Unknown { block: block + 1, frame: 0, @@ -404,12 +446,12 @@ impl ProcessReplayState { /// already has may repeat. Every id the stream sends is above /// `Last-Event-ID`; `None` when no id can be (see /// [`ProcessEventId::first_after`]). - pub(crate) fn load(session_id: DataPlaneSessionId, last_event_id: Option) -> Option { + fn load(last_event_id: Option) -> Option { let Some(last_event_id) = last_event_id else { return Some(Self { event_id: ProcessEventId::START, backend_output_cursor: 0, - pending: VecDeque::new(), + resync: None, }); }; if let Some(event_id @ ProcessEventId::Position { position, .. }) = @@ -418,18 +460,13 @@ impl ProcessReplayState { return Some(Self { event_id, backend_output_cursor: position, - pending: VecDeque::new(), + resync: None, }); } - let event_id = ProcessEventId::first_after(last_event_id)?.next(Some(0)); - let frame = resync_required_frame( - session_id.kind().as_str(), - DataPlaneResyncReason::CursorReplayUnavailable, - ); Some(Self { - event_id, + event_id: ProcessEventId::first_after(last_event_id)?.next(Some(0)), backend_output_cursor: 0, - pending: VecDeque::from([sse_event_from_frame(&frame, Some(event_id.encode()))]), + resync: Some(DataPlaneResyncReason::CursorReplayUnavailable), }) } } @@ -452,7 +489,12 @@ impl ProcessSseState { .await?; let mut pending = VecDeque::new(); pending.push_back(sse_event_from_frame(&attached_frame(&attached), None)); - pending.extend(replay.pending); + if let Some(reason) = replay.resync { + pending.push_back(sse_event_from_frame( + &resync_required_frame(session_id.kind().as_str(), reason), + Some(replay.event_id.encode()), + )); + } let process_id = attached.backend_process_id.clone(); let process_bound = process_id.is_some(); let revocation_tick = revocation_check_interval(&state); @@ -470,7 +512,9 @@ impl ProcessSseState { process_id, process_bound, binding: None, - reopened: false, + started_sent: false, + gap_reported: false, + window_reopens_without_progress: 0, revocation_tick, pending, finished: false, @@ -484,16 +528,39 @@ impl ProcessSseState { /// the position it resumed from turned out not to be in it. The ids sent /// after this carry no position: they must stay above the ones already /// sent, which positions from the start would sort below. - fn restart_from_output_start(&mut self) { - self.backend_output_cursor = Some(0); - self.output_seq = 0; - self.push_frame(resync_required_frame( - self.session_id.kind().as_str(), - DataPlaneResyncReason::CursorOutOfWindow, - )); + fn restart_from_output_start(&mut self, cause: &'static str) { + self.continue_after_gap(Some(0), cause); + } + + /// Continues the client's output at `position` of the guest's, or at the + /// live tail when `None`, behind `resync_required(cursor_out_of_window)`: + /// the output between what the client has and `position` is not sent. + fn continue_after_gap(&mut self, position: Option, cause: &'static str) { + tracing::info!( + cause, + from = ?self.backend_output_cursor, + to = ?position, + "process event stream continues after an output gap" + ); + self.backend_output_cursor = position; + if let Some(position) = position { + self.output_seq = position; + } + if !self.gap_reported { + self.gap_reported = true; + self.push_frame(resync_required_frame( + self.session_id.kind().as_str(), + DataPlaneResyncReason::CursorOutOfWindow, + )); + } } fn push_frame(&mut self, frame: DataPlaneServerFrame) { + match frame { + DataPlaneServerFrame::ProcessStarted { .. } if self.started_sent => return, + DataPlaneServerFrame::ProcessStarted { .. } => self.started_sent = true, + _ => {} + } self.event_id = self.event_id.next(self.backend_output_cursor); self.pending .push_back(sse_event_from_frame(&frame, Some(self.event_id.encode()))); @@ -520,6 +587,128 @@ impl ProcessSseState { retryable: true, }); } + + /// Reopens the backend stream after the guest refused its cursor as + /// outside the retained output, behind one + /// `resync_required(cursor_out_of_window)` for the gap: from the start of + /// the output when the cursor was past its end, from the oldest output the + /// guest still retains when it reports it, else at the live tail. + async fn reopen_after_window_miss(&mut self, status: tonic::Status, miss: OutputWindowMiss) { + let Some(process_id) = self.process_id.clone() else { + self.finish_with_error(StreamBridgeError::BackendStatus(status)); + return; + }; + if self.window_reopens_without_progress >= PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS { + self.finish_with_error(StreamBridgeError::BackendStatus(status)); + return; + } + match (miss, self.backend_output_cursor) { + // Only a position the client resumed from can be past the output. + (OutputWindowMiss::CursorAhead, Some(cursor)) if cursor > 0 => { + self.restart_from_output_start("cursor_ahead"); + } + (OutputWindowMiss::CursorAhead, _) => { + self.finish_with_error(StreamBridgeError::BackendStatus(status)); + return; + } + ( + OutputWindowMiss::Evicted { + first_retained: Some(first), + }, + Some(cursor), + ) if first > cursor => self.continue_after_gap(Some(first), "output_evicted"), + (OutputWindowMiss::Evicted { .. }, _) => { + self.continue_after_gap(None, "output_evicted"); + } + } + self.window_reopens_without_progress += 1; + match connect_process_stream( + &mut self.client, + &self.attached.sandbox_id, + &process_id, + self.backend_output_cursor, + self.attached.space_api_route_token.as_ref(), + ) + .await + { + Ok(stream) => self.stream = stream, + // The route token may have expired since the stream opened. + Err(StreamBridgeError::BackendStatus(status)) if is_backend_reconnectable(&status) => { + self.reconnect_backend(status).await; + } + Err(error) => self.finish_with_error(error), + } + } + + /// Re-resolves the route after the backend stream failed with `status` + /// and reopens it at the output position the client has reached. + async fn reconnect_backend(&mut self, status: tonic::Status) { + let Some(current_process_id) = self.process_id.clone() else { + self.finish_with_error(StreamBridgeError::BackendStatus(status)); + return; + }; + let current_generation = self.attached.route_generation; + let (refreshed, refresh_dimension) = + match refresh_native_stream_route(&self.state, &self.attached, &status).await { + Ok(refreshed) => refreshed, + Err(error) => { + self.finish_with_error(error); + return; + } + }; + let reason = + reconnect_reason_for_route_change(current_generation, refreshed.route_generation); + record_backend_channel_reset(self.session_id.kind().as_str(), &reason); + self.push_frame(DataPlaneServerFrame::BackendReconnecting { + reason: reason.clone(), + route_generation: current_generation, + }); + if let Err(error) = + record_session_reconnect(&self.state, self.session_id, reason, current_generation).await + { + self.finish_with_error(error); + return; + } + let reopened = async { + let channel = self + .state + .backend_channels + .channel(&refreshed.backend_endpoint, self.session_id) + .await?; + let mut client = WorkerApiClient::new(wrap_grpc_client_channel(channel)); + let stream = connect_process_stream( + &mut client, + &refreshed.sandbox_id, + ¤t_process_id, + self.backend_output_cursor, + refreshed.space_api_route_token.as_ref(), + ) + .await?; + Ok((client, stream)) + } + .await; + match finish_native_stream_refresh(refresh_dimension, reopened) { + Ok((client, stream)) => { + self.client = client; + self.stream = stream; + self.push_frame(DataPlaneServerFrame::BackendReconnected { + route_generation: refreshed.route_generation, + current_node_id: refreshed.current_node_id.to_string(), + }); + // From a known position the reopened stream continues where + // this one stopped; only one that follows the live tail can + // skip output. + if self.backend_output_cursor.is_none() { + self.push_frame(resync_required_frame( + self.session_id.kind().as_str(), + DataPlaneResyncReason::ProcessStreamReconnected, + )); + } + self.attached = refreshed; + } + Err(error) => self.finish_with_error(error), + } + } } impl Drop for ProcessSseState { @@ -601,13 +790,11 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { }; match event { Ok(event) => { - let mut repeated_start = false; if matches!( event.event, Some(spaced_process_event::Event::Started(_)) | Some(spaced_process_event::Event::Connected(_)) ) { - repeated_start = std::mem::take(&mut state.reopened); let event_process_id: ProcessId = match event.process_id.parse() { Ok(process_id) => process_id, Err(_) => { @@ -627,7 +814,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { // This attach started the process, so its // output begins here: the position the // client resumed from was another's. - state.restart_from_output_start(); + state.restart_from_output_start("foreign_last_event_id"); } state.process_bound = true; state.binding = Some(PendingProcessBinding::spawn( @@ -639,12 +826,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } } let mut output_seq = state.output_seq; - let mut batch = process_event_frames(event, &mut output_seq); - if repeated_start { - batch.frames.retain(|frame| { - !matches!(frame, DataPlaneServerFrame::ProcessStarted { .. }) - }); - } + let batch = process_event_frames(event, &mut output_seq); if batch .frames .iter() @@ -664,6 +846,10 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { *cursor = output_seq; } } + if process_frame_advances_backend_cursor(&frame) || exited { + state.gap_reported = false; + state.window_reopens_without_progress = 0; + } state.push_frame(frame); if exited { state.push_frame(DataPlaneServerFrame::Closed { @@ -676,132 +862,21 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { return Some(event); } } - Err(status) if is_output_window_miss(&status) => { - // The guest does not hold the output this stream asked - // for. Report the gap, then reopen: from the start of - // the output when the cursor was past its end, else at - // the live tail, as what was evicted is gone. - let Some(process_id) = state.process_id.clone() else { - state.finish_with_error(StreamBridgeError::BackendStatus(status)); - return state.pending.pop_front(); - }; - let reopen_at = (is_output_cursor_ahead(&status) - && state.backend_output_cursor != Some(0)) - .then_some(0); - if reopen_at.is_some() { - state.restart_from_output_start(); + Err(status) => { + if let Some(miss) = output_window_miss(&status) { + state.reopen_after_window_miss(status, miss).await; + } else if is_backend_reconnectable(&status) { + state.reconnect_backend(status).await; } else { - state.backend_output_cursor = None; - state.push_frame(resync_required_frame( - state.session_id.kind().as_str(), - DataPlaneResyncReason::CursorOutOfWindow, - )); - } - match connect_process_stream( - &mut state.client, - &state.attached.sandbox_id, - &process_id, - reopen_at, - state.attached.space_api_route_token.as_ref(), - ) - .await - { - Ok(stream) => { - state.stream = stream; - state.reopened = true; - } - Err(error) => state.finish_with_error(error), - } - return state.pending.pop_front(); - } - Err(status) - if status.code() == tonic::Code::Unavailable - || is_refreshable_route_identity_rejection(&status) => - { - let Some(current_process_id) = state.process_id.clone() else { state.finish_with_error(StreamBridgeError::BackendStatus(status)); - return state.pending.pop_front(); - }; - let current_generation = state.attached.route_generation; - let (refreshed, refresh_dimension) = - match refresh_native_stream_route( - &state.state, - &state.attached, - &status, - ) - .await - { - Ok(refreshed) => refreshed, - Err(error) => { - state.finish_with_error(error); - return state.pending.pop_front(); - } - }; - let reason = reconnect_reason_for_route_change( - current_generation, - refreshed.route_generation, - ); - record_backend_channel_reset(state.session_id.kind().as_str(), &reason); - state.push_frame(DataPlaneServerFrame::BackendReconnecting { - reason: reason.clone(), - route_generation: current_generation, - }); - if let Err(error) = record_session_reconnect( - &state.state, - state.session_id, - reason, - current_generation, - ) - .await - { - state.finish_with_error(error); - return state.pending.pop_front(); } - let reopened = async { - let channel = state - .state - .backend_channels - .channel(&refreshed.backend_endpoint, state.session_id).await?; - let mut client = - WorkerApiClient::new(wrap_grpc_client_channel(channel)); - let stream = connect_process_stream( - &mut client, - &refreshed.sandbox_id, - ¤t_process_id, - state.backend_output_cursor, - refreshed.space_api_route_token.as_ref(), - ) - .await?; - Ok((client, stream)) + // A reopen after a gap already reported sends nothing. + if let Some(event) = state.pending.pop_front() { + return Some(event); } - .await; - match finish_native_stream_refresh(refresh_dimension, reopened) { - Ok((client, stream)) => { - state.client = client; - state.stream = stream; - state.reopened = true; - state.push_frame(DataPlaneServerFrame::BackendReconnected { - route_generation: refreshed.route_generation, - current_node_id: refreshed.current_node_id.to_string(), - }); - // From a known position the reopened stream - // continues where this one stopped; only one - // that follows the live tail can skip output. - if state.backend_output_cursor.is_none() { - state.push_frame(resync_required_frame( - state.session_id.kind().as_str(), - DataPlaneResyncReason::ProcessStreamReconnected, - )); - } - state.attached = refreshed; - } - Err(error) => state.finish_with_error(error), + if state.finished { + return None; } - return state.pending.pop_front(); - } - Err(status) => { - state.finish_with_error(StreamBridgeError::BackendStatus(status)); - return state.pending.pop_front(); } } } @@ -809,24 +884,62 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } } -/// Whether the guest ended a process stream because its cursor is outside the -/// retained output: `resource_exhausted` once that output has been evicted (the -/// node's per-sandbox limiter uses the same code, but it refuses a call before -/// any stream exists), or a cursor past the end of the output. -fn is_output_window_miss(status: &tonic::Status) -> bool { - is_output_cursor_ahead(status) - || (status.code() == tonic::Code::ResourceExhausted - && pplx_space_common_grpc::UdsPerSandboxLimitExceeded::from_status(status).is_none()) +/// Backend reopens after an output window miss, without output or an exit +/// in between, before the stream ends with an error. A cursor past the output +/// restarts at the start, which the guest may in turn report as evicted, so +/// two in a row are expected. +const PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS: u32 = 3; + +/// Why the guest ended a process stream because its cursor is outside the +/// output it retains. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum OutputWindowMiss { + /// The cursor is past the output the process has written. + CursorAhead, + /// The output at the cursor was evicted. `first_retained` is the cursor + /// of the oldest output the guest still retains, when it reports one. + Evicted { first_retained: Option }, } -/// A guest older than this check waits for output to reach such a cursor. -fn is_output_cursor_ahead(status: &tonic::Status) -> bool { - status.code() == tonic::Code::OutOfRange - && status - .metadata() - .get(pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY) - .and_then(|value| value.to_str().ok()) - == Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD) +/// The window miss a backend stream status reports: an OUT_OF_RANGE or a +/// RESOURCE_EXHAUSTED the guest tagged as one, or, from a guest older than +/// those tags, a RESOURCE_EXHAUSTED with its lag message. Other statuses with +/// these codes (a hop's refusal of an oversized chunk, an HTTP/2 reset) are +/// not misses. +fn output_window_miss(status: &tonic::Status) -> Option { + use pplx_space_common_core::spaced; + let metadata = status.metadata(); + let reason = metadata + .get(spaced::ERROR_REASON_METADATA_KEY) + .and_then(|value| value.to_str().ok()); + match (status.code(), reason) { + (tonic::Code::OutOfRange, Some(spaced::PROCESS_OUTPUT_CURSOR_AHEAD)) => { + Some(OutputWindowMiss::CursorAhead) + } + (tonic::Code::ResourceExhausted, Some(spaced::PROCESS_OUTPUT_LAGGED)) => { + Some(OutputWindowMiss::Evicted { + first_retained: metadata + .get(spaced::PROCESS_OUTPUT_FIRST_SEQUENCE_METADATA_KEY) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse().ok()), + }) + } + (tonic::Code::ResourceExhausted, None) + if status.message() == spaced::PROCESS_OUTPUT_LAGGED_MESSAGE => + { + Some(OutputWindowMiss::Evicted { + first_retained: None, + }) + } + _ => None, + } +} + +/// Whether a backend stream that failed with `status` may be reopened after +/// re-resolving its route: the backend is unavailable, or it rejected the +/// route's identity as one a refresh replaces. +fn is_backend_reconnectable(status: &tonic::Status) -> bool { + status.code() == tonic::Code::Unavailable || is_refreshable_route_identity_rejection(status) } pub(crate) struct FileWatchSseState { @@ -1084,3 +1197,6 @@ async fn next_file_watch_sse_frame(state: &mut FileWatchSseState) -> Option [u64; 3] { + [ + PROCESS_EVENT_ID_TAG - 1, + ProcessEventId::Position { + position: PROCESS_EVENT_ID_MAX_POSITION, + frame: PROCESS_EVENT_ID_MAX_FRAME, + } + .encode(), + ProcessEventId::Unknown { block: 0, frame: 0 }.encode() - 1, + ] +} + +/// A fresh attach replays the guest journal from its start, and a resume +/// continues from the output position its id carries. An id without one, +/// including every id an earlier data plane issued, also replays from the +/// start, behind `resync_required`. +#[test] +fn process_sse_resume_starts_at_the_journal_position_its_id_carries() { + let fresh = ProcessReplayState::load(None).unwrap(); + assert_eq!(fresh.event_id, ProcessEventId::START); + assert_eq!(fresh.backend_output_cursor, 0); + assert_eq!(fresh.resync, None); + + for carried in [ + ProcessEventId::Position { + position: 3, + frame: 2, + }, + ProcessEventId::Position { + position: PROCESS_EVENT_ID_MAX_POSITION, + frame: PROCESS_EVENT_ID_MAX_FRAME, + }, + ] { + let resumed = ProcessReplayState::load(Some(carried.encode())).unwrap(); + assert_eq!(resumed.event_id, carried); + let ProcessEventId::Position { position, .. } = carried else { + unreachable!() + }; + assert_eq!(resumed.backend_output_cursor, position); + assert_eq!(resumed.resync, None); + } + + // Earlier data planes count frames from 1, and one that counted on from a + // tagged id fails its guard. Whatever the id, every id the replay sends + // is above it. + let last_accepted_block = ProcessEventId::Unknown { + block: PROCESS_EVENT_ID_MAX_RESUME_BLOCK - 1, + frame: PROCESS_EVENT_ID_MAX_FRAME, + } + .encode(); + for last_event_id in [ + 0, + 41, + layout_edges()[0], + layout_edges()[1] + 1, + layout_edges()[2], + ProcessEventId::Position { + position: 3, + frame: 2, + } + .encode() + + 1, + ProcessEventId::Unknown { block: 4, frame: 1 }.encode(), + ProcessEventId::Unknown { block: 4, frame: 1 }.encode() + 1, + last_accepted_block, + last_accepted_block + (1 << PROCESS_EVENT_ID_GUARD_BITS) - 1, + ] { + let replay = ProcessReplayState::load(Some(last_event_id)).unwrap(); + assert_eq!(replay.backend_output_cursor, 0, "{last_event_id}"); + assert_eq!( + replay.resync, + Some(DataPlaneResyncReason::CursorReplayUnavailable), + "{last_event_id}" + ); + assert!(replay.event_id.encode() > last_event_id, "{last_event_id}"); + let mut id = replay.event_id; + for position in [Some(0), Some(1), None, Some(2)] { + id = id.next(position); + assert!(id.encode() > last_event_id, "{last_event_id}"); + } + } + + // An id this close to the end, or past it, leaves a stream too few ids + // to send. + for forged in [ + ProcessEventId::Unknown { + block: PROCESS_EVENT_ID_MAX_RESUME_BLOCK, + frame: 0, + } + .encode(), + (1 << 53) - 1, + 1 << 53, + 1 << 62, + MAX_LAST_EVENT_ID, + ] { + assert!(ProcessReplayState::load(Some(forged)).is_none(), "{forged}"); + } +} + +/// Process SSE ids increase on every frame, each decodes to the position the +/// client has received output up to, and once a stream loses that position no +/// later id of it carries one, however many frames follow. +#[test] +fn process_event_ids_increase_and_carry_the_output_position() { + let mut previous = ProcessEventId::START; + let mut lost = false; + let positions = [Some(0), Some(0), Some(1), Some(2)] + .into_iter() + .chain(std::iter::repeat_n( + Some(2), + PROCESS_EVENT_ID_MAX_FRAME as usize + 1, + )) + .chain([Some(3), None, Some(4)]) + .chain(std::iter::repeat_n( + None, + PROCESS_EVENT_ID_MAX_FRAME as usize + 1, + )); + for position in positions { + let id = previous.next(position); + assert!(id.encode() > previous.encode(), "{id:?} after {previous:?}"); + assert!(id.encode() < 1 << 53); + assert_eq!(ProcessEventId::decode(id.encode()), Some(id)); + lost |= !matches!(id, ProcessEventId::Position { .. }); + match id { + ProcessEventId::Position { + position: carried, .. + } => { + assert!(!lost, "{id:?} after the position was lost"); + assert_eq!(Some(carried), position); + } + ProcessEventId::Unknown { .. } => assert!(lost), + } + previous = id; + } + assert!(lost, "running out of frames at one position loses it"); + + for (from, position) in [ + // A position the field cannot hold, and one behind the last. + ( + ProcessEventId::START, + Some(PROCESS_EVENT_ID_MAX_POSITION + 1), + ), + ( + ProcessEventId::Position { + position: 5, + frame: 0, + }, + Some(4), + ), + // The highest position, out of frames. + ( + ProcessEventId::Position { + position: PROCESS_EVENT_ID_MAX_POSITION, + frame: PROCESS_EVENT_ID_MAX_FRAME, + }, + Some(PROCESS_EVENT_ID_MAX_POSITION), + ), + ] { + let id = from.next(position); + assert!(matches!(id, ProcessEventId::Unknown { .. }), "{id:?}"); + assert!(id.encode() > from.encode()); + } + + // Each edge sits where the layout puts it: the first tagged id is the + // start, and the block without a position follows the highest position. + assert_eq!(layout_edges()[0] + 1, ProcessEventId::START.encode()); + assert_eq!( + ProcessEventId::decode(layout_edges()[2] + 1), + Some(ProcessEventId::Unknown { block: 0, frame: 0 }) + ); + assert!(layout_edges()[1] < layout_edges()[2]); + let highest = ProcessEventId::Unknown { + block: PROCESS_EVENT_ID_MAX_POSITION, + frame: PROCESS_EVENT_ID_MAX_FRAME, + }; + assert_eq!( + highest.encode(), + (1 << 53) - (1 << PROCESS_EVENT_ID_GUARD_BITS) + ); + assert_eq!(ProcessEventId::decode(highest.encode()), Some(highest)); + + // An id off by less than the guard, as `+1` counting leaves it, carries + // no position; neither does one off the layout. + let id = ProcessEventId::Position { + position: 7, + frame: 3, + } + .encode(); + for mangled in [ + id + 1, + id - 1, + id + 96, + id - 512, + id + (1 << PROCESS_EVENT_ID_GUARD_BITS) - 1, + id | 1 << 53, + id & !PROCESS_EVENT_ID_TAG, + ] { + assert_eq!(ProcessEventId::decode(mangled), None, "{mangled}"); + } + + // A double, as a JavaScript `Number`, holds every id exactly and prints it + // back unchanged. + for id in [ + ProcessEventId::START.encode(), + id, + layout_edges()[1], + highest.encode(), + ] { + assert_eq!((id as f64).to_string(), id.to_string()); + } +} diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index 4d9d78db15..008e53ce1f 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -1,5 +1,8 @@ use super::*; use axum::extract::ws::Message; +use pplx_space_common_svc::space_api_route::{ + RouteIdentityRemediation, set_route_identity_rejection_metadata, +}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -411,119 +414,37 @@ async fn full_guest_storage_is_named_and_not_retryable() { assert_eq!(body["error"]["retryable"], false); } -/// How a fake cluster API answers `BindDataPlaneSessionProcess`. +/// How a fake cluster API answers `BindDataPlaneSessionProcess` once the test +/// adds a `release` permit. #[derive(Clone, Copy)] enum FakeBind { Succeeds, NotFound, } -/// A fake cluster API and worker for one native process session. Each bind -/// waits for a `release` permit before answering, so a test can hold the -/// binding open while it watches what the client receives. -struct BindingFake { - endpoint: String, - release: Arc, - bind_started: Arc, - bound_process_ids: Arc>>, -} - +/// A process fake whose start reports the process (and its exit, if +/// `exits`) and stays open, and whose binds wait for a `release` permit, so +/// a test can hold the binding open while it watches what the client +/// receives. async fn spawn_binding_fake( session_id: DataPlaneSessionId, exits: bool, bind: FakeBind, -) -> BindingFake { - use prost::Message as _; - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let endpoint = format!("http://{}", listener.local_addr().unwrap()); - let mut route = crate::tests::route_response_with_token(session_id, None); - route.backend.as_mut().unwrap().endpoint = endpoint.clone(); - route.adapter_state_json = r#"{"command":["true"]}"#.to_string(); - let route = route.encode_to_vec(); - let process_id = pplx_space_public_api::process_id_for_session(session_id).to_string(); - let event = |event| { - let mut framed = vec![0]; - let encoded = ProcessEvent { - process_id: process_id.clone(), - pid: Some(42), - event: Some(event), - } - .encode_to_vec(); - framed.extend_from_slice(&(encoded.len() as u32).to_be_bytes()); - framed.extend(encoded); - bytes::Bytes::from(framed) - }; - let mut events = vec![event(spaced_process_event::Event::Started( - pplx_space_sbx_mgr_client::operations::ProcessStarted { start_time: None }, - ))]; - if exits { - events.push(event(spaced_process_event::Event::Exit( - pplx_space_sbx_mgr_client::proto::SpacedProcessExit { - outcome: Some( - pplx_space_sbx_mgr_client::proto::spaced_process_exit::Outcome::Exited( - pplx_space_sbx_mgr_client::proto::SpacedProcessExited { exit_code: 0 }, - ), - ), - }, - ))); - } - let release = Arc::new(tokio::sync::Semaphore::new(0)); - let bind_started = Arc::new(tokio::sync::Notify::new()); - let bound_process_ids = Arc::new(std::sync::Mutex::new(Vec::new())); - let fake = BindingFake { - endpoint, - release: Arc::clone(&release), - bind_started: Arc::clone(&bind_started), - bound_process_ids: Arc::clone(&bound_process_ids), - }; - let service = axum::Router::new().fallback(move |request: Request| { - let route = route.clone(); - let events = events.clone(); - let release = Arc::clone(&release); - let bind_started = Arc::clone(&bind_started); - let bound_process_ids = Arc::clone(&bound_process_ids); - async move { - let path = request.uri().path().to_string(); - if path.ends_with("/Check") { - return crate::tests::grpc_test_response(vec![8, 1]); - } - if path.ends_with("/ResolveDataPlaneSessionRoute") { - return crate::tests::grpc_test_response(route); - } - if path.ends_with("/BindDataPlaneSessionProcess") { - let body = request.into_body().collect().await.unwrap().to_bytes(); - let bound = BindDataPlaneSessionProcessRequest::decode(&body[5..]).unwrap(); - bound_process_ids.lock().unwrap().push(bound.process_id); - bind_started.notify_one(); - release.acquire().await.unwrap().forget(); - return match bind { - FakeBind::Succeeds => crate::tests::grpc_test_response(Vec::new()), - FakeBind::NotFound => { - tonic::Status::not_found("process-backed data-plane session not found") - .into_http::() - } - }; +) -> ProcessFake { + spawn_process_fake( + session_id, + false, + Some(bind), + Arc::new(move |_, open| { + assert_eq!(open, FakeOpen::Start); + let mut replies = vec![started()]; + if exits { + replies.push(exited()); } - assert!( - path.ends_with("/SpacedStartProcess"), - "unexpected call {path}" - ); - let frames = futures_util::stream::iter( - events - .into_iter() - .map(|event| Ok::<_, Infallible>(hyper::body::Frame::data(event))), - ) - .chain(futures_util::stream::pending()); - Response::builder() - .header("content-type", "application/grpc") - .body(Body::new(http_body_util::StreamBody::new(frames))) - .unwrap() - } - }); - tokio::spawn(async move { - axum::serve(listener, service).await.unwrap(); - }); - fake + replies + }), + ) + .await } fn binding_fake_test_process() -> bool { @@ -1715,11 +1636,66 @@ async fn websocket_resize_refusal_carries_a_fixed_message() { enum FakeReply { Event(spaced_process_event::Event), /// Ends the stream with this status. - Status(tonic::Code), - /// Ends the stream as the guest does for a cursor past the output. - CursorAhead, + End(tonic::Status), /// Refuses the call outright, before any stream. - Refuse(tonic::Code), + Refuse(tonic::Status), +} + +fn end(code: tonic::Code) -> FakeReply { + FakeReply::End(tonic::Status::new(code, "fake")) +} + +fn with_reason(mut status: tonic::Status, reason: &'static str) -> tonic::Status { + status.metadata_mut().insert( + pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY, + tonic::metadata::MetadataValue::from_static(reason), + ); + status +} + +/// Ends the stream as the guest does for a cursor past the output. +fn cursor_ahead() -> FakeReply { + FakeReply::End(with_reason( + tonic::Status::out_of_range("fake"), + pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD, + )) +} + +/// Ends the stream as the guest does for a cursor before the oldest retained +/// output, `first_retained`, when it still holds the process. +fn lagged(first_retained: Option) -> FakeReply { + let mut status = with_reason( + tonic::Status::resource_exhausted( + pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED_MESSAGE, + ), + pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED, + ); + if let Some(first_retained) = first_retained { + status.metadata_mut().insert( + pplx_space_common_core::spaced::PROCESS_OUTPUT_FIRST_SEQUENCE_METADATA_KEY, + first_retained.into(), + ); + } + FakeReply::End(status) +} + +/// Ends the stream as a guest older than the lag reason does on eviction. +fn untagged_lagged() -> FakeReply { + FakeReply::End(tonic::Status::resource_exhausted( + pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED_MESSAGE, + )) +} + +/// Refuses the call as a node does a route token that a refresh replaces, +/// such as an expired one. +fn refreshable_route_rejection() -> FakeReply { + let mut status = tonic::Status::permission_denied("route token expired"); + set_route_identity_rejection_metadata( + &mut status, + RouteIdentityRemediation::RefreshRoute, + None, + ); + FakeReply::Refuse(status) } /// How the data plane opened a fake guest process stream. @@ -1770,7 +1746,7 @@ fn journal_replay(lines: &[&str], open: FakeOpen) -> Vec { FakeOpen::Start => (vec![started()], 0), FakeOpen::Connect(None) => return vec![started(), exited()], FakeOpen::Connect(Some(cursor)) if cursor as usize > lines.len() => { - return vec![connected(), FakeReply::CursorAhead]; + return vec![connected(), cursor_ahead()]; } FakeOpen::Connect(Some(cursor)) => (vec![connected()], cursor), }; @@ -1784,18 +1760,31 @@ type FakeReplies = Arc Vec + Send + Sync>; /// A fake cluster API and worker for one native process session, bound to its /// process unless `bound` is false. Each process stream the data plane opens /// is answered by `replies`, given how many opens came before it; a stream -/// without a closing status stays open. -struct JournalFake { +/// without a closing status stays open. A bind succeeds at once, or with +/// `gated_bind`, answers as that says once the test adds a `release` permit. +struct ProcessFake { endpoint: String, bound: Arc, opens: Arc>>, + release: Arc, + bind_started: Arc, + bound_process_ids: Arc>>, } async fn spawn_journal_fake( session_id: DataPlaneSessionId, bound: bool, replies: FakeReplies, -) -> JournalFake { +) -> ProcessFake { + spawn_process_fake(session_id, bound, None, replies).await +} + +async fn spawn_process_fake( + session_id: DataPlaneSessionId, + bound: bool, + gated_bind: Option, + replies: FakeReplies, +) -> ProcessFake { use prost::Message as _; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let endpoint = format!("http://{}", listener.local_addr().unwrap()); @@ -1803,19 +1792,28 @@ async fn spawn_journal_fake( let mut route = crate::tests::route_response_with_token(session_id, None); route.backend.as_mut().unwrap().endpoint = endpoint.clone(); route.adapter_state_json = r#"{"command":["true"]}"#.to_string(); - let bound = Arc::new(AtomicBool::new(bound)); - let opens = Arc::new(std::sync::Mutex::new(Vec::new())); - let fake = JournalFake { + let fake = ProcessFake { endpoint, - bound: Arc::clone(&bound), - opens: Arc::clone(&opens), + bound: Arc::new(AtomicBool::new(bound)), + opens: Arc::new(std::sync::Mutex::new(Vec::new())), + release: Arc::new(tokio::sync::Semaphore::new(0)), + bind_started: Arc::new(tokio::sync::Notify::new()), + bound_process_ids: Arc::new(std::sync::Mutex::new(Vec::new())), }; + let bound = Arc::clone(&fake.bound); + let opens = Arc::clone(&fake.opens); + let release = Arc::clone(&fake.release); + let bind_started = Arc::clone(&fake.bind_started); + let bound_process_ids = Arc::clone(&fake.bound_process_ids); let service = axum::Router::new().fallback(move |request: Request| { let mut route = route.clone(); let process_id = process_id.clone(); let replies = Arc::clone(&replies); let opens = Arc::clone(&opens); let bound = Arc::clone(&bound); + let release = Arc::clone(&release); + let bind_started = Arc::clone(&bind_started); + let bound_process_ids = Arc::clone(&bound_process_ids); async move { let path = request.uri().path().to_string(); if path.ends_with("/Check") { @@ -1826,11 +1824,23 @@ async fn spawn_journal_fake( return crate::tests::grpc_test_response(route.encode_to_vec()); } if path.ends_with("/BindDataPlaneSessionProcess") { + let body = request.into_body().collect().await.unwrap().to_bytes(); + let request = BindDataPlaneSessionProcessRequest::decode(&body[5..]).unwrap(); + bound_process_ids.lock().unwrap().push(request.process_id); + if let Some(bind) = gated_bind { + bind_started.notify_one(); + release.acquire().await.unwrap().forget(); + if let FakeBind::NotFound = bind { + return tonic::Status::not_found( + "process-backed data-plane session not found", + ) + .into_http::(); + } + } bound.store(true, Ordering::SeqCst); + return crate::tests::grpc_test_response(Vec::new()); } - if path.ends_with("/RecordDataPlaneSessionReconnect") - || path.ends_with("/BindDataPlaneSessionProcess") - { + if path.ends_with("/RecordDataPlaneSessionReconnect") { return crate::tests::grpc_test_response(Vec::new()); } let open = if path.ends_with("/SpacedStartProcess") { @@ -1857,14 +1867,8 @@ async fn spawn_journal_fake( let mut frames = Vec::new(); let mut trailers = None; for reply in replies(opened, open) { - let (code, reason) = match reply { - FakeReply::Refuse(code) => { - return Response::builder() - .header("content-type", "application/grpc") - .header("grpc-status", code as i32) - .body(Body::empty()) - .unwrap(); - } + match reply { + FakeReply::Refuse(status) => return status.into_http::(), FakeReply::Event(event) => { let encoded = ProcessEvent { process_id: process_id.clone(), @@ -1876,23 +1880,13 @@ async fn spawn_journal_fake( framed.extend_from_slice(&(encoded.len() as u32).to_be_bytes()); framed.extend(encoded); frames.push(hyper::body::Frame::data(bytes::Bytes::from(framed))); - continue; } - FakeReply::Status(code) => (code, None), - FakeReply::CursorAhead => ( - tonic::Code::OutOfRange, - Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD), - ), - }; - let mut status = HeaderMap::new(); - status.insert("grpc-status", HeaderValue::from(code as i32)); - if let Some(reason) = reason { - status.insert( - pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY, - HeaderValue::from_static(reason), - ); + FakeReply::End(status) => { + let mut headers = status.into_http::().into_parts().0.headers; + headers.remove(header::CONTENT_TYPE); + trailers = Some(headers); + } } - trailers = Some(status); } let ending = match trailers { Some(trailers) => { @@ -2060,11 +2054,11 @@ async fn process_resume_continues_after_the_last_event_id() { } /// Output the guest evicted before the client asked for it is a real gap: -/// the stream reports it once, follows the live tail without reporting the -/// process start again, and a resume from what it sent next carries no -/// position, so it replays behind `resync_required` rather than silently. +/// the stream reports it once and continues from the oldest output the guest +/// still retains, without reporting the process start again. Its ids keep +/// carrying positions, so a resume from the gap's id is exact. #[tokio::test] -async fn evicted_process_output_is_reported_before_the_live_tail() { +async fn evicted_process_output_continues_from_the_oldest_retained_output() { if !binding_fake_test_process() { return; } @@ -2073,10 +2067,52 @@ async fn evicted_process_output_is_reported_before_the_live_tail() { session_id, true, Arc::new(|_, open| match open { - FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![ - connected(), - FakeReply::Status(tonic::Code::ResourceExhausted), - ], + FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![connected(), lagged(Some(2))], + open => journal_replay(&JOURNAL, open), + }), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), "three\nfour\n"); + assert_eq!(frames_of_type(&frames, "process_started").count(), 1); + assert!( + frames_of_type(&frames, "error").next().is_none(), + "{frames:?}" + ); + + let gap = frames_of_type(&frames, "resync_required") + .next() + .unwrap() + .0 + .unwrap(); + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(gap)).await; + assert_eq!(resync_reasons(&resumed), Vec::::new()); + assert_eq!(stdout_text(&resumed), "three\nfour\n"); + assert_ids_above(&resumed, gap); + assert_eq!( + *fake.opens.lock().unwrap(), + connects(&[Some(0), Some(2), Some(2)]) + ); +} + +/// A guest older than the lag reason does not say where its retained output +/// starts: the stream reports the gap once, follows the live tail without +/// reporting the process start again, and a resume from what it sent next +/// carries no position, so it replays behind `resync_required` rather than +/// silently. +#[tokio::test] +async fn evicted_output_on_an_older_guest_is_reported_before_the_live_tail() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| match open { + FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![connected(), untagged_lagged()], FakeOpen::Connect(None) => vec![connected(), stdout("live\n"), exited()], open => journal_replay(&JOURNAL, open), }), @@ -2105,6 +2141,200 @@ async fn evicted_process_output_is_reported_before_the_live_tail() { ); } +/// A RESOURCE_EXHAUSTED the guest did not tag as a lag, such as a hop's +/// refusal of an oversized chunk, is not reported as evicted output: the +/// stream ends with a retryable error instead of skipping to the live tail. +#[tokio::test] +async fn untagged_resource_exhausted_is_not_an_output_gap() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, _| { + vec![ + connected(), + stdout(JOURNAL[0]), + FakeReply::End(tonic::Status::resource_exhausted( + "guest process output chunk is too large", + )), + ] + }), + ) + .await; + + let (_state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; + let mut seen = String::new(); + read_until(&mut chunks, &mut seen, "backend_stream_error").await; + let frames = sse_frames(&seen); + assert_eq!(resync_reasons(&frames), Vec::::new()); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0)])); +} + +/// A cursor past the output restarts at the start; when the start has been +/// evicted too, that is the same gap, reported once. +#[tokio::test] +async fn cursor_ahead_then_evicted_start_reports_one_gap() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| match open { + FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![connected(), lagged(Some(2))], + open => journal_replay(&JOURNAL, open), + }), + ) + .await; + let ahead = ProcessEventId::Position { + position: 10, + frame: 0, + } + .encode(); + + let frames = process_events_to_close(&fake.endpoint, session_id, Some(ahead)).await; + + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), "three\nfour\n"); + assert_ids_above(&frames, ahead); + assert_eq!( + *fake.opens.lock().unwrap(), + connects(&[Some(10), Some(0), Some(2)]) + ); +} + +/// A guest that keeps refusing the cursor without ever sending output does +/// not keep the stream reopening: after a few reopens it ends with an error. +#[tokio::test] +async fn window_miss_reopens_without_progress_are_capped() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| { + let FakeOpen::Connect(Some(cursor)) = open else { + panic!("unexpected open {open:?}"); + }; + vec![connected(), lagged(Some(cursor + 1))] + }), + ) + .await; + + let (_state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; + let mut seen = String::new(); + read_until(&mut chunks, &mut seen, "backend_stream_error").await; + let frames = sse_frames(&seen); + + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!( + *fake.opens.lock().unwrap(), + connects(&[Some(0), Some(1), Some(2), Some(3)]) + ); +} + +/// Output between window misses is progress: a stream that keeps falling +/// behind but keeps receiving output is not ended by the reopen cap. +#[tokio::test] +async fn window_misses_with_output_between_are_not_capped() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| { + let FakeOpen::Connect(Some(cursor)) = open else { + panic!("unexpected open {open:?}"); + }; + // Each reopen gets one line, then falls behind by one more. + if cursor >= 12 { + return vec![connected(), exited()]; + } + vec![connected(), stdout("line\n"), lagged(Some(cursor + 2))] + }), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + + assert!( + frames_of_type(&frames, "error").next().is_none(), + "{frames:?}" + ); + assert_eq!(stdout_text(&frames), "line\n".repeat(6)); + assert_eq!(fake.opens.lock().unwrap().len(), 7); +} + +/// The reopen after a window miss uses the stream's route token; when the +/// node refuses it as one a refresh replaces, such as an expired token, the +/// stream refreshes its route and continues instead of failing. +#[tokio::test] +async fn window_miss_reopen_refreshes_a_refused_route() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|opened, open| match (opened, open) { + (0, _) => vec![connected(), lagged(Some(2))], + (1, _) => vec![refreshable_route_rejection()], + (_, open) => journal_replay(&JOURNAL, open), + }), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + + assert!( + frames_of_type(&frames, "error").next().is_none(), + "{frames:?}" + ); + assert_eq!(frames_of_type(&frames, "backend_reconnected").count(), 1); + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), "three\nfour\n"); + assert_eq!( + *fake.opens.lock().unwrap(), + connects(&[Some(0), Some(2), Some(2)]) + ); +} + +/// A bound attach whose first backend stream fails before it reports the +/// process still sends `process_started` once, from the reopened stream. +#[tokio::test] +async fn backend_failure_before_the_start_still_reports_it_once() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|opened, open| match opened { + 0 => vec![end(tonic::Code::Unavailable)], + _ => journal_replay(&JOURNAL, open), + }), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + + assert_eq!(frames_of_type(&frames, "backend_reconnected").count(), 1); + assert_eq!(frames_of_type(&frames, "process_started").count(), 1); + assert_eq!(stdout_text(&frames), JOURNAL.concat()); + assert_eq!(resync_reasons(&frames), Vec::::new()); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0), Some(0)])); +} + /// A backend reconnect after a resume reopens the guest stream at the output /// position the client has reached; that replay is lossless, so the client is /// not told to resync. @@ -2122,7 +2352,7 @@ async fn backend_reconnect_after_a_resume_continues_without_a_resync() { (1, FakeOpen::Connect(Some(cursor))) => vec![ connected(), stdout(JOURNAL[cursor as usize]), - FakeReply::Status(tonic::Code::Unavailable), + end(tonic::Code::Unavailable), ], (_, open) => journal_replay(&JOURNAL, open), }), @@ -2270,7 +2500,7 @@ async fn already_started_process_is_resumed_exactly() { session_id, false, Arc::new(|_, open| match open { - FakeOpen::Start => vec![FakeReply::Refuse(tonic::Code::AlreadyExists)], + FakeOpen::Start => vec![FakeReply::Refuse(tonic::Status::already_exists("fake"))], open => journal_replay(&JOURNAL, open), }), ) @@ -2313,6 +2543,7 @@ async fn last_event_id_without_room_after_it_is_rejected() { .unwrap(), ); let token = format!("{DATA_PLANE_TOKEN_PREFIX}{}", "c".repeat(64)); + let capture = pplx_space_metrics::test_capture::capture_counters_guard(); let response = tower::ServiceExt::oneshot( router(state), Request::get(format!("/v1/sessions/{session_id}/events")) @@ -2326,4 +2557,62 @@ async fn last_event_id_without_room_after_it_is_rejected() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert!(fake.opens.lock().unwrap().is_empty()); + assert!( + capture + .finish() + .iter() + .any(|counter| counter.name == "data_plane.attach" + && counter + .tags + .contains(&"result:invalid_last_event_id".to_string())), + "the refused attach is counted" + ); +} + +/// An attach refused because the session is already attached sends nothing, +/// so it counts no `resync_required` either. +#[tokio::test] +async fn refused_attach_counts_no_replay_miss() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| journal_replay(&JOURNAL, open)), + ) + .await; + let mut state = crate::tests::test_data_plane_state().await; + state.cluster_api = ClusterApiClients::service_only( + pplx_space_node_api_client::cluster::connect(fake.endpoint.clone(), "test") + .await + .unwrap(), + ); + let _attached = state + .native_sessions + .try_acquire(session_id, SessionKind::Process) + .unwrap(); + let token = format!("{DATA_PLANE_TOKEN_PREFIX}{}", "c".repeat(64)); + let capture = pplx_space_metrics::test_capture::capture_counters_guard(); + let response = tower::ServiceExt::oneshot( + router(state), + Request::get(format!("/v1/sessions/{session_id}/events")) + .header(header::AUTHORIZATION, format!("Bearer {token}")) + // Untagged, so a stream would open behind `resync_required`. + .header("last-event-id", "41") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CONFLICT); + let counters = capture.finish(); + assert!( + !counters + .iter() + .any(|counter| counter.name == "data_plane.replay.miss"), + "{counters:?}" + ); } diff --git a/crates/libs/data/plane/server/src/tests.rs b/crates/libs/data/plane/server/src/tests.rs index 72e37b5493..b5a195e130 100644 --- a/crates/libs/data/plane/server/src/tests.rs +++ b/crates/libs/data/plane/server/src/tests.rs @@ -1,12 +1,5 @@ use super::*; -use crate::{ - native::{ - PROCESS_EVENT_ID_GUARD_BITS, PROCESS_EVENT_ID_MAX_FRAME, PROCESS_EVENT_ID_MAX_POSITION, - PROCESS_EVENT_ID_MAX_RESUME_BLOCK, ProcessEventId, ProcessReplayState, - first_frame_attach_token_from_recv, - }, - runtime_route::sweep_expired_ecw_routes, -}; +use crate::{native::first_frame_attach_token_from_recv, runtime_route::sweep_expired_ecw_routes}; use aws_lc_rs::rand::SystemRandom; use aws_lc_rs::signature::Ed25519KeyPair; use pplx_space_common_core::{ @@ -3705,178 +3698,29 @@ async fn first_frame_attach_token_times_out_at_deadline() { assert_eq!(result, Err(DataPlaneErrorCode::AttachDeadlineExceeded)); } -/// A fresh attach replays the guest journal from its start, and a resume -/// continues from the output position its id carries. An id without one, -/// including every id an earlier data plane issued, also replays from the -/// start, behind `resync_required`. -#[test] -fn process_sse_resume_starts_at_the_journal_position_its_id_carries() { - let session_id = DataPlaneSessionId::Process(Default::default()); - - let fresh = ProcessReplayState::load(session_id, None).unwrap(); - assert_eq!(fresh.backend_output_cursor, 0); - assert!(fresh.pending.is_empty()); - - let carried = ProcessEventId::Position { - position: 3, - frame: 2, - }; - let resumed = ProcessReplayState::load(session_id, Some(carried.encode())).unwrap(); - assert_eq!(resumed.event_id, carried); - assert_eq!(resumed.backend_output_cursor, 3); - assert!(resumed.pending.is_empty()); - - // Earlier data planes count frames from 1, so their ids sit on either side - // of any decimal boundary; one counted on from a tagged id fails its guard. - // Whatever the id, every id the replay sends is above it. - for last_event_id in [ - 0, - 41, - 999_999, - 1_000_000, - 1_500_000, - carried.encode() + 1, - ProcessEventId::Unknown { block: 4, frame: 1 }.encode(), - ProcessEventId::Unknown { block: 4, frame: 1 }.encode() + 1, - ProcessEventId::Unknown { - block: PROCESS_EVENT_ID_MAX_RESUME_BLOCK - 1, - frame: PROCESS_EVENT_ID_MAX_FRAME, - } - .encode(), - ] { - let replay = ProcessReplayState::load(session_id, Some(last_event_id)).unwrap(); - assert_eq!(replay.backend_output_cursor, 0, "{last_event_id}"); - assert_eq!(replay.pending.len(), 1, "{last_event_id}"); - assert!(replay.event_id.encode() > last_event_id, "{last_event_id}"); - let mut id = replay.event_id; - for position in [Some(0), Some(1), None, Some(2)] { - id = id.next(position); - assert!(id.encode() > last_event_id, "{last_event_id}"); - } - } - - // An id this close to the end leaves a stream too few ids to send. - for forged in [ - ProcessEventId::Unknown { - block: PROCESS_EVENT_ID_MAX_RESUME_BLOCK, - frame: 0, - } - .encode(), - MAX_LAST_EVENT_ID, - ] { - assert!(ProcessReplayState::load(session_id, Some(forged)).is_none()); - } -} - -/// Process SSE ids increase on every frame, each decodes to the position the -/// client has received output up to, and once a stream loses that position no -/// later id of it carries one, however many frames follow. -#[test] -fn process_event_ids_increase_and_carry_the_output_position() { - let mut previous = ProcessEventId::START; - let mut lost = false; - let positions = [Some(0), Some(0), Some(1), Some(2)] - .into_iter() - .chain(std::iter::repeat_n( - Some(2), - PROCESS_EVENT_ID_MAX_FRAME as usize + 1, - )) - .chain([Some(3), None, Some(4)]) - .chain(std::iter::repeat_n( - None, - PROCESS_EVENT_ID_MAX_FRAME as usize + 1, - )); - for position in positions { - let id = previous.next(position); - assert!(id.encode() > previous.encode(), "{id:?} after {previous:?}"); - assert!(id.encode() <= MAX_LAST_EVENT_ID); - assert_eq!(ProcessEventId::decode(id.encode()), Some(id)); - lost |= !matches!(id, ProcessEventId::Position { .. }); - match id { - ProcessEventId::Position { - position: carried, .. - } => { - assert!(!lost, "{id:?} after the position was lost"); - assert_eq!(Some(carried), position); - } - ProcessEventId::Unknown { .. } => assert!(lost), - } - previous = id; - } - assert!(lost, "running out of frames at one position loses it"); - - for (from, position) in [ - // A position the field cannot hold, and one behind the last. - ( - ProcessEventId::START, - Some(PROCESS_EVENT_ID_MAX_POSITION + 1), - ), - ( - ProcessEventId::Position { - position: 5, - frame: 0, - }, - Some(4), - ), - ] { - let id = from.next(position); - assert!(matches!(id, ProcessEventId::Unknown { .. }), "{id:?}"); - assert!(id.encode() > from.encode()); - } - - let highest = ProcessEventId::Unknown { - block: PROCESS_EVENT_ID_MAX_POSITION, - frame: PROCESS_EVENT_ID_MAX_FRAME, - }; - assert!(highest.encode() <= MAX_LAST_EVENT_ID); - - // An id off by less than the guard, as `+1` counting or rounding through - // a double leaves it, carries no position. - let id = ProcessEventId::Position { - position: 7, - frame: 3, - } - .encode(); - for mangled in [ - id + 1, - id - 1, - id + 96, - id - 512, - id + (1 << PROCESS_EVENT_ID_GUARD_BITS) - 1, - ] { - assert_eq!(ProcessEventId::decode(mangled), None, "{mangled}"); - } - // Unrounded, a double holds the id exactly. - let double: f64 = id.to_string().parse().unwrap(); - assert_eq!(format!("{double:.0}"), id.to_string()); -} - /// The file-watch reconnection contract: `Last-Event-ID` resumes the live /// stream at `last + 1` behind a `resync_required(cursor_replay_unavailable)` /// frame; no cursor, no frame. #[test] fn last_event_id_resume_is_a_cursor_replay_unavailable_resync() { - for kind in ["process", "file_watch"] { - assert_eq!(resume_resync_frame(kind, None), None, "{kind}"); - assert_eq!( - resume_resync_frame(kind, Some(41)), - Some(( - 42, - DataPlaneServerFrame::ResyncRequired { - reason: DataPlaneResyncReason::CursorReplayUnavailable, - }, - )), - "{kind}" - ); - // The largest cursor the request boundary admits still gets a fresh id. - assert_eq!( - resume_resync_frame(kind, Some(MAX_LAST_EVENT_ID)).map(|(seq, _)| seq), - Some(MAX_LAST_EVENT_ID + 1), - "{kind}" - ); - } + let kind = "file_watch"; + assert_eq!(resume_resync_frame(kind, None), None); + assert_eq!( + resume_resync_frame(kind, Some(41)), + Some(( + 42, + DataPlaneServerFrame::ResyncRequired { + reason: DataPlaneResyncReason::CursorReplayUnavailable, + }, + )) + ); + // The largest cursor the request boundary admits still gets a fresh id. + assert_eq!( + resume_resync_frame(kind, Some(MAX_LAST_EVENT_ID)).map(|(seq, _)| seq), + Some(MAX_LAST_EVENT_ID + 1) + ); assert_eq!( - resume_resync_frame("process", Some(0)).map(|(seq, _)| seq), + resume_resync_frame(kind, Some(0)).map(|(seq, _)| seq), Some(1) ); } diff --git a/crates/libs/node/api/sim/src/processes.rs b/crates/libs/node/api/sim/src/processes.rs index e7fa63ba6c..8c7d4af3a9 100644 --- a/crates/libs/node/api/sim/src/processes.rs +++ b/crates/libs/node/api/sim/src/processes.rs @@ -9,8 +9,10 @@ //! //! Reconnects follow the guest's journal contract (`ConnectProcessRequest`): //! output records carry a sequence starting at `0`; a connect with an -//! `output_cursor` replays every record at or after that sequence, and a -//! connect without one follows the live tail and replays nothing. The +//! `output_cursor` replays every record at or after that sequence, a cursor +//! past the output ends the stream with the guest's tagged `OUT_OF_RANGE`, +//! and a connect without one follows the live tail and replays nothing. The +//! simulator retains all output, so it never reports a cursor as evicted. The //! process table is per sandbox and is dropped with the sandbox. use std::collections::HashMap; @@ -225,7 +227,8 @@ enum ConnectMode { /// background start ends after `Started`, like production's stream. Start { background: bool }, /// Reattach: `Connected`, then the journal from `output_cursor`; `None` - /// is the live tail, which replays nothing. + /// is the live tail, which replays nothing, and a cursor past the output + /// fails. Connect { output_cursor: Option }, } @@ -247,10 +250,17 @@ fn event_stream(process: &SimProcess, mode: ConnectMode) -> SimProcessEventStrea } (started, 0) } - ConnectMode::Connect { output_cursor } => ( - sbx::spaced_process_event::Event::Connected(sbx::SpacedProcessConnected {}), - output_cursor.unwrap_or(tail).min(tail), - ), + ConnectMode::Connect { output_cursor } => { + let connected = + sbx::spaced_process_event::Event::Connected(sbx::SpacedProcessConnected {}); + if output_cursor.is_some_and(|cursor| cursor > tail) { + return Box::pin(tokio_stream::iter(vec![ + Ok(event(&process_id, Some(pid), connected)), + Err(cursor_ahead_status()), + ])); + } + (connected, output_cursor.unwrap_or(tail)) + } }; let mut initial = vec![Ok(event(&process_id, Some(pid), first))]; initial.extend( @@ -282,6 +292,19 @@ fn event_stream(process: &SimProcess, mode: ConnectMode) -> SimProcessEventStrea Box::pin(tokio_stream::iter(initial).chain(exit)) } +/// The guest's status for a connect whose cursor is past the output. +fn cursor_ahead_status() -> Status { + let mut status = + Status::out_of_range("process output cursor is past the output the process has written"); + status.metadata_mut().insert( + pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY, + tonic::metadata::MetadataValue::from_static( + pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD, + ), + ); + status +} + fn event( process_id: &str, pid: Option, diff --git a/crates/libs/node/api/sim/src/processes/tests.rs b/crates/libs/node/api/sim/src/processes/tests.rs index 15e1d439af..4889326da5 100644 --- a/crates/libs/node/api/sim/src/processes/tests.rs +++ b/crates/libs/node/api/sim/src/processes/tests.rs @@ -64,9 +64,28 @@ async fn connect_replays_the_journal_from_the_cursor_only() { let live = collect(table.connect("sbx", &process_id, None).await.unwrap()).await; assert_eq!(stdout_count(&live), 0); - // A cursor past the tail is clamped rather than panicking. - let beyond = collect(table.connect("sbx", &process_id, Some(99)).await.unwrap()).await; - assert_eq!(stdout_count(&beyond), 0); + // A cursor past the tail fails, as the guest's does, instead of + // skipping the output before it. + let beyond: Vec<_> = table + .connect("sbx", &process_id, Some(2)) + .await + .unwrap() + .collect() + .await; + assert_eq!(beyond.len(), 2); + assert!(matches!( + beyond[0].as_ref().unwrap().event, + Some(sbx::spaced_process_event::Event::Connected(_)) + )); + let error = beyond[1].as_ref().unwrap_err(); + assert_eq!(error.code(), tonic::Code::OutOfRange); + assert_eq!( + error + .metadata() + .get(pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY) + .and_then(|value| value.to_str().ok()), + Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_CURSOR_AHEAD) + ); } /// Every execution-teardown path (stop, kill, delete, cleanup) ends the diff --git a/crates/libs/spaced/guest/execution/src/process.rs b/crates/libs/spaced/guest/execution/src/process.rs index 5d0163b9e3..718f91ffc6 100644 --- a/crates/libs/spaced/guest/execution/src/process.rs +++ b/crates/libs/spaced/guest/execution/src/process.rs @@ -79,8 +79,6 @@ const PROCESS_OUTPUT_GUARANTEED_BYTES: usize = const PROCESS_OUTPUT_SHARED_BYTES: usize = PROCESS_OUTPUT_RETENTION_MAX_BYTES - PROCESS_RECORD_LIMIT * PROCESS_OUTPUT_GUARANTEED_BYTES; const PROCESS_OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_secs(1); -const PROCESS_OUTPUT_LAGGED_MESSAGE: &str = - "process output stream fell behind retained output buffer"; const PROCESS_OUTPUT_CURSOR_AHEAD_MESSAGE: &str = "process output cursor is past the output the process has written"; const PROCESS_CAPACITY_MESSAGE: &str = "too many retained processes"; @@ -1211,7 +1209,9 @@ impl ProcessEventJournal { .lock() .expect("process output journal lock poisoned"); if cursor < state.first_sequence { - return ProcessJournalRead::Lagged; + return ProcessJournalRead::Lagged { + first_sequence: state.first_sequence, + }; } // A cursor only ever advances over output it has read, so one past the // end came from a caller, not from this journal: waiting for output to @@ -1288,7 +1288,7 @@ enum ProcessJournalRead { Event(ProcessEvent), Exit(ProcessEvent), Pending, - Lagged, + Lagged { first_sequence: u64 }, Ahead, } @@ -1549,8 +1549,8 @@ fn process_event_stream_with_prefix( return Some((Ok(event), (prefix, cursor))); } ProcessJournalRead::Exit(exit) => break Ok(exit), - ProcessJournalRead::Lagged => { - break Err(Status::resource_exhausted(PROCESS_OUTPUT_LAGGED_MESSAGE)); + ProcessJournalRead::Lagged { first_sequence } => { + break Err(process_output_lagged_status(Some(first_sequence))); } ProcessJournalRead::Ahead => break Err(process_output_cursor_ahead_status()), ProcessJournalRead::Pending => { @@ -1576,15 +1576,35 @@ fn process_output_cursor_ahead_status() -> Status { status } +/// The status that ends a stream whose cursor is before the oldest retained +/// output, naming where that output starts, `first_sequence`, so the caller +/// can continue from there. `None` once the journal itself is gone. +fn process_output_lagged_status(first_sequence: Option) -> Status { + let mut status = + Status::resource_exhausted(pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED_MESSAGE); + let metadata = status.metadata_mut(); + metadata.insert( + pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY, + tonic::metadata::MetadataValue::from_static( + pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED, + ), + ); + if let Some(first_sequence) = first_sequence { + metadata.insert( + pplx_space_common_core::spaced::PROCESS_OUTPUT_FIRST_SEQUENCE_METADATA_KEY, + tonic::metadata::MetadataValue::from(first_sequence), + ); + } + status +} + fn evicted_process_journal_result(cursor: &ProcessJournalCursor) -> Result { match cursor.changed.borrow().clone() { ProcessJournalChange::Exit { event, next_sequence, } if cursor.next_sequence >= next_sequence => Ok(event), - ProcessJournalChange::Exit { .. } => { - Err(Status::resource_exhausted(PROCESS_OUTPUT_LAGGED_MESSAGE)) - } + ProcessJournalChange::Exit { .. } => Err(process_output_lagged_status(None)), ProcessJournalChange::OutputChanged => Err(Status::resource_exhausted( "process replay state was evicted", )), diff --git a/crates/libs/spaced/guest/execution/src/process/tests.rs b/crates/libs/spaced/guest/execution/src/process/tests.rs index 87ded94632..ba5874d71e 100644 --- a/crates/libs/spaced/guest/execution/src/process/tests.rs +++ b/crates/libs/spaced/guest/execution/src/process/tests.rs @@ -335,7 +335,7 @@ async fn losing_start_response_preserves_output_supervision_and_idempotency() { let mut changed = record.journal.subscribe(); while record.journal.tail_cursor() < 2 || (completed && !record.journal.is_completed()) - || (evicted && !matches!(record.journal.read(0), ProcessJournalRead::Lagged)) + || (evicted && !matches!(record.journal.read(0), ProcessJournalRead::Lagged { .. })) { changed.changed().await.unwrap(); } @@ -369,7 +369,10 @@ async fn losing_start_response_preserves_output_supervision_and_idempotency() { Err(error) => { assert!(evicted); assert_eq!(error.code(), tonic::Code::ResourceExhausted); - assert_eq!(error.message(), PROCESS_OUTPUT_LAGGED_MESSAGE); + assert_eq!( + error.message(), + pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED_MESSAGE + ); lagged = true; assert!(replay.next().await.is_none()); break; @@ -521,7 +524,7 @@ async fn process_journal_reports_lag_when_local_ring_evicts_output() { )); } assert!( - matches!(journal.read(0), ProcessJournalRead::Lagged), + matches!(journal.read(0), ProcessJournalRead::Lagged { .. }), "retention must charge tiny event overhead as well as payloads" ); assert!(matches!( @@ -804,7 +807,7 @@ fn process_journal_drops_oversized_chunk_without_panicking() { vec![0; PROCESS_OUTPUT_RETENTION_BYTES + 1], )); - assert!(matches!(journal.read(0), ProcessJournalRead::Lagged)); + assert!(matches!(journal.read(0), ProcessJournalRead::Lagged { .. })); } #[test] @@ -823,7 +826,7 @@ fn output_pressure_is_isolated_to_one_process() { )); noisy.append_output(process_stdout_event(&noisy_id, Some(1), vec![1])); - assert!(matches!(noisy.read(0), ProcessJournalRead::Lagged)); + assert!(matches!(noisy.read(0), ProcessJournalRead::Lagged { .. })); assert!(matches!(quiet.read(0), ProcessJournalRead::Event(_))); } @@ -953,6 +956,50 @@ async fn cursor_past_the_output_fails_instead_of_waiting() { } } +/// A cursor before the oldest retained output fails with a status that says +/// so and where that output starts, so the caller can continue from there +/// instead of jumping to the live tail. +#[tokio::test] +async fn lagged_cursor_reports_the_oldest_retained_output() { + use pplx_space_spaced_guest_protocol::proto::bridge_guest::process_event::Event; + + let journal = Arc::new(ProcessEventJournal::new(Arc::new(Semaphore::new(0)))); + let process_id = ProcessId::new(); + journal.append_output(process_stdout_event( + &process_id, + Some(1), + vec![0; PROCESS_OUTPUT_RETENTION_BYTES], + )); + journal.append_output(process_stdout_event(&process_id, Some(1), b"kept".to_vec())); + + let mut lagged = process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(0)))); + let error = lagged.next().await.unwrap().unwrap_err(); + assert_eq!(error.code(), tonic::Code::ResourceExhausted); + let metadata = |key| { + error + .metadata() + .get(key) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned) + }; + assert_eq!( + metadata(pplx_space_common_core::spaced::ERROR_REASON_METADATA_KEY).as_deref(), + Some(pplx_space_common_core::spaced::PROCESS_OUTPUT_LAGGED) + ); + let first_sequence: u64 = + metadata(pplx_space_common_core::spaced::PROCESS_OUTPUT_FIRST_SEQUENCE_METADATA_KEY) + .expect("the oldest retained output") + .parse() + .unwrap(); + assert_eq!(first_sequence, 1); + assert!(lagged.next().await.is_none()); + + let mut retained = + process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(first_sequence)))); + let event = retained.next().await.unwrap().unwrap(); + assert!(matches!(event.event, Some(Event::Stdout(output)) if output.data == b"kept")); +} + #[tokio::test] async fn login_tty_pty_supports_output_input_and_resize() { let prepared = prepare_pty(Some(PtySize { rows: 24, cols: 80 })).expect("open test pty"); diff --git a/proto/spaced/operations/v1/operations.proto b/proto/spaced/operations/v1/operations.proto index 9250f475b6..7326a8b433 100644 --- a/proto/spaced/operations/v1/operations.proto +++ b/proto/spaced/operations/v1/operations.proto @@ -228,6 +228,17 @@ message ConnectProcessRequest { // follows the live tail. Native public data-plane replay sets this to the // number of stdout/stderr events already durably captured so output emitted // while a client was disconnected is replayed from the guest journal. + // + // A cursor outside the retained output ends the stream with a status whose + // `space-error-reason` metadata names why: + // - past the output the process has written: OUT_OF_RANGE, + // `process-output-cursor-ahead`; + // - before the oldest output the guest still retains: RESOURCE_EXHAUSTED, + // `process-output-lagged`, with `space-process-output-first-sequence` + // metadata giving the cursor of that oldest output, in decimal, while the + // guest still holds the process. + // Older guests end a lagged stream with RESOURCE_EXHAUSTED and no reason, + // and wait for output to reach a cursor past it. optional uint64 output_cursor = 3; } message ListProcessesRequest {} diff --git a/shim/proto/spaced/operations/v1/operations.pb.go b/shim/proto/spaced/operations/v1/operations.pb.go index 946e81a1e9..01fcd1685a 100644 --- a/shim/proto/spaced/operations/v1/operations.pb.go +++ b/shim/proto/spaced/operations/v1/operations.pb.go @@ -2577,6 +2577,18 @@ type ConnectProcessRequest struct { // follows the live tail. Native public data-plane replay sets this to the // number of stdout/stderr events already durably captured so output emitted // while a client was disconnected is replayed from the guest journal. + // + // A cursor outside the retained output ends the stream with a status whose + // `space-error-reason` metadata names why: + // - past the output the process has written: OUT_OF_RANGE, + // `process-output-cursor-ahead`; + // - before the oldest output the guest still retains: RESOURCE_EXHAUSTED, + // `process-output-lagged`, with `space-process-output-first-sequence` + // metadata giving the cursor of that oldest output, in decimal, while the + // guest still holds the process. + // + // Older guests end a lagged stream with RESOURCE_EXHAUSTED and no reason, + // and wait for output to reach a cursor past it. OutputCursor *uint64 `protobuf:"varint,3,opt,name=output_cursor,json=outputCursor,proto3,oneof" json:"output_cursor,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache From 626a64640fb8534c45308dfb74eda605490d987f Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 22:33:04 +0000 Subject: [PATCH 05/12] docs: describe process SSE resume from retained output The events_url schema text, the drain diagram and the API catalog still said streams do not replay missed output. The OpenAPI text now says a stream without Last-Event-ID starts at the beginning of the output, that evicted output is followed by the oldest output still retained, that ids stay below 2^53, and that an id is only valid for the session that sent it. The schema example uses ids in the current layout. --- crates/libs/api/src/routes/public_docs.rs | 2 +- .../src/routes/public_docs/schema_contract.rs | 4 ++-- crates/libs/public/api/src/lib.rs | 4 +++- docs/api/schema-catalog.mjs | 2 +- .../diagrams/data-plane-sigterm-drain.mmd | 2 +- .../diagrams/data-plane-sigterm-drain.png | Bin 179317 -> 252465 bytes .../diagrams/data-plane-sigterm-drain.svg | 2 +- scripts/tests/smoke-spaced-rest.sh | 2 +- 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index ac97cf20e4..99136825cc 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Treat it as an opaque string and echo it exactly: process stream ids are not consecutive and exceed the integers a double represents exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the oldest retained output, which can repeat output already received.\n- When the output to resume from is no longer retained, or `Last-Event-ID` is past the process's output, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with live output, or replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Echo it exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/api/src/routes/public_docs/schema_contract.rs b/crates/libs/api/src/routes/public_docs/schema_contract.rs index 2017285128..d67061de08 100644 --- a/crates/libs/api/src/routes/public_docs/schema_contract.rs +++ b/crates/libs/api/src/routes/public_docs/schema_contract.rs @@ -88,7 +88,7 @@ fn openapi_identifier(value: &str) -> String { pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { let response = json!({ - "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4611686018695823360\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 4611686018695888896\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", + "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4503599644147712\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 4503599644213248\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", "content": { "text/event-stream": { "schema": { "type": "string" }, @@ -117,7 +117,7 @@ pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { "name": "Last-Event-ID", "in": "header", "required": false, - "description": "SSE `id` of the last frame received.\n\n- Echo the `id` exactly; do not parse it into a floating-point number.\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` precedes the output it continues with: `cursor_out_of_window` when the output is evicted or the `id` is past it, `cursor_replay_unavailable` when the `id` does not resume exactly.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum, and process stream values too high to number a later frame above, return 400.", + "description": "SSE `id` of the last frame received.\n\n- Echo the `id` exactly as received, on the same session's stream.\n- A process stream resumes with the output after that frame while it is retained; otherwise `resync_required` precedes the output it continues with: `cursor_out_of_window` when the output is evicted or the `id` is past it, `cursor_replay_unavailable` when the `id` does not resume exactly.\n- A file-watch stream resumes live; its first identified frame is `resync_required` (`cursor_replay_unavailable`), with id `Last-Event-ID + 1`.\n- Values above the `int64` maximum, and process stream values too high to number a later frame above, return 400.", "schema": { "type": "integer", "format": "int64", diff --git a/crates/libs/public/api/src/lib.rs b/crates/libs/public/api/src/lib.rs index 05eadb9874..8c53f7209c 100644 --- a/crates/libs/public/api/src/lib.rs +++ b/crates/libs/public/api/src/lib.rs @@ -1877,7 +1877,9 @@ pub struct DataPlaneAttachInfo { /// and validate the destination before sending credentials. pub url: String, /// HTTP SSE endpoint for this session, when supported. Attaching a command - /// starts execution; streams are live and do not replay missed output. + /// starts execution. Reconnecting with `Last-Event-ID` resumes with the + /// output after that event while the sandbox retains it, a bounded amount + /// per process; otherwise the stream sends `resync_required` first. pub events_url: Option, /// Session-scoped credential, not a management API token. Keep it secret /// and send it only to the verified attachment destination. diff --git a/docs/api/schema-catalog.mjs b/docs/api/schema-catalog.mjs index ae6eb3c7a6..3cee223473 100644 --- a/docs/api/schema-catalog.mjs +++ b/docs/api/schema-catalog.mjs @@ -50,7 +50,7 @@ export const modelGroups = [ }, { name: 'Stream event models', - description: 'SSE events carry these frames. Streams are live, not replayable history; related WebSocket and file-watch types do not add supported creation endpoints.', + description: 'SSE events carry these frames. Streams are not durable history: a reconnect resumes only output the sandbox still retains; related WebSocket and file-watch types do not add supported creation endpoints.', models: [ ['DataPlaneServerFrame', 'Stream event', 'The tagged event payload carried by the stream, including output, resynchronization, errors and process exit.'], ['DataPlaneProcessExitStatus', 'Process exit status', 'How a streamed process ended; also inspect its exit code and any output loss or error.'], diff --git a/docs/space/diagrams/data-plane-sigterm-drain.mmd b/docs/space/diagrams/data-plane-sigterm-drain.mmd index 3c06ab7430..eca28a9f1e 100644 --- a/docs/space/diagrams/data-plane-sigterm-drain.mmd +++ b/docs/space/diagrams/data-plane-sigterm-drain.mmd @@ -16,6 +16,6 @@ sequenceDiagram DP->>K: exit early else --shutdown-grace-secs set and passed, or a second stop signal DP->>L: cancel stream_shutdown_token - Note over L: native SSE: body ends → reconnect with Last-Event-ID
(live resume, missed output not replayed)
native WebSocket: close 1012
ECW connect: unavailable end-stream → reconnect
(unreported start: unknown, no retry)
streamed StreamInput: 409
other upgrades: closed + Note over L: native SSE: body ends → reconnect with Last-Event-ID
(resumes output the sandbox still retains)
native WebSocket: close 1012
ECW connect: unavailable end-stream → reconnect
(unreported start: unknown, no retry)
streamed StreamInput: 409
other upgrades: closed DP->>K: exit (after ≤ 5s cancel grace) end diff --git a/docs/space/diagrams/data-plane-sigterm-drain.png b/docs/space/diagrams/data-plane-sigterm-drain.png index 761eae4beaaad8159e301ed2ef1861dd44cadd24..abb1024676e0403347570fc18a98c7f115bae25a 100644 GIT binary patch literal 252465 zcmd43gaK z+dp9!`5<}Db*`A1Z_FL?MqctU8W9>264GO7DKRA^B;;pENDsJC@4|Nqu!76rKV$9Ma$=2DxS&#?)msiVVRh(yO(5q8ckrygA{SD$$fD0Io4S$NGc`*IJsH_Y!jcAW7H zoE>>1hqNxj82-;&1739_`v3U~38}#rOaI^K{+~DRyLV{*_j4qqLE8T>emHu=)HFUc z)ATx`xR}Y(w4CQ!2Y+7x8z;M93q7=F@Ur&Vg3L<2Wv`5-g}K3rt8mkq|7)u5M<)4% zXrJl6-Y}tW#LbL$)VW3!_wC#;p(4$5(MLQlZYttb3KqE*wrC7Yj{I3wGlgb$dvCw^ z%NuE&AN*S#B2Ky+U57E0_T2N{5pO>Y9k-Q-%Thdk+V*A8=TnC4KKa6TMLBm%Yxk7s zUsbyr(Kk#O2ow#{RN8c7@;Ua`^sI;g-%tPXAZ&kfmi<0V7JqIQ6EA}vGIUYP{e||g z_0F59A0Lltq{Wdluu?aXj-|EHYZ)u0|MTZ{uMY2ezt?cuBW>>Hk(Zi~K}pkd+3Mem zgqU;wS=MuuQo#QCkCHi^`yAYI17+U(eL-e!Yfpjxpq+62LmWFkBCijrUu)DURym7| zR)Y>sTs8l*ZNJaThO;HN-WA%tf9$H+be31$Ks8kvD!7`MUI>q*AbZ+#B)&gIoLb$ypcnV}zo$24BCp7Z%oAYHjlO-~%0^;o{v zUb9XJ@{twF+`FOjCDrZ2`no0Ozk!1g`CTLOnpec0BJNrTYf!uI@wb(Aj+$kZ|C}Cr z5e7c{<5r`hDQv0hVd$Iiz^SOAhKi#?&370nnCZ^`ul;?+@H`pSgkP(5=M9#P{(}~Z z=45I1OISL07qmsQVG{Q^U}ngAxZ4BY9Jfw`_d2!sV6b3*K?MQhgoWx zPkiq8`64w)v-4^yeK@EeXItp;I=hn_$%)Zhd;cQ%HC6c}3I6sa8PmnlhS~ZvoR1F) z{v%g3TZ!c4-fq+({Y%0caNqFh9mG~XyOQVhnYVgzcG|r@I9t6o z&%t^x_yGy`|9pkCfs4xW|BD}9eVwp!F*2Ii9%qJA_HTe2C=VXEM@PR~UCn_hL~6qP zI5qzO3F$p~bG4)VpFgT?qR4&sNo`E1x#h*1-@c6v3!@T|Qd070{3e!XS0n z-TQcjcX;GTO5(!Jr-zS^zq$A0ea7tdrB7yN^Kz%cE>jb`$MJ;sWu&FW)=lRFs?qUr z>$y6Iy*b0s(BIdp$Vg4<#}RYtC`j-36xHx!x>s0|QqT5^(go!ver><;_Ws)u(Nk%q z*}iD!(U=iM9%eh=pr(;{yZ7V?(xB_r)m;<}D?`>EnC=X0ycJ@yo2o{cgL1cnC3pRW z*|D+se*r8bFOSP%xr4Mdu;5Jn&gaj%50K|pckf{!AxeZXAzULa#KgqJ#g2deI>CRb z#bZ6ijTyyIQ*#kUP)f;a$DO*e;(v8D6CZE#AfUOX2Dz^8(bZ@W=456jCYA^+FCATo zg9F;q5)PsOel0wFG&Iz~++0&mj+BlriR|~AH=opXbP95Fg=v%x45`r34*mW8<>X$% zJv}E1fBm9<^{OF1|Gh*u9X-9J<+eTMfsBl&XpWMqY7iF(hlF^5x=Ji2(N}$a{oc;b zd2=DS-QHZiIO=1vZ&vy&p(2@AewCQ-_u6vh_@c;W%RSHVxwr-fOiBt0BE!PynVFy9 z<8yFwM#si>Y*ttAZEsop8D-ye;*~eIvHAV`_4=xDejx@eCo?niHKI&IQL(z#=hfzB zfxA1;uU{OWKMzw$My{3c0L{dL5Bbd;T*fgv;$4H=V6geE3~ z{~3?YFCKH*>Mm>OYE7lU7OWOhLuwGNGKfwf?;RM(NKgN*zyFxDaEwmD z!U8SqPi79s-3%K>{OvzQ1n|&R0$;MTZzr==)L503aC&-V0s_Qg>Vo@U!!*MoLCxIR zdBmE;ZxjrRMa99PDJG^OEKJAAdDt3s=HlWQ!YQGvD{o-nrK}ti-rMxbumk5Yt%$X? zoP&div~=|Q_Y0pW3N0*%+LRgb!zLVsg>}8W*hRX!y6%sQM?^e^-~$UuPv3fcEa9Ne zXvKMXvYikRaL%X}l$u)C(C{h_O??o_30)o&4g!Jn>(_cUPCdiJP2HOdEddPn-)Y#6 zW`D;y4v#2fix{yc^_hf)VcNMi8X6cxVog;$-fIx#ak1JfGxQutwb!e%6SuVNsd1M1 z_|a?9igV5%cIU-*`rm0xC)W)&)5zI6_sQwY#SizU_sg(EL_}KDOE1ESxiWj?KKkuh zPghiAWj!LlOTq3{m7BNjy`*G!u(XMXcCyr-f=(b$&g> z$GJLpxf~_w=SrTQwV}URTwQIQor_EN1Mlv?Ok!!~ef4T?VDB`QC#6l#Sb6s&U{)$RHyNlyhp&L8zOHOGS%ljBCIbWaO9QM>bT|~9?s61B%7n;?&emid!kux68-5D3kJ)B2j@1^6=U6- zEr+X?A+7EnH}=OH=?v<1R@UoFvE`;w8T{_~)zu-0?`oxUKYSahN}8sprYfwpm;ZW~ zXJ_AA&wKL%a*U%xja2qfM--P)nFea6W;qR||L4z#9;i8Y2KRPnsz0pj|0qx4@@1e^#1*`XbNU#(;03ZYwP;&-_=nw+uGbbFCRc)a^l4d`_pVt_UKVK43xs)Tbdm= z2})2}0&^{v_l4B+&vAcLxVRJq1#be-^OiOrjFsqwW1(S061{3^Y7y~mxo=5I>P?$> z50wHp#vTpr^~s4dF&7giMne$R(&D1ExjFumCw$%)p}&5S+JBQt=qC|sD9y}FH*1Bh zF;``e*+_z4)Cw0`5+jBHG+j~Lyv`i=y56ZsnXIh^;0cF^KmQ9cvBo_nCZ?vTLcaB1 zzy7MKS{*BHy11x`V_0YB;Gp#HPphqX*XrUwm?_#H62dc*Hz0w^!%K#Wx}PNEtEq-x zUYGg=H-^(GKRITkxvT5@Ze&=gOr^E9p)m&sb=y6gYJ~O;ZhW5s&-uuPa@oF?lRN24xd{k}c<|sc zt^8YA*>DJ6M3ITl{7-jhiSY4%xcnVUP2KwOBQiL6oR!tV^VCXOTDpyJKV2b1u<+~G z&*{3!G0Fml9anXAsjz4Pfr7!|vUGIj)n4Z=l9HW6*`76?wfpjQ^#V^H1?HI!4rQBS zi$EC&lM@%`?c{pK^ummh(Td-(<9fE1>u62R+}5^}*k;xuBRbl|wvmoDk5{O1aZ5Gh z@ANna`>67Z+XbJkVr_Ly(y5%B9Ae_jlKgzSm&HcE@und*rVDtInC>6fZEc=+*15|l zD~Bb?$~!pRe0;zFfek|0dn}K0)}Ai2Ms%T;Y{B$S*eOofLd>^rZOdQs(^~*xn*f=0+AbV+P?ek|fz4cXoX6J%33 zYinxI=I`x%&C2TSx7ke>kd%xhk)w=8AP~Nn)_Y%s(U^6`updjptf{KPrkYA(BS1w( zh0E_vuLonbWG`Zg%*}Bf9XfDYPgOf@TJWX?Nn~qkPhUgAg+o|rt9R+K_X24ijfdwv zjgRzSjZTW`l=Pv`gpY?54jN2PwzgdCBrl&m>+)t2 z^1X4oIOc)M2*3yTYHQHW(vpZwkF*Zi5|j{2OZVcLbl^9oGT*SX|D2t%As6zAY}Vi3 z-=}}^g2I2pspjLyVW{;6_2NWbTrU1jpLIrM80hPVW7RlqI$2xyl^HU{&?-Q7fGIKV z8SBkx7MY%L^o`U@7(~nyo)KrZZPnBCiL3gDsibATH-!jMU z?m7cQ5(;K|qM$ct-0PeXcYLO|WiY#tp(!Ztpk~6%y?aN&%6boz)Bzt4FKftbZ4Gz5 zPkU-^ZtgONpNFJB1Q#X1-@jDvto2^N*RMuQZ>uO?zVuvdktXSGQ55pL>>D2bfl9%{ z<4&80E*Y6RG(0ShO84T$;yXS+WcdF@^Bbal(j);-3XvDjnu7Qa<^-5|(`-z+Fez5n zTq<&NFS?@H#>QN#VirDApQ00#$jZrekP-`>T%5@G4-V%P9WXBA;C!xiA?0=6!rA@t zl9o1#m02tE&U29ug zA=mzgK*O#Qk#Kes^D1dwK z+?7Q_+Aqb$!I3~+Wa4jWq0Cdz>(D_)ty)yo?B21#1ZZ31u!<4MWN)kV=F|95=0OwI z-G=}Ax}>hz^C~LlsvXCx2eJ>Q?ThxlD@{5aIH-|SzM^_Xwe)KG%6cqHq#XZ{ED*X1%3FhCx z;O63l!ApjpmbR#>YH(sgOkp56m{(jpfXbhdu?BD)ue10(+8V*W}m@MerZ>l+*UVTFcW9<5wopS7x&xGjb?HDa(orBG2l@pVo} zN}_({_mGU6ih-e1AR{(BoFu5V;M=#2Ot<*>m@3-vuwYbifw7mGnwwUf(=bxLRR5p> zr{uf>6hcCCSO6NDwW^W(9s2u8iHYN*DyAm66#n}wnt2N3#rZE<3G-D7>#uVavwJ<< z@sJwih=_Or89>IO^dQO0sqgNRN`c{W2~kp#!7@X@n$dPHq~>iF71Pm43kyXHZljjj zfnz!>D(&V@sGNH>JKQ9Pu035nv;HG<@r9yd83OV2%I*q5N5DL{l78oXhTeZ7B0mZX zKYI=U0M^o?3J$7@qBv9IevmN>-|+IfvbR@+tg!UT6_w)Ny=Cz*9}1ez`$$fA&s33+ z7WCAWl_RhyP2>v-3T~F8!ph2`sj0_11tTPvQI<9uaYTR7?7FBfH2_-@rf+3aq(a$45<- zwx>JdD44r$$1lv_Gt^AavpOshH!qTlZT7d{_36)O+?=2&R7~K5uf*LD%R!j;B*UM6 zD=D$Iuy|)|Y-nUO>^PyWq=beUM;#f41xGfu@s@Pg>hIsb&d$F`NlB}U7J&W&IFuR} zr>msougwhKPs68JEJ+*GyXR2N@M3o=Bq6~bfDu&lVfbp4;%vq#wq%vy6ILjwNl-Ubysb1<3yNR zO-&xCGNF$4Lvi&w9ZXAxR9m-ETJ%GHY1z=EKefrEJWnpg*jm63Hm#HtVnrIo;GFi2i@c?v0=ddJ>*y%x)qA}AQy4L$@T;PN5>i{b zJZ*MlWJ6@6AUQd?NY~5^8U-FPu{i{ht8--_^02XohO#>$Owm4XBP33d5El<8WQ(lb z#;ROw<-o<6^K{qoy+Zh?sJLlpbT#`;s~1mI`8FzJqI0u8^_sziP2O$fkS7uv9E?H2 zjXJJl#R&<<91_#kmX>nv#A}(J!y|N{W84o{GUYvQoTR1*zEO1#-1;U2SX&oA$Jvb8 z<{dI0F}KM(*qjB)>Ae$rP=9YWa7yfT7EysV|w{Oi*jOkQ4j$f)lNmZXgZ~>x z$Cxi)24`z4$4d2}S}?NY8y|CF5^6*eYHYG?EZ)Vtzvs|og_&(;ZDsQG=~GX)s-S>? z`wz}9Eo zsLw8(y)Zqcq^02$73Xj)HT%6-O8%6Xnq!(4ShBELU;9lpl5~1{W@crR&|fl5zikJ( z-k;bP1%vxBVb(@jQsi2=&&m9I!*uLk(wGA z5iV}GL0MKd6+WZJ2dv>?B8q3vo|0b6YH0lY@guADVIm7`={Sb*(zn$OIXOIxJB!g& zwamIsYKnvvW<$*NjE%v;8mdo5`u@7Ojv$Lm1O@~Igp&yXgy|F`XumwW_NcxU#KFb} zuxD?7?&qmN&PCF)`!lhrGm1Ln?^=i_uRfx6c#O>cq)_2Z`x$GJm9@30wRNXSxn4uK zv%USJ$B!ZLF2^zG)_d_Fd8HT`uXcnJ5)ht$&CTUyV5tA{%(gH2B|Fh$AqIMSHC^4U zEswC;7zbP6bnak^dLFHv-;4GD-i|uU9Ye`83W#!5}K9q&ddyXt8 zB?+ww(+HrrkNUuVy4a^qc4NLZYR0z~m4|bxvNIYH6W=_38%bK^U3ag*JVhg%LXqJw1s+)2QhC zg5u%`#3B$>8rsu34PL|$SuX3&R>ubiP0BQE=V|O|U7Vb-+B=>Mt#pr6m%jAW#6C$2 z4CEAf!Nc=`FS^|TyFKRfX9_gwrg9o=J3F)}O&UAbVHM?(uWw!m_-c?~pq#~vphkBa zQDwg(BfDc@ummYu4>5&=i0@7Q^}V?GZ+8reu&^t(2mnFq^kKMf`D9y*^;6fEL)5$9 zOXJ6a!w76K$V-wwVU1q~e#kW;kV9{CHW(WpkBg7bFD%3zqV|6yB0}PdK=iP)kK+2d zx3{$=FzG-9QN#!unN3wjLPBNn4g-!=tICc(P5$h_y-P>YCk3!F{>$W>b1$STBMnA1 z$Uf3B64bP`i75(i%M3{6J8|3AR*V*dNHEq{xzl0NvP)`{l`mZj}G~4=@ zmknXNVTq*W&fkmzT0UT;N6n0k#1Lwn&QD?ZbLZYyEv@9RF!Y1gwYA)r+G-C`QIFSN z_-mHqf0p_R6og$=ORf9SkHW3@ObI3T79?5UeLNX0U?UemTZ-HX4hhlLkp11~9W@{b z8PR1&gVfSed1<(PW(Iit_CJ4;B(fKsN3cY)C6elWt`EwJ!+9rKM%&zo-Ik zepiftfaulZmvOJRM}Ms=mz%s4Lrvp%53cQr@@vI#cGe^rYkq>Q=t|+AF6iyAZny3I zUjDy+NlQwigtfD>yeX^TTy1zVKKN#SW`>dG z^sF{Qa8u?%(^*T039?@^0nx>?=gaFVP_eFqM)_bik;n*EFzK@%eLb!npHpoBS^Zq%XKnDbM<(<>boB%b!O} zo$Usk6 zen3|no0@KHM*J*GqF^q4inY+!+RA2?2+`sK;(B_zu;ZGnyi~BGvw(o}R`2fG4^ke7 zW5huqBH8>`IXU%ZztURk{n6fyeR}{$w=b3F9aLQ~RW~5MhwV?C%?Z(m%zkEW8D9+`@MepMmQVO=ubt{P} z{)urQiveB$J~(hUGCzye3d(ul9{YcLaGCUSBl|Hk?}_SviTeJ%3kYBP^i0t||4wR! zRc>(b#KAI?&aUZN?*k|pqR1Y|C9_)$e>H~~3lxE(;vz1M#xjxNOw~g`sBlRLSbiuB zjqWjcWH(){R83=JOK>KDriE4bdr3S2$A=HV#m$W?jqFo%$UBR6uyb2l$y1Uvw>Bfh zPyu>sSMCbh>0hpNXG3H{oIik{GB6bFAKnNs#C-Tr78@%oCbj{jE9iBg#etr}pkCtY zEFibS<8dtO;Lrl>WmZ-TXkPRTNuY^>@&Wn3{ZB2xB%NB$WDzdD=4NAfW2iC4{teEF+#-rw|aSM0z4?BL;>o6n}E zw4z8Rd#_{DX6wE@n@7RCvVRHuG^l-4RIQ9JXfwH{!xED%fTHRC{rk`OH~@pj z{QQN5W?XJW7%_jXt1TIuNow4mQKl5MyLUh4$yXQa);A3g>(ABY+(VWES{QNZ$@bLC z%~%@bBEyb4?aGpWN^#5m?X4|(a`LDr3}f}~M;FcaZlMI8nChp#dsc61%%Q63zERpZ z1r=0&ktIkT$H${zzSI^Lw$jQapDLUoH!0k`D?1+#IGDe?Bx62Ceu&!Aa^&J-s;dic zS4>&?<-2!7muFPfxWIWwl1#>=-^8~rK7AU_>#)M^x}5u+Wq!!s*UZdLRh62V*>I(6 z2$J_+W`A*UF^~O{KFF{jnQc1N?7@ya5W&XxQQN>B`Jol2YP`KrZ0 zEF~3{soJxNAI6r!mL^C(NFWfIn8ZWvoi?14V;n^;wEou{mqJYTZB~9h30eBa($Z5x z!qfeCUt(j!8ycchrmaO=725(IA>E2*rg0MW89qp^3+hfVhx35C<)*X`Zel$g)Di}prJN~~~upivz5{AFqM8h2fn z15+~Qt<}dz05QSffSE}G(z8L2nv!yB{cL40QzwdiK5t~x!$Sg-OsuF7L&&AKy|s0A zeBAL0(GBGqTn{AhHXG^ZKLFg@b@cNV>D!5kraHP!_x3Y}Mn^YoP9N2oHGL5dg~zEf zdB&(?p{J?&0V|ouQP7n)M6AZ+PiO0LXlZF)%CshL8u0HsJ3E}NyTiLSTGJD~`)9r$ zf2VS~x>#t+-@f&FANu?IJrsGUUD^o2Rgm=g^HTzX6M%=QW)>C)o0N$a6^<(_E5N(l zyIYr%lCrw8Lhw?n+N4h{>%j4NLlX{NXVe~uu@che!G5jXK>r#UeT6A)>yRkSf5gU0 zWN2bS$9(I{7FJYXuA-{xFd+1Pn_OI++8+y0lpo4a0&~V=bWgDWZHNd6P*oZ(;&H@hpeq1q;O3#bx=s)r zDc?TZf{uRF8O8Ojs7R;6>Q_U9A}CHp+vYUoS7XKh$@Fypll2fGxDhZ#?ia^T{NIR* z*2l%!LyqlDLT6sK*<&s9*~IG@-RQj zm{E>0;4z+4t0{m&C&W&Lt%d)v#4%$rDO zs~*y=y`BOZdRSEzcTH`^eC^p&k&UJg)=&WfwyLP`JvGb_^p1=k`VMLj_45@k-mkke zJbB^aKS4UTnr|S_19rT3LgOzSMK+UO0$kkrD*H}y-?uAv+{9cyKGjbD%trt7cJ)dB z^Pa7Uxp_!&vG}`pf_}}kFJDe4=zx*I;Y_W`yZqoc&C8d4Kx7qy?;}eFHS@zg6uBe= z?Khtil9R7C=XPgnTRS@yWEtLL8L%J#n8MNM5}^BkZH+!AgMOPSSa>T8%%xS{mp8by zBx?he7!O0R@o!q>8`IR%l$D4+VqGB0o>Ck`gw)n1j(ZJNHLELGU5kqDT#)?H4JQZ3 z+FAoS%YR!StIuq@*|yQY{NE=)LUaqi-%R;ZYP+&}@}G*zH^`k(r7@`Wo*sJqC#!WH zB_(w=AM62wNAp6fWybQ8|3-$DDr|n;I*Hc3=B>QA;PrnaF0RRZbPuV4?(&p$zHIrK zZ1H0xL{d&(!gV%><3F?5e!G6~4A;9hRN&%X`45)p1kos@-|S4rkn)Y4nY2bmM#|I4 z$;qX$f=>WYE2aPb>v~|nlVtnze2dhqcYZRY`gURcVLIaDO}`yZKu1C_vpn^pq|9>D z`8&t$g*Xc%NV{}<14=144z`x%nRNM`SGsluckf^6gCrLP?7{Hx4D3r~d;3hk)_y>< z4f@kpS8qYchEmb{MCn>a7eKh(gr=@8n8wa={$0CFNMS&zKV4vMzWv;yKb2=~#pz2z z((FusI``t>+}w?Ae7+FS06g3~SNjx~%#lrY%Yn1BNqDUu@>;yr72N&3l1oVQGneR_F^m zJx}hhJZTs`{cmm(jP!By{RN8WICNZR-puG!jEs~_Oo}QhgJWZ-)Xd08`#L9%Gz3 z+S{)zFYhslf?Ee5UIb}TQ7+9(+RnV9A~Qq7wJnc}k<|`xuc;L$3z>umhK5#s{P-nv zad5J{xUSyTp%NcBE)9*1vwZ@*m1)Nh5Ps@i`czeZhEWqNFJ*bmodc)AYmGy87{Zht;a;Vl0C zeQ=Kg4d_Vl%sr>2J-&L~NCj4on(DN_#wL#+|Bay~#UPnDEyXWrTv;)Csly9ef%pEx z05dZ&Hul!il7^Dfs7_#Rt^^oM-Xqyw?XUJ|z^6Mq>1q^pPMf{IEK6>GPRsydk(6u! zcDdcFb6la}%45rd(o&e4D=WDZ)vr1A_V)I&uRTuwXFZoYRzXDe7MxEQ9vk!AZ}J_= z+H|}B2$A&fAhr%LFDzoeU=mzt`tTRHLb00ISSM|5Z3r&W4re9nc`1?-fvRe(AwfV; zqEg7o>G*%j&z}g!4ophg!@*gq;<9(-^W05UPN)p%>njETeto@XM+;mYIT$N`{P-w! z{*&qnI1|e($GymfA}_rJ_Z_=>dM1CGw^!NYj+O5HUA5QN-v0hh>D@cbkXZFn-A9C! zYRbyjd$9)@A;GAm>{7y@#UuBqjm*pwo%BNOMoavnCLV*oj#=NgkYJxhK zJ!_U{qpkiw?KE>?K|?HRRjjgdwKN&N_X886v-O_k6*QOkB*X3%}525 zk(OhOGf>zzeK-V-l~$hnQ}>E-kJ_@S>#kFx!1V7qpATUD(9!XmnzA4#$AnM_&o=u# zGfewq<4p}MHv)1^N`XpYQj)QZOi#9?FdUJD_;@gh5%ja^zgJOI{7`u&XS|x(8f1BD zCxefKX)6>!a)C<`Z+S_+wzI}a&PAw zJkK~88QXqaSbz&Bf*6F|C1Cf-1&yXB`(k2Z)&&QF=8K8J1z3(rUJ`-@W2^-L`J)Imf#{cCA% zP6udxYRZGnlxd;i;x~|O+hc!PTDywZI7t%0)qqkRS@R* z+bTQbf$gS_4nbRMYeK$??GpC$b8@f@%gXjvXUm{c+_}??OZ(Q(?>e{&n3}s`?aA!s zM6ON`DcAvL$IvqW3&O<@Z-B`ZM?E3Y-->`TTEK0g6oA(@ap=5nxEQ5e3TIfe{x^+R zw7k46EiJ!?9auxeJT8uJ1Icr9Y1CC>!Bo1}_Am&PpqV?l@DY^)K!*T{041dp6d-75 zezjg>eSaAvB(xj6Ub<$5g=JSE-akN(r}9@)RtByqf*k58lZ7-sE+yrd0+p@ASIav) zYth}lQONnSw@2Lx66}UB6 zGw=ZiJyN7jH)cUVD5$WVw-pp5bHFQ7;JPIPO){0oQ5HrChTW;_G?a@4Mb9VZP zNy_WhpV|&e{0w22kponk3F9Yl8y}xLpu;x@>GB z6B7|_A)J7N>**{2*#WHpq&bi{g@u62jt=COz?5h?{`TNIx3BLWG!)=G4wNS%+B#71 z_4b~9e@6*y3h-{kS`F@|o^1~hIsw-~KY?HKSr0QYm~%u#{b*&sOQ7o1uF*3x9$xMR z9r&Q39q!H^LHYUR%?qHXOnVdO3oy*h%}>wHdl)0QF&|Tb3HYc3>$N^4Am4N-^siO%XZvQ2K`UVhcU3>Km)bHqW~|?5s1CnBO;F9vfA1M;9Q2I z4Jy^fn_u7=JN_Z9tfaKEy?wqUju+Gl^&1`zj&9BIfX#gfZ=OYQA-*li0%J3U&rwV7}5z4b8y<|L0# zR`zNESgG?PF1XkAO}tOD-c(VpV@mn-^u+bGGjjbQ#jnMg_?-r?B|<`DaK2-Synam& zn#|&0$M<(zo0~b@qBb_g(L+!DKYeV`hj@TUHUiEX9E05V8q|Fk-s9SInptiyZu|c; zuJgv%)Ziz{`W)p#dGnlRZNV}SM11g8Ep@Cy#$uqKsAs-{A&QB&gY_6#&U{4^Fvnqu zIIc0$u#)gNAX7N4h1X_fv0?}E09ywH-8W{4>F9|T~K*tJ;q%_ThN zoY8s(Ifw4hy~f8p;n7R67)PGJ%!zPAl7Z(!HP625|>iO`^b>s|+ z7HD`PW=%+R0`<|5l~FT6*mm?~OL(cx=Cb1kDE!_%Loe2UtOR{1)9qNv>}#5s+RNF2 zt{0-H&>KTQz;$-@00Ye<5R2%a62gG>miFp&dXAY-xwn@Q<+0Mn?j#I|%HO0n@pkfB zHpw6ii`VzY!NeiA zbzt$qX$6GgG>bbMCH5;1@SgQbtV3Kf4X=wO&;bLJiF)~6^&EvN6#SqckNuqJ&2rW_h) zG&GE+E4(3)IXD~&c%7@4&V^%vp@4&}d~}>EYs4MacX#IDJ64{8f_BJL2vaRBEj3}` zP3T8ZIKv_$Vy2{oW&HE!S44!|0hcvYSL8w!?QLJd2?-K3eO%#}FJFr+n~WT0X6vy< zF3(RZ1MwNE+`lke2I z^}j#!+Fy7Gg*UvvEXIaT75O|Nucv&*p~Y1QB-mLbv%2LbGJD%e3kxsc0nq}o3JNT( ztU`;FA@w1CQ}XbTQ?P@MN65AY)4jP(y!a*Y+cy%$gsG{1aa0(w{T_Z)sGJOgv}~{9 zn>SfX1xBoz!oubgEY7L#R)3!$vO%A#tEs8*_g{flENqb=44&mjU@+&dV`5-1efKV> zqy+4}K>02&u_nq!mX{NYY#eX)v|%&r)C4e>%{RQH!~vseC|J*~_V?d0ikS7p&mV-y zvYW@o1>xg!o~}iB9IoVi{D>1G2DVg~?2e9H?(4RYFctc^sHpiav)+v%!}Ie8P-+A0 zG7yECAS2UVdS|u6_v+OFuzWCci?Oha3RLL*noYsarLQlWHv;WQckfXJ%eA#rLt7Rp ziTuI-{?-=JLV39Sy8H3Q1J{Rup{1THX=zD!Mf}h|`+QSE|DVu%B;2cJ^81MmY~GkG z*0VLl6e}xEAEc_?4ru!2KhF_=yT^z6Maf;#JFs*jL_dIw57y_xRb zZLvqwT~w5nO|AOb)%BuPu54w+D>bX6Bmj%jfLBvfmIU=u@(vBy&i%0t91ah+piRV_ zod{dMMO_*4K)EUN#eOETo&bQ1&r;8y#Jv+k@I8MXD3bYtt^mt(4TB{hh;}KV+-&GK zn8ayfl$C}JEG$0!bGpgvH6UZ@Uvw-Y#l(-H(#QSwF8})F%ZKI#TwGk*h5^Wca8AJT zh`>j;kDRZ0B4rhDxnXR2JE{NTr8NNod6WJ=v^?CbV+w19#Kt~~kf7SlUfA{r+T8bQ zqQl4m4V@Ugmmje}Xb3u2e=jX2<}~?(gIml9=&}z?98K!)XRjC2Ha0fEb9Ibm zLeFFqpP(*=3Nk<1qwtMIWCgvuvasv;`Rl^)i+7kr_wLDZlgWAA7r0$i*avH|SSXkg zK{XYB0G6^RD)bz*c|>`%&+8;nPp;yZL|o96UFfw3?$CHCe~60mJ=>+?&PqtpF3Qf1 z!g2sVmwRs?t;6jPe%~7jdHHm3(!F_un#b>k6;b*e3XcX~y?~;&@lr?W=fexlqQSKx z3h8xUhs?lYCANX3_q?|1>I3=O`ZzIRM00{KH#KdZ7OZ22-ZOS~vb;LaGesSp6j>S} z&-mW!EQkS6aIP18Bd2Qe=L@l$&(LS(G3<;4!?>7%kx}TUPv5Jm?)$Xbzn9k18XM<| zMSL}5Plmps$lc}GoB#C!WFC{~x7_o+K`b-*AQnX6dfII z?OR5LddOrC0c(PbE3{x$R<;K{jc}TRS^ADQ%dtiN>CBiiueI#zB~>iPOD`$>p+pGg zcYEsp_3O`wzVOeV@o8dWJ}WhKb`I$8pWJjDbwy_vs8|>oq4{WNO#wGW?+@}7%TXy( zOmOf9a7_Et@cqckSrS1nBHLld_YZ|)qY0tJli&|4vRD1H*k&6PB#DHb zD(J^asMH`8M2S&uZfxK_Na1n3Q#NpZlmP0SVu6afIwqwev_e7~Kuq^TbkBek_J2LE z$?S$5;mbElCu>n)Xdzu{G#&U1waSmfH>+UTn+qo))txQ9>GdxtmpCFRq1*G~!lSUr^7iKP-ApZPi@ll6XABIkm#5@2 zJ^UbXlJKpy3@|tK^k5N?vw(dREd0<>DTd51d;@JFSR%}N9W>PS&_@8uG36B_GjnVA z=G`RO2A^xCJ@i|EA@XT8V&PE>>jX_y+~5(dDhG`Or#_eZ`Sap$xs?8ghYnaG(6VZ7 zZu|461Qyj4w7OK<%#N7P&7=d}X=sq%-Q7KqdcKa(1SBiq8R=8=$i>*WeRCuMJS_)R zT4ZE%N9&~-yw1{6Qcq1;*hzAVixW~V8-mWMq@Eu_3j_4x{cBN5Vl#aul7D7t@NrMY zQd=2ZiTjIMZIurwz&5!CXmjZhj%xFss3up$|T}=mk9Yw<9eRBmSMSP0q)YNQ{ zxBz_v5L=4f9-r+pI^1g{?=X}heAH0y8KtQyS6qDUnQMpax7QUt!hH|AYtz#RGR$}!ON16DEQ&dOEt6?HW)Hp`%4b!sJ=W8I}lij(eVwt?Mq1cXnnM7CKy>Ubl4#OG|G;r+WG1 zP6`*ZBl&EX8Ne`Y7hNUt`A}Q2w{O}1RbDrqC1o=+X9MX1uK>;XEI^#{xpt0%=zqq` z1I;JFBX$ToIRSy}z(5b0^1Ag_Xr-$6I1$lwfAQ+8l$4a)x_nUjcug~UFDdUzIEpIx z_rG1igCTlGh=HSV;*F}g$hh}`4(~Vg1W{2b16y?RUDeRg?_W~C?kiGf{#jYc$D{MC zBNT(Qu)=;ha%zgmKp+Fs2uM-1G(>qL?(s~&BvFAraoBV3!cv_%{SUP-@fA4*Fsd$y zeRGmDLT>NJQOucD#p;r(HSo;6cFYu7Jd&!1i)9P&MkjQ{ZE1 zAum7l^GyI)%gfEhZw)jD=X@(2^r1tuL6s04T7bv#MpEg-*J}G^*0A=evSp-KJf%=U z#lLJU*Z4MFdAku?T~lLmYE=ZPp5AUxVkzNXgDVRPF497tp}zjk_I9<;^)M+P#uDMjLA|h- zl>Rul%Tv?So#BK%0_DgN5^O9i7?0|--@g5lk66cf4y^n_Kbz-i*5Kem(|_bMB1?AM zK0dlojJoMd{cE(&A9)f9@%+c(AX)s_&FH@H^w!J_mTy1lMJQBfJe5o#*292^{+TwKdVVs+qkP5ZQO0L)L;uLvw6!M8`C$M0T4 zU9pT#|0u&u!2flIfM*N5>Vj^MEOf5kTz7$20vZHELfnDe=$gYXU-;Un^r%>}zc<);ild?i@cL*iN9710R|_X2cdpV3T) zM?i4@!7RKQ1+c06izBF+Yil(ASl~g+bFcZD$EEc4EhP=jkDQ#OgoJ~u^O4h2m#c=P zB+bv=EBpJB2ugC4#QnEyv;S0}S1sRbw{p%T!dX_*v5`AD4i5#N`JYsR@MeY3vI(S_LJiH}<#q*Z0I5}7Q14Tc~?7wikYrp1_9FZTx zV z^{QfX!0d=fX}bOu5*0-gEj2?y>_3>Vn2So0Mf&lQfx;gw1+eJQYXLh9HdZF(?*B#J zn@3~Wwr|6l40n+d6-7!k88S~%LYb2}nU$F`L^4$7kPwQb%yXGDXDnnEnddP==K0;! z{XD<-`{!HVU*B5qde7Cm@6y$EUFUHg$3ATPwr_jHBXk~~3p&ThSfZg0+TjUTFeU_) z`YsiG1``crrPh=+AYrd0WBwr~J~+#0ihSi)Lf0&BNs+YKbKwSAg=s_vgIqhvMbL8u z1}B(IU9~_)Vq<0T9EXN>Ba1}iOKNHyS`7g&Sy`ro-hdT^W%N=TntA$)iprVj6|^%v z6HL7hmlhH)3%j)uO}7x1)pkTjt8{tQ*2z;;#w$fF3hT8yn3$Yr?nB zEYbP3mn!+QmGR4v5PH|Ht~wM?H>^8BH1i^$SnBd1rayn4hW`Clnr-iW8I5VW&vJx2 zN_%K__@6%EDSdQ+K$KkZve=#+-kHwCtXtQzo<-eSqUHT5dMy6L&HH`IA3Y-nM30`l z`kMa5(9)L;E}2~?sSh>QuYFh1vb6eDWfd3}HdMt|FVFvAk~~XMLA|bNmDq5b(iGch=nC4~I)lU~h<2Z=vTS8_^OLvNR z{&*@N7YQZRDL!3lkNmrJPVfuq-=ZRckw1m-6bBS_^Twdrt7n0+f5V`tlTY)8tt*8KYwv zYpSZeoh~~^q-x~LTMa`v!ExocTOgAk1H-Y`oJMw_?6n_Cag z8_1?l=0@w?-BynCzhns2d-i;DoI=@ON=7C-YfAcigt3UW_VwnZuRv&;4Ko2`3q`Po z2076Yx->>zo4)F?c)Q5q{k8qYR-<(dhMW`k6qUZ4nh7X8Jk1jFXuOF}MrMjJc*B0> zcg5hB{c&+S9IGXCL(1VXPq|kUT}W8dG>q~f?O}sGtCX;#SDy$v|4E=896u# z8>LR#R8T!~c@Z2eudIB4OdzHwJI~f)=*pQhUnw>M`SohfOZ7f_VI~POjNwg}^*QCW z5*3!9P4dVd1?uCgOo@%Qa!N`z#%Cm>z)IkILw)Jet~4>WgiNV9qU8(f*)XubTN0um zxMG@pyDhaFMxSUOv4!riXj7l-g>&Z)5$|4Iopfh*88<>iB3USEcXDFp@dh=@GkFE75{a#AsyR|^zHDV<(aY*@IB zr_tw#4L{d6e|hzug0m-Gt*3wG+#js231K~QG^^Nd7OkScCMK$eE%o$x3haZDK3Ldc z5xLc$OSax9XW1D*cRlJpDq0jG&%AqybBK38m5(CSEOyF$WUG~?)PsWys%T~=b=5vk z&8shil*;QC-KXspHIwXZYmCVx~(NHEof3t07wRA-qOA?~>3 zz+2Ut&G9|<+J0fux9tC7>{>h{GQ8H_ViO&m%~uy2pFG*c$(8k5dgOaj2%`*j&ztVEf|{pXR>Ele zw~7`?Lwr$y3klw=ryAedbi^jSs7DCRfQ`*Hdt>Px;dQULnI%mt&Rne3il8kz-I80a zzd1_N0{Yb2()3woX1MH=Dk^dl>b0v2&gGTnhEDbS>3a4P&)}gc#MLd%Prgr01;hzv zK#}F}$;2Ht)s8I%jr#}YRt~d7f69HxHEWYePfD87*Y^>>UcPL0AtFjv%TVMjVW@6s zF#P($|EjxB*GHX-u3Uw-6m`b_CJXS7_tSPojgU!sR_MNdWsL^a#*H=G3aZ@>5t@bT z=-dwswa4NWx3p|`_?BAK^z;awS7E8HdU!)oQB_Il=&@t1<);VMS2A2*?63Q6LY8(w zwI%%K)?s35VNf>i@V=pLWRASrLrf1QzOTV@m$if%qT_$VS$Q)Xe|cGDus?wzzBG$+5$^J(Ua_zE3%5Gq~~4kqRj zo)gi@7=G|e@b{Sf;%g2P=ZzriO^4s@8FAd^w-3-oh&V=Ay!^4oO3%q3US!(t?*8HM z1Q7>I2+g6PjhX(9D=sI?O5Nc}(o~b*yzwZ_irboskLi60C%H5w^|h{!--@xnE0+T$ z2}x zqR>ZulZedcll9r}Wl{NKNZ=fz-#5 z3na9n2aiEjMJ4CqoRH4ZLrr}SoA2uoPe?93TBV_;F0`BV3FT7O&{!Cg=T$U(16eZ^sOzp&4*o^HRyi_*lii;H*IUx%>7jEo4? z*E)g)QEXCa`?J+#xLVA}D4~U5w~kES(lN30r%xk)TzKVPUS!P8pXo{+%ODzV)52pl z5pd=~siNY`ke3BUs{x;Ee%=!K+Ee&N%=r=drO~a|2i7CJ&1`Ix#KicxCW6;F^uKo( zhW%*y=pO%;l2V9`Aw}k0qhWP*qOJv9OGtk`r~N<#Ptn$GvuEDkq1V{}7FU1$TI8hh zd2m*gDDg&=h%2R4Xw|oGP2aw;^V{1?NX+6C;_MaPt(}I}jLC!bTc=N3?A*Cp%*tdl zJD`Q5Ki$a#Mt^vzFCCY+mPl}LD}D*Dvt@kxgeqaxH9uN3058ms&xQ70bvbOQVDNM% z(C5h&f&R+KNN25)mZY$}I?u@L8#(B`w4R!<8Lr+#>j$z5zy8-V6R~8+FR*9{S(tR@ ze)<$Yep4-=xuN0Qg>S6zL&}s4TW|Ct^`?&Qs~m4OT3wwgD4ZJo_IhG$?A*GN($%X! zE4<_A>92}1y>X(RxOkYRk+7HY&Y46J?zE_kRDbpw&AwKd#sIA&!!2WdE@v^y^ce)V^;*?Zr()K&$5B zzZxgsa>np{l5-6U9K-VRt|2|Ak1>*`dW#%2tOtE*R6=tF+*Ub*-#T5Lw=D?c5$=+Z zZ#3Z#XSt=?U*htpW#5+HhS=evhriReWUm~I)Z7zEQjK=oxzWI?FKD{_(V-yin&(s2%sW}578RSz#yhqcaV-bqo6nc6JBA!J>^uFR!a>eu=RV6O@!ghCJ>kJxo z`Q_dU=2?wWt&A#0+t+>l)Glh*5-pWRM>8=o0lMS4MaQtvmMDfTUxy_H1qJ7|Y{mH_ z%omcYgoAQjm6JVZ=QAl&%GZtJd4$qz<%~VM)*8mGiHJs5S69Kdd1hYxh{uKV%;>yO zR@NJnwx+j2wFk`Q6%-->4Xun;$gcG5`PeuYot!-1Q@F+2BBo&~#_HorZp^!AKz&HO z|DG{|y7_^MVzB}A-5S*0zD)dH=oJ?cSA)d-Mr3sK^6a38ZmlR?jJPlv$zHYtBHwCL zkcoimRi6H=#Z85`Ci#(VvC0QaL@iD=XP z%Con6(y=dneG8vGn+RrJTXE=#m*3@9K^0G1uE~osGEU*zkI(V*=4HQ$XErWBB_Svh z(EH-P|7erSyq=nJYcC`fz$4T}6q)dy`z`O?bD!gUOenp$-G(#YvTD9Yx0ss+*NO9KRn-NfXnf^G$4zvWg1N zqeVYO|7p+!9kfTCRn^+5sj2VWjomAJ2Rzu6TUno8H_xBp=eG~%=R*kfXn97{Sjxz# zs?;r`{2v`|ebcVD^`i=Oxy1C?NuUaN=H^n;i&}fp5r}xcNlt(NULH3Q=`xj|@?=Iz zL!v57KB_c6H>p&?^PB^ty?9!?>B-;h+mta^VwIEUS)F+vnDQ)dZ? zLPC#+j6fZoABU0zIvv3BK=)Eh`=(GJ^Lq&BJD zl&|0Lh6u5Dg{9S8p8bNZ{VIga_V@pDgS0!>DhV6W#pUBZQr}OvSr)`X$$t-2WMHpw z6@(5C36f{yCKLD_TT1t$CR$ggp;cH-ncDFH?v|8hb6NY&{Rs!Ex6fEL1!*qX-|!#{ z9*Ss&A*?AW$^FQ-_4$d0Vi@Qbd-mRa=6;x(UsOVZkNbhkJ$f-(zZ~rnpPs_*^4RB_ z8%t)?zO6kU)iMqhJK<5z*a3a5aDQ5*E~9V`{`65?*Epm zwZrcr{~5;u82#GN7#TVH7{-&#YV0b@dQC1wON*23v8ky?kMO?ZTT+*o7t+2As?qyM zBeHXcFFTskyMAo9ymKc;hSi!)SM-oeiE8#FkB-uDvN%S5d)E+^I6RA!TepvZi=OZ% z{PGFaT+7BlCcYt#;wvF94-paBM(|#|Id34Cwrl;w)vKpOzP`WuD1QHSJX_%nPEFHW z=fjv)F{eP(HVb11ON$McA~hDMgdibd&uS0>qVjr`z| zi3uZ}M*ohc!hBYGecX{Wqk<|^XKgK{2fLP+H*n4rO_@DwgVBkdHRJQl=hzp0_O|2| z4ONS^+ORf+^mo!!%iMj;PH; z+~k_?KRU=r{6@uNPht!s8_bMp%B3C?iTPW$aY|xds`- z@glUOM3y`_Kj<~aeM05p>Lp^0yjNt@ht9Hon331>q+-=XN1nia-Dq!b@cX>OH3a19 zf32>%yK;W7D)(wWvQOO3?y_OL#f=v^`S~)2V=oSzs-zH!m7yutuj(2hBIw=4`ax4k zDJb&PwMyT$h~x{8Dt*R^9P{1WSNVA}dAPYZrgDZ|U)-s2iV|W`sPfcu4o4$5iU#Z= zo`DJ~BG>W8*eRKrEqG(JF<#IV{L)+e^YfEJ0bB0i;9v+a(9r8Q#|G(D?qhV_cbwn; z^WzyLj@jAnJ9g0Gza%8rM6P-~OC4-=00abf>56u2S$FrNja3b4#<{o%##gV-bp9C2 z&sDP-d)Jx~r)R(F>*<-6VX);K6yIMKb+_*J%%a)&SO51@9cQh7nNCg^8<{4z6g$_K zmOfR>(B00~I_6dvYGh=TXf{~c8j^Gv)YNPxC3`rnoVvUy?oa6H>+3I_YPiL_5 z(m($^LvH#pi;L}|d~0){&HXgqd)g{~v`$`LAr;~p@+M`M97CJg>c6L`e|Y+|0DX%6 zkt)8%b@3NPzK%5p8gh2Hk{l2csw*mD`Sq(+uTqpJx&D%aUCU&V3@zZ7n>lwQEk`~ zx-yq0h7XbV@!8#b9Ila1N~64+$b*!Q_H+R&S=ecEB?&YpTGoaiXisKvwoHJ-v{QjuSWcszDvY z`tl%ai==;D-7gjvS_Jdv=F0v3PjE75`7=?G+l?&uXIr!TzVS7ZLAB7Ua`oD1Qbru9 zrv7u!ZaefMz@ive`WTv+}_-Ji@iY6dD{Ch&6PK3}ed}!Tkjyn@A?UCKG5t~W*`Ab;T@bFp> zGSZ3m=KC%2Z16;lYOb-dBU2qm4fMM*b8HU@#VH<*?acY${CwrUtgHs~34iuA^i)(t zCMKrqUF&@HN(orin>YNf?yHN_z0K|If~>4de!|>278b3B`{6VT3mu;y7s#|4WyCoQ z>@0X#JUS#e)>OfX7@eW~OKWQ%a&GW<$RwfkC)=@)wd?i#X>X_tqK1j$=PAi+cK=L*kQ+QQkx}yl$sjB_xmK1%TWh} zA|OS=Z#Of;WuogQe6gxJy{zn7Xqfu!3Wy?kNE+NpX)=tzEBA-c2}OK(_ipUhFPV+y z3zDGBR|I5d{5*KzqBv)XD6zakv!~~gs5pbq!TkC0aqYzM-UY&7jc*t6e(RjJyw3{cSiR1gjcS-Z)#${2^na6Laby|q|-Y_ zD*2=nb1M!q*fg#<1*U2Wn|1Cg~q{gUcIyK zws}g5nZd*yOGG`C1P^W`yy!S{zJ}`Oir%G~7s(PavBF%|Cam4lTV7Olxg`Z*$FVmR(;yJlyn z78U6LEe&Sw?3?UDqI`tV6JV?de zQeCZl?OI4}Fb_w@$BdtmNlA3ajxnA(b^i3}n4~1(`vaLI6tPW)oFL2X_xq(N%*c2g z!qcgdk+;-q-lx9KxLa|9#IfJ;^y$;b_|h%PJ1Yyjs=v zA|N71Bcn9Ki(HhH5Y6`w4ei+ZlY?%c+t{>DyU5p5{lE(c8kr)*x1~r2qe?g zyc8e!qJA_#&;G$g%WLw|!C63;-Q9%=_Lr~(J9ccqj>kl+5)w{1tXChK8$!&a}TstlQP&<$>t zkI#+v(mDbh0qxJ)6Ux-S+}r=xgdRO`S;T0XGBQ!>IS_`lL}xgj0_c43-S|x@r40%F zF|sdJ2}y>GU{Te5jW3Kd@Vo0&dLOb#({$3u(L(w$EiDY2-fgc65U~Jfc2R6wZte%A z8rb}SXNM;yBz(^)-MBH?$ph)?E4OZmD=3)Ek4ZGg*1m5ZcXt=Z=O_vz@4i4KB_YvX zSy==G*~sYi=g;J@_ApL9K0fdSbk3%~8cIZzgQo@t%nc2tiTwitz=^tRWK=dd`>E>| z=)qW87Of(AQxbVnQW-tH=FJTvlB5&hVQjx|r+~Yg81Y{E*%gPgXJ>iHo;+qfU-PTu z`%lNJckTs+k1ihBcjrz}U3hq&&72O~k#GTlJ-he4@b^an`2{4!7f4mpbTejFw{vGT zlAH#^%wOG(3_k&u*l&fuFT%n#(VoHS*F9<#Fj>`K0H&Oyr1tZ<~FL|D`K^;e1CN)&wy8KG9r zBVrppZ_m2zR-l=g)cl?>wa3N4u}_Nna`R?KU|g?eCG(8>nFnh!)m8E9CeHF>^^8tV zsY}znwO{R8%XK9PBEI{dTC4P>VMm#o&goUVXa*W)yn|bSZ%j2@sYL?@SC;=xHLX{d zPmDE2)j+bjxjEv_+x~bXsfVP7WvuZUdo``cBK3bo3)=q5M2g1iu60CPs!f+9I$jD?qJ){$6I*t{sNe@$x#s!5t_a z#0&4)S@L_h_<)H0g0ZoGKI;f-DX(9TqYFx2ekIoFC1h{%AOSsHT)&pA6u+a-rKgCw z^JD-1^XJ2yHVCsGTxbXtii|#w#z4mQcC{82f5x$i30BpQ16J35FPgQtBn%4uFkM>| zSI*tJd<;bu=tF7cn5)&lElltVB+z=FpfGy-(+hToWdha@Z=nVH`t?!U>AMiSR+t;S zrQ)=il%NpR)m0EH{rY=HS|*!~O)cU!QW~lPU9oSOMpm~3j*%nZ{D#^dXZPLTLlFWt zmdot34z-)B=Z$)ce#Cb2@_vFc-Ed9$oc!*cSFGnfp*U*RN%pMno7*(G`1VMO&ni

JLZFK(spUmwW$wc)>&K5@3Kx&Mb7upDfwfKxXEbx9 zc!~WgdS&x7*k(*S&2g>7?-p}r3e35H)yUJKp+G=U##5>UMGAn^tLvS!%)9GGSl5Yq z&$Vdpyi6L)P001?o!C3VW))dgmjpbO<<{H0FScr1w2h}O6TG`0UoziaO8no3?ia+! z<^H`+9AJ3$pWk0TiF(k#e|RkV;(x~6c%1-@{_l103J{)uFNUYP|2viA`fif{Q$E0G zj)ajqTzq%R@!gsC9f}?|5Es@KQx_fHN9apT9O0)TbltU{qEBgyD~iMO^5YP`%Qb^@ z#No0gQ)d`N;wW|$XSonOUZR*G32)5)z%gUgvF>3)NtzJgAZv1Mhnczr?X@dn{~RvK z7Lm|+Mt(C?tFz~eEPOY~r8!Q(>{U-uz?`Vm`V!BI zA=0OQi$Lq*lgBTq?!ElpxoATuBoijb)^2x(QYgvktx_Z2OMM~RGt6-+B|Pq?3DM5l zm}r*obphdT*JD$ugtbcJ$Y&(%Ez>eDbeD^#X6Zdyy4ieZ{B?DYti3{g#rfm!+=#D> zOI{3@<^NHY^Wel7+}U_`fxI&|*76K?rX8Vqw>E2s(&KECj1wo+ z1%f}2ec0S$osvHLrhP!Q$y$R#&A?enOv$IqeAlOAn+wrRqf3dr-wRrw@9Ejw_J;e) zGqJXd!S`oX6jqf-QUr@tg8WyrlRaw&9rtP(6d5eo>=YsP6+a?<`)UU_VPNW=6H`NR ziSJpvJJK%yToyklJtK5_rE8FKN zA0Q6AvAO`^b~@9Qk=n2TwA2M{m}@P~%@o`anpADpQB_yp zTA!=O3n-XmO(S2�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

fmMoqM*riq@0n++MhviMg*m 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 dIAoo7puZoXg~9^ literal 179317 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

dkM=k>6Ofa|zpWV@n2$n37i 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%0AnkN0+xSB;(Q(!6_6JwfccDvb` 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 streams(child of stream_shutdown_token)New stream request(attach / SSE / ECW / upgrade)Data planeserve_and_drainKubeletLive streams(child of stream_shutdown_token)New stream request(attach / SSE / ECW / upgrade)Data planeserve_and_drainKubeletnew connections (probes too) refusedidle keep-alive connections closenative SSE: body ends → reconnect with Last-Event-ID(live resume, missed output not replayed)native WebSocket: close 1012ECW connect: unavailable end-stream → reconnect(unreported start: unknown, no retry)streamed StreamInput: 409other upgrades: closedalt[every stream ends before the deadline][--shutdown-grace-secs set and passed, or a second stop signal]SIGTERM (shutdown_token)1drain_started cancelledlistener stops accepting2request already in flight3503 + Retry-After: 1(+ Connection: close on HTTP/1)unattached WebSocket: close 10124keep running untouched5last connection / tracked task finishes6exit early7cancel stream_shutdown_token8exit (after ≤ 5s cancel grace)9 \ No newline at end of file +Live streams(child of stream_shutdown_token)New stream request(attach / SSE / ECW / upgrade)Data planeserve_and_drainKubeletLive streams(child of stream_shutdown_token)New stream request(attach / SSE / ECW / upgrade)Data planeserve_and_drainKubeletnew connections (probes too) refusedidle keep-alive connections closenative SSE: body ends → reconnect with Last-Event-ID(resumes output the sandbox still retains)native WebSocket: close 1012ECW connect: unavailable end-stream → reconnect(unreported start: unknown, no retry)streamed StreamInput: 409other upgrades: closedalt[every stream ends before the deadline][--shutdown-grace-secs set and passed, or a second stop signal]SIGTERM (shutdown_token)1drain_started cancelledlistener stops accepting2request already in flight3503 + Retry-After: 1(+ Connection: close on HTTP/1)unattached WebSocket: close 10124keep running untouched5last connection / tracked task finishes6exit early7cancel stream_shutdown_token8exit (after ≤ 5s cancel grace)9 \ No newline at end of file diff --git a/scripts/tests/smoke-spaced-rest.sh b/scripts/tests/smoke-spaced-rest.sh index 09ea79f19d..af769a4240 100755 --- a/scripts/tests/smoke-spaced-rest.sh +++ b/scripts/tests/smoke-spaced-rest.sh @@ -612,7 +612,7 @@ function parseEvents(buffer) { else if (name === 'id') id = value; else if (name === 'data') data.push(value); } - // Ids are opaque and exceed what a JavaScript number holds exactly. + // Ids are opaque: keep them as received, to echo them exactly. if (data.length) events.push({ event, id, data: JSON.parse(data.join('\n')) }); } return { events, rest }; From 73f0fbe97c3405a49d748368a4b28c3a7a130a6c Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 22:51:24 +0000 Subject: [PATCH 06/12] data plane: give process SSE ids only to output and resync_required A resumed response spent a frame of its 8-bit counter on `process_started`, and each backend reconnect two, all at the client's position, so about 255 resumes or 100 reconnects against an idle process moved its ids out of positions for good. Frames that carry no output (`process_started`, `process_exited`, `backend_reconnecting`, `backend_reconnected`, `error`, `closed`) now go without an `id`, so an EventSource keeps the id of the last output it received. Only output and `resync_required`, which acknowledges a gap and so moves the position, consume ids. File-watch streams are unchanged. The OpenAPI text, the getting-started and troubleshooting docs, and both smokes say which frames carry ids. They also state that ids carry a position only for a process's first 2^27 - 1 output chunks. --- crates/libs/api/src/routes/public_docs.rs | 2 +- .../src/routes/public_docs/schema_contract.rs | 2 +- .../libs/data/plane/server/src/native/sse.rs | 28 +++- .../data/plane/server/src/native/tests.rs | 134 +++++++++++++++++- docs/first-sandbox.md | 2 +- docs/troubleshooting.md | 2 +- scripts/tests/smoke-spaced-rest.sh | 5 +- space_smoke/m1_conformance.py | 17 ++- 8 files changed, 169 insertions(+), 23 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index 99136825cc..b36a8fe56b 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- Every frame after `attached` has an increasing SSE `id`. Echo it exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- SSE `id`s increase. Every file-watch frame after `attached` has one. On a process stream only `stdout`, `stderr` and `resync_required` frames do; other frames carry none, so the last `id` received stays the one to resume from.\n- Echo the `id` exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- Process stream ids carry the output position for a process's first 2^27 - 1 output chunks (about 134 million; some 37 hours at 1,000 separate writes a second). After that, each resume replays the output still retained behind `resync_required`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/api/src/routes/public_docs/schema_contract.rs b/crates/libs/api/src/routes/public_docs/schema_contract.rs index d67061de08..212043e437 100644 --- a/crates/libs/api/src/routes/public_docs/schema_contract.rs +++ b/crates/libs/api/src/routes/public_docs/schema_contract.rs @@ -88,7 +88,7 @@ fn openapi_identifier(value: &str) -> String { pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { let response = json!({ - "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4503599644147712\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nid: 4503599644213248\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", + "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4503599644147712\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", "content": { "text/event-stream": { "schema": { "type": "string" }, diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index f48b5db5e2..fe561d8707 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -306,9 +306,10 @@ pub(crate) enum ProcessEventId { const PROCESS_EVENT_ID_TAG_BIT: u32 = 52; const PROCESS_EVENT_ID_TAG: u64 = 1 << PROCESS_EVENT_ID_TAG_BIT; const PROCESS_EVENT_ID_GUARD_BITS: u32 = 16; -/// Frames at one position: those sent between two outputs, such as a -/// resume's `process_started` or a backend reconnect's two frames. The rest -/// of the 36 bits below the tag go to positions, up to 2^27 - 1 outputs. +/// Identified frames at one position: output moves it, so only +/// `resync_required` frames share one (see [`process_frame_has_event_id`]). +/// The rest of the 36 bits below the tag go to positions: ids carry one for a +/// process's first 2^27 - 1 outputs. const PROCESS_EVENT_ID_FRAME_BITS: u32 = 8; const PROCESS_EVENT_ID_FIELD_SHIFT: u32 = PROCESS_EVENT_ID_GUARD_BITS + PROCESS_EVENT_ID_FRAME_BITS; const PROCESS_EVENT_ID_FIELD_BITS: u32 = PROCESS_EVENT_ID_TAG_BIT - PROCESS_EVENT_ID_FIELD_SHIFT; @@ -561,9 +562,11 @@ impl ProcessSseState { DataPlaneServerFrame::ProcessStarted { .. } => self.started_sent = true, _ => {} } - self.event_id = self.event_id.next(self.backend_output_cursor); - self.pending - .push_back(sse_event_from_frame(&frame, Some(self.event_id.encode()))); + let id = process_frame_has_event_id(&frame).then(|| { + self.event_id = self.event_id.next(self.backend_output_cursor); + self.event_id.encode() + }); + self.pending.push_back(sse_event_from_frame(&frame, id)); } fn finish(&mut self, result: &'static str) { @@ -884,6 +887,19 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } } +/// Whether a process stream frame carries an SSE id: only one that moves the +/// client's position does. That is output, and `resync_required`, which +/// acknowledges a gap, so a resume from it continues past the gap without +/// reporting it again; a gap moves the position, so it never repeats at one. +/// Every other frame (`process_started`, `process_exited`, a backend +/// reconnect's, `error`, `closed`) leaves the client's last id unchanged, so +/// resumes and reconnects against an idle process spend no frames of the +/// [`ProcessEventId`] at its position. +fn process_frame_has_event_id(frame: &DataPlaneServerFrame) -> bool { + process_frame_advances_backend_cursor(frame) + || matches!(frame, DataPlaneServerFrame::ResyncRequired { .. }) +} + /// Backend reopens after an output window miss, without output or an exit /// in between, before the stream ends with an error. A cursor past the output /// restarts at the start, which the guest may in turn report as evicted, so diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index 008e53ce1f..dfc23f3155 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -1967,17 +1967,18 @@ async fn process_events_to_close( ) -> Vec<(Option, serde_json::Value)> { let (_state, mut chunks) = open_process_events(endpoint, session_id, last_event_id).await; let mut seen = String::new(); - tokio::time::timeout(Duration::from_secs(5), async { + tokio::time::timeout(Duration::from_secs(30), async { while let Some(chunk) = chunks.recv().await { seen.push_str(&chunk); } }) .await - .unwrap_or_else(|_| panic!("stream ends within 5s; saw {seen}")); + .unwrap_or_else(|_| panic!("stream ends within 30s; saw {seen}")); let frames = sse_frames(&seen); assert_eq!(frames.last().unwrap().1["type"], "closed", "{seen}"); let ids: Vec = frames.iter().filter_map(|(id, _)| *id).collect(); assert!(ids.is_sorted_by(|a, b| a < b), "ids increase: {ids:?}"); + assert_identified_frames(&frames); frames } @@ -1991,6 +1992,22 @@ fn assert_ids_above(frames: &[(Option, serde_json::Value)], last_event_id: ); } +/// The id a client resumes from: the last one it received. +fn last_id(frames: &[(Option, serde_json::Value)]) -> u64 { + frames.iter().rev().find_map(|(id, _)| *id).unwrap() +} + +/// Only output and `resync_required` carry an id. +fn assert_identified_frames(frames: &[(Option, serde_json::Value)]) { + for (id, frame) in frames { + let identified = matches!( + frame["type"].as_str(), + Some("stdout" | "stderr" | "resync_required") + ); + assert_eq!(id.is_some(), identified, "{frame}"); + } +} + fn connects(cursors: &[Option]) -> Vec { cursors.iter().copied().map(FakeOpen::Connect).collect() } @@ -2037,7 +2054,7 @@ async fn process_resume_continues_after_the_last_event_id() { .take_while(|(_, frame)| frame["seq"] != 3) .cloned() .collect(); - let last_event_id = received.last().unwrap().0.unwrap(); + let last_event_id = last_id(&received); let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; @@ -2128,7 +2145,7 @@ async fn evicted_output_on_an_older_guest_is_reported_before_the_live_tail() { "{frames:?}" ); - let last_event_id = frames.last().unwrap().0.unwrap(); + let last_event_id = last_id(&frames); let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; assert_eq!( resync_reasons(&resumed), @@ -2308,6 +2325,113 @@ async fn window_miss_reopen_refreshes_a_refused_route() { ); } +/// Backend reconnects and client resumes before a process's next output. +const IDLE_ROUNDS: usize = 300; + +/// The id of the output after the two lines the idle fakes start with. +fn third_line_id() -> u64 { + ProcessEventId::Position { + position: 3, + frame: 0, + } + .encode() +} + +/// How an idle fake answers `open`, the `opened`th: the lines from its cursor, +/// then, once `rounds` opens have gone by, a third line and the exit. +/// `fails` ends each earlier stream as an unavailable backend. +fn idle_replies(rounds: usize, fails: bool, opened: usize, open: FakeOpen) -> Vec { + let FakeOpen::Connect(Some(cursor)) = open else { + panic!("unexpected open {open:?}"); + }; + let lines = ["one\n", "two\n", "three\n"]; + let written = if opened > rounds { 3 } else { 2 }; + let mut replies = vec![connected()]; + replies.extend( + lines[..written] + .iter() + .skip(cursor as usize) + .map(|line| stdout(line)), + ); + if opened > rounds { + replies.push(exited()); + } else if fails { + replies.push(end(tonic::Code::Unavailable)); + } + replies +} + +/// The id of the frame that carries the third line. +fn third_line_frame_id(frames: &[(Option, serde_json::Value)]) -> Option { + frames_of_type(frames, "stdout") + .find(|(_, frame)| frame["seq"] == 3) + .and_then(|(id, _)| *id) +} + +/// Frames that carry no output carry no id either, so resumes against an +/// idle process, however many, spend none of the ids at its position: the +/// next output still carries it. +#[tokio::test] +async fn idle_resumes_keep_positional_ids() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|opened, open| idle_replies(IDLE_ROUNDS, false, opened, open)), + ) + .await; + let (_state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; + let mut seen = String::new(); + read_until(&mut chunks, &mut seen, &BASE64_STANDARD.encode("two\n")).await; + read_until(&mut chunks, &mut seen, "\n\n").await; + drop(chunks); + // Each resume reports the process and is dropped; like an EventSource, + // the client resumes from the last id it has received. + let mut last_event_id = last_id(&sse_frames(&seen)); + for _ in 0..IDLE_ROUNDS { + let (_state, mut chunks) = + open_process_events(&fake.endpoint, session_id, Some(last_event_id)).await; + let mut seen = String::new(); + read_until(&mut chunks, &mut seen, "process_started").await; + read_until(&mut chunks, &mut seen, "\n\n").await; + if let Some(id) = sse_frames(&seen).iter().rev().find_map(|(id, _)| *id) { + last_event_id = id; + } + } + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; + assert_eq!(stdout_text(&resumed), "three\n"); + assert_eq!(third_line_frame_id(&resumed), Some(third_line_id())); +} + +/// Likewise, backend reconnects before the next output spend no ids at the +/// position, however many there are. +#[tokio::test] +async fn idle_backend_reconnects_keep_positional_ids() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|opened, open| idle_replies(IDLE_ROUNDS, true, opened, open)), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + + assert_eq!( + frames_of_type(&frames, "backend_reconnected").count(), + IDLE_ROUNDS + 1 + ); + assert_eq!(resync_reasons(&frames), Vec::::new()); + assert_eq!(stdout_text(&frames), "one\ntwo\nthree\n"); + assert_eq!(third_line_frame_id(&frames), Some(third_line_id())); +} + /// A bound attach whose first backend stream fails before it reports the /// process still sends `process_started` once, from the reopened stream. #[tokio::test] @@ -2475,7 +2599,7 @@ async fn starting_attach_ignores_a_foreign_last_event_id() { assert_eq!(resync_reasons(&first), ["cursor_out_of_window"]); assert_eq!(stdout_text(&first), "one\ntwo\n"); - let last_event_id = first.last().unwrap().0.unwrap(); + let last_event_id = last_id(&first); let resumed = process_events_to_close(&fake.endpoint, session_id, Some(last_event_id)).await; assert_ids_above(&resumed, last_event_id); diff --git a/docs/first-sandbox.md b/docs/first-sandbox.md index 1eb82abec6..59d7c2889f 100644 --- a/docs/first-sandbox.md +++ b/docs/first-sandbox.md @@ -85,7 +85,7 @@ Read `/openapi-m1.json` for exact request constraints and supported fields. This The example displays decoded stdout and stderr and saves the raw event stream in its run directory. Handle binary output separately rather than displaying it in a terminal. -Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID`, set to the last event's `id` exactly as received, resumes with the output after that event while it is retained, and otherwise produces `resync_required` first. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. +Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID`, set to the last `id` received, exactly as received (only output and `resync_required` events carry one), resumes with the output after that event while it is retained, and otherwise produces `resync_required` first. After a process's first 2^27 - 1 output chunks, ids no longer carry its output position, and each reconnect replays the retained output behind `resync_required`. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. ## Inspect and manage the sandbox diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3c24147d68..da15d31594 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -78,7 +78,7 @@ This prints sandbox listings and the saved creation/deletion operations, sandbox | Stream authentication fails | Use `attach.token`, not `SPACE_API_TOKEN`, and the correct session URL. Check expiry and attach promptly after creating the command. | | Example rejects an endpoint, events URL, or redirect | Use HTTPS except for loopback HTTP. Check the data-plane origin and session ID; correct the configuration rather than removing destination or redirect checks. | | HTTP stream succeeds but output is incomplete | Check for `process_exited`, its exit result, and any `error` or `resync_required` frame. A closed connection is not proof of successful execution. | -| Reconnect reports `resync_required` | The stream could not resume exactly: output from before the reconnect may be missing or repeated. Check that the client sends the last event `id` exactly as received. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | +| Reconnect reports `resync_required` | The stream could not resume exactly: output from before the reconnect may be missing or repeated. Check that the client sends the last `id` it received exactly as received. A process that has written more than 2^27 - 1 output chunks always resumes this way. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | | Command exits nonzero | Read stderr and the exit result from the saved event transcript. Check that the executable and working directory exist inside the sandbox, not just on the host. | | Single-process GET returns `not_found` | This endpoint inspects a running process, not historical command results. It returns `not_found` before attachment and after exit. | diff --git a/scripts/tests/smoke-spaced-rest.sh b/scripts/tests/smoke-spaced-rest.sh index af769a4240..833c750091 100755 --- a/scripts/tests/smoke-spaced-rest.sh +++ b/scripts/tests/smoke-spaced-rest.sh @@ -687,13 +687,14 @@ async function readStream(resumeFrom, onEvent) { // replaying output already received or asking to resync, and the exited // process still reports its exit. const resumeFrom = lastEventId; - const resume = { attached_first: null, ids_continue: null, resync: null, stdout: '', exit_code: null }; + const resume = { attached_first: null, ids_continue: true, resync: null, stdout: '', exit_code: null }; let seen = 0; await readStream(resumeFrom, (event) => { const frame = event.data; seen += 1; if (seen === 1) resume.attached_first = frame.type === 'attached' && event.id === null; - if (event.id !== null && resume.ids_continue === null) resume.ids_continue = BigInt(event.id) > BigInt(resumeFrom); + // Only output and resync_required carry ids; any sent follow Last-Event-ID. + if (event.id !== null && BigInt(event.id) <= BigInt(resumeFrom)) resume.ids_continue = false; if (frame.type === 'resync_required' && resume.resync === null) resume.resync = frame.reason; if (frame.type === 'stdout') resume.stdout += Buffer.from(frame.data_base64, 'base64').toString('utf8'); if (frame.type === 'process_exited') { diff --git a/space_smoke/m1_conformance.py b/space_smoke/m1_conformance.py index 8c6a4a03c7..9900f0e016 100644 --- a/space_smoke/m1_conformance.py +++ b/space_smoke/m1_conformance.py @@ -634,12 +634,12 @@ def exercise_command(self, command: Any) -> None: ) if not second.attached_first: raise InvariantViolation("reconnected stream did not start with an attached frame") - if not second.identified: - raise InvariantViolation("reconnected stream delivered no identified events") - first_id, _ = second.identified[0] - if first_id <= first.last_id: + # Only output and resync_required carry ids, so a resume that sends + # neither has none; any it does send must follow Last-Event-ID. + if second.identified and second.identified[0][0] <= first.last_id: raise InvariantViolation( - f"resumed stream restarted ids at {first_id}, after {first.last_id}" + f"resumed stream restarted ids at {second.identified[0][0]}, " + f"after {first.last_id}" ) resyncs = [frame for frame in resumed if frame.get("type") == "resync_required"] if resyncs: @@ -668,7 +668,8 @@ def read_session_events( ) -> SessionRead: """Read one SSE connection until `stop()` holds, asserting id monotonicity. - Every identified frame must carry a greater id than the one before; + Every identified frame must carry a greater id than the one before, and + on a process stream only output and resync_required frames carry one; `on_connected` runs once the stream reports the process (started or reconnected), so process control can be exercised while attached. """ @@ -685,6 +686,10 @@ def read_session_events( if first: result.attached_first = frame.get("type") == "attached" and frame_id is None first = False + if (frame_id is not None) != ( + frame.get("type") in {"stdout", "stderr", "resync_required"} + ): + raise InvariantViolation(f"unexpected SSE id {frame_id} on {frame}") if frame_id is not None: if result.last_id is not None and frame_id <= result.last_id: raise InvariantViolation( From 530d96811081e0708e5fdfa3ce17ef53e69a0e96 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 23:02:03 +0000 Subject: [PATCH 07/12] space_smoke: take the recovery cursor from the first output process_started no longer carries an SSE id, so the data-plane restart scenario reads through the command's first output, the first frame with one, before taking its reconnect cursor. --- .../native_recovery_command_survives_data_plane_restart.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/space_smoke/scenarios/native_recovery_command_survives_data_plane_restart.py b/space_smoke/scenarios/native_recovery_command_survives_data_plane_restart.py index 40a821f1b2..2f34a967d4 100644 --- a/space_smoke/scenarios/native_recovery_command_survives_data_plane_restart.py +++ b/space_smoke/scenarios/native_recovery_command_survives_data_plane_restart.py @@ -36,8 +36,9 @@ def command_survives_data_plane_restart(fixture: KindNativeHardeningFixture) -> fixture, fixture.command(sandbox_id, ["sh", "-c", script], timeout_seconds=180), 201 ) with fixture.context.phase("start command and replace data-plane pods"): - # The SSE contract starts with attached, then process_started. - initial = attach_after_recovery(fixture, command, max_events=2) + # The SSE contract starts with attached, then process_started; the + # first output ("begun") is the first frame with an id. + initial = attach_after_recovery(fixture, command, max_events=3) fixture.context.verify( any(event.data.get("type") == "process_started" for event in initial.events), "fault requires an observed running guest process", From 09121e81a45e6ee665c6ee7810444fae792c0842 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Thu, 22 Oct 2026 18:46:20 +0000 Subject: [PATCH 08/12] data plane: reattach to a busy process at the oldest retained output A reader whose cursor was evicted never becomes the guest journal's primary reader, and without one the journal evicts its oldest output on every append once it is full. Reopening at the first sequence the lag status reported took a data-plane-to-guest round trip, so against a process that keeps writing that output was usually gone again: after three reopens the stream ended with a retryable error, and a retry from the gap's id repeated the chase, so the client got no output until the process slowed down or exited. ConnectProcessRequest gains resume_from_oldest_retained. With it, the guest starts a cursor before its oldest retained output at that output, chosen under the lock that also claims the primary reader's place, so output written afterwards waits for the reader instead of evicting it. ProcessConnected.output_cursor reports where the stream starts, and sandbox-manager forwards both fields. Older guests ignore the flag, and the ECW streams do not set it. The native data plane sets the flag on every connect and treats a reported start past its position as the eviction gap: one cursor_out_of_window, then the output from there, with no reopen. When the guest ends the stream as lagged instead, it still reopens at the reported first sequence. A stream already moved to the oldest retained output that falls behind again before any output follows the live tail rather than chasing it; only a cursor past the output, or a live tail that keeps falling behind, ends with an error once the reopen cap is reached. --- crates/apps/spaced/host/cli/src/main.rs | 2 +- .../plane/server/src/ecw_connect/tests.rs | 4 +- .../libs/data/plane/server/src/ecw_process.rs | 1 + .../plane/server/src/ecw_process/stream.rs | 1 + .../plane/server/src/ecw_process/tests.rs | 4 +- .../libs/data/plane/server/src/native/sse.rs | 72 +++-- .../data/plane/server/src/native/streams.rs | 3 + .../data/plane/server/src/native/tests.rs | 245 ++++++++++++++---- crates/libs/node/api/sim/src/processes.rs | 8 +- .../libs/node/api/sim/src/processes/tests.rs | 9 +- crates/libs/node/api/sim/src/worker/tests.rs | 1 + .../service/grpc_spaced_process_snapshots.rs | 1 + .../mgr/server/src/service/spaced_bridge.rs | 4 +- .../server/src/service/spaced_bridge/tests.rs | 26 ++ .../libs/sbx/mgr/server/src/service/tests.rs | 1 + .../spaced/guest/execution/src/process.rs | 57 +++- .../guest/execution/src/process/tests.rs | 54 +++- .../spaced/guest/server/tests/it/process.rs | 17 ++ crates/libs/spaced/host/client/src/process.rs | 4 + crates/libs/spaced/host/client/src/tests.rs | 3 +- .../spaced/host/server/tests/it/process.rs | 2 +- .../server/tests/support/mock_bridge_guest.rs | 4 +- proto/spaced/operations/v1/operations.proto | 14 +- .../spaced/operations/v1/operations.pb.go | 45 +++- 24 files changed, 472 insertions(+), 110 deletions(-) diff --git a/crates/apps/spaced/host/cli/src/main.rs b/crates/apps/spaced/host/cli/src/main.rs index 7362548f16..044533798e 100644 --- a/crates/apps/spaced/host/cli/src/main.rs +++ b/crates/apps/spaced/host/cli/src/main.rs @@ -288,7 +288,7 @@ async fn run() -> anyhow::Result<()> { } Commands::Connect { process_id } => { let mut stream = client - .connect_process_stream_with_cursor(process_id, None, None) + .connect_process_stream_with_cursor(process_id, None, None, false) .await?; while let Some(event) = stream.message().await? { print_process_event(event).await?; diff --git a/crates/libs/data/plane/server/src/ecw_connect/tests.rs b/crates/libs/data/plane/server/src/ecw_connect/tests.rs index 888afdcabb..540f683052 100644 --- a/crates/libs/data/plane/server/src/ecw_connect/tests.rs +++ b/crates/libs/data/plane/server/src/ecw_connect/tests.rs @@ -43,7 +43,9 @@ fn process_events_encode_connect_wire_shape() { process_id: "proc-1".to_string(), pid: None, event: Some(spaced_process_event::Event::Connected( - SpacedProcessConnected {}, + SpacedProcessConnected { + output_cursor: None, + }, )), }, true, diff --git a/crates/libs/data/plane/server/src/ecw_process.rs b/crates/libs/data/plane/server/src/ecw_process.rs index 7b6d0cbbe2..46fb18dd18 100644 --- a/crates/libs/data/plane/server/src/ecw_process.rs +++ b/crates/libs/data/plane/server/src/ecw_process.rs @@ -406,6 +406,7 @@ pub(super) async fn process_connect( process_id, timeout, output_cursor: None, + resume_from_oldest_retained: false, }), }; let route_state = state.clone(); diff --git a/crates/libs/data/plane/server/src/ecw_process/stream.rs b/crates/libs/data/plane/server/src/ecw_process/stream.rs index a6c228fecc..54cbec9159 100644 --- a/crates/libs/data/plane/server/src/ecw_process/stream.rs +++ b/crates/libs/data/plane/server/src/ecw_process/stream.rs @@ -449,6 +449,7 @@ pub(super) fn build_process_start_resume_request( .as_ref() .and_then(|operation| operation.timeout), output_cursor: Some(output_cursor), + resume_from_oldest_retained: false, }), } } diff --git a/crates/libs/data/plane/server/src/ecw_process/tests.rs b/crates/libs/data/plane/server/src/ecw_process/tests.rs index e335e9e72a..9c73fd2e2f 100644 --- a/crates/libs/data/plane/server/src/ecw_process/tests.rs +++ b/crates/libs/data/plane/server/src/ecw_process/tests.rs @@ -73,7 +73,9 @@ fn connected_process_event(process_id: &ProcessId) -> SpacedProcessEvent { process_id: process_id.to_string(), pid: Some(123), event: Some(spaced_ops::process_event::Event::Connected( - spaced_ops::ProcessConnected {}, + spaced_ops::ProcessConnected { + output_cursor: None, + }, )), } } diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index fe561d8707..33c9ad30bc 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -240,6 +240,10 @@ pub(crate) struct ProcessSseState { /// `resync_required(cursor_out_of_window)` was sent and no output has /// followed it, so another gap before any output is the same one. gap_reported: bool, + /// The stream moved to the oldest output the guest retains after an + /// eviction, and no output has followed. Falling behind again before any + /// does is a race the stream keeps losing, so it follows the live tail. + at_oldest_retained: bool, /// Backend reopens after an output window miss since the last output or /// exit, capped at [`PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS`]. window_reopens_without_progress: u32, @@ -515,6 +519,7 @@ impl ProcessSseState { binding: None, started_sent: false, gap_reported: false, + at_oldest_retained: false, window_reopens_without_progress: 0, revocation_tick, pending, @@ -591,38 +596,61 @@ impl ProcessSseState { }); } + /// Continues at `start`, where the guest reports a backend stream starts: + /// past the client's position when the output there was evicted. The guest + /// chose it under the lock that makes the stream its primary reader, so + /// unless another reader holds that place, the output there stays until + /// the stream reads it. + fn continue_at_reported_start(&mut self, start: u64) { + if self + .backend_output_cursor + .is_some_and(|cursor| start > cursor) + { + self.continue_after_gap(Some(start), "output_evicted"); + self.at_oldest_retained = true; + } + } + /// Reopens the backend stream after the guest refused its cursor as /// outside the retained output, behind one /// `resync_required(cursor_out_of_window)` for the gap: from the start of - /// the output when the cursor was past its end, from the oldest output the - /// guest still retains when it reports it, else at the live tail. + /// the output when the cursor was past its end, and from the oldest output + /// the guest still retains when it reports it. The stream follows the live + /// tail instead when the guest does not report it, or when the stream + /// already moved there and fell behind again before any output. After + /// [`PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS`] reopens, a cursor + /// past the output, or a live tail that falls behind, ends the stream. async fn reopen_after_window_miss(&mut self, status: tonic::Status, miss: OutputWindowMiss) { let Some(process_id) = self.process_id.clone() else { self.finish_with_error(StreamBridgeError::BackendStatus(status)); return; }; - if self.window_reopens_without_progress >= PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS { - self.finish_with_error(StreamBridgeError::BackendStatus(status)); - return; - } + let out_of_reopens = + self.window_reopens_without_progress >= PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS; match (miss, self.backend_output_cursor) { // Only a position the client resumed from can be past the output. - (OutputWindowMiss::CursorAhead, Some(cursor)) if cursor > 0 => { + (OutputWindowMiss::CursorAhead, Some(cursor)) if cursor > 0 && !out_of_reopens => { self.restart_from_output_start("cursor_ahead"); } - (OutputWindowMiss::CursorAhead, _) => { - self.finish_with_error(StreamBridgeError::BackendStatus(status)); - return; - } ( OutputWindowMiss::Evicted { first_retained: Some(first), }, Some(cursor), - ) if first > cursor => self.continue_after_gap(Some(first), "output_evicted"), - (OutputWindowMiss::Evicted { .. }, _) => { + ) if first > cursor && !self.at_oldest_retained => { + self.continue_after_gap(Some(first), "output_evicted"); + self.at_oldest_retained = true; + } + (OutputWindowMiss::Evicted { .. }, Some(_)) => { + self.continue_after_gap(None, "output_evicted"); + } + (OutputWindowMiss::Evicted { .. }, None) if !out_of_reopens => { self.continue_after_gap(None, "output_evicted"); } + _ => { + self.finish_with_error(StreamBridgeError::BackendStatus(status)); + return; + } } self.window_reopens_without_progress += 1; match connect_process_stream( @@ -828,6 +856,12 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { )); } } + let reported_start = match &event.event { + Some(spaced_process_event::Event::Connected(connected)) => { + connected.output_cursor + } + _ => None, + }; let mut output_seq = state.output_seq; let batch = process_event_frames(event, &mut output_seq); if batch @@ -851,6 +885,7 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { } if process_frame_advances_backend_cursor(&frame) || exited { state.gap_reported = false; + state.at_oldest_retained = false; state.window_reopens_without_progress = 0; } state.push_frame(frame); @@ -861,6 +896,9 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { state.finish("backend_closed"); } } + if let Some(start) = reported_start { + state.continue_at_reported_start(start); + } if let Some(event) = state.pending.pop_front() { return Some(event); } @@ -901,9 +939,11 @@ fn process_frame_has_event_id(frame: &DataPlaneServerFrame) -> bool { } /// Backend reopens after an output window miss, without output or an exit -/// in between, before the stream ends with an error. A cursor past the output -/// restarts at the start, which the guest may in turn report as evicted, so -/// two in a row are expected. +/// in between, after which a cursor past the output, or a live tail that +/// falls behind, ends the stream with an error; evicted output still +/// continues at the live tail. A cursor past the output restarts at the +/// start, which a guest may in turn report as evicted, so two in a row are +/// expected. const PROCESS_OUTPUT_WINDOW_REOPENS_WITHOUT_PROGRESS: u32 = 3; /// Why the guest ended a process stream because its cursor is outside the diff --git a/crates/libs/data/plane/server/src/native/streams.rs b/crates/libs/data/plane/server/src/native/streams.rs index e1ff4adf2e..9abd4f9348 100644 --- a/crates/libs/data/plane/server/src/native/streams.rs +++ b/crates/libs/data/plane/server/src/native/streams.rs @@ -271,6 +271,9 @@ pub(crate) async fn connect_process_stream( process_id: process_id.to_string(), timeout: None, output_cursor, + // A cursor whose output was evicted starts at the oldest + // output retained, which `ProcessConnected` reports. + resume_from_oldest_retained: true, }), }, route_token, diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index dfc23f3155..7bdebab5e3 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -1711,9 +1711,23 @@ fn started() -> FakeReply { )) } +/// A reconnect's first event, from a guest that does not say where the +/// stream starts. fn connected() -> FakeReply { FakeReply::Event(spaced_process_event::Event::Connected( - pplx_space_sbx_mgr_client::operations::ProcessConnected {}, + pplx_space_sbx_mgr_client::operations::ProcessConnected { + output_cursor: None, + }, + )) +} + +/// A reconnect's first event, from a guest that started the stream at +/// `start`, its oldest retained output, because the cursor's was evicted. +fn connected_at(start: u64) -> FakeReply { + FakeReply::Event(spaced_process_event::Event::Connected( + pplx_space_sbx_mgr_client::operations::ProcessConnected { + output_cursor: Some(start), + }, )) } @@ -1851,13 +1865,15 @@ async fn spawn_process_fake( "unexpected call {path}" ); let body = request.into_body().collect().await.unwrap().to_bytes(); - FakeOpen::Connect( - SpacedConnectProcessRequest::decode(&body[5..]) - .unwrap() - .operation - .unwrap() - .output_cursor, - ) + let operation = SpacedConnectProcessRequest::decode(&body[5..]) + .unwrap() + .operation + .unwrap(); + assert!( + operation.resume_from_oldest_retained, + "a connect asks to start at the oldest retained output" + ); + FakeOpen::Connect(operation.output_cursor) }; let opened = { let mut opens = opens.lock().unwrap(); @@ -2114,6 +2130,50 @@ async fn evicted_process_output_continues_from_the_oldest_retained_output() { ); } +/// A guest that starts a lagged connect at its oldest retained output, and +/// says so, needs no reopen: the stream reports the gap once and continues +/// from there, with ids that keep their positions. +#[tokio::test] +async fn evicted_output_continues_where_the_guest_reports_the_stream_starts() { + if !binding_fake_test_process() { + return; + } + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(|_, open| match open { + FakeOpen::Connect(Some(cursor)) if cursor < 2 => { + let mut replies = vec![connected_at(2)]; + replies.extend( + journal_replay(&JOURNAL, FakeOpen::Connect(Some(2))) + .into_iter() + .skip(1), + ); + replies + } + open => journal_replay(&JOURNAL, open), + }), + ) + .await; + + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), "three\nfour\n"); + assert_eq!(frames_of_type(&frames, "process_started").count(), 1); + + let gap = frames_of_type(&frames, "resync_required") + .next() + .unwrap() + .0 + .unwrap(); + let resumed = process_events_to_close(&fake.endpoint, session_id, Some(gap)).await; + assert_eq!(resync_reasons(&resumed), Vec::::new()); + assert_eq!(stdout_text(&resumed), "three\nfour\n"); + assert_ids_above(&resumed, gap); + assert_eq!(*fake.opens.lock().unwrap(), connects(&[Some(0), Some(2)])); +} + /// A guest older than the lag reason does not say where its retained output /// starts: the stream reports the gap once, follows the live tail without /// reporting the process start again, and a resume from what it sent next @@ -2191,69 +2251,148 @@ async fn untagged_resource_exhausted_is_not_an_output_gap() { } /// A cursor past the output restarts at the start; when the start has been -/// evicted too, that is the same gap, reported once. +/// evicted too, that is the same gap, reported once, whether the guest ends +/// the stream as lagged or starts it at its oldest retained output. #[tokio::test] async fn cursor_ahead_then_evicted_start_reports_one_gap() { if !binding_fake_test_process() { return; } - let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); - let fake = spawn_journal_fake( - session_id, - true, - Arc::new(|_, open| match open { - FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![connected(), lagged(Some(2))], - open => journal_replay(&JOURNAL, open), - }), - ) - .await; - let ahead = ProcessEventId::Position { - position: 10, - frame: 0, + for reports_start in [false, true] { + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + Arc::new(move |_, open| match open { + FakeOpen::Connect(Some(cursor)) if cursor < 2 && reports_start => { + let mut replies = vec![connected_at(2)]; + replies.extend( + journal_replay(&JOURNAL, FakeOpen::Connect(Some(2))) + .into_iter() + .skip(1), + ); + replies + } + FakeOpen::Connect(Some(cursor)) if cursor < 2 => vec![connected(), lagged(Some(2))], + open => journal_replay(&JOURNAL, open), + }), + ) + .await; + let ahead = ProcessEventId::Position { + position: 10, + frame: 0, + } + .encode(); + + let frames = process_events_to_close(&fake.endpoint, session_id, Some(ahead)).await; + + assert_eq!( + resync_reasons(&frames), + ["cursor_out_of_window"], + "{reports_start}" + ); + assert_eq!(stdout_text(&frames), "three\nfour\n"); + assert_ids_above(&frames, ahead); + let opens = if reports_start { + connects(&[Some(10), Some(0)]) + } else { + connects(&[Some(10), Some(0), Some(2)]) + }; + assert_eq!(*fake.opens.lock().unwrap(), opens); } - .encode(); +} - let frames = process_events_to_close(&fake.endpoint, session_id, Some(ahead)).await; +/// A process that keeps writing can evict its oldest retained output before +/// a stream that moved there reads it, when the guest could not make the +/// stream its primary reader. Chasing the next oldest would lose that race +/// on every reopen, so the stream follows the live tail instead, whether the +/// guest reported the start or ended the stream as lagged. +#[tokio::test] +async fn evicted_output_that_falls_behind_again_follows_the_live_tail() { + if !binding_fake_test_process() { + return; + } + for reports_start in [false, true] { + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = spawn_journal_fake( + session_id, + true, + // The oldest retained output moves past every cursor connected at. + Arc::new(move |_, open| match open { + FakeOpen::Connect(Some(cursor)) if reports_start => { + vec![connected_at(cursor + 1), lagged(Some(cursor + 2))] + } + FakeOpen::Connect(Some(cursor)) => vec![connected(), lagged(Some(cursor + 1))], + FakeOpen::Connect(None) => vec![connected(), stdout("live\n"), exited()], + FakeOpen::Start => panic!("unexpected start"), + }), + ) + .await; - assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); - assert_eq!(stdout_text(&frames), "three\nfour\n"); - assert_ids_above(&frames, ahead); - assert_eq!( - *fake.opens.lock().unwrap(), - connects(&[Some(10), Some(0), Some(2)]) - ); + let frames = process_events_to_close(&fake.endpoint, session_id, None).await; + + assert!( + frames_of_type(&frames, "error").next().is_none(), + "{frames:?}" + ); + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(stdout_text(&frames), "live\n"); + let opens = if reports_start { + connects(&[Some(0), None]) + } else { + connects(&[Some(0), Some(1), None]) + }; + assert_eq!(*fake.opens.lock().unwrap(), opens, "{reports_start}"); + } } /// A guest that keeps refusing the cursor without ever sending output does -/// not keep the stream reopening: after a few reopens it ends with an error. +/// not keep the stream reopening: a start that is past the output too, a +/// guest that keeps moving the start past the output, and a live tail that +/// keeps falling behind, end with an error. #[tokio::test] async fn window_miss_reopens_without_progress_are_capped() { if !binding_fake_test_process() { return; } - let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); - let fake = spawn_journal_fake( - session_id, - true, - Arc::new(|_, open| { - let FakeOpen::Connect(Some(cursor)) = open else { - panic!("unexpected open {open:?}"); - }; - vec![connected(), lagged(Some(cursor + 1))] - }), - ) - .await; + let ahead = ProcessEventId::Position { + position: 10, + frame: 0, + } + .encode(); + for (last_event_id, replies, opens) in [ + ( + Some(ahead), + (|_| vec![connected(), cursor_ahead()]) as fn(FakeOpen) -> Vec, + connects(&[Some(10), Some(0)]), + ), + ( + Some(ahead), + |open| match open { + FakeOpen::Connect(Some(0)) => vec![connected_at(5), cursor_ahead()], + _ => vec![connected(), cursor_ahead()], + }, + connects(&[Some(10), Some(0), Some(0), Some(0)]), + ), + ( + None, + |_| vec![connected(), lagged(None)], + connects(&[Some(0), None, None, None]), + ), + ] { + let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); + let fake = + spawn_journal_fake(session_id, true, Arc::new(move |_, open| replies(open))).await; - let (_state, mut chunks) = open_process_events(&fake.endpoint, session_id, None).await; - let mut seen = String::new(); - read_until(&mut chunks, &mut seen, "backend_stream_error").await; - let frames = sse_frames(&seen); + let (_state, mut chunks) = + open_process_events(&fake.endpoint, session_id, last_event_id).await; + let mut seen = String::new(); + read_until(&mut chunks, &mut seen, "backend_stream_error").await; + let frames = sse_frames(&seen); - assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); - assert_eq!( - *fake.opens.lock().unwrap(), - connects(&[Some(0), Some(1), Some(2), Some(3)]) - ); + assert_eq!(resync_reasons(&frames), ["cursor_out_of_window"]); + assert_eq!(*fake.opens.lock().unwrap(), opens); + } } /// Output between window misses is progress: a stream that keeps falling diff --git a/crates/libs/node/api/sim/src/processes.rs b/crates/libs/node/api/sim/src/processes.rs index 8c7d4af3a9..86f54117e6 100644 --- a/crates/libs/node/api/sim/src/processes.rs +++ b/crates/libs/node/api/sim/src/processes.rs @@ -12,7 +12,8 @@ //! `output_cursor` replays every record at or after that sequence, a cursor //! past the output ends the stream with the guest's tagged `OUT_OF_RANGE`, //! and a connect without one follows the live tail and replays nothing. The -//! simulator retains all output, so it never reports a cursor as evicted. The +//! simulator retains all output, so it never reports a cursor as evicted, and +//! `ProcessConnected` reports that the stream starts at the cursor. The //! process table is per sandbox and is dropped with the sandbox. use std::collections::HashMap; @@ -251,8 +252,11 @@ fn event_stream(process: &SimProcess, mode: ConnectMode) -> SimProcessEventStrea (started, 0) } ConnectMode::Connect { output_cursor } => { + // Nothing is evicted, so the stream starts at the cursor. let connected = - sbx::spaced_process_event::Event::Connected(sbx::SpacedProcessConnected {}); + sbx::spaced_process_event::Event::Connected(sbx::SpacedProcessConnected { + output_cursor, + }); if output_cursor.is_some_and(|cursor| cursor > tail) { return Box::pin(tokio_stream::iter(vec![ Ok(event(&process_id, Some(pid), connected)), diff --git a/crates/libs/node/api/sim/src/processes/tests.rs b/crates/libs/node/api/sim/src/processes/tests.rs index 4889326da5..a0f2138805 100644 --- a/crates/libs/node/api/sim/src/processes/tests.rs +++ b/crates/libs/node/api/sim/src/processes/tests.rs @@ -46,10 +46,13 @@ async fn connect_replays_the_journal_from_the_cursor_only() { // Everything already delivered: nothing to replay. let at_tail = collect(table.connect("sbx", &process_id, Some(1)).await.unwrap()).await; - assert!(matches!( + assert_eq!( at_tail[0], - sbx::spaced_process_event::Event::Connected(_) - )); + sbx::spaced_process_event::Event::Connected(sbx::SpacedProcessConnected { + output_cursor: Some(1) + }), + "reports where it starts" + ); assert_eq!(stdout_count(&at_tail), 0); assert!(matches!( at_tail.last(), diff --git a/crates/libs/node/api/sim/src/worker/tests.rs b/crates/libs/node/api/sim/src/worker/tests.rs index 5dad827b08..6060f8380e 100644 --- a/crates/libs/node/api/sim/src/worker/tests.rs +++ b/crates/libs/node/api/sim/src/worker/tests.rs @@ -686,6 +686,7 @@ async fn stop_and_kill_end_the_sandbox_processes_before_a_restart() { process_id: process_id.clone(), timeout: None, output_cursor: None, + resume_from_oldest_retained: false, }), })) .await; diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots.rs b/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots.rs index 33fe045d71..a247d4b6d3 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots.rs @@ -223,6 +223,7 @@ impl SandboxManagerService { operation.process_id.clone(), proto_duration_to_std(operation.timeout), operation.output_cursor, + operation.resume_from_oldest_retained, ) .await?; // A reconnected foreground stream is the same liveness signal as the diff --git a/crates/libs/sbx/mgr/server/src/service/spaced_bridge.rs b/crates/libs/sbx/mgr/server/src/service/spaced_bridge.rs index cfaf5970db..252d02d0db 100644 --- a/crates/libs/sbx/mgr/server/src/service/spaced_bridge.rs +++ b/crates/libs/sbx/mgr/server/src/service/spaced_bridge.rs @@ -709,7 +709,9 @@ pub(super) fn process_event_to_spaced( HostEvent::Started(started) => SpacedEvent::Started(SpacedProcessStarted { start_time: started.start_time, }), - HostEvent::Connected(_) => SpacedEvent::Connected(SpacedProcessConnected {}), + HostEvent::Connected(connected) => SpacedEvent::Connected(SpacedProcessConnected { + output_cursor: connected.output_cursor, + }), HostEvent::Stdout(output) => SpacedEvent::Stdout(SpacedProcessOutput { data: output.data }), HostEvent::Stderr(output) => SpacedEvent::Stderr(SpacedProcessOutput { data: output.data }), HostEvent::Exit(exit) => { diff --git a/crates/libs/sbx/mgr/server/src/service/spaced_bridge/tests.rs b/crates/libs/sbx/mgr/server/src/service/spaced_bridge/tests.rs index 180c903096..409fdd8372 100644 --- a/crates/libs/sbx/mgr/server/src/service/spaced_bridge/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/spaced_bridge/tests.rs @@ -265,6 +265,32 @@ fn process_info_to_spaced_preserves_process_config() { assert_eq!(spaced.pty, Some(true)); } +/// The guest's report of where a reconnected stream starts reaches the data +/// plane, which relies on it to tell evicted output apart. +#[test] +fn process_event_to_spaced_keeps_the_connected_output_cursor() { + let event = host_proto::ProcessEvent { + process_id: "proc-1".to_string(), + pid: Some(123), + event: Some(host_proto::process_event::Event::Connected( + host_proto::ProcessConnected { + output_cursor: Some(9), + }, + )), + }; + + let spaced = process_event_to_spaced(event).unwrap(); + + assert_eq!( + spaced.event, + Some(crate::proto::spaced_process_event::Event::Connected( + SpacedProcessConnected { + output_cursor: Some(9) + } + )) + ); +} + #[tokio::test] async fn identity_fence_aborts_an_idle_stream() { let fence = RouteStreamFence::new(); diff --git a/crates/libs/sbx/mgr/server/src/service/tests.rs b/crates/libs/sbx/mgr/server/src/service/tests.rs index 65b32d784f..1d371139ac 100644 --- a/crates/libs/sbx/mgr/server/src/service/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/tests.rs @@ -16256,6 +16256,7 @@ pub(crate) mod service_tests { process_id: "process".to_string(), timeout: None, output_cursor: Some(7), + resume_from_oldest_retained: false, }, ), }); diff --git a/crates/libs/spaced/guest/execution/src/process.rs b/crates/libs/spaced/guest/execution/src/process.rs index 718f91ffc6..8596c8191b 100644 --- a/crates/libs/spaced/guest/execution/src/process.rs +++ b/crates/libs/spaced/guest/execution/src/process.rs @@ -427,7 +427,7 @@ impl ProcessLaunch { )); claim.publish(Arc::clone(&record)); - let cursor = record.journal.cursor(Some(0)); + let cursor = record.journal.cursor(Some(0), false); let mut output_tasks = VecDeque::new(); if let Some(reader) = pty_reader { output_tasks.push_back(spawn_process_output_reader( @@ -1028,12 +1028,25 @@ impl ProcessEventJournal { .completed_at } - fn cursor(self: &Arc, requested: Option) -> ProcessJournalCursor { + /// A reader from `requested`, or from the live tail when `None`. With + /// `from_oldest_retained`, a cursor before the oldest retained output + /// starts there instead of lagging. That start is chosen under the lock + /// that also claims the primary reader's place, so no output written + /// meanwhile can evict it. + fn cursor( + self: &Arc, + requested: Option, + from_oldest_retained: bool, + ) -> ProcessJournalCursor { let mut state = self .state .lock() .expect("process output journal lock poisoned"); - let next_sequence = requested.unwrap_or(state.next_sequence); + let next_sequence = match requested { + Some(cursor) if from_oldest_retained => cursor.max(state.first_sequence), + Some(cursor) => cursor, + None => state.next_sequence, + }; // Only one reader controls the producer. Observers cannot stall an // existing consumer; after disconnect a new attachment can take over. let primary = if !state.backpressure_disabled @@ -1416,7 +1429,12 @@ impl ProcessRegistry { Some(record) } - fn replay(&self, process_id: &ProcessId, output_cursor: Option) -> Option { + fn replay( + &self, + process_id: &ProcessId, + output_cursor: Option, + from_oldest_retained: bool, + ) -> Option { let record = self.get_record(process_id)?; if output_cursor.is_none() && let Some(exit) = record.journal.exit() @@ -1428,7 +1446,7 @@ impl ProcessRegistry { } Some(ProcessReplay::Running { started: record.started.clone(), - cursor: record.journal.cursor(output_cursor), + cursor: record.journal.cursor(output_cursor, from_oldest_retained), }) } @@ -1626,13 +1644,19 @@ fn process_started_event(process_id: &ProcessId, pid: Option) -> Result) -> ProcessEvent { +fn process_connected_event( + process_id: &ProcessId, + pid: Option, + output_cursor: Option, +) -> ProcessEvent { ProcessEvent { process_id: process_id.as_str().to_string(), pid, event: Some( pplx_space_spaced_guest_protocol::proto::bridge_guest::process_event::Event::Connected( - pplx_space_spaced_guest_protocol::proto::bridge_guest::ProcessConnected {}, + pplx_space_spaced_guest_protocol::proto::bridge_guest::ProcessConnected { + output_cursor, + }, ), ), } @@ -2107,7 +2131,7 @@ impl Process for ExecutionService { let replay = self .inner .processes - .replay(&process_id, Some(0)) + .replay(&process_id, Some(0), false) .ok_or_else(|| Status::failed_precondition("process replay state expired"))?; return Ok(Response::new( replay_process_event_stream(replay) as Self::StartStream @@ -2234,7 +2258,11 @@ impl Process for ExecutionService { let replay = self .inner .processes - .replay(&process_id, request.output_cursor) + .replay( + &process_id, + request.output_cursor, + request.resume_from_oldest_retained, + ) .ok_or_else(|| Status::not_found("process not found"))?; let pid = match &replay { ProcessReplay::Running { started, .. } | ProcessReplay::Completed { started, .. } => { @@ -2242,10 +2270,13 @@ impl Process for ExecutionService { } }; let stream = match replay { - ProcessReplay::Running { cursor, .. } => process_event_stream_with_prefix( - vec![process_connected_event(&process_id, pid)], - Some(cursor), - ), + ProcessReplay::Running { cursor, .. } => { + let output_cursor = request.output_cursor.map(|_| cursor.next_sequence); + process_event_stream_with_prefix( + vec![process_connected_event(&process_id, pid, output_cursor)], + Some(cursor), + ) + } replay @ ProcessReplay::Completed { .. } => replay_process_event_stream(replay), }; tracing::debug!( diff --git a/crates/libs/spaced/guest/execution/src/process/tests.rs b/crates/libs/spaced/guest/execution/src/process/tests.rs index ba5874d71e..b1bc7ce61c 100644 --- a/crates/libs/spaced/guest/execution/src/process/tests.rs +++ b/crates/libs/spaced/guest/execution/src/process/tests.rs @@ -548,7 +548,7 @@ async fn attached_output_backpressures_and_disconnect_releases_the_producer() { shared_bytes, )))); let process_id = ProcessId::new(); - let cursor = journal.cursor(Some(0)); + let cursor = journal.cursor(Some(0), false); let event = process_stdout_event(&process_id, Some(1), vec![b'a'; 8192]); while journal.try_append_output(event.clone()).is_none() {} let tail = journal.tail_cursor(); @@ -649,7 +649,8 @@ async fn fallback_drain_bounds_inherited_writer_despite_consuming_reader() { let id = ProcessId::new(); let pid = std::process::id(); let journal = Arc::new(ProcessEventJournal::new(output_slots())); - let mut stream = process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(0)))); + let mut stream = + process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(0), false))); let event = process_stdout_event(&id, Some(pid), vec![0; 8192]); while journal.try_append_output(event.clone()).is_none() {} let (blocked, mut pending) = tokio::sync::mpsc::channel(1); @@ -925,7 +926,8 @@ async fn cursor_past_the_output_fails_instead_of_waiting() { journal.complete(exit.clone()); } - let mut ahead = process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(5)))); + let mut ahead = + process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(5), false))); let error = tokio::time::timeout(Duration::from_secs(1), ahead.next()) .await .expect("a cursor past the output must not wait") @@ -946,7 +948,7 @@ async fn cursor_past_the_output_fails_instead_of_waiting() { assert!(ahead.next().await.is_none()); let mut at_end = - process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(1)))); + process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(1), false))); let next = tokio::time::timeout(Duration::from_millis(100), at_end.next()).await; if completed { assert_eq!(next.unwrap().unwrap().unwrap(), exit); @@ -972,7 +974,8 @@ async fn lagged_cursor_reports_the_oldest_retained_output() { )); journal.append_output(process_stdout_event(&process_id, Some(1), b"kept".to_vec())); - let mut lagged = process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(0)))); + let mut lagged = + process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(0), false))); let error = lagged.next().await.unwrap().unwrap_err(); assert_eq!(error.code(), tonic::Code::ResourceExhausted); let metadata = |key| { @@ -994,8 +997,45 @@ async fn lagged_cursor_reports_the_oldest_retained_output() { assert_eq!(first_sequence, 1); assert!(lagged.next().await.is_none()); - let mut retained = - process_event_stream_with_prefix(Vec::new(), Some(journal.cursor(Some(first_sequence)))); + let mut retained = process_event_stream_with_prefix( + Vec::new(), + Some(journal.cursor(Some(first_sequence), false)), + ); + let event = retained.next().await.unwrap().unwrap(); + assert!(matches!(event.event, Some(Event::Stdout(output)) if output.data == b"kept")); +} + +/// A lagged cursor that asks for the oldest retained output starts there, +/// and holds it: the start is chosen under the lock that makes the reader +/// primary, so output written before it reads anything waits for it instead +/// of evicting that output, and the reader cannot lag again. +#[tokio::test] +async fn lagged_cursor_from_oldest_retained_starts_there_and_holds_it() { + use pplx_space_spaced_guest_protocol::proto::bridge_guest::process_event::Event; + + let journal = Arc::new(ProcessEventJournal::new(Arc::new(Semaphore::new(0)))); + let process_id = ProcessId::new(); + journal.append_output(process_stdout_event( + &process_id, + Some(1), + vec![0; PROCESS_OUTPUT_RETENTION_BYTES], + )); + journal.append_output(process_stdout_event(&process_id, Some(1), b"kept".to_vec())); + + let cursor = journal.cursor(Some(0), true); + assert_eq!( + cursor.next_sequence, 1, + "starts at the oldest retained output" + ); + let more = process_stdout_event(&process_id, Some(1), vec![0; 8192]); + let waited = (0..2 * PROCESS_OUTPUT_RETENTION_BYTES / 8192) + .any(|_| journal.try_append_output(more.clone()).is_some()); + assert!( + waited, + "later output waits for the reader instead of evicting" + ); + + let mut retained = process_event_stream_with_prefix(Vec::new(), Some(cursor)); let event = retained.next().await.unwrap().unwrap(); assert!(matches!(event.event, Some(Event::Stdout(output)) if output.data == b"kept")); } diff --git a/crates/libs/spaced/guest/server/tests/it/process.rs b/crates/libs/spaced/guest/server/tests/it/process.rs index 4547603dc7..ee40caabc9 100644 --- a/crates/libs/spaced/guest/server/tests/it/process.rs +++ b/crates/libs/spaced/guest/server/tests/it/process.rs @@ -191,6 +191,7 @@ async fn bridge_guest_process_stalled_stream_does_not_block_reconnect_exit() { process_id: started.process_id, timeout: None, output_cursor: None, + resume_from_oldest_retained: false, })) .await .expect("connect response") @@ -244,10 +245,23 @@ async fn bridge_guest_process_connect_replays_from_output_cursor() { process_id: started.process_id, timeout: None, output_cursor: Some(0), + resume_from_oldest_retained: true, })) .await .expect("connect response") .into_inner(); + let first = connected + .next() + .await + .expect("connected event") + .expect("connected event response"); + assert_eq!( + first.event, + Some(Event::Connected(bridge_guest::ProcessConnected { + output_cursor: Some(0) + })), + "the stream reports where it starts" + ); let mut replayed = Vec::new(); tokio::time::timeout(std::time::Duration::from_secs(2), async { @@ -545,6 +559,7 @@ async fn bridge_guest_process_start_replays_output_while_cursorless_connect_retu process_id: started.process_id.clone(), timeout: None, output_cursor: None, + resume_from_oldest_retained: false, }) .await .expect("connect exited process") @@ -699,6 +714,7 @@ async fn bridge_guest_process_connect_and_signal_validation_work() { process_id: process_id.clone(), timeout: None, output_cursor: None, + resume_from_oldest_retained: false, }) .await .expect("connect process") @@ -798,6 +814,7 @@ async fn bridge_guest_process_connect_unknown_returns_not_found() { process_id: "missing".into(), timeout: None, output_cursor: None, + resume_from_oldest_retained: false, }) .await .expect_err("missing process rejected"); diff --git a/crates/libs/spaced/host/client/src/process.rs b/crates/libs/spaced/host/client/src/process.rs index 0061b3c8f4..ecfdac8d3b 100644 --- a/crates/libs/spaced/host/client/src/process.rs +++ b/crates/libs/spaced/host/client/src/process.rs @@ -12,11 +12,13 @@ pub(super) fn connect_process_request( process_id: &ProcessId, timeout: Option, output_cursor: Option, + resume_from_oldest_retained: bool, ) -> ConnectProcessRequest { ConnectProcessRequest { process_id: process_id.as_str().to_string(), timeout: timeout.map(proto_duration), output_cursor, + resume_from_oldest_retained, } } @@ -89,6 +91,7 @@ impl SpacedHostClient { process_id: String, timeout: Option, output_cursor: Option, + resume_from_oldest_retained: bool, ) -> Result, tonic::Status> { let process_id = ProcessId::parse(process_id).map_err(invalid_argument_status)?; let timeout = timeout @@ -108,6 +111,7 @@ impl SpacedHostClient { &process_id, timeout, output_cursor, + resume_from_oldest_retained, )) .await .map(Response::into_inner) diff --git a/crates/libs/spaced/host/client/src/tests.rs b/crates/libs/spaced/host/client/src/tests.rs index ee6f0f0a7b..879dbe7284 100644 --- a/crates/libs/spaced/host/client/src/tests.rs +++ b/crates/libs/spaced/host/client/src/tests.rs @@ -121,10 +121,11 @@ impl RejectingControlService { fn connect_process_request_preserves_output_cursor() { let process_id = ProcessId::new(); - let request = connect_process_request(&process_id, None, Some(7)); + let request = connect_process_request(&process_id, None, Some(7), true); assert_eq!(request.process_id, process_id.as_str()); assert_eq!(request.output_cursor, Some(7)); + assert!(request.resume_from_oldest_retained); } #[tokio::test] diff --git a/crates/libs/spaced/host/server/tests/it/process.rs b/crates/libs/spaced/host/server/tests/it/process.rs index 6859dc1f2e..a350950384 100644 --- a/crates/libs/spaced/host/server/tests/it/process.rs +++ b/crates/libs/spaced/host/server/tests/it/process.rs @@ -233,7 +233,7 @@ async fn process_connect_and_kill_relay_to_guest_bridge() { let client = fixture.client().await; let mut events = client - .connect_process_stream_with_cursor("proc-1".into(), None, None) + .connect_process_stream_with_cursor("proc-1".into(), None, None, false) .await .expect("connect response"); assert_eq!( diff --git a/crates/libs/spaced/host/server/tests/support/mock_bridge_guest.rs b/crates/libs/spaced/host/server/tests/support/mock_bridge_guest.rs index 10de0568e1..5d76e542ed 100644 --- a/crates/libs/spaced/host/server/tests/support/mock_bridge_guest.rs +++ b/crates/libs/spaced/host/server/tests/support/mock_bridge_guest.rs @@ -447,7 +447,9 @@ impl Process for MockBridgeGuest { pid: Some(123), event: Some( pplx_space_spaced_common::proto::bridge_guest::process_event::Event::Connected( - pplx_space_spaced_common::proto::bridge_guest::ProcessConnected {}, + pplx_space_spaced_common::proto::bridge_guest::ProcessConnected { + output_cursor: None, + }, ), ), })]; diff --git a/proto/spaced/operations/v1/operations.proto b/proto/spaced/operations/v1/operations.proto index 7326a8b433..a89e66b8d8 100644 --- a/proto/spaced/operations/v1/operations.proto +++ b/proto/spaced/operations/v1/operations.proto @@ -240,6 +240,12 @@ message ConnectProcessRequest { // Older guests end a lagged stream with RESOURCE_EXHAUSTED and no reason, // and wait for output to reach a cursor past it. optional uint64 output_cursor = 3; + // With `output_cursor` before the oldest output the guest still retains, + // start the stream at that oldest output instead of ending it as lagged. + // The guest chooses the start under the lock that also gives the stream + // its reader position, so output written meanwhile cannot evict it, and + // reports it in `ProcessConnected.output_cursor`. Older guests ignore it. + bool resume_from_oldest_retained = 4; } message ListProcessesRequest {} message ProcessInfo { @@ -287,7 +293,13 @@ message ProcessEvent { } } message ProcessStarted { optional google.protobuf.Timestamp start_time = 1; } -message ProcessConnected {} +message ProcessConnected { + // The journal cursor of the first output this stream sends, when the + // request set `output_cursor`: that cursor, or a later one when + // `resume_from_oldest_retained` moved the start past evicted output. + // Unset by older guests. + optional uint64 output_cursor = 1; +} message ProcessOutput { bytes data = 1; } message ProcessExit { oneof outcome { diff --git a/shim/proto/spaced/operations/v1/operations.pb.go b/shim/proto/spaced/operations/v1/operations.pb.go index 01fcd1685a..9e86f5df4a 100644 --- a/shim/proto/spaced/operations/v1/operations.pb.go +++ b/shim/proto/spaced/operations/v1/operations.pb.go @@ -2589,9 +2589,15 @@ type ConnectProcessRequest struct { // // Older guests end a lagged stream with RESOURCE_EXHAUSTED and no reason, // and wait for output to reach a cursor past it. - OutputCursor *uint64 `protobuf:"varint,3,opt,name=output_cursor,json=outputCursor,proto3,oneof" json:"output_cursor,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + OutputCursor *uint64 `protobuf:"varint,3,opt,name=output_cursor,json=outputCursor,proto3,oneof" json:"output_cursor,omitempty"` + // With `output_cursor` before the oldest output the guest still retains, + // start the stream at that oldest output instead of ending it as lagged. + // The guest chooses the start under the lock that also gives the stream + // its reader position, so output written meanwhile cannot evict it, and + // reports it in `ProcessConnected.output_cursor`. Older guests ignore it. + ResumeFromOldestRetained bool `protobuf:"varint,4,opt,name=resume_from_oldest_retained,json=resumeFromOldestRetained,proto3" json:"resume_from_oldest_retained,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConnectProcessRequest) Reset() { @@ -2645,6 +2651,13 @@ func (x *ConnectProcessRequest) GetOutputCursor() uint64 { return 0 } +func (x *ConnectProcessRequest) GetResumeFromOldestRetained() bool { + if x != nil { + return x.ResumeFromOldestRetained + } + return false +} + type ListProcessesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -3399,7 +3412,12 @@ func (x *ProcessStarted) GetStartTime() *timestamppb.Timestamp { } type ProcessConnected struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + // The journal cursor of the first output this stream sends, when the + // request set `output_cursor`: that cursor, or a later one when + // `resume_from_oldest_retained` moved the start past evicted output. + // Unset by older guests. + OutputCursor *uint64 `protobuf:"varint,1,opt,name=output_cursor,json=outputCursor,proto3,oneof" json:"output_cursor,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3434,6 +3452,13 @@ func (*ProcessConnected) Descriptor() ([]byte, []int) { return file_spaced_operations_v1_operations_proto_rawDescGZIP(), []int{53} } +func (x *ProcessConnected) GetOutputCursor() uint64 { + if x != nil && x.OutputCursor != nil { + return *x.OutputCursor + } + return 0 +} + type ProcessOutput struct { state protoimpl.MessageState `protogen:"open.v1"` Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` @@ -4164,12 +4189,13 @@ const file_spaced_operations_v1_operations_proto_rawDesc = "" + "\x04rows\x18\x01 \x01(\rR\x04rows\x12\x12\n" + "\x04cols\x18\x02 \x01(\rR\x04cols\"D\n" + "\tPtyConfig\x127\n" + - "\x04size\x18\x01 \x01(\v2#.space.spaced.operations.v1.PtySizeR\x04size\"\xb8\x01\n" + + "\x04size\x18\x01 \x01(\v2#.space.spaced.operations.v1.PtySizeR\x04size\"\xf7\x01\n" + "\x15ConnectProcessRequest\x12\x1d\n" + "\n" + "process_id\x18\x01 \x01(\tR\tprocessId\x128\n" + "\atimeout\x18\x02 \x01(\v2\x19.google.protobuf.DurationH\x00R\atimeout\x88\x01\x01\x12(\n" + - "\routput_cursor\x18\x03 \x01(\x04H\x01R\foutputCursor\x88\x01\x01B\n" + + "\routput_cursor\x18\x03 \x01(\x04H\x01R\foutputCursor\x88\x01\x01\x12=\n" + + "\x1bresume_from_oldest_retained\x18\x04 \x01(\bR\x18resumeFromOldestRetainedB\n" + "\n" + "\b_timeoutB\x10\n" + "\x0e_output_cursor\"\x16\n" + @@ -4228,8 +4254,10 @@ const file_spaced_operations_v1_operations_proto_rawDesc = "" + "\x0eProcessStarted\x12>\n" + "\n" + "start_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\tstartTime\x88\x01\x01B\r\n" + - "\v_start_time\"\x12\n" + - "\x10ProcessConnected\"#\n" + + "\v_start_time\"N\n" + + "\x10ProcessConnected\x12(\n" + + "\routput_cursor\x18\x01 \x01(\x04H\x00R\foutputCursor\x88\x01\x01B\x10\n" + + "\x0e_output_cursor\"#\n" + "\rProcessOutput\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data\"\xbc\x02\n" + "\vProcessExit\x12C\n" + @@ -4457,6 +4485,7 @@ func file_spaced_operations_v1_operations_proto_init() { (*ProcessEvent_Exit)(nil), } file_spaced_operations_v1_operations_proto_msgTypes[52].OneofWrappers = []any{} + file_spaced_operations_v1_operations_proto_msgTypes[53].OneofWrappers = []any{} file_spaced_operations_v1_operations_proto_msgTypes[55].OneofWrappers = []any{ (*ProcessExit_Exited)(nil), (*ProcessExit_Signaled)(nil), From 55d1ba61a96cbebe4c7b9ee716c5ac47b263487b Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Thu, 22 Oct 2026 18:46:37 +0000 Subject: [PATCH 09/12] data plane: give process SSE ids positions for 2^33 - 1 outputs Only output and resync_required carry ids, and every gap either moves the position or drops it, so a position never has more than two ids: the unsent start and the resync_required that begins a replay for an id without a position. The 8-bit frame counter therefore cost the position field 6 bits for nothing. Two frame bits leave ids a position for a process's first 2^33 - 1 output chunks (about 99 days at 1,000 writes a second) instead of 2^27 - 1 (about 37 hours). The blocks without a position still hold 2^35 ids, bit 52 stays the tag, and the 2^53 bound holds. Decided before the first release because the split cannot change after it without a new tag scheme. The OpenAPI text, first-sandbox.md and troubleshooting.md publish the new limit, and the example id in the process stream schema is the new encoding of position 1. The comments on the id layout now state its finite headroom: a stream has at least 2^34 ids after the block it starts in, and would wrap past them. --- crates/libs/api/src/routes/public_docs.rs | 2 +- .../src/routes/public_docs/schema_contract.rs | 2 +- .../libs/data/plane/server/src/native/sse.rs | 22 ++++++++------ .../data/plane/server/src/native/sse/tests.rs | 29 +++++++++++++++++++ docs/first-sandbox.md | 2 +- docs/troubleshooting.md | 2 +- 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index b36a8fe56b..bbe5b2c7c1 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- SSE `id`s increase. Every file-watch frame after `attached` has one. On a process stream only `stdout`, `stderr` and `resync_required` frames do; other frames carry none, so the last `id` received stays the one to resume from.\n- Echo the `id` exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- Process stream ids carry the output position for a process's first 2^27 - 1 output chunks (about 134 million; some 37 hours at 1,000 separate writes a second). After that, each resume replays the output still retained behind `resync_required`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- SSE `id`s increase. Every file-watch frame after `attached` has one. On a process stream only `stdout`, `stderr` and `resync_required` frames do; other frames carry none, so the last `id` received stays the one to resume from.\n- Echo the `id` exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- Process stream ids carry the output position for a process's first 2^33 - 1 output chunks (about 8.6 billion; some 99 days at 1,000 separate writes a second). After that, each resume replays the output still retained behind `resync_required`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/api/src/routes/public_docs/schema_contract.rs b/crates/libs/api/src/routes/public_docs/schema_contract.rs index 212043e437..4b4fd6093d 100644 --- a/crates/libs/api/src/routes/public_docs/schema_contract.rs +++ b/crates/libs/api/src/routes/public_docs/schema_contract.rs @@ -88,7 +88,7 @@ fn openapi_identifier(value: &str) -> String { pub(super) fn insert_command_event_stream_schema(spec: &mut Value) { let response = json!({ - "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4503599644147712\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", + "description": "### Read the stream\n\n- Each SSE event carries one `DataPlaneServerFrame` JSON object. Ignore comment heartbeats.\n- Decode stdout and stderr's `data_base64` using standard base64.\n- Success requires `process_exited` with exit code zero and no error or missing output. HTTP 200 or a closed stream alone is not success.\n\n### Example output\n\nExcerpt, not a complete stream:\n\n```text\nid: 4503599627632640\nevent: stdout\ndata: {\"type\":\"stdout\",\"seq\":1,\"data_base64\":\"SGVsbG8gZnJvbSBTUEFDRQo=\"}\n\nevent: process_exited\ndata: {\"type\":\"process_exited\",\"process_id\":\"proc_00000000-0000-0000-0000-000000000000\",\"exit_code\":0,\"status\":\"exited\"}\n\n```", "content": { "text/event-stream": { "schema": { "type": "string" }, diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index 33c9ad30bc..ae9a48b14d 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -310,11 +310,13 @@ pub(crate) enum ProcessEventId { const PROCESS_EVENT_ID_TAG_BIT: u32 = 52; const PROCESS_EVENT_ID_TAG: u64 = 1 << PROCESS_EVENT_ID_TAG_BIT; const PROCESS_EVENT_ID_GUARD_BITS: u32 = 16; -/// Identified frames at one position: output moves it, so only -/// `resync_required` frames share one (see [`process_frame_has_event_id`]). -/// The rest of the 36 bits below the tag go to positions: ids carry one for a -/// process's first 2^27 - 1 outputs. -const PROCESS_EVENT_ID_FRAME_BITS: u32 = 8; +/// Identified frames at one position: output moves it, and so does every gap +/// that keeps it, so a position has at most two ids, the unsent +/// [`ProcessEventId::START`] and the `resync_required` that starts a replay +/// for an id without a position (see [`process_frame_has_event_id`]). The +/// rest of the 36 bits below the tag go to positions: ids carry one for a +/// process's first 2^33 - 1 outputs. +const PROCESS_EVENT_ID_FRAME_BITS: u32 = 2; const PROCESS_EVENT_ID_FIELD_SHIFT: u32 = PROCESS_EVENT_ID_GUARD_BITS + PROCESS_EVENT_ID_FRAME_BITS; const PROCESS_EVENT_ID_FIELD_BITS: u32 = PROCESS_EVENT_ID_TAG_BIT - PROCESS_EVENT_ID_FIELD_SHIFT; const PROCESS_EVENT_ID_FIELD_MASK: u64 = (1 << PROCESS_EVENT_ID_FIELD_BITS) - 1; @@ -322,7 +324,7 @@ const PROCESS_EVENT_ID_MAX_FRAME: u64 = (1 << PROCESS_EVENT_ID_FRAME_BITS) - 1; const PROCESS_EVENT_ID_UNKNOWN: u64 = 1 << (PROCESS_EVENT_ID_FIELD_BITS - 1); const PROCESS_EVENT_ID_MAX_POSITION: u64 = PROCESS_EVENT_ID_UNKNOWN - 1; /// The highest block a resumed stream starts in: half the blocks, so every -/// stream has at least 2^(field + frame - 2) ids left to send. +/// stream has at least 2^(field + frame - 2) = 2^34 ids left to send. const PROCESS_EVENT_ID_MAX_RESUME_BLOCK: u64 = 1 << (PROCESS_EVENT_ID_FIELD_BITS - 2); const _: () = { @@ -330,7 +332,8 @@ const _: () = { // and an id, whose bits all lie at or below the tag, is below 2^53. assert!(PROCESS_EVENT_ID_FIELD_BITS > 2); assert!(PROCESS_EVENT_ID_TAG_BIT < 53); - // A stream that starts in the highest resume block cannot run out. + // A stream that starts in the highest resume block has blocks left. They + // are finite: one that sent 2^34 ids without a position would wrap. assert!(PROCESS_EVENT_ID_MAX_RESUME_BLOCK < PROCESS_EVENT_ID_MAX_POSITION); }; @@ -406,7 +409,8 @@ impl ProcessEventId { } /// The id of the next frame, once the client has received output up to - /// `position`. Always greater than this one. + /// `position`. Greater than this one while the blocks without a position + /// last: a stream has at least 2^34 ids after the block it starts in. fn next(self, position: Option) -> Self { match self { Self::Position { @@ -433,7 +437,7 @@ impl ProcessEventId { }, // A resumed stream starts at most at block // `PROCESS_EVENT_ID_MAX_RESUME_BLOCK` (`first_after`), and would - // need 2^34 frames to exhaust the rest. + // need 2^34 frames to exhaust the rest; past them this wraps. Self::Unknown { block, .. } => Self::Unknown { block: block + 1, frame: 0, diff --git a/crates/libs/data/plane/server/src/native/sse/tests.rs b/crates/libs/data/plane/server/src/native/sse/tests.rs index 7046fb6391..2358941afe 100644 --- a/crates/libs/data/plane/server/src/native/sse/tests.rs +++ b/crates/libs/data/plane/server/src/native/sse/tests.rs @@ -213,3 +213,32 @@ fn process_event_ids_increase_and_carry_the_output_position() { assert_eq!((id as f64).to_string(), id.to_string()); } } + +/// Ids carry the output position for a process's first 2^33 - 1 outputs, as +/// the API docs publish, and none after; the id the docs show for the first +/// output decodes to it. +#[test] +fn process_event_ids_carry_positions_up_to_the_published_limit() { + let last = (1 << 33) - 1; + let id = ProcessEventId::START.next(Some(last)); + assert_eq!( + id, + ProcessEventId::Position { + position: last, + frame: 0 + } + ); + assert_eq!(ProcessEventId::decode(id.encode()), Some(id)); + assert!(matches!( + id.next(Some(last + 1)), + ProcessEventId::Unknown { .. } + )); + + assert_eq!( + ProcessEventId::decode(4_503_599_627_632_640), + Some(ProcessEventId::Position { + position: 1, + frame: 0 + }) + ); +} diff --git a/docs/first-sandbox.md b/docs/first-sandbox.md index 59d7c2889f..a1ae6f1584 100644 --- a/docs/first-sandbox.md +++ b/docs/first-sandbox.md @@ -85,7 +85,7 @@ Read `/openapi-m1.json` for exact request constraints and supported fields. This The example displays decoded stdout and stderr and saves the raw event stream in its run directory. Handle binary output separately rather than displaying it in a terminal. -Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID`, set to the last `id` received, exactly as received (only output and `resync_required` events carry one), resumes with the output after that event while it is retained, and otherwise produces `resync_required` first. After a process's first 2^27 - 1 output chunks, ids no longer carry its output position, and each reconnect replays the retained output behind `resync_required`. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. +Command output is a live stream, not durable log storage. The sandbox retains a bounded amount of each process's recent output: reconnecting with `Last-Event-ID`, set to the last `id` received, exactly as received (only output and `resync_required` events carry one), resumes with the output after that event while it is retained, and otherwise produces `resync_required` first. After a process's first 2^33 - 1 output chunks, ids no longer carry its output position, and each reconnect replays the retained output behind `resync_required`. Keep the transcript when a stream fails; repeat a command only if repeating its effects is safe. ## Inspect and manage the sandbox diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index da15d31594..ae18c3eb14 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -78,7 +78,7 @@ This prints sandbox listings and the saved creation/deletion operations, sandbox | Stream authentication fails | Use `attach.token`, not `SPACE_API_TOKEN`, and the correct session URL. Check expiry and attach promptly after creating the command. | | Example rejects an endpoint, events URL, or redirect | Use HTTPS except for loopback HTTP. Check the data-plane origin and session ID; correct the configuration rather than removing destination or redirect checks. | | HTTP stream succeeds but output is incomplete | Check for `process_exited`, its exit result, and any `error` or `resync_required` frame. A closed connection is not proof of successful execution. | -| Reconnect reports `resync_required` | The stream could not resume exactly: output from before the reconnect may be missing or repeated. Check that the client sends the last `id` it received exactly as received. A process that has written more than 2^27 - 1 output chunks always resumes this way. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | +| Reconnect reports `resync_required` | The stream could not resume exactly: output from before the reconnect may be missing or repeated. Check that the client sends the last `id` it received exactly as received. A process that has written more than 2^33 - 1 output chunks always resumes this way. Inspect the saved event transcript and query the process only if it is still running. Rerun only if repeating the command's effects is safe. | | Command exits nonzero | Read stderr and the exit result from the saved event transcript. Check that the executable and working directory exist inside the sandbox, not just on the host. | | Single-process GET returns `not_found` | This endpoint inspects a running process, not historical command results. It returns `not_found` before attachment and after exit. | From 1347f59715a29fad64a0efbd8a0dc664a1db5de0 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Thu, 22 Oct 2026 18:46:42 +0000 Subject: [PATCH 10/12] data plane: stop asserting on metrics in process SSE tests The repository's test rules rule out unit tests that assert on metrics or their tags. The 400 test keeps its status and no-backend-open checks without the attach-result tag, and the 409 test, which only checked that no replay-miss counter was recorded, is gone. --- .../data/plane/server/src/native/tests.rs | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/crates/libs/data/plane/server/src/native/tests.rs b/crates/libs/data/plane/server/src/native/tests.rs index 7bdebab5e3..ace909a146 100644 --- a/crates/libs/data/plane/server/src/native/tests.rs +++ b/crates/libs/data/plane/server/src/native/tests.rs @@ -2806,7 +2806,6 @@ async fn last_event_id_without_room_after_it_is_rejected() { .unwrap(), ); let token = format!("{DATA_PLANE_TOKEN_PREFIX}{}", "c".repeat(64)); - let capture = pplx_space_metrics::test_capture::capture_counters_guard(); let response = tower::ServiceExt::oneshot( router(state), Request::get(format!("/v1/sessions/{session_id}/events")) @@ -2820,62 +2819,4 @@ async fn last_event_id_without_room_after_it_is_rejected() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert!(fake.opens.lock().unwrap().is_empty()); - assert!( - capture - .finish() - .iter() - .any(|counter| counter.name == "data_plane.attach" - && counter - .tags - .contains(&"result:invalid_last_event_id".to_string())), - "the refused attach is counted" - ); -} - -/// An attach refused because the session is already attached sends nothing, -/// so it counts no `resync_required` either. -#[tokio::test] -async fn refused_attach_counts_no_replay_miss() { - if !binding_fake_test_process() { - return; - } - let session_id = DataPlaneSessionId::new_for_kind(SessionKind::Process); - let fake = spawn_journal_fake( - session_id, - true, - Arc::new(|_, open| journal_replay(&JOURNAL, open)), - ) - .await; - let mut state = crate::tests::test_data_plane_state().await; - state.cluster_api = ClusterApiClients::service_only( - pplx_space_node_api_client::cluster::connect(fake.endpoint.clone(), "test") - .await - .unwrap(), - ); - let _attached = state - .native_sessions - .try_acquire(session_id, SessionKind::Process) - .unwrap(); - let token = format!("{DATA_PLANE_TOKEN_PREFIX}{}", "c".repeat(64)); - let capture = pplx_space_metrics::test_capture::capture_counters_guard(); - let response = tower::ServiceExt::oneshot( - router(state), - Request::get(format!("/v1/sessions/{session_id}/events")) - .header(header::AUTHORIZATION, format!("Bearer {token}")) - // Untagged, so a stream would open behind `resync_required`. - .header("last-event-id", "41") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::CONFLICT); - let counters = capture.finish(); - assert!( - !counters - .iter() - .any(|counter| counter.name == "data_plane.replay.miss"), - "{counters:?}" - ); } From 4428439cf4236c43304c81eda62153c5b6d97f31 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Thu, 22 Oct 2026 18:46:49 +0000 Subject: [PATCH 11/12] docs: ids sent after evicted output resume exactly After an eviction gap a process stream keeps its position, at the oldest output retained, so a resume from the cursor_out_of_window frame's id, or from any later one, continues without a resync. The OpenAPI text still listed "one sent after cursor_out_of_window" among ids that do not resume exactly, which also contradicted the bullet before it. Only ids sent after the stream replayed the output from its start behind cursor_out_of_window (a Last-Event-ID past the output, another stream's id on the starting attach) or after it continued with live output lose the position. The doc comment on which frames carry ids made the same claim and now says the same. --- crates/libs/api/src/routes/public_docs.rs | 2 +- crates/libs/data/plane/server/src/native/sse.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/libs/api/src/routes/public_docs.rs b/crates/libs/api/src/routes/public_docs.rs index bbe5b2c7c1..167f642dcc 100644 --- a/crates/libs/api/src/routes/public_docs.rs +++ b/crates/libs/api/src/routes/public_docs.rs @@ -71,7 +71,7 @@ fn public_openapi_value() -> Value { let data_plane_events_item = json!({ "get": { "summary": "Stream process output or file-watch events over SSE", - "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- SSE `id`s increase. Every file-watch frame after `attached` has one. On a process stream only `stdout`, `stderr` and `resync_required` frames do; other frames carry none, so the last `id` received stays the one to resume from.\n- Echo the `id` exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one sent after `cursor_out_of_window`, or one altered in transit) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- Process stream ids carry the output position for a process's first 2^33 - 1 output chunks (about 8.6 billion; some 99 days at 1,000 separate writes a second). After that, each resume replays the output still retained behind `resync_required`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", + "description": "Streams output from process or file-watch sessions.\n\n### Connect\n\n- Use the session token, not your management API token. Send it in `Authorization: Bearer ...` or `Sec-WebSocket-Protocol`; query-string tokens are rejected.\n- SSE `id`s increase. Every file-watch frame after `attached` has one. On a process stream only `stdout`, `stderr` and `resync_required` frames do; other frames carry none, so the last `id` received stays the one to resume from.\n- Echo the `id` exactly as received: process stream ids are not consecutive. They stay below 2^53, so a JavaScript `Number` holds them exactly.\n\n### Reconnect\n\n- Send the last received `id` as `Last-Event-ID`. An `id` is only valid for the session whose stream sent it.\n- A process stream resumes with the output after that frame while the sandbox retains it, a bounded amount per process. Without `Last-Event-ID` it starts from the beginning of the output, which can repeat output already received.\n- When the output to resume from is no longer retained, the stream sends `resync_required` with reason `cursor_out_of_window`, then continues with the oldest output still retained (with live output on sandboxes from older templates). When `Last-Event-ID` is past the process's output, it sends the same frame and replays the output from its start. Keep output already received.\n- An `id` that does not resume exactly (one from an earlier SPACE version, one altered in transit, or one sent after the stream replayed the output from its start behind `cursor_out_of_window` or continued with live output) replays the retained output behind `resync_required` with reason `cursor_replay_unavailable`.\n- Every `id` a resumed process stream sends is above `Last-Event-ID`.\n- Process stream ids carry the output position for a process's first 2^33 - 1 output chunks (about 8.6 billion; some 99 days at 1,000 separate writes a second). After that, each resume replays the output still retained behind `resync_required`.\n- If the process exited while disconnected, reconnecting still reports `process_exited`.\n- A file-watch stream resumes live: its first identified frame is `resync_required`, with reason `cursor_replay_unavailable` and id `Last-Event-ID + 1`. Re-list the watched path before using new events.", "responses": { "200": { "description": "Server-sent event stream using DataPlaneServerFrame JSON payloads" }, "401": { "description": "Invalid or expired data-plane token" }, diff --git a/crates/libs/data/plane/server/src/native/sse.rs b/crates/libs/data/plane/server/src/native/sse.rs index ae9a48b14d..358d833964 100644 --- a/crates/libs/data/plane/server/src/native/sse.rs +++ b/crates/libs/data/plane/server/src/native/sse.rs @@ -931,12 +931,15 @@ async fn next_process_sse_frame(state: &mut ProcessSseState) -> Option { /// Whether a process stream frame carries an SSE id: only one that moves the /// client's position does. That is output, and `resync_required`, which -/// acknowledges a gap, so a resume from it continues past the gap without -/// reporting it again; a gap moves the position, so it never repeats at one. -/// Every other frame (`process_started`, `process_exited`, a backend -/// reconnect's, `error`, `closed`) leaves the client's last id unchanged, so -/// resumes and reconnects against an idle process spend no frames of the -/// [`ProcessEventId`] at its position. +/// acknowledges a gap. After evicted output the stream keeps its position, at +/// the oldest output retained, so a resume from that id continues past the +/// gap without reporting it again. After a restart from the start of the +/// output (a cursor past it, a foreign id) or a move to the live tail, the +/// ids carry no position, and a resume from them replays behind +/// `resync_required`. Every other frame (`process_started`, `process_exited`, +/// a backend reconnect's, `error`, `closed`) leaves the client's last id +/// unchanged, so resumes and reconnects against an idle process spend no +/// frames of the [`ProcessEventId`] at its position. fn process_frame_has_event_id(frame: &DataPlaneServerFrame) -> bool { process_frame_advances_backend_cursor(frame) || matches!(frame, DataPlaneServerFrame::ResyncRequired { .. }) From 6e686977c43a4133986a33dad47a7cbc4ba5bc44 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Thu, 22 Oct 2026 18:46:54 +0000 Subject: [PATCH 12/12] smoke: check which REST stream frames carry ids The REST smoke checked only that ids increase. It now also checks that only stdout, stderr and resync_required frames carry one, on both connections. The check that the resumed stream's ids follow Last-Event-ID is gone: the resumed stream of an exited process sends no ids, so it could not fail. The no-resync and no-duplicate checks on the resume stay. --- scripts/tests/smoke-spaced-rest.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/tests/smoke-spaced-rest.sh b/scripts/tests/smoke-spaced-rest.sh index 833c750091..0f10734b98 100755 --- a/scripts/tests/smoke-spaced-rest.sh +++ b/scripts/tests/smoke-spaced-rest.sh @@ -587,6 +587,7 @@ let status = null; let attached = false; let lastEventId = null; let eventIdsMonotonic = true; +let eventIdsOnOutputOnly = true; const timeout = setTimeout(() => { console.error('timed out waiting for command event stream'); @@ -619,7 +620,8 @@ function parseEvents(buffer) { } // Open one SSE connection and feed events to `onEvent` until it returns true. -// Every identified frame must carry a greater id than the one before. +// Every identified frame must carry a greater id than the one before, and +// only output and resync_required frames may carry one. async function readStream(resumeFrom, onEvent) { const headers = { Accept: 'text/event-stream', Authorization: `Bearer ${command.attach.token}` }; if (resumeFrom !== null) headers['Last-Event-ID'] = resumeFrom; @@ -642,6 +644,8 @@ async function readStream(resumeFrom, onEvent) { lastEventId = event.id; } if (event.event !== null && event.event !== event.data.type) eventIdsMonotonic = false; + const identified = ['stdout', 'stderr', 'resync_required'].includes(event.data.type); + if ((event.id !== null) !== identified) eventIdsOnOutputOnly = false; if (onEvent(event)) { await reader.cancel().catch(() => {}); return true; @@ -687,14 +691,12 @@ async function readStream(resumeFrom, onEvent) { // replaying output already received or asking to resync, and the exited // process still reports its exit. const resumeFrom = lastEventId; - const resume = { attached_first: null, ids_continue: true, resync: null, stdout: '', exit_code: null }; + const resume = { attached_first: null, resync: null, stdout: '', exit_code: null }; let seen = 0; await readStream(resumeFrom, (event) => { const frame = event.data; seen += 1; if (seen === 1) resume.attached_first = frame.type === 'attached' && event.id === null; - // Only output and resync_required carry ids; any sent follow Last-Event-ID. - if (event.id !== null && BigInt(event.id) <= BigInt(resumeFrom)) resume.ids_continue = false; if (frame.type === 'resync_required' && resume.resync === null) resume.resync = frame.reason; if (frame.type === 'stdout') resume.stdout += Buffer.from(frame.data_base64, 'base64').toString('utf8'); if (frame.type === 'process_exited') { @@ -715,6 +717,7 @@ async function readStream(resumeFrom, onEvent) { exit_code: exitCode, status, event_ids_monotonic: eventIdsMonotonic, + event_ids_on_output_only: eventIdsOnOutputOnly, resume_from: resumeFrom, resume, })); @@ -842,10 +845,10 @@ assert_eq "$(jq -r '.stdout' <<<"$command_result")" "$RUN_ID" "command stdout" assert_eq "$(jq -r '.stderr' <<<"$command_result")" "command-err" "command stderr" assert_eq "$(jq -r '.exit_code' <<<"$command_result")" "6" "command exit code" assert_eq "$(jq -r '.event_ids_monotonic' <<<"$command_result")" "true" "command stream ids increase" +assert_eq "$(jq -r '.event_ids_on_output_only' <<<"$command_result")" "true" "only output and resync_required carry ids" assert_eq "$(jq -r '.resume.attached_first' <<<"$command_result")" "true" "resumed stream starts with attached" assert_eq "$(jq -r '.resume.resync' <<<"$command_result")" "null" "lossless resume does not resync" assert_eq "$(jq -r '.resume.stdout' <<<"$command_result")" "" "resume does not replay received output" -assert_eq "$(jq -r '.resume.ids_continue' <<<"$command_result")" "true" "resumed ids continue after Last-Event-ID" assert_eq "$(jq -r '.resume.exit_code' <<<"$command_result")" "6" "resumed stream reports the process exit" log "one-shot command event stream ok (Last-Event-ID resume verified)"