workspace: remove observation cursor surface
This commit is contained in:
@@ -270,12 +270,10 @@ fn backend_command_from_method(method: &Method) -> BackendCommand {
|
||||
}
|
||||
|
||||
async fn observe_worker_events(target: BackendRuntimeTarget, tx: mpsc::UnboundedSender<Event>) {
|
||||
let mut cursor: Option<String> = None;
|
||||
let mut last_sequence = 0_u64;
|
||||
let mut attempts = 0_usize;
|
||||
|
||||
loop {
|
||||
let url = observation_ws_url(&target, cursor.as_deref());
|
||||
let url = observation_ws_url(&target);
|
||||
match connect_async(&url).await {
|
||||
Ok((mut ws, _)) => {
|
||||
attempts = 0;
|
||||
@@ -295,19 +293,6 @@ async fn observe_worker_events(target: BackendRuntimeTarget, tx: mpsc::Unbounded
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
if let Some(sequence) = decode_backend_cursor(&envelope.cursor)
|
||||
{
|
||||
if sequence <= last_sequence {
|
||||
continue;
|
||||
}
|
||||
last_sequence = sequence;
|
||||
} else {
|
||||
let _ = tx.send(diagnostic_event(format!(
|
||||
"Backend observation cursor was malformed: {}",
|
||||
envelope.cursor
|
||||
)));
|
||||
}
|
||||
cursor = Some(envelope.cursor.clone());
|
||||
let _ = tx.send(envelope.payload);
|
||||
}
|
||||
Ok(ClientWorkerEventWsFrame::Diagnostic { diagnostic }) => {
|
||||
@@ -316,11 +301,6 @@ async fn observe_worker_events(target: BackendRuntimeTarget, tx: mpsc::Unbounded
|
||||
diagnostic.code, diagnostic.message
|
||||
);
|
||||
let _ = tx.send(diagnostic_event(message));
|
||||
if diagnostic.code == "backend.cursor_unknown_or_expired" {
|
||||
cursor = None;
|
||||
last_sequence = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = tx.send(diagnostic_event(format!(
|
||||
@@ -395,18 +375,13 @@ fn validate_target(target: &BackendRuntimeTarget) -> Result<(), BackendRuntimeCl
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn observation_ws_url(target: &BackendRuntimeTarget, cursor: Option<&str>) -> String {
|
||||
fn observation_ws_url(target: &BackendRuntimeTarget) -> String {
|
||||
let path = format!(
|
||||
"/api/runtimes/{}/workers/{}/events/ws",
|
||||
path_segment_encode(&target.runtime_id),
|
||||
path_segment_encode(&target.worker_id)
|
||||
);
|
||||
let mut url = join_base_and_path(&http_base_to_ws(&target.base_url), &path);
|
||||
if let Some(cursor) = cursor {
|
||||
url.push_str("?cursor=");
|
||||
url.push_str(&query_value_encode(cursor));
|
||||
}
|
||||
url
|
||||
join_base_and_path(&http_base_to_ws(&target.base_url), &path)
|
||||
}
|
||||
|
||||
fn http_base_to_ws(base: &str) -> String {
|
||||
@@ -423,26 +398,12 @@ fn join_base_and_path(base: &str, path: &str) -> String {
|
||||
format!("{}{}", base.trim_end_matches('/'), path)
|
||||
}
|
||||
|
||||
fn decode_backend_cursor(cursor: &str) -> Option<u64> {
|
||||
let encoded = cursor.strip_prefix("bo_")?;
|
||||
if encoded.len() != 16 {
|
||||
return None;
|
||||
}
|
||||
u64::from_str_radix(encoded, 16).ok()
|
||||
}
|
||||
|
||||
fn path_segment_encode(input: &str) -> String {
|
||||
percent_encode(input, |byte| {
|
||||
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
|
||||
})
|
||||
}
|
||||
|
||||
fn query_value_encode(input: &str) -> String {
|
||||
percent_encode(input, |byte| {
|
||||
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
|
||||
})
|
||||
}
|
||||
|
||||
fn percent_encode(input: &str, keep: impl Fn(u8) -> bool) -> String {
|
||||
let mut encoded = String::with_capacity(input.len());
|
||||
for byte in input.bytes() {
|
||||
@@ -507,7 +468,6 @@ enum ClientWorkerEventWsFrame {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ClientWorkerEventWsEnvelope {
|
||||
cursor: String,
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
payload: Event,
|
||||
@@ -547,8 +507,8 @@ mod tests {
|
||||
let target =
|
||||
BackendRuntimeTarget::new("http://127.0.0.1:8787/", "runtime/one", "worker one");
|
||||
assert_eq!(
|
||||
observation_ws_url(&target, Some("bo_0000000000000001")),
|
||||
"ws://127.0.0.1:8787/api/runtimes/runtime%2Fone/workers/worker%20one/events/ws?cursor=bo_0000000000000001"
|
||||
observation_ws_url(&target),
|
||||
"ws://127.0.0.1:8787/api/runtimes/runtime%2Fone/workers/worker%20one/events/ws"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use worker_runtime::identity::WorkerRef;
|
||||
use worker_runtime::observation::{WorkerObservationCursor, WorkerObservationEvent};
|
||||
@@ -100,7 +100,7 @@ impl std::fmt::Debug for RuntimeObservationSource {
|
||||
pub struct RuntimeObservationUpstreamEvent {
|
||||
pub runtime_id: String,
|
||||
pub worker_id: String,
|
||||
pub runtime_cursor: String,
|
||||
pub runtime_event_id: String,
|
||||
pub payload: protocol::Event,
|
||||
}
|
||||
|
||||
@@ -116,11 +116,10 @@ pub enum ClientWorkerEventWsFrame {
|
||||
},
|
||||
}
|
||||
|
||||
/// Backend-owned opaque event envelope. It intentionally omits Runtime endpoints,
|
||||
/// Backend-owned event envelope. It intentionally omits Runtime endpoints,
|
||||
/// credentials, sockets and session paths.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ClientWorkerEventWsEnvelope {
|
||||
pub cursor: String,
|
||||
pub event_id: String,
|
||||
pub runtime_id: String,
|
||||
pub worker_id: String,
|
||||
@@ -134,17 +133,10 @@ pub struct ClientWorkerEventWsDiagnostic {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct ClientWorkerEventsWsQuery {
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ObservationProxyError {
|
||||
RuntimeUnavailable(String),
|
||||
WorkerNotFound(String),
|
||||
CursorMalformed(String),
|
||||
CursorUnknownOrExpired(String),
|
||||
UpstreamDisconnect(String),
|
||||
MalformedFrame(String),
|
||||
ObservationOnly,
|
||||
@@ -155,8 +147,6 @@ impl ObservationProxyError {
|
||||
match self {
|
||||
ObservationProxyError::RuntimeUnavailable(_) => "backend.runtime_unavailable",
|
||||
ObservationProxyError::WorkerNotFound(_) => "backend.worker_not_found",
|
||||
ObservationProxyError::CursorMalformed(_) => "backend.cursor_malformed",
|
||||
ObservationProxyError::CursorUnknownOrExpired(_) => "backend.cursor_unknown_or_expired",
|
||||
ObservationProxyError::UpstreamDisconnect(_) => "backend.upstream_disconnect",
|
||||
ObservationProxyError::MalformedFrame(_) => "backend.malformed_frame",
|
||||
ObservationProxyError::ObservationOnly => "backend.observation_only",
|
||||
@@ -167,8 +157,6 @@ impl ObservationProxyError {
|
||||
match self {
|
||||
ObservationProxyError::RuntimeUnavailable(message)
|
||||
| ObservationProxyError::WorkerNotFound(message)
|
||||
| ObservationProxyError::CursorMalformed(message)
|
||||
| ObservationProxyError::CursorUnknownOrExpired(message)
|
||||
| ObservationProxyError::UpstreamDisconnect(message)
|
||||
| ObservationProxyError::MalformedFrame(message) => message,
|
||||
ObservationProxyError::ObservationOnly => {
|
||||
@@ -193,46 +181,6 @@ impl ClientWorkerEventWsFrame {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct BackendObservationCursor {
|
||||
pub sequence: u64,
|
||||
}
|
||||
|
||||
impl BackendObservationCursor {
|
||||
pub fn new(sequence: u64) -> Self {
|
||||
Self { sequence }
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self { sequence: 0 }
|
||||
}
|
||||
|
||||
pub fn encode(self) -> String {
|
||||
format!("bo_{:016x}", self.sequence)
|
||||
}
|
||||
|
||||
pub fn decode(value: &str) -> Option<Self> {
|
||||
let encoded = value.strip_prefix("bo_")?;
|
||||
if encoded.len() != 16 {
|
||||
return None;
|
||||
}
|
||||
u64::from_str_radix(encoded, 16)
|
||||
.ok()
|
||||
.map(|sequence| Self { sequence })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct BackendObservationState {
|
||||
next_sequence: u64,
|
||||
}
|
||||
|
||||
impl BackendObservationState {
|
||||
fn new() -> Self {
|
||||
Self { next_sequence: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct ObservationKey {
|
||||
runtime_id: String,
|
||||
@@ -243,14 +191,12 @@ struct ObservationKey {
|
||||
#[derive(Clone)]
|
||||
pub struct BackendObservationProxy {
|
||||
sources: Arc<BTreeMap<ObservationKey, RuntimeObservationSourceConfig>>,
|
||||
state: Arc<Mutex<BackendObservationState>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BackendObservationProxy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BackendObservationProxy")
|
||||
.field("source_count", &self.sources.len())
|
||||
.field("state", &"<omitted>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -271,7 +217,6 @@ impl BackendObservationProxy {
|
||||
.collect();
|
||||
Self {
|
||||
sources: Arc::new(sources),
|
||||
state: Arc::new(Mutex::new(BackendObservationState::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,41 +239,13 @@ impl BackendObservationProxy {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
&self,
|
||||
_runtime_id: &str,
|
||||
_worker_id: &str,
|
||||
cursor: Option<&str>,
|
||||
) -> Result<(), ObservationProxyError> {
|
||||
if let Some(raw) = cursor {
|
||||
BackendObservationCursor::decode(raw).ok_or_else(|| {
|
||||
ObservationProxyError::CursorMalformed(format!(
|
||||
"malformed backend observation cursor: {raw}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store(
|
||||
&self,
|
||||
event: RuntimeObservationUpstreamEvent,
|
||||
) -> Result<ClientWorkerEventWsEnvelope, ObservationProxyError> {
|
||||
let mut state = self.state.lock().map_err(|_| {
|
||||
ObservationProxyError::RuntimeUnavailable(
|
||||
"backend observation state lock poisoned".into(),
|
||||
)
|
||||
})?;
|
||||
let sequence = state.next_sequence;
|
||||
state.next_sequence += 1;
|
||||
let cursor = BackendObservationCursor::new(sequence).encode();
|
||||
Ok(ClientWorkerEventWsEnvelope {
|
||||
cursor: cursor.clone(),
|
||||
event_id: cursor,
|
||||
pub fn map_event(&self, event: RuntimeObservationUpstreamEvent) -> ClientWorkerEventWsEnvelope {
|
||||
ClientWorkerEventWsEnvelope {
|
||||
event_id: event.runtime_event_id,
|
||||
runtime_id: event.runtime_id,
|
||||
worker_id: event.worker_id,
|
||||
payload: event.payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,11 +256,6 @@ fn map_runtime_connect_error(error: TungsteniteError) -> ObservationProxyError {
|
||||
"runtime worker observation endpoint returned 404 not found".into(),
|
||||
)
|
||||
}
|
||||
TungsteniteError::Http(response) if response.status() == StatusCode::BAD_REQUEST => {
|
||||
ObservationProxyError::CursorMalformed(
|
||||
"runtime worker observation endpoint rejected the request as malformed".into(),
|
||||
)
|
||||
}
|
||||
TungsteniteError::Http(response) => ObservationProxyError::RuntimeUnavailable(format!(
|
||||
"runtime worker observation endpoint rejected WebSocket upgrade with status {}",
|
||||
response.status()
|
||||
@@ -357,10 +269,9 @@ fn map_runtime_connect_error(error: TungsteniteError) -> ObservationProxyError {
|
||||
fn map_runtime_diagnostic(code: String, message: String) -> ObservationProxyError {
|
||||
match code.as_str() {
|
||||
"runtime.worker_not_found" => ObservationProxyError::WorkerNotFound(message),
|
||||
"runtime.cursor_malformed" => ObservationProxyError::CursorMalformed(message),
|
||||
"runtime.cursor_unknown_or_expired" | "runtime.cursor_expired" => {
|
||||
ObservationProxyError::CursorUnknownOrExpired(message)
|
||||
}
|
||||
"runtime.cursor_malformed"
|
||||
| "runtime.cursor_unknown_or_expired"
|
||||
| "runtime.cursor_expired" => ObservationProxyError::RuntimeUnavailable(message),
|
||||
"runtime.unavailable" => ObservationProxyError::RuntimeUnavailable(message),
|
||||
"runtime.upstream_closed" | "runtime.websocket_error" => {
|
||||
ObservationProxyError::UpstreamDisconnect(message)
|
||||
@@ -384,15 +295,8 @@ pub struct RuntimeWsObservationClient {
|
||||
impl RuntimeWsObservationClient {
|
||||
pub async fn connect(
|
||||
source: &RuntimeObservationSourceConfig,
|
||||
runtime_cursor: Option<&str>,
|
||||
) -> Result<Self, ObservationProxyError> {
|
||||
let mut endpoint = source.endpoint.clone();
|
||||
if let Some(cursor) = runtime_cursor {
|
||||
let separator = if endpoint.contains('?') { '&' } else { '?' };
|
||||
endpoint.push(separator);
|
||||
endpoint.push_str("cursor=");
|
||||
endpoint.push_str(cursor);
|
||||
}
|
||||
let endpoint = source.endpoint.clone();
|
||||
let mut request = endpoint.into_client_request().map_err(|error| {
|
||||
ObservationProxyError::RuntimeUnavailable(format!(
|
||||
"failed to build runtime WebSocket request: {error}"
|
||||
@@ -481,7 +385,7 @@ impl RuntimeWsObservationClient {
|
||||
RuntimeObservationUpstreamEvent {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
worker_id: self.worker_id.clone(),
|
||||
runtime_cursor: envelope.cursor,
|
||||
runtime_event_id: envelope.event_id,
|
||||
payload: envelope.payload,
|
||||
}
|
||||
}
|
||||
@@ -493,18 +397,15 @@ pub enum RuntimeObservationClient {
|
||||
}
|
||||
|
||||
impl RuntimeObservationClient {
|
||||
pub async fn connect(
|
||||
source: &RuntimeObservationSource,
|
||||
runtime_cursor: Option<&str>,
|
||||
) -> Result<Self, ObservationProxyError> {
|
||||
pub async fn connect(source: &RuntimeObservationSource) -> Result<Self, ObservationProxyError> {
|
||||
match source {
|
||||
RuntimeObservationSource::RemoteWs(config) => {
|
||||
RuntimeWsObservationClient::connect(config, runtime_cursor)
|
||||
RuntimeWsObservationClient::connect(config)
|
||||
.await
|
||||
.map(Self::RemoteWs)
|
||||
}
|
||||
RuntimeObservationSource::Embedded(source) => {
|
||||
EmbeddedObservationClient::connect(source, runtime_cursor).map(Self::Embedded)
|
||||
EmbeddedObservationClient::connect(source).map(Self::Embedded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,26 +430,16 @@ pub struct EmbeddedObservationClient {
|
||||
}
|
||||
|
||||
impl EmbeddedObservationClient {
|
||||
fn connect(
|
||||
source: &EmbeddedRuntimeObservationSource,
|
||||
runtime_cursor: Option<&str>,
|
||||
) -> Result<Self, ObservationProxyError> {
|
||||
let cursor = match runtime_cursor {
|
||||
Some(raw) => WorkerObservationCursor::decode(raw).ok_or_else(|| {
|
||||
ObservationProxyError::CursorMalformed(
|
||||
"embedded runtime cursor is malformed".into(),
|
||||
)
|
||||
})?,
|
||||
None => source
|
||||
.runtime
|
||||
.worker_observation_cursor_now(&source.worker_ref)
|
||||
.map_err(|err| {
|
||||
ObservationProxyError::WorkerNotFound(format!(
|
||||
"embedded Worker '{}' is not observable: {err}",
|
||||
source.worker_id
|
||||
))
|
||||
})?,
|
||||
};
|
||||
fn connect(source: &EmbeddedRuntimeObservationSource) -> Result<Self, ObservationProxyError> {
|
||||
let cursor = source
|
||||
.runtime
|
||||
.worker_observation_cursor_now(&source.worker_ref)
|
||||
.map_err(|err| {
|
||||
ObservationProxyError::WorkerNotFound(format!(
|
||||
"embedded Worker '{}' is not observable: {err}",
|
||||
source.worker_id
|
||||
))
|
||||
})?;
|
||||
let receiver = source
|
||||
.runtime
|
||||
.subscribe_worker_observation()
|
||||
@@ -559,29 +450,27 @@ impl EmbeddedObservationClient {
|
||||
))
|
||||
})?;
|
||||
let mut queued = VecDeque::new();
|
||||
if runtime_cursor.is_none() {
|
||||
let snapshot = source
|
||||
.runtime
|
||||
.worker_observation_snapshot(&source.worker_ref)
|
||||
.map_err(|err| {
|
||||
ObservationProxyError::WorkerNotFound(format!(
|
||||
"embedded Worker '{}' snapshot is unavailable: {err}",
|
||||
source.worker_id
|
||||
))
|
||||
})?;
|
||||
queued.push_back(RuntimeObservationUpstreamEvent {
|
||||
runtime_id: source.runtime_id.clone(),
|
||||
worker_id: source.worker_id.clone(),
|
||||
runtime_cursor: cursor.encode(),
|
||||
payload: snapshot,
|
||||
});
|
||||
}
|
||||
let snapshot = source
|
||||
.runtime
|
||||
.worker_observation_snapshot(&source.worker_ref)
|
||||
.map_err(|err| {
|
||||
ObservationProxyError::WorkerNotFound(format!(
|
||||
"embedded Worker '{}' snapshot is unavailable: {err}",
|
||||
source.worker_id
|
||||
))
|
||||
})?;
|
||||
queued.push_back(RuntimeObservationUpstreamEvent {
|
||||
runtime_id: source.runtime_id.clone(),
|
||||
worker_id: source.worker_id.clone(),
|
||||
runtime_event_id: "snapshot".to_string(),
|
||||
payload: snapshot,
|
||||
});
|
||||
for event in source
|
||||
.runtime
|
||||
.read_worker_observation_events(&source.worker_ref, cursor)
|
||||
.map_err(|err| {
|
||||
ObservationProxyError::CursorUnknownOrExpired(format!(
|
||||
"embedded Worker '{}' cursor is unavailable: {err}",
|
||||
ObservationProxyError::RuntimeUnavailable(format!(
|
||||
"embedded Worker '{}' observation cursor is unavailable: {err}",
|
||||
source.worker_id
|
||||
))
|
||||
})?
|
||||
@@ -606,9 +495,6 @@ impl EmbeddedObservationClient {
|
||||
&mut self,
|
||||
) -> Result<RuntimeObservationUpstreamEvent, ObservationProxyError> {
|
||||
if let Some(event) = self.queued.pop_front() {
|
||||
if let Some(cursor) = WorkerObservationCursor::decode(&event.runtime_cursor) {
|
||||
self.cursor = cursor;
|
||||
}
|
||||
return Ok(event);
|
||||
}
|
||||
loop {
|
||||
@@ -619,7 +505,7 @@ impl EmbeddedObservationClient {
|
||||
{
|
||||
self.cursor =
|
||||
WorkerObservationCursor::decode(&event.cursor).ok_or_else(|| {
|
||||
ObservationProxyError::CursorMalformed(
|
||||
ObservationProxyError::RuntimeUnavailable(
|
||||
"embedded runtime emitted a malformed cursor".into(),
|
||||
)
|
||||
})?;
|
||||
@@ -627,7 +513,7 @@ impl EmbeddedObservationClient {
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
return Err(ObservationProxyError::CursorUnknownOrExpired(
|
||||
return Err(ObservationProxyError::RuntimeUnavailable(
|
||||
"embedded runtime observation backlog was exceeded".into(),
|
||||
));
|
||||
}
|
||||
@@ -648,7 +534,7 @@ impl EmbeddedObservationClient {
|
||||
RuntimeObservationUpstreamEvent {
|
||||
runtime_id: runtime_id.to_string(),
|
||||
worker_id: worker_id.to_string(),
|
||||
runtime_cursor: event.cursor,
|
||||
runtime_event_id: event.cursor.clone(),
|
||||
payload: event.payload,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ use crate::hosts::{
|
||||
};
|
||||
use crate::identity::WorkspaceIdentity;
|
||||
use crate::observation::{
|
||||
BackendObservationProxy, ClientWorkerEventWsFrame, ClientWorkerEventsWsQuery,
|
||||
ObservationProxyError, RuntimeObservationClient, RuntimeObservationSourceConfig,
|
||||
BackendObservationProxy, ClientWorkerEventWsFrame, ObservationProxyError,
|
||||
RuntimeObservationClient, RuntimeObservationSourceConfig,
|
||||
};
|
||||
use crate::profile_settings::{
|
||||
CreateWorkspaceProfileSourceRequest, DeleteWorkspaceProfileSourceRequest,
|
||||
@@ -2119,19 +2119,13 @@ async fn scoped_worker_observation_ws(
|
||||
ws: WebSocketUpgrade,
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
|
||||
Query(query): Query<ClientWorkerEventsWsQuery>,
|
||||
) -> Response {
|
||||
if let Err(err) = validate_workspace_scope(&api, &path.workspace_id) {
|
||||
return err.into_response();
|
||||
}
|
||||
worker_observation_ws(
|
||||
State(api),
|
||||
AxumPath((path.runtime_id, path.worker_id)),
|
||||
Query(query),
|
||||
ws,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
worker_observation_ws(State(api), AxumPath((path.runtime_id, path.worker_id)), ws)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn scoped_list_host_workers(
|
||||
@@ -2946,17 +2940,16 @@ async fn cancel_runtime_worker(
|
||||
async fn worker_observation_ws(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
|
||||
Query(query): Query<ClientWorkerEventsWsQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
match api.observation_proxy.source(&runtime_id, &worker_id) {
|
||||
Ok(source) => ws.on_upgrade(move |socket| {
|
||||
worker_observation_ws_session(api.observation_proxy, source, query, socket)
|
||||
worker_observation_ws_session(api.observation_proxy, source, socket)
|
||||
}),
|
||||
Err(ObservationProxyError::WorkerNotFound(_)) => {
|
||||
match api.runtime.observation_source(&runtime_id, &worker_id) {
|
||||
Ok(source) => ws.on_upgrade(move |socket| {
|
||||
worker_observation_ws_session(api.observation_proxy, source, query, socket)
|
||||
worker_observation_ws_session(api.observation_proxy, source, socket)
|
||||
}),
|
||||
Err(error) => ApiError::from(error.into_error()).into_response(),
|
||||
}
|
||||
@@ -2978,20 +2971,9 @@ async fn worker_observation_ws(
|
||||
async fn worker_observation_ws_session(
|
||||
proxy: BackendObservationProxy,
|
||||
source: crate::observation::RuntimeObservationSource,
|
||||
query: ClientWorkerEventsWsQuery,
|
||||
mut socket: WebSocket,
|
||||
) {
|
||||
if let Err(error) = proxy.open(
|
||||
source.runtime_id(),
|
||||
source.worker_id(),
|
||||
query.cursor.as_deref(),
|
||||
) {
|
||||
let _ =
|
||||
send_client_ws_frame(&mut socket, ClientWorkerEventWsFrame::diagnostic(error)).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut upstream = match RuntimeObservationClient::connect(&source, None).await {
|
||||
let mut upstream = match RuntimeObservationClient::connect(&source).await {
|
||||
Ok(client) => client,
|
||||
Err(error) => {
|
||||
let _ = send_client_ws_frame(&mut socket, ClientWorkerEventWsFrame::diagnostic(error))
|
||||
@@ -3033,14 +3015,9 @@ async fn worker_observation_ws_session(
|
||||
}
|
||||
upstream_event = upstream.next_event() => {
|
||||
match upstream_event {
|
||||
Ok(event) => match proxy.store(event) {
|
||||
Ok(envelope) => {
|
||||
if !send_client_ws_frame(&mut socket, ClientWorkerEventWsFrame::event(envelope)).await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = send_client_ws_frame(&mut socket, ClientWorkerEventWsFrame::diagnostic(error)).await;
|
||||
Ok(event) => {
|
||||
let envelope = proxy.map_event(event);
|
||||
if !send_client_ws_frame(&mut socket, ClientWorkerEventWsFrame::event(envelope)).await {
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -7266,38 +7243,13 @@ mod tests {
|
||||
ClientWorkerEventWsFrame::Event { envelope } if matches!(envelope.payload, protocol::Event::TextDone { .. })
|
||||
));
|
||||
|
||||
let (mut resumed, _) = connect_async(format!("{url}?cursor={}", live.cursor))
|
||||
.await
|
||||
.unwrap();
|
||||
let _snapshot = next_client_frame(&mut resumed).await;
|
||||
runtime
|
||||
.observe_worker_event(
|
||||
&worker_ref,
|
||||
protocol::Event::TextDone {
|
||||
text: "done".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let resumed_event = next_client_frame(&mut resumed).await;
|
||||
let ClientWorkerEventWsFrame::Event {
|
||||
envelope: resumed_event,
|
||||
} = resumed_event
|
||||
else {
|
||||
panic!("expected resumed live event");
|
||||
};
|
||||
assert_ne!(resumed_event.cursor, live.cursor);
|
||||
let (mut query_stream, _) = connect_async(format!("{url}?cursor=bad")).await.unwrap();
|
||||
let query_snapshot = next_client_frame(&mut query_stream).await;
|
||||
assert!(matches!(
|
||||
resumed_event.payload,
|
||||
protocol::Event::TextDone { .. }
|
||||
query_snapshot,
|
||||
ClientWorkerEventWsFrame::Event { envelope } if matches!(envelope.payload, protocol::Event::Snapshot { .. })
|
||||
));
|
||||
|
||||
let (mut malformed, _) = connect_async(format!("{url}?cursor=bad")).await.unwrap();
|
||||
let diagnostic = next_client_frame(&mut malformed).await;
|
||||
let ClientWorkerEventWsFrame::Diagnostic { diagnostic } = diagnostic else {
|
||||
panic!("expected malformed cursor diagnostic");
|
||||
};
|
||||
assert_eq!(diagnostic.code, "backend.cursor_malformed");
|
||||
|
||||
stream.send(Message::Text("{}".into())).await.unwrap();
|
||||
let mut saw_observation_only = false;
|
||||
for _ in 0..3 {
|
||||
@@ -7312,41 +7264,6 @@ mod tests {
|
||||
assert!(saw_observation_only, "expected observation-only diagnostic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proxy_does_not_validate_backend_cursor_against_replay_history() {
|
||||
let source = RuntimeObservationSourceConfig {
|
||||
runtime_id: "runtime-a".into(),
|
||||
worker_id: "worker-a".into(),
|
||||
endpoint: "ws://127.0.0.1:9/not-used".into(),
|
||||
bearer_token: None,
|
||||
};
|
||||
let (url, _dir) = spawn_workspace_proxy(source).await;
|
||||
let (mut stream, _) = connect_async(format!("{url}?cursor=bo_ffffffffffffffff"))
|
||||
.await
|
||||
.unwrap();
|
||||
let diagnostic = next_client_diagnostic(&mut stream).await;
|
||||
assert_eq!(diagnostic.code, "backend.runtime_unavailable");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proxy_maps_runtime_cursor_diagnostic_to_typed_backend_diagnostic() {
|
||||
let (_runtime, _worker_ref, endpoint) = spawn_runtime_worker().await;
|
||||
let source = RuntimeObservationSourceConfig {
|
||||
runtime_id: "runtime-a".into(),
|
||||
worker_id: "worker-a".into(),
|
||||
endpoint: format!("{endpoint}?cursor=wo_ffffffffffffffff"),
|
||||
bearer_token: None,
|
||||
};
|
||||
let (url, _dir) = spawn_workspace_proxy(source).await;
|
||||
let (mut stream, _) = connect_async(&url).await.unwrap();
|
||||
assert!(matches!(
|
||||
next_client_frame(&mut stream).await,
|
||||
ClientWorkerEventWsFrame::Event { envelope } if matches!(envelope.payload, protocol::Event::Snapshot { .. })
|
||||
));
|
||||
let diagnostic = next_client_diagnostic(&mut stream).await;
|
||||
assert_eq!(diagnostic.code, "backend.cursor_unknown_or_expired");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proxy_maps_runtime_worker_not_found_http_404_to_typed_backend_diagnostic() {
|
||||
let (_runtime, _worker_ref, endpoint) = spawn_runtime_worker().await;
|
||||
|
||||
Reference in New Issue
Block a user