diff --git a/engine/artifacts/config-schema.json b/engine/artifacts/config-schema.json index 5b7f92ffe2..ded32d15e7 100644 --- a/engine/artifacts/config-schema.json +++ b/engine/artifacts/config-schema.json @@ -1004,6 +1004,15 @@ "format": "uint64", "minimum": 0.0 }, + "gateway_response_chunk_idle_timeout_ms": { + "description": "Timeout between streaming HTTP response chunks in milliseconds.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "gateway_response_start_timeout_ms": { "description": "Timeout for response to start in milliseconds.", "type": [ @@ -1508,4 +1517,4 @@ "additionalProperties": false } } -} \ No newline at end of file +} diff --git a/engine/packages/config/src/config/pegboard.rs b/engine/packages/config/src/config/pegboard.rs index 616af06a36..7f7767c4d1 100644 --- a/engine/packages/config/src/config/pegboard.rs +++ b/engine/packages/config/src/config/pegboard.rs @@ -105,6 +105,8 @@ pub struct Pegboard { pub gateway_websocket_open_timeout_ms: Option, /// Timeout for response to start in milliseconds. pub gateway_response_start_timeout_ms: Option, + /// Timeout between streaming HTTP response chunks in milliseconds. + pub gateway_response_chunk_idle_timeout_ms: Option, /// Ping interval for gateway updates in milliseconds. pub gateway_update_ping_interval_ms: Option, /// GC interval for in-flight requests in milliseconds. @@ -280,6 +282,10 @@ impl Pegboard { .unwrap_or(5 * 60 * 1000) } + pub fn gateway_response_chunk_idle_timeout_ms(&self) -> u64 { + self.gateway_response_chunk_idle_timeout_ms.unwrap_or(30_000) + } + pub fn gateway_update_ping_interval_ms(&self) -> u64 { self.gateway_update_ping_interval_ms.unwrap_or(3_000) } diff --git a/engine/packages/guard-core/src/custom_serve.rs b/engine/packages/guard-core/src/custom_serve.rs index 6dac8ce2d1..71951b197c 100644 --- a/engine/packages/guard-core/src/custom_serve.rs +++ b/engine/packages/guard-core/src/custom_serve.rs @@ -2,7 +2,7 @@ use anyhow::{Result, bail}; use async_trait::async_trait; use bytes::Bytes; use http_body_util::Full; -use hyper::{Request, Response}; +use hyper::{Request, Response, body::Incoming as BodyIncoming}; use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame; use crate::WebSocketHandle; @@ -17,6 +17,12 @@ pub enum HibernationResult { /// Trait for custom request serving logic that can handle both HTTP and WebSocket requests #[async_trait] pub trait CustomServeTrait: Send + Sync { + /// Returns true when this service wants the original request body stream. + /// The default buffered path keeps retry semantics for existing custom routes. + fn streams_request_body(&self) -> bool { + false + } + /// Handle a regular HTTP request async fn handle_request( &self, @@ -24,6 +30,15 @@ pub trait CustomServeTrait: Send + Sync { req_ctx: &mut RequestContext, ) -> Result>; + /// Handle a regular HTTP request with the original inbound body stream. + async fn handle_streaming_request( + &self, + _req: Request, + _req_ctx: &mut RequestContext, + ) -> Result> { + bail!("service does not support streaming request bodies"); + } + /// Handle a WebSocket connection after upgrade. Supports connection retries. async fn handle_websocket( &self, diff --git a/engine/packages/guard-core/src/proxy_service.rs b/engine/packages/guard-core/src/proxy_service.rs index f54bfe8f64..f50ed3ee55 100644 --- a/engine/packages/guard-core/src/proxy_service.rs +++ b/engine/packages/guard-core/src/proxy_service.rs @@ -926,6 +926,10 @@ impl ProxyService { .build()); } ResolveRouteOutput::CustomServe(mut handler) => { + if handler.streams_request_body() { + return handler.handle_streaming_request(req, req_ctx).await; + } + // Collect request body let (req_parts, body) = req.into_parts(); let req_body = diff --git a/engine/packages/guard-core/src/response_body.rs b/engine/packages/guard-core/src/response_body.rs index 417ddb983e..7b0b3dbedc 100644 --- a/engine/packages/guard-core/src/response_body.rs +++ b/engine/packages/guard-core/src/response_body.rs @@ -1,6 +1,9 @@ use bytes::Bytes; use http_body_util::Full; use hyper::body::Incoming as BodyIncoming; +use tokio::sync::mpsc; + +pub type ResponseBodyError = Box; /// Response body type that can handle both streaming and buffered responses #[derive(Debug)] @@ -9,11 +12,13 @@ pub enum ResponseBody { Full(Full), /// Streaming response body Incoming(BodyIncoming), + /// Channel-backed streaming response body + Channel(mpsc::Receiver>), } impl http_body::Body for ResponseBody { type Data = Bytes; - type Error = Box; + type Error = ResponseBodyError; fn poll_frame( self: std::pin::Pin<&mut Self>, @@ -46,6 +51,14 @@ impl http_body::Body for ResponseBody { std::task::Poll::Pending => std::task::Poll::Pending, } } + ResponseBody::Channel(rx) => match rx.poll_recv(cx) { + std::task::Poll::Ready(Some(Ok(bytes))) => { + std::task::Poll::Ready(Some(Ok(http_body::Frame::data(bytes)))) + } + std::task::Poll::Ready(Some(Err(err))) => std::task::Poll::Ready(Some(Err(err))), + std::task::Poll::Ready(None) => std::task::Poll::Ready(None), + std::task::Poll::Pending => std::task::Poll::Pending, + }, } } @@ -53,6 +66,7 @@ impl http_body::Body for ResponseBody { match self { ResponseBody::Full(body) => body.is_end_stream(), ResponseBody::Incoming(body) => body.is_end_stream(), + ResponseBody::Channel(rx) => rx.is_closed() && rx.is_empty(), } } @@ -60,6 +74,7 @@ impl http_body::Body for ResponseBody { match self { ResponseBody::Full(body) => body.size_hint(), ResponseBody::Incoming(body) => body.size_hint(), + ResponseBody::Channel(_) => http_body::SizeHint::default(), } } } diff --git a/engine/packages/guard-core/tests/response_body.rs b/engine/packages/guard-core/tests/response_body.rs new file mode 100644 index 0000000000..e15654e5b0 --- /dev/null +++ b/engine/packages/guard-core/tests/response_body.rs @@ -0,0 +1,30 @@ +use bytes::Bytes; +use http_body_util::BodyExt; +use rivet_guard_core::ResponseBody; +use tokio::sync::mpsc; + +#[tokio::test] +async fn channel_body_yields_sent_chunks() { + let (tx, rx) = mpsc::channel(2); + tx.send(Ok(Bytes::from_static(b"hello "))).await.unwrap(); + tx.send(Ok(Bytes::from_static(b"world"))).await.unwrap(); + drop(tx); + + let collected = ResponseBody::Channel(rx).collect().await.unwrap(); + + assert_eq!(collected.to_bytes(), Bytes::from_static(b"hello world")); +} + +#[tokio::test] +async fn channel_body_surfaces_errors() { + let (tx, rx) = mpsc::channel(1); + tx.send(Err(std::io::Error::other("stream failed").into())) + .await + .unwrap(); + drop(tx); + + let mut body = ResponseBody::Channel(rx); + let frame = body.frame().await.expect("expected frame"); + + assert!(frame.is_err()); +} diff --git a/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs b/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs index fc54b98b11..b91db26627 100644 --- a/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs +++ b/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs @@ -258,7 +258,7 @@ fn to_envoy_tunnel_message_kind_name(kind: &protocol::ToEnvoyTunnelMessageKind) match kind { protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(_) => "ToEnvoyRequestStart", protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(_) => "ToEnvoyRequestChunk", - protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort => "ToEnvoyRequestAbort", + protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_) => "ToEnvoyRequestAbort", protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(_) => "ToEnvoyWebSocketOpen", protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(_) => "ToEnvoyWebSocketMessage", protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(_) => "ToEnvoyWebSocketClose", @@ -272,7 +272,7 @@ fn to_envoy_tunnel_message_inner_data_len(kind: &protocol::ToEnvoyTunnelMessageK } protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(msg) => msg.body.len(), protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(msg) => msg.data.len(), - protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort + protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_) | protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(_) | protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(_) => 0, } diff --git a/engine/packages/pegboard-envoy/src/ws_to_tunnel_task.rs b/engine/packages/pegboard-envoy/src/ws_to_tunnel_task.rs index 1e3bedc558..a506c71133 100644 --- a/engine/packages/pegboard-envoy/src/ws_to_tunnel_task.rs +++ b/engine/packages/pegboard-envoy/src/ws_to_tunnel_task.rs @@ -1400,7 +1400,7 @@ fn tunnel_message_kind_name(kind: &protocol::ToRivetTunnelMessageKind) -> &'stat match kind { ToRivetTunnelMessageKind::ToRivetResponseStart(_) => "ToRivetResponseStart", ToRivetTunnelMessageKind::ToRivetResponseChunk(_) => "ToRivetResponseChunk", - ToRivetTunnelMessageKind::ToRivetResponseAbort => "ToRivetResponseAbort", + ToRivetTunnelMessageKind::ToRivetResponseAbort(_) => "ToRivetResponseAbort", ToRivetTunnelMessageKind::ToRivetWebSocketOpen(_) => "ToRivetWebSocketOpen", ToRivetTunnelMessageKind::ToRivetWebSocketMessage(_) => "ToRivetWebSocketMessage", ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(_) => "ToRivetWebSocketMessageAck", @@ -2067,7 +2067,7 @@ fn tunnel_message_inner_data_len(kind: &protocol::ToRivetTunnelMessageKind) -> u } ToRivetTunnelMessageKind::ToRivetResponseChunk(chunk) => chunk.body.len(), ToRivetTunnelMessageKind::ToRivetWebSocketMessage(msg) => msg.data.len(), - ToRivetTunnelMessageKind::ToRivetResponseAbort + ToRivetTunnelMessageKind::ToRivetResponseAbort(_) | ToRivetTunnelMessageKind::ToRivetWebSocketOpen(_) | ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(_) | ToRivetTunnelMessageKind::ToRivetWebSocketClose(_) => 0, diff --git a/engine/packages/pegboard-gateway2/src/hibernation_task.rs b/engine/packages/pegboard-gateway2/src/hibernation_task.rs index a2ce76dd31..6a3ad46a8f 100644 --- a/engine/packages/pegboard-gateway2/src/hibernation_task.rs +++ b/engine/packages/pegboard-gateway2/src/hibernation_task.rs @@ -15,7 +15,7 @@ use std::sync::{ use tokio::sync::{mpsc, watch}; use tokio_tungstenite::tungstenite::Message; -use crate::shared_state::{InFlightRequestHandle, MsgGcReason}; +use crate::shared_state::{InFlightRequestHandle, InFlightTunnelMessage, MsgGcReason}; use super::HibernationLifecycleResult; @@ -26,7 +26,7 @@ pub async fn task( in_flight_req: InFlightRequestHandle, ctx: StandaloneCtx, actor_id: Id, - mut msg_rx: mpsc::UnboundedReceiver, + mut msg_rx: mpsc::UnboundedReceiver, mut drop_rx: watch::Receiver>, egress_bytes: Arc, mut hibernation_abort_rx: watch::Receiver<()>, @@ -55,7 +55,7 @@ pub async fn task( tokio::select! { res = msg_rx.recv() => { if let Some(msg) = res { - match msg { + match msg.message_kind { protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(ws_msg) => { tracing::trace!( request_id=%protocol::util::id_to_string(&in_flight_req.request_id), diff --git a/engine/packages/pegboard-gateway2/src/lib.rs b/engine/packages/pegboard-gateway2/src/lib.rs index 4bd33f15cd..40a47830b3 100644 --- a/engine/packages/pegboard-gateway2/src/lib.rs +++ b/engine/packages/pegboard-gateway2/src/lib.rs @@ -1,9 +1,12 @@ -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; use bytes::Bytes; use gas::prelude::*; -use http_body_util::{BodyExt, Full}; -use hyper::{Request, Response, StatusCode, body::Body}; +use http_body_util::{BodyExt, Full, Limited}; +use hyper::{ + Method, Request, Response, StatusCode, + body::{Body, Incoming as BodyIncoming, SizeHint}, +}; use rivet_envoy_protocol as protocol; use rivet_error::*; use rivet_guard_core::{ @@ -23,12 +26,13 @@ use std::{ sync::{Arc, atomic::AtomicU64}, time::{Duration, Instant}, }; -use tokio::sync::watch; +use tokio::sync::{mpsc, watch}; use tokio_tungstenite::tungstenite::protocol::frame::{CloseFrame, coding::CloseCode}; use universaldb::utils::IsolationLevel::*; use crate::shared_state::{ - InFlightRequestCtx, RequestProtocol, RequestStopResult, SharedState, display_id, + InFlightRequestCtx, InFlightRequestHandle, InFlightTunnelMessage, MsgGcReason, + RequestProtocol, RequestStopResult, SharedState, display_id, }; mod hibernation_task; @@ -47,6 +51,61 @@ const PHASE_WAITING_FOR_RESPONSE_START: &str = "waiting_for_response_start"; const PHASE_PRE_WEBSOCKET_OPEN: &str = "pre_websocket_open"; const PHASE_WAITING_FOR_WEBSOCKET_OPEN: &str = "waiting_for_websocket_open"; const SLOW_WEBSOCKET_OPEN_WAIT_THRESHOLD: Duration = Duration::from_secs(1); +const HTTP_BODY_CHUNK_SIZE: usize = 64 * 1024; +const HTTP_RESPONSE_BODY_CHANNEL_CAPACITY: usize = 16; + +type ResponseBodyError = Box; + +fn should_stream_http_request_body(body_len: usize) -> bool { + body_len > HTTP_BODY_CHUNK_SIZE +} + +fn should_stream_http_request_body_hint(size_hint: &SizeHint) -> bool { + size_hint + .upper() + .map_or(true, |body_len| should_stream_http_request_body(body_len as usize)) +} + +fn advance_http_stream_message_index( + expected: protocol::MessageIndex, + actual: protocol::MessageIndex, +) -> std::result::Result { + if actual == expected { + Ok(expected.wrapping_add(1)) + } else { + Err(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_body_streams_only_after_one_chunk() { + assert!(!should_stream_http_request_body(0)); + assert!(!should_stream_http_request_body(HTTP_BODY_CHUNK_SIZE)); + assert!(should_stream_http_request_body(HTTP_BODY_CHUNK_SIZE + 1)); + } + + #[test] + fn response_stream_message_index_advances_and_wraps() { + assert_eq!(advance_http_stream_message_index(7, 7), Ok(8)); + assert_eq!( + advance_http_stream_message_index( + protocol::MessageIndex::MAX, + protocol::MessageIndex::MAX + ), + Ok(0) + ); + } + + #[test] + fn response_stream_message_index_rejects_gaps() { + assert_eq!(advance_http_stream_message_index(7, 8), Err(())); + assert_eq!(advance_http_stream_message_index(7, 6), Err(())); + } +} #[derive(RivetError, Serialize, Deserialize)] #[error( @@ -70,6 +129,312 @@ enum HibernationLifecycleResult { Aborted, } +fn response_body_error(message: impl Into) -> ResponseBodyError { + Box::new(std::io::Error::other(message.into())) +} + +fn http_abort_reason( + kind: protocol::HttpStreamAbortReasonKind, + detail: impl Into>, +) -> protocol::HttpStreamAbortReason { + protocol::HttpStreamAbortReason { + kind, + detail: detail.into(), + } +} + +fn abort_reason_message(reason: &protocol::HttpStreamAbortReason) -> String { + match &reason.detail { + Some(detail) => format!("{:?}: {detail}", reason.kind), + None => format!("{:?}", reason.kind), + } +} + +async fn send_http_body_error( + body_tx: &mpsc::Sender>, + message: impl Into, +) { + let _ = body_tx.send(Err(response_body_error(message))).await; +} + +async fn send_to_envoy_or_actor_stopped( + in_flight_req: &InFlightRequestHandle, + stopped_sub: &mut message::SubscriptionHandle, + actor_id: Id, + phase: &'static str, + message: protocol::ToEnvoyTunnelMessageKind, + ephemeral: bool, +) -> Result<()> { + tokio::select! { + biased; + _ = stopped_sub.next() => { + tracing::debug!("actor stopped while sending request"); + Err(ActorStoppedWhileWaiting { + actor_id: actor_id.to_string(), + phase: phase.to_owned(), + } + .build()) + } + res = in_flight_req.send_message(message, ephemeral) => res, + } +} + +async fn send_streaming_http_request_body_chunks( + in_flight_req: &InFlightRequestHandle, + stopped_sub: &mut message::SubscriptionHandle, + actor_id: Id, + mut body: B, +) -> Result<()> +where + B: Body + Unpin, + B::Error: std::fmt::Display, +{ + while let Some(frame) = body.frame().await { + let frame = match frame { + Ok(frame) => frame, + Err(error) => { + send_http_request_abort( + in_flight_req, + protocol::HttpStreamAbortReasonKind::ClientDisconnect, + Some(error.to_string()), + ) + .await; + return Err(anyhow!("failed to read streaming request body: {error}")); + } + }; + let Ok(data) = frame.into_data() else { + continue; + }; + + for chunk in data.chunks(HTTP_BODY_CHUNK_SIZE) { + let message = protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk( + protocol::ToEnvoyRequestChunk { + body: chunk.to_vec(), + finish: false, + }, + ); + send_to_envoy_or_actor_stopped( + in_flight_req, + stopped_sub, + actor_id, + PHASE_PRE_REQUEST, + message, + false, + ) + .await?; + } + } + + let message = protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk( + protocol::ToEnvoyRequestChunk { + body: Vec::new(), + finish: true, + }, + ); + send_to_envoy_or_actor_stopped( + in_flight_req, + stopped_sub, + actor_id, + PHASE_PRE_REQUEST, + message, + false, + ) + .await?; + + Ok(()) +} + +async fn send_http_request_abort( + in_flight_req: &InFlightRequestHandle, + kind: protocol::HttpStreamAbortReasonKind, + detail: impl Into>, +) { + let message = protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort( + protocol::ToEnvoyRequestAbort { + reason: http_abort_reason(kind, detail), + }, + ); + if let Err(err) = in_flight_req.send_message(message, true).await { + tracing::debug!(?err, "failed sending http request abort to envoy"); + } +} + +async fn handle_http_stream_abort( + in_flight_req: &InFlightRequestHandle, + body_tx: &mpsc::Sender>, + abort: protocol::ToRivetResponseAbort, +) { + let message = abort_reason_message(&abort.reason); + tracing::warn!( + reason_kind = ?abort.reason.kind, + reason_detail = ?abort.reason.detail, + "streaming http response aborted by envoy" + ); + send_http_body_error(body_tx, format!("response stream aborted: {message}")).await; + in_flight_req.stop(RequestStopResult::EnvoyError).await; +} + +async fn send_http_response_body_bytes( + in_flight_req: &InFlightRequestHandle, + body_tx: &mpsc::Sender>, + body: Vec, + detail: &'static str, +) -> bool { + if body.len() <= HTTP_BODY_CHUNK_SIZE { + if body_tx.send(Ok(Bytes::from(body))).await.is_ok() { + return true; + } + } else { + for chunk in body.chunks(HTTP_BODY_CHUNK_SIZE) { + if body_tx + .send(Ok(Bytes::copy_from_slice(chunk))) + .await + .is_err() + { + break; + } + } + } + + tracing::debug!("client dropped streaming http response body"); + send_http_request_abort( + in_flight_req, + protocol::HttpStreamAbortReasonKind::ClientDisconnect, + Some(detail.to_owned()), + ) + .await; + in_flight_req + .stop(RequestStopResult::ClientDisconnect) + .await; + false +} + +async fn drain_http_response_stream( + in_flight_req: InFlightRequestHandle, + mut msg_rx: mpsc::UnboundedReceiver, + mut drop_rx: watch::Receiver>, + mut stopped_sub: message::SubscriptionHandle, + body_tx: mpsc::Sender>, + initial_body: Option>, + mut expected_message_index: protocol::MessageIndex, + actor_id: Id, + idle_timeout: Duration, +) { + if let Some(body) = initial_body.filter(|body| !body.is_empty()) { + if !send_http_response_body_bytes( + &in_flight_req, + &body_tx, + body, + "client dropped response before initial body was sent", + ) + .await + { + return; + } + } + + loop { + tokio::select! { + res = msg_rx.recv() => { + let Some(msg) = res else { + tracing::warn!("streaming response tunnel channel closed"); + send_http_body_error(&body_tx, "response stream closed before finish").await; + in_flight_req.stop(RequestStopResult::EnvoyError).await; + return; + }; + + match advance_http_stream_message_index( + expected_message_index, + msg.message_id.message_index, + ) { + Ok(next_message_index) => expected_message_index = next_message_index, + Err(()) => { + tracing::warn!( + expected_message_index, + actual_message_index = msg.message_id.message_index, + "streaming response message index gap" + ); + send_http_request_abort( + &in_flight_req, + protocol::HttpStreamAbortReasonKind::InternalError, + Some("gateway detected response stream message index gap".to_owned()), + ) + .await; + send_http_body_error(&body_tx, "response stream message index gap").await; + in_flight_req.stop(RequestStopResult::EnvoyError).await; + return; + } + } + + match msg.message_kind { + protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk(chunk) => { + if !chunk.body.is_empty() && !send_http_response_body_bytes( + &in_flight_req, + &body_tx, + chunk.body, + "client dropped streaming response body", + ).await { + return; + } + + if chunk.finish { + in_flight_req.stop(RequestStopResult::Success).await; + return; + } + } + protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort(abort) => { + handle_http_stream_abort(&in_flight_req, &body_tx, abort).await; + return; + } + other => { + tracing::warn!( + message_kind = ?other, + "unexpected message while streaming http response" + ); + send_http_request_abort( + &in_flight_req, + protocol::HttpStreamAbortReasonKind::InternalError, + Some("gateway received unexpected response stream message".to_owned()), + ) + .await; + send_http_body_error(&body_tx, "unexpected response stream message").await; + in_flight_req.stop(RequestStopResult::EnvoyError).await; + return; + } + } + } + _ = drop_rx.changed() => { + let reason = format!("{:?}", drop_rx.borrow().as_ref()); + tracing::warn!(reason, "streaming response tunnel message timeout"); + send_http_body_error(&body_tx, format!("response stream garbage collected: {reason}")).await; + in_flight_req.stop(RequestStopResult::RequestTimeout).await; + return; + } + _ = stopped_sub.next() => { + tracing::debug!(%actor_id, "actor stopped while streaming response"); + send_http_body_error(&body_tx, "actor stopped while streaming response").await; + in_flight_req.stop(RequestStopResult::EnvoyError).await; + return; + } + _ = tokio::time::sleep(idle_timeout) => { + tracing::warn!( + timeout_ms = idle_timeout.as_millis() as u64, + "timed out waiting for streaming response chunk" + ); + send_http_request_abort( + &in_flight_req, + protocol::HttpStreamAbortReasonKind::IdleTimeout, + Some("gateway timed out waiting for response stream chunk".to_owned()), + ) + .await; + send_http_body_error(&body_tx, "response stream idle timeout").await; + in_flight_req.stop(RequestStopResult::RequestTimeout).await; + return; + } + } + } +} + pub struct PegboardGateway2 { ctx: StandaloneCtx, shared_state: SharedState, @@ -110,19 +475,27 @@ impl PegboardGateway2 { } impl PegboardGateway2 { - async fn handle_request_inner( + async fn handle_request_inner( &self, ctx: &StandaloneCtx, - req: Request>, + req: Request, req_ctx: &mut RequestContext, - ) -> Result> { + ) -> Result> + where + B: Body + Unpin, + B::Error: std::error::Error + Send + Sync + 'static, + { // Use the actor ID from the gateway instance let actor_id = self.actor_id.to_string(); let request_id = req_ctx.in_flight_request_id()?; // Extract request parts - let headers = req - .headers() + let request_body_size_hint = req.body().size_hint(); + let request_stream = !matches!(req_ctx.method(), &Method::GET | &Method::HEAD) + && should_stream_http_request_body_hint(&request_body_size_hint); + let (req_parts, body) = req.into_parts(); + let headers = req_parts + .headers .iter() .filter_map(|(name, value)| { value @@ -131,14 +504,18 @@ impl PegboardGateway2 { .map(|value_str| (name.to_string(), value_str.to_string())) }) .collect::>(); - - // NOTE: Size constraints have already been applied by guard - let body_bytes = req - .into_body() - .collect() - .await - .context("failed to read body")? - .to_bytes(); + let (body_bytes, streaming_body) = if request_stream { + (Bytes::new(), Some(body)) + } else { + ( + Limited::new(body, ctx.config().guard().http_max_request_body_size()) + .collect() + .await + .map_err(|error| anyhow!("failed to read body: {error}"))? + .to_bytes(), + None, + ) + }; let mut stopped_sub = ctx .subscribe::(("actor_id", self.actor_id)) @@ -208,27 +585,33 @@ impl PegboardGateway2 { method: req_ctx.method().to_string(), path: self.path.clone(), headers, - body: if body_bytes.is_empty() { + body: if body_bytes.is_empty() || request_stream { None } else { Some(body_bytes.to_vec()) }, - stream: false, + stream: request_stream, }, ); - tokio::select! { - // Prefer quick stop path - biased; - _ = stopped_sub.next() => { - tracing::debug!("actor stopped while sending request"); - return Err(ActorStoppedWhileWaiting { - actor_id: self.actor_id.to_string(), - phase: PHASE_PRE_REQUEST.to_owned(), - } - .build()); - } - res = in_flight_req.send_message(message, false) => res?, + send_to_envoy_or_actor_stopped( + &in_flight_req, + &mut stopped_sub, + self.actor_id, + PHASE_PRE_REQUEST, + message, + false, + ) + .await?; + + if let Some(body) = streaming_body { + send_streaming_http_request_body_chunks( + &in_flight_req, + &mut stopped_sub, + self.actor_id, + body, + ) + .await?; } // Wait for response @@ -238,14 +621,18 @@ impl PegboardGateway2 { tokio::select! { res = msg_rx.recv() => { if let Some(msg) = res { - match msg { + match msg.message_kind { protocol::ToRivetTunnelMessageKind::ToRivetResponseStart( response_start, ) => { - return anyhow::Ok(response_start); + return anyhow::Ok((msg.message_id, response_start)); } - protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort => { - tracing::warn!("request aborted"); + protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort(abort) => { + tracing::warn!( + reason_kind = ?abort.reason.kind, + reason_detail = ?abort.reason.detail, + "request aborted" + ); return Err(TunnelRequestAborted { phase: PHASE_WAITING_FOR_RESPONSE_START.to_owned(), } @@ -302,6 +689,7 @@ impl PegboardGateway2 { } .build() })??; + let (response_start_message_id, mut response_start) = response_start; tracing::debug!("response handler task ended"); // Build HTTP response @@ -313,12 +701,46 @@ impl PegboardGateway2 { response_builder = response_builder.header(key, value); } - // Add body - let body = response_start.body.unwrap_or_default(); - let response = - response_builder.body(ResponseBody::Full(Full::new(Bytes::from(body))))?; + let response = if response_start.stream { + let (body_tx, body_rx) = + mpsc::channel::>( + HTTP_RESPONSE_BODY_CHANNEL_CAPACITY, + ); + let idle_timeout_ms = self + .ctx + .config() + .pegboard() + .gateway_response_chunk_idle_timeout_ms() + .max(1); + let idle_timeout = Duration::from_millis(idle_timeout_ms); + let expected_message_index = + response_start_message_id.message_index.wrapping_add(1); + let initial_body = response_start.body.take(); + + tokio::spawn( + drain_http_response_stream( + in_flight_req.clone(), + msg_rx, + drop_rx, + stopped_sub, + body_tx, + initial_body, + expected_message_index, + self.actor_id, + idle_timeout, + ) + .in_current_span(), + ); + + response_builder.body(ResponseBody::Channel(body_rx))? + } else { + let body = response_start.body.unwrap_or_default(); + let response = + response_builder.body(ResponseBody::Full(Full::new(Bytes::from(body))))?; - in_flight_req.stop(RequestStopResult::Success).await; + in_flight_req.stop(RequestStopResult::Success).await; + response + }; Ok(response) } @@ -456,7 +878,7 @@ impl PegboardGateway2 { tokio::select! { res = msg_rx.recv() => { if let Some(msg) = res { - match msg { + match msg.message_kind { protocol::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(msg) => { tracing::trace!( actor_id = %self.actor_id, @@ -912,6 +1334,10 @@ impl PegboardGateway2 { #[async_trait] impl CustomServeTrait for PegboardGateway2 { + fn streams_request_body(&self) -> bool { + true + } + #[tracing::instrument(skip_all, fields(actor_id=?self.actor_id, actor_key=?self.actor_key, actor_generation=?self.actor_generation, namespace_id=?self.namespace_id, pool_name=%self.pool_name, envoy_key=%self.envoy_key))] async fn handle_request( &self, @@ -971,6 +1397,65 @@ impl CustomServeTrait for PegboardGateway2 { res } + #[tracing::instrument(skip_all, fields(actor_id=?self.actor_id, actor_key=?self.actor_key, actor_generation=?self.actor_generation, namespace_id=?self.namespace_id, pool_name=%self.pool_name, envoy_key=%self.envoy_key))] + async fn handle_streaming_request( + &self, + req: Request, + req_ctx: &mut RequestContext, + ) -> Result> { + let ctx = self.ctx.with_ray(req_ctx.ray_id(), req_ctx.req_id())?; + let req_body_size_hint = req.body().size_hint(); + + let (res, metrics_res) = tokio::join!( + self.handle_request_inner(&ctx, req, req_ctx), + record_req_metrics( + &ctx, + self.actor_id, + self.namespace_id, + Metric::HttpIngress( + req_body_size_hint + .upper() + .unwrap_or(req_body_size_hint.lower()) as usize + ), + ), + ); + + let response_size = match &res { + Ok(res) => res.size_hint().upper().unwrap_or(res.size_hint().lower()), + Err(_) => 0, + }; + + if let Err(err) = metrics_res { + tracing::error!(?err, "http req ingress metrics failed"); + } else { + let actor_id = self.actor_id; + let namespace_id = self.namespace_id; + let envoy_key = self.envoy_key.clone(); + tokio::spawn( + async move { + if let Err(err) = record_req_metrics( + &ctx, + actor_id, + namespace_id, + Metric::HttpEgress(response_size as usize), + ) + .await + { + tracing::error!( + ?err, + ?namespace_id, + %envoy_key, + "http req egress metrics failed, likely corrupt now", + ); + } + } + .in_current_span(), + ); + } + + res + } + #[tracing::instrument(skip_all, fields(actor_id=?self.actor_id, actor_key=?self.actor_key, actor_generation=?self.actor_generation, namespace_id=?self.namespace_id, pool_name=%self.pool_name, envoy_key=%self.envoy_key))] async fn handle_websocket( &self, diff --git a/engine/packages/pegboard-gateway2/src/shared_state.rs b/engine/packages/pegboard-gateway2/src/shared_state.rs index ff870d330d..fd696d765b 100644 --- a/engine/packages/pegboard-gateway2/src/shared_state.rs +++ b/engine/packages/pegboard-gateway2/src/shared_state.rs @@ -69,7 +69,7 @@ fn to_envoy_tunnel_message_kind_name(kind: &protocol::ToEnvoyTunnelMessageKind) match kind { protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(_) => "ToEnvoyRequestStart", protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(_) => "ToEnvoyRequestChunk", - protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort => "ToEnvoyRequestAbort", + protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_) => "ToEnvoyRequestAbort", protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(_) => "ToEnvoyWebSocketOpen", protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(_) => "ToEnvoyWebSocketMessage", protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(_) => "ToEnvoyWebSocketClose", @@ -80,7 +80,7 @@ fn to_rivet_tunnel_message_kind_name(kind: &protocol::ToRivetTunnelMessageKind) match kind { protocol::ToRivetTunnelMessageKind::ToRivetResponseStart(_) => "ToRivetResponseStart", protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk(_) => "ToRivetResponseChunk", - protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort => "ToRivetResponseAbort", + protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort(_) => "ToRivetResponseAbort", protocol::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(_) => "ToRivetWebSocketOpen", protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(_) => "ToRivetWebSocketMessage", protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(_) => { @@ -545,7 +545,7 @@ impl Deref for SharedState { } pub struct InFlightRequestCtx { - pub msg_rx: mpsc::UnboundedReceiver, + pub msg_rx: mpsc::UnboundedReceiver, /// Used to check if the request handler has been dropped. /// /// This is separate from `msg_rx` there may still be messages that need to be sent to the @@ -554,6 +554,12 @@ pub struct InFlightRequestCtx { pub handle: InFlightRequestHandle, } +#[derive(Debug)] +pub struct InFlightTunnelMessage { + pub message_id: protocol::MessageId, + pub message_kind: protocol::ToRivetTunnelMessageKind, +} + #[derive(Clone)] pub struct InFlightRequestHandle { shared_state: SharedState, @@ -1221,7 +1227,7 @@ impl InFlightRequest { fn wake( &mut self, receiver_subject: String, - msg_tx: mpsc::UnboundedSender, + msg_tx: mpsc::UnboundedSender, drop_tx: watch::Sender>, ) { self.receiver_subject = receiver_subject; @@ -1282,7 +1288,7 @@ impl InFlightRequest { /// Transition from pending hibernation to hibernating fn hibernate( &mut self, - msg_tx: mpsc::UnboundedSender, + msg_tx: mpsc::UnboundedSender, drop_tx: watch::Sender>, ) { let mut pending_tunnel_msgs = Vec::new(); @@ -1382,7 +1388,7 @@ impl InFlightRequest { enum InFlightRequestState { Active { /// Sender for incoming messages to this request. - msg_tx: mpsc::UnboundedSender, + msg_tx: mpsc::UnboundedSender, /// Used to check if the request handler has been dropped. drop_tx: watch::Sender>, last_pong: i64, @@ -1392,7 +1398,7 @@ enum InFlightRequestState { PendingHibernation { hibernation_state: HibernationState }, Hibernating { /// Sender for incoming messages to this request. - msg_tx: mpsc::UnboundedSender, + msg_tx: mpsc::UnboundedSender, /// Used to check if the hibernation handler has been dropped. drop_tx: watch::Sender>, hibernation_state: HibernationState, @@ -1420,41 +1426,49 @@ fn forward_tunnel_message( receiver_subject: &str, actor_key: Option<&str>, actor_generation: Option, - msg_tx: &mpsc::UnboundedSender, - mut msg: protocol::ToRivetTunnelMessage, + msg_tx: &mpsc::UnboundedSender, + msg: protocol::ToRivetTunnelMessage, ) -> Option { + let message_id = msg.message_id; // Send message to the request handler to emulate the real network action let inner_size = match &msg.message_kind { protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(ws_msg) => ws_msg.data.len(), _ => 0, }; tracing::debug!( - gateway_id=%display_id(&msg.message_id.gateway_id), - request_id=%display_id(&msg.message_id.request_id), - message_index=msg.message_id.message_index, + gateway_id=%display_id(&message_id.gateway_id), + request_id=%display_id(&message_id.request_id), + message_index=message_id.message_index, actor_key, actor_generation, inner_size, "forwarding message to request handler" ); - if let Err(send_err) = msg_tx.send(msg.message_kind) { + let tunnel_msg = InFlightTunnelMessage { + message_id: message_id.clone(), + message_kind: msg.message_kind, + }; + + if let Err(send_err) = msg_tx.send(tunnel_msg) { tracing::debug!( - gateway_id=%display_id(&msg.message_id.gateway_id), - request_id=%display_id(&msg.message_id.request_id), + gateway_id=%display_id(&send_err.0.message_id.gateway_id), + request_id=%display_id(&send_err.0.message_id.request_id), receiver_subject=%receiver_subject, actor_key, actor_generation, "message handler channel closed, saving to pending msgs", ); - msg.message_kind = send_err.0; - Some(msg) + Some(protocol::ToRivetTunnelMessage { + message_id: send_err.0.message_id, + message_kind: send_err.0.message_kind, + }) } else { tracing::trace!( - gateway_id=%display_id(&msg.message_id.gateway_id), - request_id=%display_id(&msg.message_id.request_id), - message_index=msg.message_id.message_index, + gateway_id=%display_id(&message_id.gateway_id), + request_id=%display_id(&message_id.request_id), + message_index=message_id.message_index, actor_key, actor_generation, inner_size, diff --git a/engine/packages/pegboard-gateway2/src/tunnel_to_ws_task.rs b/engine/packages/pegboard-gateway2/src/tunnel_to_ws_task.rs index 83cd92c81e..1d311f6f77 100644 --- a/engine/packages/pegboard-gateway2/src/tunnel_to_ws_task.rs +++ b/engine/packages/pegboard-gateway2/src/tunnel_to_ws_task.rs @@ -16,14 +16,16 @@ use tokio::sync::{mpsc, watch}; use tokio_tungstenite::tungstenite::Message; use super::LifecycleResult; -use crate::shared_state::{InFlightRequestHandle, MsgGcReason, display_id}; +use crate::shared_state::{ + InFlightRequestHandle, InFlightTunnelMessage, MsgGcReason, display_id, +}; #[tracing::instrument(name = "tunnel_to_ws_task", skip_all)] pub async fn task( in_flight_req: InFlightRequestHandle, client_ws: WebSocketHandle, mut stopped_sub: message::SubscriptionHandle, - mut msg_rx: mpsc::UnboundedReceiver, + mut msg_rx: mpsc::UnboundedReceiver, mut drop_rx: watch::Receiver>, can_hibernate: bool, egress_bytes: Arc, @@ -33,7 +35,7 @@ pub async fn task( tokio::select! { res = msg_rx.recv() => { if let Some(msg) = res { - match msg { + match msg.message_kind { protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(ws_msg) => { let data_len = ws_msg.data.len(); let binary = ws_msg.binary; diff --git a/engine/sdks/rust/envoy-client/src/actor.rs b/engine/sdks/rust/envoy-client/src/actor.rs index 56ea338958..373b7b5df2 100644 --- a/engine/sdks/rust/envoy-client/src/actor.rs +++ b/engine/sdks/rust/envoy-client/src/actor.rs @@ -10,7 +10,10 @@ use tokio::sync::oneshot::error::TryRecvError; use tokio::task::{JoinError, JoinSet}; use tracing::Instrument; -use crate::config::{HttpRequest, HttpResponse, WebSocketMessage}; +use crate::config::{ + HTTP_BODY_MAX_CHUNK_SIZE, HTTP_BODY_STREAM_CHANNEL_CAPACITY, HttpRequest, HttpResponse, + ResponseChunk, WebSocketMessage, +}; use crate::connection::ws_send; use crate::context::SharedContext; use crate::handle::EnvoyHandle; @@ -66,7 +69,7 @@ pub enum ToActor { struct PendingRequest { envoy_message_index: u16, - body_tx: Option>>, + body_tx: Option>>, } struct WsEntry { @@ -84,6 +87,7 @@ struct ActorContext { event_index: i64, error: Option, pending_requests: BufferMap, + early_request_chunks: BufferMap>, ws_entries: BufferMap, hibernating_requests: Vec, active_http_request_count: Arc, @@ -104,6 +108,12 @@ enum StopProgress { Pending(PendingStop), } +/// Counts one HTTP request task from dispatch through the full response drain. +/// +/// This guard is created before invoking the runtime callback and is held across +/// `send_response`, including streaming `body_stream` drains. Sleep/shutdown +/// logic relies on this counter staying non-zero until the final response chunk +/// is sent or the task is aborted. impl ActiveHttpRequestGuard { fn new(active_http_request_count: Arc) -> Self { active_http_request_count.increment(); @@ -175,6 +185,7 @@ async fn actor_inner( event_index: 0, error: None, pending_requests: BufferMap::new(), + early_request_chunks: BufferMap::new(), ws_entries: BufferMap::new(), hibernating_requests, active_http_request_count, @@ -326,10 +337,10 @@ async fn actor_inner( } } ToActor::ReqStart { message_id, req } => { - handle_req_start(&mut ctx, &handle, &mut http_request_tasks, message_id, req); + handle_req_start(&mut ctx, &handle, &mut http_request_tasks, message_id, req).await; } ToActor::ReqChunk { message_id, chunk } => { - handle_req_chunk(&mut ctx, message_id, chunk); + handle_req_chunk(&mut ctx, message_id, chunk).await; } ToActor::ReqAbort { message_id } => { handle_req_abort(&mut ctx, message_id); @@ -508,7 +519,7 @@ fn send_stopped_event( ); } -fn handle_req_start( +async fn handle_req_start( ctx: &mut ActorContext, handle: &EnvoyHandle, http_request_tasks: &mut JoinSet<()>, @@ -529,7 +540,7 @@ fn handle_req_start( .collect(); let body_stream = if req.stream { - let (body_tx, body_rx) = mpsc::unbounded_channel::>(); + let (body_tx, body_rx) = mpsc::channel::>(HTTP_BODY_STREAM_CHANNEL_CAPACITY); if let Some(pending) = ctx .pending_requests .get_mut(&[&message_id.gateway_id, &message_id.request_id]) @@ -581,6 +592,15 @@ fn handle_req_start( #[cfg(not(target_arch = "wasm32"))] http_request_tasks.spawn(task); + if let Some(chunks) = ctx + .early_request_chunks + .remove(&[&message_id.gateway_id, &message_id.request_id]) + { + for chunk in chunks { + handle_req_chunk(ctx, message_id.clone(), chunk).await; + } + } + if !req.stream { ctx.pending_requests .remove(&[&message_id.gateway_id, &message_id.request_id]); @@ -618,23 +638,39 @@ async fn abort_and_join_http_request_tasks( } } -fn handle_req_chunk( +async fn handle_req_chunk( ctx: &mut ActorContext, message_id: protocol::MessageId, chunk: protocol::ToEnvoyRequestChunk, ) { let finish = chunk.finish; - let pending = ctx + let body_tx = ctx .pending_requests - .get(&[&message_id.gateway_id, &message_id.request_id]); - if let Some(pending) = pending { - if let Some(body_tx) = &pending.body_tx { - let _ = body_tx.send(chunk.body); - } else { + .get(&[&message_id.gateway_id, &message_id.request_id]) + .map(|pending| pending.body_tx.clone()); + match body_tx { + Some(Some(body_tx)) => { + if !chunk.body.is_empty() && let Err(error) = body_tx.send(chunk.body).await { + tracing::warn!(?error, "failed to enqueue streamed request chunk"); + } + } + Some(None) => { tracing::warn!("received chunk for pending request without stream controller"); } - } else { - tracing::warn!("received chunk for unknown pending request"); + None => { + if let Some(chunks) = ctx + .early_request_chunks + .get_mut(&[&message_id.gateway_id, &message_id.request_id]) + { + chunks.push(chunk); + } else { + ctx.early_request_chunks.insert( + &[&message_id.gateway_id, &message_id.request_id], + vec![chunk], + ); + } + return; + } } if finish { @@ -646,6 +682,8 @@ fn handle_req_chunk( fn handle_req_abort(ctx: &mut ActorContext, message_id: protocol::MessageId) { ctx.pending_requests .remove(&[&message_id.gateway_id, &message_id.request_id]); + ctx.early_request_chunks + .remove(&[&message_id.gateway_id, &message_id.request_id]); } fn spawn_ws_outgoing_task( @@ -1333,33 +1371,137 @@ async fn send_response( // If streaming, read chunks from the body_stream and forward them if let Some(ref mut body_stream) = response.body_stream { let mut message_index: u16 = 1; + let mut saw_finish = false; while let Some(chunk) = body_stream.recv().await { - let finish = chunk.finish; - ws_send( - shared, - protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage { - message_id: protocol::MessageId { + let finish = match chunk { + ResponseChunk::Data { data, finish } => { + send_response_data_chunks( + shared, + gateway_id, + request_id, + &mut message_index, + data, + finish, + ) + .await; + finish + } + ResponseChunk::Error(detail) => { + send_response_abort( + shared, gateway_id, request_id, message_index, - }, - message_kind: protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk( - protocol::ToRivetResponseChunk { - body: chunk.data, - finish, + protocol::HttpStreamAbortReason { + kind: protocol::HttpStreamAbortReasonKind::HandlerError, + detail: Some(detail), }, - ), - }), - ) - .await; - message_index = message_index.wrapping_add(1); + ) + .await; + message_index = message_index.wrapping_add(1); + true + } + }; if finish { + saw_finish = true; break; } } + if !saw_finish { + send_response_abort( + shared, + gateway_id, + request_id, + message_index, + protocol::HttpStreamAbortReason { + kind: protocol::HttpStreamAbortReasonKind::HandlerError, + detail: Some("response body stream closed before finish".to_string()), + }, + ) + .await; + } + } +} + +async fn send_response_data_chunks( + shared: &SharedContext, + gateway_id: protocol::GatewayId, + request_id: protocol::RequestId, + message_index: &mut u16, + data: Vec, + finish: bool, +) { + if data.is_empty() { + send_response_data_chunk(shared, gateway_id, request_id, *message_index, data, finish) + .await; + *message_index = (*message_index).wrapping_add(1); + return; + } + + let total_len = data.len(); + for (idx, chunk) in data.chunks(HTTP_BODY_MAX_CHUNK_SIZE).enumerate() { + let end = (idx + 1) * HTTP_BODY_MAX_CHUNK_SIZE; + let chunk_finish = finish && end >= total_len; + send_response_data_chunk( + shared, + gateway_id, + request_id, + *message_index, + chunk.to_vec(), + chunk_finish, + ) + .await; + *message_index = (*message_index).wrapping_add(1); } } +async fn send_response_data_chunk( + shared: &SharedContext, + gateway_id: protocol::GatewayId, + request_id: protocol::RequestId, + message_index: u16, + body: Vec, + finish: bool, +) { + ws_send( + shared, + protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage { + message_id: protocol::MessageId { + gateway_id, + request_id, + message_index, + }, + message_kind: protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk( + protocol::ToRivetResponseChunk { body, finish }, + ), + }), + ) + .await; +} + +async fn send_response_abort( + shared: &SharedContext, + gateway_id: protocol::GatewayId, + request_id: protocol::RequestId, + message_index: u16, + reason: protocol::HttpStreamAbortReason, +) { + ws_send( + shared, + protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage { + message_id: protocol::MessageId { + gateway_id, + request_id, + message_index, + }, + message_kind: protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort( + protocol::ToRivetResponseAbort { reason }, + ), + }), + ) + .await; +} + async fn send_fetch_error_response( shared: &SharedContext, gateway_id: protocol::GatewayId, @@ -1408,6 +1550,7 @@ mod tests { use tokio::sync::Notify; use tokio::sync::oneshot; use tokio::task::yield_now; + use vbare::OwnedVersionedData; use super::*; use crate::config::{BoxFuture, EnvoyCallbacks, WebSocketHandler, WebSocketSender}; @@ -1467,6 +1610,10 @@ mod tests { stop_handle_tx: Mutex>>, } + struct StreamingCallbacks { + body_tx: Mutex>>>, + } + impl EnvoyCallbacks for TestCallbacks { fn on_actor_start( &self, @@ -1646,6 +1793,85 @@ mod tests { } } + impl EnvoyCallbacks for StreamingCallbacks { + fn on_actor_start( + &self, + _handle: EnvoyHandle, + _actor_id: String, + _generation: u32, + _config: protocol::ActorConfig, + _preloaded_kv: Option, + ) -> BoxFuture> { + Box::pin(async { Ok(()) }) + } + + fn on_actor_stop( + &self, + _handle: EnvoyHandle, + _actor_id: String, + _generation: u32, + _reason: protocol::StopActorReason, + ) -> BoxFuture> { + Box::pin(async { Ok(()) }) + } + + fn on_shutdown(&self) {} + + fn fetch( + &self, + _handle: EnvoyHandle, + _actor_id: String, + _gateway_id: protocol::GatewayId, + _request_id: protocol::RequestId, + _request: HttpRequest, + ) -> BoxFuture> { + let body_tx = self + .body_tx + .lock() + .expect("streaming body mutex poisoned") + .take(); + + Box::pin(async move { + let (tx, rx) = mpsc::channel(HTTP_BODY_STREAM_CHANNEL_CAPACITY); + if let Some(body_tx) = body_tx { + let _ = body_tx.send(tx); + } + Ok(HttpResponse { + status: 200, + headers: HashMap::new(), + body: None, + body_stream: Some(rx), + }) + }) + } + + fn websocket( + &self, + _handle: EnvoyHandle, + _actor_id: String, + _gateway_id: protocol::GatewayId, + _request_id: protocol::RequestId, + _request: HttpRequest, + _path: String, + _headers: HashMap, + _is_hibernatable: bool, + _is_restoring_hibernatable: bool, + _sender: WebSocketSender, + ) -> BoxFuture> { + Box::pin(async { anyhow::bail!("websocket should not be called in streaming test") }) + } + + fn can_hibernate( + &self, + _actor_id: &str, + _gateway_id: &protocol::GatewayId, + _request_id: &protocol::RequestId, + _request: &HttpRequest, + ) -> BoxFuture> { + Box::pin(async { Ok(false) }) + } + } + fn build_shared_context( callbacks: Arc, ) -> (Arc, mpsc::UnboundedReceiver) { @@ -1717,6 +1943,31 @@ mod tests { ); } + async fn recv_ws_tunnel_msg( + ws_rx: &mut mpsc::UnboundedReceiver, + ) -> protocol::ToRivetTunnelMessage { + tokio::time::timeout(Duration::from_secs(2), async { + loop { + let Some(msg) = ws_rx.recv().await else { + panic!("websocket channel closed before tunnel message"); + }; + let WsTxMessage::Send(bytes) = msg else { + continue; + }; + let message = protocol::versioned::ToRivet::deserialize( + &bytes, + protocol::PROTOCOL_VERSION, + ) + .expect("failed to decode ToRivet message"); + if let protocol::ToRivet::ToRivetTunnelMessage(msg) = message { + return msg; + } + } + }) + .await + .expect("timed out waiting for tunnel message") + } + async fn wait_for_stopped_event(envoy_rx: &mut mpsc::UnboundedReceiver) { tokio::time::timeout(Duration::from_secs(2), async { loop { @@ -1855,6 +2106,92 @@ mod tests { wait_for_stopped_event(&mut envoy_rx).await; } + #[tokio::test] + async fn active_http_request_count_spans_streaming_response_drain() { + let (body_tx_tx, body_tx_rx) = oneshot::channel(); + let callbacks = Arc::new(StreamingCallbacks { + body_tx: Mutex::new(Some(body_tx_tx)), + }); + let (shared, _envoy_rx) = build_shared_context(callbacks); + let (ws_tx, mut ws_rx) = mpsc::unbounded_channel(); + *shared.ws_tx.lock().await = Some(ws_tx); + let (actor_tx, active_http_request_count) = create_actor( + shared, + "actor-stream".to_string(), + 1, + actor_config(), + Vec::new(), + None, + ); + + actor_tx + .send(ToActor::ReqStart { + message_id: message_id(), + req: request_start(), + }) + .expect("failed to send request start"); + + let body_tx = tokio::time::timeout(Duration::from_secs(2), body_tx_rx) + .await + .expect("timed out waiting for response body sender") + .expect("response body sender dropped"); + let start_msg = recv_ws_tunnel_msg(&mut ws_rx).await; + assert!(matches!( + start_msg.message_kind, + protocol::ToRivetTunnelMessageKind::ToRivetResponseStart( + protocol::ToRivetResponseStart { stream: true, .. } + ) + )); + assert_eq!(active_http_request_count.load(), 1); + + body_tx + .send(ResponseChunk::Data { + data: vec![7; HTTP_BODY_MAX_CHUNK_SIZE + 3], + finish: false, + }) + .await + .expect("failed to send response data"); + + let first = recv_ws_tunnel_msg(&mut ws_rx).await; + let second = recv_ws_tunnel_msg(&mut ws_rx).await; + match first.message_kind { + protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk(chunk) => { + assert_eq!(first.message_id.message_index, 1); + assert_eq!(chunk.body.len(), HTTP_BODY_MAX_CHUNK_SIZE); + assert!(!chunk.finish); + } + other => panic!("expected first response chunk, got {other:?}"), + } + match second.message_kind { + protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk(chunk) => { + assert_eq!(second.message_id.message_index, 2); + assert_eq!(chunk.body.len(), 3); + assert!(!chunk.finish); + } + other => panic!("expected second response chunk, got {other:?}"), + } + assert_eq!(active_http_request_count.load(), 1); + + body_tx + .send(ResponseChunk::Data { + data: Vec::new(), + finish: true, + }) + .await + .expect("failed to finish response stream"); + let finish = recv_ws_tunnel_msg(&mut ws_rx).await; + match finish.message_kind { + protocol::ToRivetTunnelMessageKind::ToRivetResponseChunk(chunk) => { + assert_eq!(finish.message_id.message_index, 3); + assert!(chunk.body.is_empty()); + assert!(chunk.finish); + } + other => panic!("expected finish response chunk, got {other:?}"), + } + + wait_for_zero(&active_http_request_count).await; + } + #[tokio::test] async fn actor_stop_aborts_in_flight_http_requests_before_stopped_event() { let (fetch_started_tx, fetch_started_rx) = oneshot::channel(); diff --git a/engine/sdks/rust/envoy-client/src/config.rs b/engine/sdks/rust/envoy-client/src/config.rs index 3c6800aa9b..08bfdae277 100644 --- a/engine/sdks/rust/envoy-client/src/config.rs +++ b/engine/sdks/rust/envoy-client/src/config.rs @@ -9,6 +9,9 @@ use tokio::sync::{mpsc, oneshot}; use crate::handle::EnvoyHandle; +pub const HTTP_BODY_STREAM_CHANNEL_CAPACITY: usize = 16; +pub const HTTP_BODY_MAX_CHUNK_SIZE: usize = 64 * 1024; + #[cfg(not(target_arch = "wasm32"))] pub type BoxFuture = Pin + Send>>; @@ -22,7 +25,7 @@ pub struct HttpRequest { pub headers: HashMap, pub body: Option>, /// If the request is streamed, body chunks arrive on this channel. - pub body_stream: Option>>, + pub body_stream: Option>>, } pub struct HttpResponse { @@ -31,13 +34,13 @@ pub struct HttpResponse { pub body: Option>, /// If set, the response is streamed. The envoy client reads chunks and sends /// `ToRivetResponseChunk` for each one. - pub body_stream: Option>, + pub body_stream: Option>, } /// A chunk in a streaming HTTP response. -pub struct ResponseChunk { - pub data: Vec, - pub finish: bool, +pub enum ResponseChunk { + Data { data: Vec, finish: bool }, + Error(String), } pub struct EnvoyConfig { diff --git a/engine/sdks/rust/envoy-client/src/envoy.rs b/engine/sdks/rust/envoy-client/src/envoy.rs index 360b258ca2..7cd8d398f6 100644 --- a/engine/sdks/rust/envoy-client/src/envoy.rs +++ b/engine/sdks/rust/envoy-client/src/envoy.rs @@ -57,6 +57,7 @@ pub struct EnvoyContext { pub remote_sqlite_requests: HashMap, pub next_remote_sqlite_request_id: u32, pub request_to_actor: BufferMap, + pub pending_request_chunks: BufferMap>, pub buffered_messages: Vec, /// Highest command index processed per `(actor_id, generation)`, used to /// drop replayed commands from `pegboard-envoy` after a reconnect. Persists @@ -331,6 +332,7 @@ fn start_envoy_sync_inner(config: EnvoyConfig) -> EnvoyHandle { remote_sqlite_requests: HashMap::new(), next_remote_sqlite_request_id: 0, request_to_actor: BufferMap::new(), + pending_request_chunks: BufferMap::new(), buffered_messages: Vec::new(), processed_command_idx: HashMap::new(), }; diff --git a/engine/sdks/rust/envoy-client/src/events.rs b/engine/sdks/rust/envoy-client/src/events.rs index 74091b7508..5e497e099b 100644 --- a/engine/sdks/rust/envoy-client/src/events.rs +++ b/engine/sdks/rust/envoy-client/src/events.rs @@ -192,6 +192,7 @@ mod tests { remote_sqlite_requests: HashMap::new(), next_remote_sqlite_request_id: 0, request_to_actor: crate::utils::BufferMap::new(), + pending_request_chunks: crate::utils::BufferMap::new(), buffered_messages: Vec::new(), processed_command_idx: HashMap::new(), }, diff --git a/engine/sdks/rust/envoy-client/src/sqlite.rs b/engine/sdks/rust/envoy-client/src/sqlite.rs index b16ad3784d..5484ad4712 100644 --- a/engine/sdks/rust/envoy-client/src/sqlite.rs +++ b/engine/sdks/rust/envoy-client/src/sqlite.rs @@ -539,6 +539,7 @@ mod tests { remote_sqlite_requests: HashMap::new(), next_remote_sqlite_request_id: 0, request_to_actor: BufferMap::new(), + pending_request_chunks: BufferMap::new(), buffered_messages: Vec::new(), processed_command_idx: HashMap::new(), } diff --git a/engine/sdks/rust/envoy-client/src/stringify.rs b/engine/sdks/rust/envoy-client/src/stringify.rs index 203d447329..a62f8dc19b 100644 --- a/engine/sdks/rust/envoy-client/src/stringify.rs +++ b/engine/sdks/rust/envoy-client/src/stringify.rs @@ -46,7 +46,7 @@ pub fn stringify_to_rivet_tunnel_message_kind(kind: &protocol::ToRivetTunnelMess val.finish ) } - protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort => { + protocol::ToRivetTunnelMessageKind::ToRivetResponseAbort(_) => { "ToRivetResponseAbort".to_string() } protocol::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(val) => { @@ -106,7 +106,7 @@ pub fn stringify_to_envoy_tunnel_message_kind(kind: &protocol::ToEnvoyTunnelMess val.finish ) } - protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort => { + protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_) => { "ToEnvoyRequestAbort".to_string() } protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(val) => { diff --git a/engine/sdks/rust/envoy-client/src/tunnel.rs b/engine/sdks/rust/envoy-client/src/tunnel.rs index b862873d0c..cf9364e21b 100644 --- a/engine/sdks/rust/envoy-client/src/tunnel.rs +++ b/engine/sdks/rust/envoy-client/src/tunnel.rs @@ -29,7 +29,7 @@ pub async fn handle_tunnel_message(ctx: &mut EnvoyContext, msg: protocol::ToEnvo protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(chunk) => { handle_request_chunk(ctx, message_id, chunk); } - protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort => { + protocol::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_) => { handle_request_abort(ctx, message_id); } protocol::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(open) => { @@ -64,9 +64,28 @@ async fn handle_request_start( ); let actor = ctx.get_actor(&actor_id, None).unwrap(); - let _ = actor - .handle - .send(crate::actor::ToActor::ReqStart { message_id, req }); + let actor_handle = actor.handle.clone(); + let _ = actor_handle.send(crate::actor::ToActor::ReqStart { + message_id: message_id.clone(), + req, + }); + + if let Some(chunks) = ctx + .pending_request_chunks + .remove(&[&message_id.gateway_id, &message_id.request_id]) + { + for chunk in chunks { + let finish = chunk.finish; + let _ = actor_handle.send(crate::actor::ToActor::ReqChunk { + message_id: message_id.clone(), + chunk, + }); + if finish { + ctx.request_to_actor + .remove(&[&message_id.gateway_id, &message_id.request_id]); + } + } + } } fn handle_request_chunk( @@ -87,10 +106,22 @@ fn handle_request_chunk( message_id: message_id.clone(), chunk, }); + } else { + tracing::warn!(actor_id = %actor_id, "received request chunk for unknown actor"); } + } else if let Some(chunks) = ctx + .pending_request_chunks + .get_mut(&[&message_id.gateway_id, &message_id.request_id]) + { + chunks.push(chunk); + } else { + ctx.pending_request_chunks.insert( + &[&message_id.gateway_id, &message_id.request_id], + vec![chunk], + ); } - if finish { + if actor_id.is_some() && finish { ctx.request_to_actor .remove(&[&message_id.gateway_id, &message_id.request_id]); } @@ -111,6 +142,8 @@ fn handle_request_abort(ctx: &mut EnvoyContext, message_id: protocol::MessageId) ctx.request_to_actor .remove(&[&message_id.gateway_id, &message_id.request_id]); + ctx.pending_request_chunks + .remove(&[&message_id.gateway_id, &message_id.request_id]); } async fn handle_ws_open( diff --git a/engine/sdks/rust/envoy-protocol/schemas/v6.bare b/engine/sdks/rust/envoy-protocol/schemas/v6.bare new file mode 100644 index 0000000000..fed130ef7e --- /dev/null +++ b/engine/sdks/rust/envoy-protocol/schemas/v6.bare @@ -0,0 +1,663 @@ +# MARK: Core Primitives + +type Id str +type Json str + +type GatewayId data[4] +type RequestId data[4] +type MessageIndex u16 + +# MARK: KV + +# Basic types +type KvKey data +type KvValue data +type KvMetadata struct { + version: data + updateTs: i64 +} + +# Query types +type KvListAllQuery void +type KvListRangeQuery struct { + start: KvKey + end: KvKey + exclusive: bool +} + +type KvListPrefixQuery struct { + key: KvKey +} + +type KvListQuery union { + KvListAllQuery | + KvListRangeQuery | + KvListPrefixQuery +} + +# Request types +type KvGetRequest struct { + keys: list +} + +type KvListRequest struct { + query: KvListQuery + reverse: optional + limit: optional +} + +type KvPutRequest struct { + keys: list + values: list +} + +type KvDeleteRequest struct { + keys: list +} + +type KvDeleteRangeRequest struct { + start: KvKey + end: KvKey +} + +type KvDropRequest void + +# Response types +type KvErrorResponse struct { + message: str +} + +type KvGetResponse struct { + keys: list + values: list + metadata: list +} + +type KvListResponse struct { + keys: list + values: list + metadata: list +} + +type KvPutResponse void +type KvDeleteResponse void +type KvDropResponse void + +# Request/Response unions +type KvRequestData union { + KvGetRequest | + KvListRequest | + KvPutRequest | + KvDeleteRequest | + KvDeleteRangeRequest | + KvDropRequest +} + +type KvResponseData union { + KvErrorResponse | + KvGetResponse | + KvListResponse | + KvPutResponse | + KvDeleteResponse | + KvDropResponse +} + +# MARK: SQLite + +type SqlitePgno u32 +type SqliteGeneration u64 +type SqlitePageBytes data + +type SqliteDirtyPage struct { + pgno: SqlitePgno + bytes: SqlitePageBytes +} + +type SqliteFetchedPage struct { + pgno: SqlitePgno + bytes: optional +} + +type SqliteGetPagesRequest struct { + actorId: Id + pgnos: list + expectedGeneration: optional + expectedHeadTxid: optional +} + +type SqliteGetPagesOk struct { + pages: list + headTxid: optional +} + +type SqliteErrorResponse struct { + group: str + code: str + message: str +} + +type SqliteGetPagesResponse union { + SqliteGetPagesOk | + SqliteErrorResponse +} + +type SqliteCommitRequest struct { + actorId: Id + dirtyPages: list + dbSizePages: u32 + nowMs: i64 + expectedGeneration: optional + expectedHeadTxid: optional +} + +type SqliteCommitOk struct { + headTxid: optional +} + +type SqliteCommitResponse union { + SqliteCommitOk | + SqliteErrorResponse +} + +# MARK: SQLite Remote Execution + +type SqliteValueNull void + +type SqliteValueInteger struct { + value: i64 +} + +type SqliteValueFloat struct { + value: data[8] +} + +type SqliteValueText struct { + value: str +} + +type SqliteValueBlob struct { + value: data +} + +type SqliteBindParam union { + SqliteValueNull | + SqliteValueInteger | + SqliteValueFloat | + SqliteValueText | + SqliteValueBlob +} + +type SqliteColumnValue union { + SqliteValueNull | + SqliteValueInteger | + SqliteValueFloat | + SqliteValueText | + SqliteValueBlob +} + +type SqliteQueryResult struct { + columns: list + rows: list> +} + +type SqliteExecuteResult struct { + columns: list + rows: list> + changes: i64 + lastInsertRowId: optional +} + +type SqliteExecRequest struct { + namespaceId: Id + actorId: Id + generation: SqliteGeneration + sql: str +} + +type SqliteExecuteRequest struct { + namespaceId: Id + actorId: Id + generation: SqliteGeneration + sql: str + params: optional> +} + +type SqliteExecOk struct { + result: SqliteQueryResult +} + +type SqliteExecuteOk struct { + result: SqliteExecuteResult +} + +type SqliteExecResponse union { + SqliteExecOk | + SqliteErrorResponse +} + +type SqliteExecuteResponse union { + SqliteExecuteOk | + SqliteErrorResponse +} + +# MARK: Actor + +# Core +type StopCode enum { + OK + ERROR +} + +type ActorName struct { + metadata: Json +} + +type ActorConfig struct { + name: str + key: optional + createTs: i64 + input: optional +} + +type ActorCheckpoint struct { + actorId: Id + generation: u32 + index: i64 +} + +# Intent +type ActorIntentSleep void + +type ActorIntentStop void + +type ActorIntent union { + ActorIntentSleep | + ActorIntentStop +} + +# State +type ActorStateRunning void + +type ActorStateStopped struct { + code: StopCode + message: optional +} + +type ActorState union { + ActorStateRunning | + ActorStateStopped +} + +# MARK: Events +type EventActorIntent struct { + intent: ActorIntent +} + +type EventActorStateUpdate struct { + state: ActorState +} + +type EventActorSetAlarm struct { + alarmTs: optional +} + +type Event union { + EventActorIntent | + EventActorStateUpdate | + EventActorSetAlarm +} + +type EventWrapper struct { + checkpoint: ActorCheckpoint + inner: Event +} + +# MARK: Preloaded KV + +type PreloadedKvEntry struct { + key: KvKey + value: KvValue + metadata: KvMetadata +} + +type PreloadedKv struct { + entries: list + requestedGetKeys: list + requestedPrefixes: list +} + +# MARK: Commands + +type HibernatingRequest struct { + gatewayId: GatewayId + requestId: RequestId +} + +type CommandStartActor struct { + config: ActorConfig + hibernatingRequests: list + preloadedKv: optional +} + +type StopActorReason enum { + SLEEP_INTENT + STOP_INTENT + DESTROY + GOING_AWAY + LOST +} + +type CommandStopActor struct { + reason: StopActorReason +} + +type Command union { + CommandStartActor | + CommandStopActor +} + +type CommandWrapper struct { + checkpoint: ActorCheckpoint + inner: Command +} + +# We redeclare this so its top level +type ActorCommandKeyData union { + CommandStartActor | + CommandStopActor +} + +# MARK: Tunnel + +# Message ID + +type MessageId struct { + # Globally unique ID + gatewayId: GatewayId + # Unique ID to the gateway + requestId: RequestId + # Unique ID to the request + messageIndex: MessageIndex +} + +# HTTP +type ToEnvoyRequestStart struct { + actorId: Id + method: str + path: str + headers: map + body: optional + stream: bool +} + +type ToEnvoyRequestChunk struct { + body: data + finish: bool +} + +type HttpStreamAbortReasonKind enum { + UNKNOWN + CLIENT_DISCONNECT + HANDLER_ERROR + IDLE_TIMEOUT + OVERLOADED + BODY_TOO_LARGE + OUT_OF_MEMORY + SHUTDOWN + INTERNAL_ERROR +} + +type HttpStreamAbortReason struct { + kind: HttpStreamAbortReasonKind + detail: optional +} + +type ToEnvoyRequestAbort struct { + reason: HttpStreamAbortReason +} + +type ToRivetResponseStart struct { + status: u16 + headers: map + body: optional + stream: bool +} + +type ToRivetResponseChunk struct { + body: data + finish: bool +} + +type ToRivetResponseAbort struct { + reason: HttpStreamAbortReason +} + +# WebSocket +type ToEnvoyWebSocketOpen struct { + actorId: Id + path: str + headers: map +} + +type ToEnvoyWebSocketMessage struct { + data: data + binary: bool +} + +type ToEnvoyWebSocketClose struct { + code: optional + reason: optional +} + +type ToRivetWebSocketOpen struct { + canHibernate: bool +} + +type ToRivetWebSocketMessage struct { + data: data + binary: bool +} + +type ToRivetWebSocketMessageAck struct { + index: MessageIndex +} + +type ToRivetWebSocketClose struct { + code: optional + reason: optional + hibernate: bool +} + +# To Rivet +type ToRivetTunnelMessageKind union { + # HTTP + ToRivetResponseStart | + ToRivetResponseChunk | + ToRivetResponseAbort | + + # WebSocket + ToRivetWebSocketOpen | + ToRivetWebSocketMessage | + ToRivetWebSocketMessageAck | + ToRivetWebSocketClose +} + +type ToRivetTunnelMessage struct { + messageId: MessageId + messageKind: ToRivetTunnelMessageKind +} + +# To Envoy +type ToEnvoyTunnelMessageKind union { + # HTTP + ToEnvoyRequestStart | + ToEnvoyRequestChunk | + ToEnvoyRequestAbort | + + # WebSocket + ToEnvoyWebSocketOpen | + ToEnvoyWebSocketMessage | + ToEnvoyWebSocketClose +} + +type ToEnvoyTunnelMessage struct { + messageId: MessageId + messageKind: ToEnvoyTunnelMessageKind +} + +type ToEnvoyPing struct { + ts: i64 +} + +# MARK: To Rivet +type ToRivetMetadata struct { + prepopulateActorNames: optional> + metadata: optional +} + +type ToRivetEvents list + +type ToRivetAckCommands struct { + lastCommandCheckpoints: list +} + +type ToRivetStopping void + +type ToRivetPong struct { + ts: i64 +} + +type ToRivetKvRequest struct { + actorId: Id + requestId: u32 + data: KvRequestData +} + +type ToRivetSqliteGetPagesRequest struct { + requestId: u32 + data: SqliteGetPagesRequest +} + +type ToRivetSqliteCommitRequest struct { + requestId: u32 + data: SqliteCommitRequest +} + +type ToRivetSqliteExecRequest struct { + requestId: u32 + data: SqliteExecRequest +} + +type ToRivetSqliteExecuteRequest struct { + requestId: u32 + data: SqliteExecuteRequest +} + +type ToRivet union { + ToRivetMetadata | + ToRivetEvents | + ToRivetAckCommands | + ToRivetStopping | + ToRivetPong | + ToRivetKvRequest | + ToRivetTunnelMessage | + ToRivetSqliteGetPagesRequest | + ToRivetSqliteCommitRequest | + ToRivetSqliteExecRequest | + ToRivetSqliteExecuteRequest +} + +# MARK: To Envoy +type ProtocolMetadata struct { + envoyLostThreshold: i64 + actorStopThreshold: i64 + maxResponsePayloadSize: u64 +} + +type ToEnvoyInit struct { + metadata: ProtocolMetadata +} + +type ToEnvoyCommands list + +type ToEnvoyAckEvents struct { + lastEventCheckpoints: list +} + +type ToEnvoyKvResponse struct { + requestId: u32 + data: KvResponseData +} + +type ToEnvoySqliteGetPagesResponse struct { + requestId: u32 + data: SqliteGetPagesResponse +} + +type ToEnvoySqliteCommitResponse struct { + requestId: u32 + data: SqliteCommitResponse +} + +type ToEnvoySqliteExecResponse struct { + requestId: u32 + data: SqliteExecResponse +} + +type ToEnvoySqliteExecuteResponse struct { + requestId: u32 + data: SqliteExecuteResponse +} + +type ToEnvoy union { + ToEnvoyInit | + ToEnvoyCommands | + ToEnvoyAckEvents | + ToEnvoyKvResponse | + ToEnvoyTunnelMessage | + ToEnvoyPing | + ToEnvoySqliteGetPagesResponse | + ToEnvoySqliteCommitResponse | + ToEnvoySqliteExecResponse | + ToEnvoySqliteExecuteResponse +} + +# MARK: To Envoy Conn +type ToEnvoyConnPing struct { + gatewayId: GatewayId + requestId: RequestId + ts: i64 +} + +type ToEnvoyConnClose void + +type ToEnvoyConn union { + ToEnvoyConnPing | + ToEnvoyConnClose | + ToEnvoyCommands | + ToEnvoyAckEvents | + ToEnvoyTunnelMessage +} + +# MARK: To Gateway +type ToGatewayPong struct { + requestId: RequestId + ts: i64 +} + +type ToGateway union { + ToGatewayPong | + ToRivetTunnelMessage +} + +# MARK: To Outbound +type ToOutboundActorStart struct { + namespaceId: Id + poolName: str + checkpoint: ActorCheckpoint + actorConfig: ActorConfig +} + +type ToOutbound union { + ToOutboundActorStart +} diff --git a/engine/sdks/rust/envoy-protocol/src/lib.rs b/engine/sdks/rust/envoy-protocol/src/lib.rs index 87d6be2058..94a5a3228f 100644 --- a/engine/sdks/rust/envoy-protocol/src/lib.rs +++ b/engine/sdks/rust/envoy-protocol/src/lib.rs @@ -3,6 +3,6 @@ pub mod util; pub mod versioned; // Re-export latest -pub use generated::v5::*; +pub use generated::v6::*; pub use generated::PROTOCOL_VERSION; diff --git a/engine/sdks/rust/envoy-protocol/src/versioned/mod.rs b/engine/sdks/rust/envoy-protocol/src/versioned/mod.rs index 7f945fc9b9..9bcb59ff01 100644 --- a/engine/sdks/rust/envoy-protocol/src/versioned/mod.rs +++ b/engine/sdks/rust/envoy-protocol/src/versioned/mod.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt}; use anyhow::{Result, bail}; use vbare::OwnedVersionedData; -use crate::generated::{v1, v2, v3, v4, v5}; +use crate::generated::{v1, v2, v3, v4, v5, v6}; mod v1_to_v2; mod v2_to_v1; @@ -12,7 +12,9 @@ mod v3_to_v2; mod v3_to_v4; mod v4_to_v3; mod v4_to_v5; +mod v5_to_v6; mod v5_to_v4; +mod v6_to_v5; // MARK: Protocol compatibility errors @@ -102,18 +104,19 @@ pub enum ToEnvoy { V3(v3::ToEnvoy), V4(v4::ToEnvoy), V5(v5::ToEnvoy), + V6(v6::ToEnvoy), } impl OwnedVersionedData for ToEnvoy { - type Latest = v5::ToEnvoy; + type Latest = v6::ToEnvoy; fn wrap_latest(latest: Self::Latest) -> Self { - Self::V5(latest) + Self::V6(latest) } fn unwrap_latest(self) -> Result { match self { - Self::V5(x) => Ok(x), + Self::V6(x) => Ok(x), _ => bail!("version not latest"), } } @@ -125,6 +128,7 @@ impl OwnedVersionedData for ToEnvoy { 3 => Ok(Self::V3(serde_bare::from_slice(payload)?)), 4 => Ok(Self::V4(serde_bare::from_slice(payload)?)), 5 => Ok(Self::V5(serde_bare::from_slice(payload)?)), + 6 => Ok(Self::V6(serde_bare::from_slice(payload)?)), _ => bail!("invalid version: {version}"), } } @@ -136,6 +140,7 @@ impl OwnedVersionedData for ToEnvoy { Self::V3(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V4(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V5(x) => serde_bare::to_vec(&x).map_err(Into::into), + Self::V6(x) => serde_bare::to_vec(&x).map_err(Into::into), } } @@ -145,11 +150,13 @@ impl OwnedVersionedData for ToEnvoy { Self::v2_to_v3, Self::v3_to_v4, Self::v4_to_v5, + Self::v5_to_v6, ] } fn serialize_converters() -> Vec Result> { vec![ + Self::v6_to_v5, Self::v5_to_v4, Self::v4_to_v3, Self::v3_to_v2, @@ -207,6 +214,18 @@ impl ToEnvoy { _ => bail!("unexpected version"), } } + fn v5_to_v6(self) -> Result { + match self { + Self::V5(x) => Ok(Self::V6(v5_to_v6::convert_to_envoy_v5_to_v6(x)?)), + _ => bail!("unexpected version"), + } + } + fn v6_to_v5(self) -> Result { + match self { + Self::V6(x) => Ok(Self::V5(v6_to_v5::convert_to_envoy_v6_to_v5(x)?)), + _ => bail!("unexpected version"), + } + } } // MARK: ToRivet @@ -217,18 +236,19 @@ pub enum ToRivet { V3(v3::ToRivet), V4(v4::ToRivet), V5(v5::ToRivet), + V6(v6::ToRivet), } impl OwnedVersionedData for ToRivet { - type Latest = v5::ToRivet; + type Latest = v6::ToRivet; fn wrap_latest(latest: Self::Latest) -> Self { - Self::V5(latest) + Self::V6(latest) } fn unwrap_latest(self) -> Result { match self { - Self::V5(x) => Ok(x), + Self::V6(x) => Ok(x), _ => bail!("version not latest"), } } @@ -240,6 +260,7 @@ impl OwnedVersionedData for ToRivet { 3 => Ok(Self::V3(serde_bare::from_slice(payload)?)), 4 => Ok(Self::V4(serde_bare::from_slice(payload)?)), 5 => Ok(Self::V5(serde_bare::from_slice(payload)?)), + 6 => Ok(Self::V6(serde_bare::from_slice(payload)?)), _ => bail!("invalid version: {version}"), } } @@ -251,6 +272,7 @@ impl OwnedVersionedData for ToRivet { Self::V3(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V4(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V5(x) => serde_bare::to_vec(&x).map_err(Into::into), + Self::V6(x) => serde_bare::to_vec(&x).map_err(Into::into), } } @@ -260,11 +282,13 @@ impl OwnedVersionedData for ToRivet { Self::v2_to_v3, Self::v3_to_v4, Self::v4_to_v5, + Self::v5_to_v6, ] } fn serialize_converters() -> Vec Result> { vec![ + Self::v6_to_v5, Self::v5_to_v4, Self::v4_to_v3, Self::v3_to_v2, @@ -322,6 +346,18 @@ impl ToRivet { _ => bail!("unexpected version"), } } + fn v5_to_v6(self) -> Result { + match self { + Self::V5(x) => Ok(Self::V6(v5_to_v6::convert_to_rivet_v5_to_v6(x)?)), + _ => bail!("unexpected version"), + } + } + fn v6_to_v5(self) -> Result { + match self { + Self::V6(x) => Ok(Self::V5(v6_to_v5::convert_to_rivet_v6_to_v5(x)?)), + _ => bail!("unexpected version"), + } + } } // MARK: ToEnvoyConn @@ -332,18 +368,19 @@ pub enum ToEnvoyConn { V3(v3::ToEnvoyConn), V4(v4::ToEnvoyConn), V5(v5::ToEnvoyConn), + V6(v6::ToEnvoyConn), } impl OwnedVersionedData for ToEnvoyConn { - type Latest = v5::ToEnvoyConn; + type Latest = v6::ToEnvoyConn; fn wrap_latest(latest: Self::Latest) -> Self { - Self::V5(latest) + Self::V6(latest) } fn unwrap_latest(self) -> Result { match self { - Self::V5(x) => Ok(x), + Self::V6(x) => Ok(x), _ => bail!("version not latest"), } } @@ -355,6 +392,7 @@ impl OwnedVersionedData for ToEnvoyConn { 3 => Ok(Self::V3(serde_bare::from_slice(payload)?)), 4 => Ok(Self::V4(serde_bare::from_slice(payload)?)), 5 => Ok(Self::V5(serde_bare::from_slice(payload)?)), + 6 => Ok(Self::V6(serde_bare::from_slice(payload)?)), _ => bail!("invalid version: {version}"), } } @@ -366,6 +404,7 @@ impl OwnedVersionedData for ToEnvoyConn { Self::V3(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V4(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V5(x) => serde_bare::to_vec(&x).map_err(Into::into), + Self::V6(x) => serde_bare::to_vec(&x).map_err(Into::into), } } @@ -375,11 +414,13 @@ impl OwnedVersionedData for ToEnvoyConn { Self::v2_to_v3, Self::v3_to_v4, Self::v4_to_v5, + Self::v5_to_v6, ] } fn serialize_converters() -> Vec Result> { vec![ + Self::v6_to_v5, Self::v5_to_v4, Self::v4_to_v3, Self::v3_to_v2, @@ -437,6 +478,18 @@ impl ToEnvoyConn { _ => bail!("unexpected version"), } } + fn v5_to_v6(self) -> Result { + match self { + Self::V5(x) => Ok(Self::V6(v5_to_v6::convert_to_envoy_conn_v5_to_v6(x)?)), + _ => bail!("unexpected version"), + } + } + fn v6_to_v5(self) -> Result { + match self { + Self::V6(x) => Ok(Self::V5(v6_to_v5::convert_to_envoy_conn_v6_to_v5(x)?)), + _ => bail!("unexpected version"), + } + } } // MARK: ToGateway @@ -447,18 +500,19 @@ pub enum ToGateway { V3(v3::ToGateway), V4(v4::ToGateway), V5(v5::ToGateway), + V6(v6::ToGateway), } impl OwnedVersionedData for ToGateway { - type Latest = v5::ToGateway; + type Latest = v6::ToGateway; fn wrap_latest(latest: Self::Latest) -> Self { - Self::V5(latest) + Self::V6(latest) } fn unwrap_latest(self) -> Result { match self { - Self::V5(x) => Ok(x), + Self::V6(x) => Ok(x), _ => bail!("version not latest"), } } @@ -470,6 +524,7 @@ impl OwnedVersionedData for ToGateway { 3 => Ok(Self::V3(serde_bare::from_slice(payload)?)), 4 => Ok(Self::V4(serde_bare::from_slice(payload)?)), 5 => Ok(Self::V5(serde_bare::from_slice(payload)?)), + 6 => Ok(Self::V6(serde_bare::from_slice(payload)?)), _ => bail!("invalid version: {version}"), } } @@ -481,6 +536,7 @@ impl OwnedVersionedData for ToGateway { Self::V3(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V4(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V5(x) => serde_bare::to_vec(&x).map_err(Into::into), + Self::V6(x) => serde_bare::to_vec(&x).map_err(Into::into), } } @@ -490,11 +546,13 @@ impl OwnedVersionedData for ToGateway { Self::v2_to_v3, Self::v3_to_v4, Self::v4_to_v5, + Self::v5_to_v6, ] } fn serialize_converters() -> Vec Result> { vec![ + Self::v6_to_v5, Self::v5_to_v4, Self::v4_to_v3, Self::v3_to_v2, @@ -552,6 +610,18 @@ impl ToGateway { _ => bail!("unexpected version"), } } + fn v5_to_v6(self) -> Result { + match self { + Self::V5(x) => Ok(Self::V6(v5_to_v6::convert_to_gateway_v5_to_v6(x)?)), + _ => bail!("unexpected version"), + } + } + fn v6_to_v5(self) -> Result { + match self { + Self::V6(x) => Ok(Self::V5(v6_to_v5::convert_to_gateway_v6_to_v5(x)?)), + _ => bail!("unexpected version"), + } + } } // MARK: ToOutbound @@ -562,18 +632,19 @@ pub enum ToOutbound { V3(v3::ToOutbound), V4(v4::ToOutbound), V5(v5::ToOutbound), + V6(v6::ToOutbound), } impl OwnedVersionedData for ToOutbound { - type Latest = v5::ToOutbound; + type Latest = v6::ToOutbound; fn wrap_latest(latest: Self::Latest) -> Self { - Self::V5(latest) + Self::V6(latest) } fn unwrap_latest(self) -> Result { match self { - Self::V5(x) => Ok(x), + Self::V6(x) => Ok(x), _ => bail!("version not latest"), } } @@ -585,6 +656,7 @@ impl OwnedVersionedData for ToOutbound { 3 => Ok(Self::V3(serde_bare::from_slice(payload)?)), 4 => Ok(Self::V4(serde_bare::from_slice(payload)?)), 5 => Ok(Self::V5(serde_bare::from_slice(payload)?)), + 6 => Ok(Self::V6(serde_bare::from_slice(payload)?)), _ => bail!("invalid version: {version}"), } } @@ -596,6 +668,7 @@ impl OwnedVersionedData for ToOutbound { Self::V3(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V4(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V5(x) => serde_bare::to_vec(&x).map_err(Into::into), + Self::V6(x) => serde_bare::to_vec(&x).map_err(Into::into), } } @@ -605,11 +678,13 @@ impl OwnedVersionedData for ToOutbound { Self::v2_to_v3, Self::v3_to_v4, Self::v4_to_v5, + Self::v5_to_v6, ] } fn serialize_converters() -> Vec Result> { vec![ + Self::v6_to_v5, Self::v5_to_v4, Self::v4_to_v3, Self::v3_to_v2, @@ -667,6 +742,18 @@ impl ToOutbound { _ => bail!("unexpected version"), } } + fn v5_to_v6(self) -> Result { + match self { + Self::V5(x) => Ok(Self::V6(v5_to_v6::convert_to_outbound_v5_to_v6(x)?)), + _ => bail!("unexpected version"), + } + } + fn v6_to_v5(self) -> Result { + match self { + Self::V6(x) => Ok(Self::V5(v6_to_v5::convert_to_outbound_v6_to_v5(x)?)), + _ => bail!("unexpected version"), + } + } } // MARK: ActorCommandKeyData @@ -677,18 +764,19 @@ pub enum ActorCommandKeyData { V3(v3::ActorCommandKeyData), V4(v4::ActorCommandKeyData), V5(v5::ActorCommandKeyData), + V6(v6::ActorCommandKeyData), } impl OwnedVersionedData for ActorCommandKeyData { - type Latest = v5::ActorCommandKeyData; + type Latest = v6::ActorCommandKeyData; fn wrap_latest(latest: Self::Latest) -> Self { - Self::V5(latest) + Self::V6(latest) } fn unwrap_latest(self) -> Result { match self { - Self::V5(x) => Ok(x), + Self::V6(x) => Ok(x), _ => bail!("version not latest"), } } @@ -700,6 +788,7 @@ impl OwnedVersionedData for ActorCommandKeyData { 3 => Ok(Self::V3(serde_bare::from_slice(payload)?)), 4 => Ok(Self::V4(serde_bare::from_slice(payload)?)), 5 => Ok(Self::V5(serde_bare::from_slice(payload)?)), + 6 => Ok(Self::V6(serde_bare::from_slice(payload)?)), _ => bail!("invalid version: {version}"), } } @@ -711,6 +800,7 @@ impl OwnedVersionedData for ActorCommandKeyData { Self::V3(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V4(x) => serde_bare::to_vec(&x).map_err(Into::into), Self::V5(x) => serde_bare::to_vec(&x).map_err(Into::into), + Self::V6(x) => serde_bare::to_vec(&x).map_err(Into::into), } } @@ -720,11 +810,13 @@ impl OwnedVersionedData for ActorCommandKeyData { Self::v2_to_v3, Self::v3_to_v4, Self::v4_to_v5, + Self::v5_to_v6, ] } fn serialize_converters() -> Vec Result> { vec![ + Self::v6_to_v5, Self::v5_to_v4, Self::v4_to_v3, Self::v3_to_v2, @@ -798,6 +890,22 @@ impl ActorCommandKeyData { _ => bail!("unexpected version"), } } + fn v5_to_v6(self) -> Result { + match self { + Self::V5(x) => Ok(Self::V6(v5_to_v6::convert_actor_command_key_data_v5_to_v6( + x, + )?)), + _ => bail!("unexpected version"), + } + } + fn v6_to_v5(self) -> Result { + match self { + Self::V6(x) => Ok(Self::V5(v6_to_v5::convert_actor_command_key_data_v6_to_v5( + x, + )?)), + _ => bail!("unexpected version"), + } + } } // MARK: Tests @@ -810,12 +918,12 @@ mod tests { use super::{ActorCommandKeyData, ToEnvoy}; use crate::{ PROTOCOL_VERSION, - generated::{v1, v2, v5}, + generated::{v1, v2, v5, v6}, }; #[test] fn protocol_version_constant_matches_schema_version() { - assert_eq!(PROTOCOL_VERSION, 5); + assert_eq!(PROTOCOL_VERSION, 6); } #[test] @@ -840,10 +948,10 @@ mod tests { }]))?; let decoded = ToEnvoy::deserialize(&payload, 1)?; - let v5::ToEnvoy::ToEnvoyCommands(commands) = decoded else { + let v6::ToEnvoy::ToEnvoyCommands(commands) = decoded else { panic!("expected commands"); }; - let v5::Command::CommandStartActor(start) = &commands[0].inner else { + let v6::Command::CommandStartActor(start) = &commands[0].inner else { panic!("expected start actor"); }; @@ -870,9 +978,9 @@ mod tests { #[test] fn actor_command_key_data_round_trips_to_v1() -> Result<()> { - let encoded = ActorCommandKeyData::wrap_latest(v5::ActorCommandKeyData::CommandStartActor( - v5::CommandStartActor { - config: v5::ActorConfig { + let encoded = ActorCommandKeyData::wrap_latest(v6::ActorCommandKeyData::CommandStartActor( + v6::CommandStartActor { + config: v6::ActorConfig { name: "demo".into(), key: None, create_ts: 7, @@ -885,11 +993,69 @@ mod tests { .serialize(1)?; let decoded = ActorCommandKeyData::deserialize(&encoded, 1)?; - let v5::ActorCommandKeyData::CommandStartActor(start) = decoded else { + let v6::ActorCommandKeyData::CommandStartActor(start) = decoded else { panic!("expected start actor"); }; assert_eq!(start.config.name, "demo"); Ok(()) } + + #[test] + fn v5_request_abort_deserializes_with_unknown_reason() -> Result<()> { + let payload = serde_bare::to_vec(&v5::ToEnvoy::ToEnvoyTunnelMessage( + v5::ToEnvoyTunnelMessage { + message_id: v5::MessageId { + gateway_id: [1; 4], + request_id: [7; 4], + message_index: 1, + }, + message_kind: v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort, + }, + ))?; + + let decoded = ToEnvoy::deserialize(&payload, 5)?; + let v6::ToEnvoy::ToEnvoyTunnelMessage(msg) = decoded else { + panic!("expected tunnel message"); + }; + let v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(abort) = msg.message_kind else { + panic!("expected request abort"); + }; + + assert_eq!(abort.reason.kind, v6::HttpStreamAbortReasonKind::Unknown); + assert!(abort.reason.detail.is_none()); + Ok(()) + } + + #[test] + fn v6_request_abort_serializes_to_v5_void_abort() -> Result<()> { + let encoded = ToEnvoy::wrap_latest(v6::ToEnvoy::ToEnvoyTunnelMessage( + v6::ToEnvoyTunnelMessage { + message_id: v6::MessageId { + gateway_id: [1; 4], + request_id: [7; 4], + message_index: 1, + }, + message_kind: v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort( + v6::ToEnvoyRequestAbort { + reason: v6::HttpStreamAbortReason { + kind: v6::HttpStreamAbortReasonKind::ClientDisconnect, + detail: Some("client closed connection".into()), + }, + }, + ), + }, + )) + .serialize(5)?; + + let decoded: v5::ToEnvoy = serde_bare::from_slice(&encoded)?; + let v5::ToEnvoy::ToEnvoyTunnelMessage(msg) = decoded else { + panic!("expected tunnel message"); + }; + assert!(matches!( + msg.message_kind, + v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort + )); + Ok(()) + } } diff --git a/engine/sdks/rust/envoy-protocol/src/versioned/v5_to_v6.rs b/engine/sdks/rust/envoy-protocol/src/versioned/v5_to_v6.rs new file mode 100644 index 0000000000..d734e550c6 --- /dev/null +++ b/engine/sdks/rust/envoy-protocol/src/versioned/v5_to_v6.rs @@ -0,0 +1,786 @@ +// @generated initial scaffold by scripts/vbare-gen-converters +// from: v5.bare, to: v6.bare +// Replace each todo!() with the migration semantics, then drop the @generated marker. + +#![allow(dead_code, unused_variables)] + +use anyhow::Result; + +use crate::generated::{v5, v6}; + +pub fn convert_kv_metadata_v5_to_v6(x: v5::KvMetadata) -> Result { + Ok(v6::KvMetadata { + version: x.version, + update_ts: x.update_ts, + }) +} + +pub fn convert_kv_list_range_query_v5_to_v6(x: v5::KvListRangeQuery) -> Result { + Ok(v6::KvListRangeQuery { + start: x.start, + end: x.end, + exclusive: x.exclusive, + }) +} + +pub fn convert_kv_list_prefix_query_v5_to_v6(x: v5::KvListPrefixQuery) -> Result { + Ok(v6::KvListPrefixQuery { + key: x.key, + }) +} + +pub fn convert_kv_list_query_v5_to_v6(x: v5::KvListQuery) -> Result { + Ok(match x { + v5::KvListQuery::KvListAllQuery => v6::KvListQuery::KvListAllQuery, + v5::KvListQuery::KvListRangeQuery(v) => v6::KvListQuery::KvListRangeQuery(convert_kv_list_range_query_v5_to_v6(v)?), + v5::KvListQuery::KvListPrefixQuery(v) => v6::KvListQuery::KvListPrefixQuery(convert_kv_list_prefix_query_v5_to_v6(v)?), + }) +} + +pub fn convert_kv_get_request_v5_to_v6(x: v5::KvGetRequest) -> Result { + Ok(v6::KvGetRequest { + keys: x.keys, + }) +} + +pub fn convert_kv_list_request_v5_to_v6(x: v5::KvListRequest) -> Result { + Ok(v6::KvListRequest { + query: convert_kv_list_query_v5_to_v6(x.query)?, + reverse: x.reverse, + limit: x.limit, + }) +} + +pub fn convert_kv_put_request_v5_to_v6(x: v5::KvPutRequest) -> Result { + Ok(v6::KvPutRequest { + keys: x.keys, + values: x.values, + }) +} + +pub fn convert_kv_delete_request_v5_to_v6(x: v5::KvDeleteRequest) -> Result { + Ok(v6::KvDeleteRequest { + keys: x.keys, + }) +} + +pub fn convert_kv_delete_range_request_v5_to_v6(x: v5::KvDeleteRangeRequest) -> Result { + Ok(v6::KvDeleteRangeRequest { + start: x.start, + end: x.end, + }) +} + +pub fn convert_kv_error_response_v5_to_v6(x: v5::KvErrorResponse) -> Result { + Ok(v6::KvErrorResponse { + message: x.message, + }) +} + +pub fn convert_kv_get_response_v5_to_v6(x: v5::KvGetResponse) -> Result { + Ok(v6::KvGetResponse { + keys: x.keys, + values: x.values, + metadata: x.metadata.into_iter().map(|v| convert_kv_metadata_v5_to_v6(v)).collect::>>()?, + }) +} + +pub fn convert_kv_list_response_v5_to_v6(x: v5::KvListResponse) -> Result { + Ok(v6::KvListResponse { + keys: x.keys, + values: x.values, + metadata: x.metadata.into_iter().map(|v| convert_kv_metadata_v5_to_v6(v)).collect::>>()?, + }) +} + +pub fn convert_kv_request_data_v5_to_v6(x: v5::KvRequestData) -> Result { + Ok(match x { + v5::KvRequestData::KvGetRequest(v) => v6::KvRequestData::KvGetRequest(convert_kv_get_request_v5_to_v6(v)?), + v5::KvRequestData::KvListRequest(v) => v6::KvRequestData::KvListRequest(convert_kv_list_request_v5_to_v6(v)?), + v5::KvRequestData::KvPutRequest(v) => v6::KvRequestData::KvPutRequest(convert_kv_put_request_v5_to_v6(v)?), + v5::KvRequestData::KvDeleteRequest(v) => v6::KvRequestData::KvDeleteRequest(convert_kv_delete_request_v5_to_v6(v)?), + v5::KvRequestData::KvDeleteRangeRequest(v) => v6::KvRequestData::KvDeleteRangeRequest(convert_kv_delete_range_request_v5_to_v6(v)?), + v5::KvRequestData::KvDropRequest => v6::KvRequestData::KvDropRequest, + }) +} + +pub fn convert_kv_response_data_v5_to_v6(x: v5::KvResponseData) -> Result { + Ok(match x { + v5::KvResponseData::KvErrorResponse(v) => v6::KvResponseData::KvErrorResponse(convert_kv_error_response_v5_to_v6(v)?), + v5::KvResponseData::KvGetResponse(v) => v6::KvResponseData::KvGetResponse(convert_kv_get_response_v5_to_v6(v)?), + v5::KvResponseData::KvListResponse(v) => v6::KvResponseData::KvListResponse(convert_kv_list_response_v5_to_v6(v)?), + v5::KvResponseData::KvPutResponse => v6::KvResponseData::KvPutResponse, + v5::KvResponseData::KvDeleteResponse => v6::KvResponseData::KvDeleteResponse, + v5::KvResponseData::KvDropResponse => v6::KvResponseData::KvDropResponse, + }) +} + +pub fn convert_sqlite_dirty_page_v5_to_v6(x: v5::SqliteDirtyPage) -> Result { + Ok(v6::SqliteDirtyPage { + pgno: x.pgno, + bytes: x.bytes, + }) +} + +pub fn convert_sqlite_fetched_page_v5_to_v6(x: v5::SqliteFetchedPage) -> Result { + Ok(v6::SqliteFetchedPage { + pgno: x.pgno, + bytes: x.bytes, + }) +} + +pub fn convert_sqlite_get_pages_request_v5_to_v6(x: v5::SqliteGetPagesRequest) -> Result { + Ok(v6::SqliteGetPagesRequest { + actor_id: x.actor_id, + pgnos: x.pgnos, + expected_generation: x.expected_generation, + expected_head_txid: x.expected_head_txid, + }) +} + +pub fn convert_sqlite_get_pages_ok_v5_to_v6(x: v5::SqliteGetPagesOk) -> Result { + Ok(v6::SqliteGetPagesOk { + pages: x.pages.into_iter().map(|v| convert_sqlite_fetched_page_v5_to_v6(v)).collect::>>()?, + head_txid: x.head_txid, + }) +} + +pub fn convert_sqlite_error_response_v5_to_v6(x: v5::SqliteErrorResponse) -> Result { + Ok(v6::SqliteErrorResponse { + group: x.group, + code: x.code, + message: x.message, + }) +} + +pub fn convert_sqlite_get_pages_response_v5_to_v6(x: v5::SqliteGetPagesResponse) -> Result { + Ok(match x { + v5::SqliteGetPagesResponse::SqliteGetPagesOk(v) => v6::SqliteGetPagesResponse::SqliteGetPagesOk(convert_sqlite_get_pages_ok_v5_to_v6(v)?), + v5::SqliteGetPagesResponse::SqliteErrorResponse(v) => v6::SqliteGetPagesResponse::SqliteErrorResponse(convert_sqlite_error_response_v5_to_v6(v)?), + }) +} + +pub fn convert_sqlite_commit_request_v5_to_v6(x: v5::SqliteCommitRequest) -> Result { + Ok(v6::SqliteCommitRequest { + actor_id: x.actor_id, + dirty_pages: x.dirty_pages.into_iter().map(|v| convert_sqlite_dirty_page_v5_to_v6(v)).collect::>>()?, + db_size_pages: x.db_size_pages, + now_ms: x.now_ms, + expected_generation: x.expected_generation, + expected_head_txid: x.expected_head_txid, + }) +} + +pub fn convert_sqlite_commit_ok_v5_to_v6(x: v5::SqliteCommitOk) -> Result { + Ok(v6::SqliteCommitOk { + head_txid: x.head_txid, + }) +} + +pub fn convert_sqlite_commit_response_v5_to_v6(x: v5::SqliteCommitResponse) -> Result { + Ok(match x { + v5::SqliteCommitResponse::SqliteCommitOk(v) => v6::SqliteCommitResponse::SqliteCommitOk(convert_sqlite_commit_ok_v5_to_v6(v)?), + v5::SqliteCommitResponse::SqliteErrorResponse(v) => v6::SqliteCommitResponse::SqliteErrorResponse(convert_sqlite_error_response_v5_to_v6(v)?), + }) +} + +pub fn convert_sqlite_value_integer_v5_to_v6(x: v5::SqliteValueInteger) -> Result { + Ok(v6::SqliteValueInteger { + value: x.value, + }) +} + +pub fn convert_sqlite_value_float_v5_to_v6(x: v5::SqliteValueFloat) -> Result { + Ok(v6::SqliteValueFloat { + value: x.value, + }) +} + +pub fn convert_sqlite_value_text_v5_to_v6(x: v5::SqliteValueText) -> Result { + Ok(v6::SqliteValueText { + value: x.value, + }) +} + +pub fn convert_sqlite_value_blob_v5_to_v6(x: v5::SqliteValueBlob) -> Result { + Ok(v6::SqliteValueBlob { + value: x.value, + }) +} + +pub fn convert_sqlite_bind_param_v5_to_v6(x: v5::SqliteBindParam) -> Result { + Ok(match x { + v5::SqliteBindParam::SqliteValueNull => v6::SqliteBindParam::SqliteValueNull, + v5::SqliteBindParam::SqliteValueInteger(v) => v6::SqliteBindParam::SqliteValueInteger(convert_sqlite_value_integer_v5_to_v6(v)?), + v5::SqliteBindParam::SqliteValueFloat(v) => v6::SqliteBindParam::SqliteValueFloat(convert_sqlite_value_float_v5_to_v6(v)?), + v5::SqliteBindParam::SqliteValueText(v) => v6::SqliteBindParam::SqliteValueText(convert_sqlite_value_text_v5_to_v6(v)?), + v5::SqliteBindParam::SqliteValueBlob(v) => v6::SqliteBindParam::SqliteValueBlob(convert_sqlite_value_blob_v5_to_v6(v)?), + }) +} + +pub fn convert_sqlite_column_value_v5_to_v6(x: v5::SqliteColumnValue) -> Result { + Ok(match x { + v5::SqliteColumnValue::SqliteValueNull => v6::SqliteColumnValue::SqliteValueNull, + v5::SqliteColumnValue::SqliteValueInteger(v) => v6::SqliteColumnValue::SqliteValueInteger(convert_sqlite_value_integer_v5_to_v6(v)?), + v5::SqliteColumnValue::SqliteValueFloat(v) => v6::SqliteColumnValue::SqliteValueFloat(convert_sqlite_value_float_v5_to_v6(v)?), + v5::SqliteColumnValue::SqliteValueText(v) => v6::SqliteColumnValue::SqliteValueText(convert_sqlite_value_text_v5_to_v6(v)?), + v5::SqliteColumnValue::SqliteValueBlob(v) => v6::SqliteColumnValue::SqliteValueBlob(convert_sqlite_value_blob_v5_to_v6(v)?), + }) +} + +pub fn convert_sqlite_query_result_v5_to_v6(x: v5::SqliteQueryResult) -> Result { + Ok(v6::SqliteQueryResult { + columns: x.columns, + rows: x.rows.into_iter().map(|v| v.into_iter().map(|v| convert_sqlite_column_value_v5_to_v6(v)).collect::>>()).collect::>>()?, + }) +} + +pub fn convert_sqlite_execute_result_v5_to_v6(x: v5::SqliteExecuteResult) -> Result { + Ok(v6::SqliteExecuteResult { + columns: x.columns, + rows: x.rows.into_iter().map(|v| v.into_iter().map(|v| convert_sqlite_column_value_v5_to_v6(v)).collect::>>()).collect::>>()?, + changes: x.changes, + last_insert_row_id: x.last_insert_row_id, + }) +} + +pub fn convert_sqlite_exec_request_v5_to_v6(x: v5::SqliteExecRequest) -> Result { + Ok(v6::SqliteExecRequest { + namespace_id: x.namespace_id, + actor_id: x.actor_id, + generation: x.generation, + sql: x.sql, + }) +} + +pub fn convert_sqlite_execute_request_v5_to_v6(x: v5::SqliteExecuteRequest) -> Result { + Ok(v6::SqliteExecuteRequest { + namespace_id: x.namespace_id, + actor_id: x.actor_id, + generation: x.generation, + sql: x.sql, + params: x.params.map(|v| v.into_iter().map(|v| convert_sqlite_bind_param_v5_to_v6(v)).collect::>>()).transpose()?, + }) +} + +pub fn convert_sqlite_exec_ok_v5_to_v6(x: v5::SqliteExecOk) -> Result { + Ok(v6::SqliteExecOk { + result: convert_sqlite_query_result_v5_to_v6(x.result)?, + }) +} + +pub fn convert_sqlite_execute_ok_v5_to_v6(x: v5::SqliteExecuteOk) -> Result { + Ok(v6::SqliteExecuteOk { + result: convert_sqlite_execute_result_v5_to_v6(x.result)?, + }) +} + +pub fn convert_sqlite_exec_response_v5_to_v6(x: v5::SqliteExecResponse) -> Result { + Ok(match x { + v5::SqliteExecResponse::SqliteExecOk(v) => v6::SqliteExecResponse::SqliteExecOk(convert_sqlite_exec_ok_v5_to_v6(v)?), + v5::SqliteExecResponse::SqliteErrorResponse(v) => v6::SqliteExecResponse::SqliteErrorResponse(convert_sqlite_error_response_v5_to_v6(v)?), + }) +} + +pub fn convert_sqlite_execute_response_v5_to_v6(x: v5::SqliteExecuteResponse) -> Result { + Ok(match x { + v5::SqliteExecuteResponse::SqliteExecuteOk(v) => v6::SqliteExecuteResponse::SqliteExecuteOk(convert_sqlite_execute_ok_v5_to_v6(v)?), + v5::SqliteExecuteResponse::SqliteErrorResponse(v) => v6::SqliteExecuteResponse::SqliteErrorResponse(convert_sqlite_error_response_v5_to_v6(v)?), + }) +} + +pub fn convert_stop_code_v5_to_v6(x: v5::StopCode) -> Result { + Ok(match x { + v5::StopCode::Ok => v6::StopCode::Ok, + v5::StopCode::Error => v6::StopCode::Error, + }) +} + +pub fn convert_actor_name_v5_to_v6(x: v5::ActorName) -> Result { + Ok(v6::ActorName { + metadata: x.metadata, + }) +} + +pub fn convert_actor_config_v5_to_v6(x: v5::ActorConfig) -> Result { + Ok(v6::ActorConfig { + name: x.name, + key: x.key, + create_ts: x.create_ts, + input: x.input, + }) +} + +pub fn convert_actor_checkpoint_v5_to_v6(x: v5::ActorCheckpoint) -> Result { + Ok(v6::ActorCheckpoint { + actor_id: x.actor_id, + generation: x.generation, + index: x.index, + }) +} + +pub fn convert_actor_intent_v5_to_v6(x: v5::ActorIntent) -> Result { + Ok(match x { + v5::ActorIntent::ActorIntentSleep => v6::ActorIntent::ActorIntentSleep, + v5::ActorIntent::ActorIntentStop => v6::ActorIntent::ActorIntentStop, + }) +} + +pub fn convert_actor_state_stopped_v5_to_v6(x: v5::ActorStateStopped) -> Result { + Ok(v6::ActorStateStopped { + code: convert_stop_code_v5_to_v6(x.code)?, + message: x.message, + }) +} + +pub fn convert_actor_state_v5_to_v6(x: v5::ActorState) -> Result { + Ok(match x { + v5::ActorState::ActorStateRunning => v6::ActorState::ActorStateRunning, + v5::ActorState::ActorStateStopped(v) => v6::ActorState::ActorStateStopped(convert_actor_state_stopped_v5_to_v6(v)?), + }) +} + +pub fn convert_event_actor_intent_v5_to_v6(x: v5::EventActorIntent) -> Result { + Ok(v6::EventActorIntent { + intent: convert_actor_intent_v5_to_v6(x.intent)?, + }) +} + +pub fn convert_event_actor_state_update_v5_to_v6(x: v5::EventActorStateUpdate) -> Result { + Ok(v6::EventActorStateUpdate { + state: convert_actor_state_v5_to_v6(x.state)?, + }) +} + +pub fn convert_event_actor_set_alarm_v5_to_v6(x: v5::EventActorSetAlarm) -> Result { + Ok(v6::EventActorSetAlarm { + alarm_ts: x.alarm_ts, + }) +} + +pub fn convert_event_v5_to_v6(x: v5::Event) -> Result { + Ok(match x { + v5::Event::EventActorIntent(v) => v6::Event::EventActorIntent(convert_event_actor_intent_v5_to_v6(v)?), + v5::Event::EventActorStateUpdate(v) => v6::Event::EventActorStateUpdate(convert_event_actor_state_update_v5_to_v6(v)?), + v5::Event::EventActorSetAlarm(v) => v6::Event::EventActorSetAlarm(convert_event_actor_set_alarm_v5_to_v6(v)?), + }) +} + +pub fn convert_event_wrapper_v5_to_v6(x: v5::EventWrapper) -> Result { + Ok(v6::EventWrapper { + checkpoint: convert_actor_checkpoint_v5_to_v6(x.checkpoint)?, + inner: convert_event_v5_to_v6(x.inner)?, + }) +} + +pub fn convert_preloaded_kv_entry_v5_to_v6(x: v5::PreloadedKvEntry) -> Result { + Ok(v6::PreloadedKvEntry { + key: x.key, + value: x.value, + metadata: convert_kv_metadata_v5_to_v6(x.metadata)?, + }) +} + +pub fn convert_preloaded_kv_v5_to_v6(x: v5::PreloadedKv) -> Result { + Ok(v6::PreloadedKv { + entries: x.entries.into_iter().map(|v| convert_preloaded_kv_entry_v5_to_v6(v)).collect::>>()?, + requested_get_keys: x.requested_get_keys, + requested_prefixes: x.requested_prefixes, + }) +} + +pub fn convert_hibernating_request_v5_to_v6(x: v5::HibernatingRequest) -> Result { + Ok(v6::HibernatingRequest { + gateway_id: x.gateway_id, + request_id: x.request_id, + }) +} + +pub fn convert_command_start_actor_v5_to_v6(x: v5::CommandStartActor) -> Result { + Ok(v6::CommandStartActor { + config: convert_actor_config_v5_to_v6(x.config)?, + hibernating_requests: x.hibernating_requests.into_iter().map(|v| convert_hibernating_request_v5_to_v6(v)).collect::>>()?, + preloaded_kv: x.preloaded_kv.map(|v| convert_preloaded_kv_v5_to_v6(v)).transpose()?, + }) +} + +pub fn convert_stop_actor_reason_v5_to_v6(x: v5::StopActorReason) -> Result { + Ok(match x { + v5::StopActorReason::SleepIntent => v6::StopActorReason::SleepIntent, + v5::StopActorReason::StopIntent => v6::StopActorReason::StopIntent, + v5::StopActorReason::Destroy => v6::StopActorReason::Destroy, + v5::StopActorReason::GoingAway => v6::StopActorReason::GoingAway, + v5::StopActorReason::Lost => v6::StopActorReason::Lost, + }) +} + +pub fn convert_command_stop_actor_v5_to_v6(x: v5::CommandStopActor) -> Result { + Ok(v6::CommandStopActor { + reason: convert_stop_actor_reason_v5_to_v6(x.reason)?, + }) +} + +pub fn convert_command_v5_to_v6(x: v5::Command) -> Result { + Ok(match x { + v5::Command::CommandStartActor(v) => v6::Command::CommandStartActor(convert_command_start_actor_v5_to_v6(v)?), + v5::Command::CommandStopActor(v) => v6::Command::CommandStopActor(convert_command_stop_actor_v5_to_v6(v)?), + }) +} + +pub fn convert_command_wrapper_v5_to_v6(x: v5::CommandWrapper) -> Result { + Ok(v6::CommandWrapper { + checkpoint: convert_actor_checkpoint_v5_to_v6(x.checkpoint)?, + inner: convert_command_v5_to_v6(x.inner)?, + }) +} + +pub fn convert_actor_command_key_data_v5_to_v6(x: v5::ActorCommandKeyData) -> Result { + Ok(match x { + v5::ActorCommandKeyData::CommandStartActor(v) => v6::ActorCommandKeyData::CommandStartActor(convert_command_start_actor_v5_to_v6(v)?), + v5::ActorCommandKeyData::CommandStopActor(v) => v6::ActorCommandKeyData::CommandStopActor(convert_command_stop_actor_v5_to_v6(v)?), + }) +} + +pub fn convert_message_id_v5_to_v6(x: v5::MessageId) -> Result { + Ok(v6::MessageId { + gateway_id: x.gateway_id, + request_id: x.request_id, + message_index: x.message_index, + }) +} + +pub fn convert_to_envoy_request_start_v5_to_v6(x: v5::ToEnvoyRequestStart) -> Result { + Ok(v6::ToEnvoyRequestStart { + actor_id: x.actor_id, + method: x.method, + path: x.path, + headers: x.headers, + body: x.body, + stream: x.stream, + }) +} + +pub fn convert_to_envoy_request_chunk_v5_to_v6(x: v5::ToEnvoyRequestChunk) -> Result { + Ok(v6::ToEnvoyRequestChunk { + body: x.body, + finish: x.finish, + }) +} + +pub fn convert_to_rivet_response_start_v5_to_v6(x: v5::ToRivetResponseStart) -> Result { + Ok(v6::ToRivetResponseStart { + status: x.status, + headers: x.headers, + body: x.body, + stream: x.stream, + }) +} + +pub fn convert_to_rivet_response_chunk_v5_to_v6(x: v5::ToRivetResponseChunk) -> Result { + Ok(v6::ToRivetResponseChunk { + body: x.body, + finish: x.finish, + }) +} + +pub fn convert_to_envoy_web_socket_open_v5_to_v6(x: v5::ToEnvoyWebSocketOpen) -> Result { + Ok(v6::ToEnvoyWebSocketOpen { + actor_id: x.actor_id, + path: x.path, + headers: x.headers, + }) +} + +pub fn convert_to_envoy_web_socket_message_v5_to_v6(x: v5::ToEnvoyWebSocketMessage) -> Result { + Ok(v6::ToEnvoyWebSocketMessage { + data: x.data, + binary: x.binary, + }) +} + +pub fn convert_to_envoy_web_socket_close_v5_to_v6(x: v5::ToEnvoyWebSocketClose) -> Result { + Ok(v6::ToEnvoyWebSocketClose { + code: x.code, + reason: x.reason, + }) +} + +pub fn convert_to_rivet_web_socket_open_v5_to_v6(x: v5::ToRivetWebSocketOpen) -> Result { + Ok(v6::ToRivetWebSocketOpen { + can_hibernate: x.can_hibernate, + }) +} + +pub fn convert_to_rivet_web_socket_message_v5_to_v6(x: v5::ToRivetWebSocketMessage) -> Result { + Ok(v6::ToRivetWebSocketMessage { + data: x.data, + binary: x.binary, + }) +} + +pub fn convert_to_rivet_web_socket_message_ack_v5_to_v6(x: v5::ToRivetWebSocketMessageAck) -> Result { + Ok(v6::ToRivetWebSocketMessageAck { + index: x.index, + }) +} + +pub fn convert_to_rivet_web_socket_close_v5_to_v6(x: v5::ToRivetWebSocketClose) -> Result { + Ok(v6::ToRivetWebSocketClose { + code: x.code, + reason: x.reason, + hibernate: x.hibernate, + }) +} + +pub fn convert_to_rivet_tunnel_message_kind_v5_to_v6(x: v5::ToRivetTunnelMessageKind) -> Result { + Ok(match x { + v5::ToRivetTunnelMessageKind::ToRivetResponseStart(v) => v6::ToRivetTunnelMessageKind::ToRivetResponseStart(convert_to_rivet_response_start_v5_to_v6(v)?), + v5::ToRivetTunnelMessageKind::ToRivetResponseChunk(v) => v6::ToRivetTunnelMessageKind::ToRivetResponseChunk(convert_to_rivet_response_chunk_v5_to_v6(v)?), + v5::ToRivetTunnelMessageKind::ToRivetResponseAbort => { + v6::ToRivetTunnelMessageKind::ToRivetResponseAbort(v6::ToRivetResponseAbort { + reason: v6::HttpStreamAbortReason { + kind: v6::HttpStreamAbortReasonKind::Unknown, + detail: None, + }, + }) + } + v5::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(v) => v6::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(convert_to_rivet_web_socket_open_v5_to_v6(v)?), + v5::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(v) => v6::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(convert_to_rivet_web_socket_message_v5_to_v6(v)?), + v5::ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(v) => v6::ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(convert_to_rivet_web_socket_message_ack_v5_to_v6(v)?), + v5::ToRivetTunnelMessageKind::ToRivetWebSocketClose(v) => v6::ToRivetTunnelMessageKind::ToRivetWebSocketClose(convert_to_rivet_web_socket_close_v5_to_v6(v)?), + }) +} + +pub fn convert_to_rivet_tunnel_message_v5_to_v6(x: v5::ToRivetTunnelMessage) -> Result { + Ok(v6::ToRivetTunnelMessage { + message_id: convert_message_id_v5_to_v6(x.message_id)?, + message_kind: convert_to_rivet_tunnel_message_kind_v5_to_v6(x.message_kind)?, + }) +} + +pub fn convert_to_envoy_tunnel_message_kind_v5_to_v6(x: v5::ToEnvoyTunnelMessageKind) -> Result { + Ok(match x { + v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(v) => v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(convert_to_envoy_request_start_v5_to_v6(v)?), + v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(v) => v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(convert_to_envoy_request_chunk_v5_to_v6(v)?), + v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort => { + v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(v6::ToEnvoyRequestAbort { + reason: v6::HttpStreamAbortReason { + kind: v6::HttpStreamAbortReasonKind::Unknown, + detail: None, + }, + }) + } + v5::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(v) => v6::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(convert_to_envoy_web_socket_open_v5_to_v6(v)?), + v5::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(v) => v6::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(convert_to_envoy_web_socket_message_v5_to_v6(v)?), + v5::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(v) => v6::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(convert_to_envoy_web_socket_close_v5_to_v6(v)?), + }) +} + +pub fn convert_to_envoy_tunnel_message_v5_to_v6(x: v5::ToEnvoyTunnelMessage) -> Result { + Ok(v6::ToEnvoyTunnelMessage { + message_id: convert_message_id_v5_to_v6(x.message_id)?, + message_kind: convert_to_envoy_tunnel_message_kind_v5_to_v6(x.message_kind)?, + }) +} + +pub fn convert_to_envoy_ping_v5_to_v6(x: v5::ToEnvoyPing) -> Result { + Ok(v6::ToEnvoyPing { + ts: x.ts, + }) +} + +pub fn convert_to_rivet_metadata_v5_to_v6(x: v5::ToRivetMetadata) -> Result { + Ok(v6::ToRivetMetadata { + prepopulate_actor_names: x.prepopulate_actor_names.map(|v| v.into_iter().map(|(k, v)| -> Result<_> { Ok((k, convert_actor_name_v5_to_v6(v)?)) }).collect::>()).transpose()?, + metadata: x.metadata, + }) +} + +pub fn convert_to_rivet_events_v5_to_v6(x: v5::ToRivetEvents) -> Result { + Ok(x.into_iter().map(|v| convert_event_wrapper_v5_to_v6(v)).collect::>>()?) +} + +pub fn convert_to_rivet_ack_commands_v5_to_v6(x: v5::ToRivetAckCommands) -> Result { + Ok(v6::ToRivetAckCommands { + last_command_checkpoints: x.last_command_checkpoints.into_iter().map(|v| convert_actor_checkpoint_v5_to_v6(v)).collect::>>()?, + }) +} + +pub fn convert_to_rivet_pong_v5_to_v6(x: v5::ToRivetPong) -> Result { + Ok(v6::ToRivetPong { + ts: x.ts, + }) +} + +pub fn convert_to_rivet_kv_request_v5_to_v6(x: v5::ToRivetKvRequest) -> Result { + Ok(v6::ToRivetKvRequest { + actor_id: x.actor_id, + request_id: x.request_id, + data: convert_kv_request_data_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_get_pages_request_v5_to_v6(x: v5::ToRivetSqliteGetPagesRequest) -> Result { + Ok(v6::ToRivetSqliteGetPagesRequest { + request_id: x.request_id, + data: convert_sqlite_get_pages_request_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_commit_request_v5_to_v6(x: v5::ToRivetSqliteCommitRequest) -> Result { + Ok(v6::ToRivetSqliteCommitRequest { + request_id: x.request_id, + data: convert_sqlite_commit_request_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_exec_request_v5_to_v6(x: v5::ToRivetSqliteExecRequest) -> Result { + Ok(v6::ToRivetSqliteExecRequest { + request_id: x.request_id, + data: convert_sqlite_exec_request_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_execute_request_v5_to_v6(x: v5::ToRivetSqliteExecuteRequest) -> Result { + Ok(v6::ToRivetSqliteExecuteRequest { + request_id: x.request_id, + data: convert_sqlite_execute_request_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_rivet_v5_to_v6(x: v5::ToRivet) -> Result { + Ok(match x { + v5::ToRivet::ToRivetMetadata(v) => v6::ToRivet::ToRivetMetadata(convert_to_rivet_metadata_v5_to_v6(v)?), + v5::ToRivet::ToRivetEvents(v) => v6::ToRivet::ToRivetEvents(convert_to_rivet_events_v5_to_v6(v)?), + v5::ToRivet::ToRivetAckCommands(v) => v6::ToRivet::ToRivetAckCommands(convert_to_rivet_ack_commands_v5_to_v6(v)?), + v5::ToRivet::ToRivetStopping => v6::ToRivet::ToRivetStopping, + v5::ToRivet::ToRivetPong(v) => v6::ToRivet::ToRivetPong(convert_to_rivet_pong_v5_to_v6(v)?), + v5::ToRivet::ToRivetKvRequest(v) => v6::ToRivet::ToRivetKvRequest(convert_to_rivet_kv_request_v5_to_v6(v)?), + v5::ToRivet::ToRivetTunnelMessage(v) => v6::ToRivet::ToRivetTunnelMessage(convert_to_rivet_tunnel_message_v5_to_v6(v)?), + v5::ToRivet::ToRivetSqliteGetPagesRequest(v) => v6::ToRivet::ToRivetSqliteGetPagesRequest(convert_to_rivet_sqlite_get_pages_request_v5_to_v6(v)?), + v5::ToRivet::ToRivetSqliteCommitRequest(v) => v6::ToRivet::ToRivetSqliteCommitRequest(convert_to_rivet_sqlite_commit_request_v5_to_v6(v)?), + v5::ToRivet::ToRivetSqliteExecRequest(v) => v6::ToRivet::ToRivetSqliteExecRequest(convert_to_rivet_sqlite_exec_request_v5_to_v6(v)?), + v5::ToRivet::ToRivetSqliteExecuteRequest(v) => v6::ToRivet::ToRivetSqliteExecuteRequest(convert_to_rivet_sqlite_execute_request_v5_to_v6(v)?), + }) +} + +pub fn convert_protocol_metadata_v5_to_v6(x: v5::ProtocolMetadata) -> Result { + Ok(v6::ProtocolMetadata { + envoy_lost_threshold: x.envoy_lost_threshold, + actor_stop_threshold: x.actor_stop_threshold, + max_response_payload_size: x.max_response_payload_size, + }) +} + +pub fn convert_to_envoy_init_v5_to_v6(x: v5::ToEnvoyInit) -> Result { + Ok(v6::ToEnvoyInit { + metadata: convert_protocol_metadata_v5_to_v6(x.metadata)?, + }) +} + +pub fn convert_to_envoy_commands_v5_to_v6(x: v5::ToEnvoyCommands) -> Result { + Ok(x.into_iter().map(|v| convert_command_wrapper_v5_to_v6(v)).collect::>>()?) +} + +pub fn convert_to_envoy_ack_events_v5_to_v6(x: v5::ToEnvoyAckEvents) -> Result { + Ok(v6::ToEnvoyAckEvents { + last_event_checkpoints: x.last_event_checkpoints.into_iter().map(|v| convert_actor_checkpoint_v5_to_v6(v)).collect::>>()?, + }) +} + +pub fn convert_to_envoy_kv_response_v5_to_v6(x: v5::ToEnvoyKvResponse) -> Result { + Ok(v6::ToEnvoyKvResponse { + request_id: x.request_id, + data: convert_kv_response_data_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_get_pages_response_v5_to_v6(x: v5::ToEnvoySqliteGetPagesResponse) -> Result { + Ok(v6::ToEnvoySqliteGetPagesResponse { + request_id: x.request_id, + data: convert_sqlite_get_pages_response_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_commit_response_v5_to_v6(x: v5::ToEnvoySqliteCommitResponse) -> Result { + Ok(v6::ToEnvoySqliteCommitResponse { + request_id: x.request_id, + data: convert_sqlite_commit_response_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_exec_response_v5_to_v6(x: v5::ToEnvoySqliteExecResponse) -> Result { + Ok(v6::ToEnvoySqliteExecResponse { + request_id: x.request_id, + data: convert_sqlite_exec_response_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_execute_response_v5_to_v6(x: v5::ToEnvoySqliteExecuteResponse) -> Result { + Ok(v6::ToEnvoySqliteExecuteResponse { + request_id: x.request_id, + data: convert_sqlite_execute_response_v5_to_v6(x.data)?, + }) +} + +pub fn convert_to_envoy_v5_to_v6(x: v5::ToEnvoy) -> Result { + Ok(match x { + v5::ToEnvoy::ToEnvoyInit(v) => v6::ToEnvoy::ToEnvoyInit(convert_to_envoy_init_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoyCommands(v) => v6::ToEnvoy::ToEnvoyCommands(convert_to_envoy_commands_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoyAckEvents(v) => v6::ToEnvoy::ToEnvoyAckEvents(convert_to_envoy_ack_events_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoyKvResponse(v) => v6::ToEnvoy::ToEnvoyKvResponse(convert_to_envoy_kv_response_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoyTunnelMessage(v) => v6::ToEnvoy::ToEnvoyTunnelMessage(convert_to_envoy_tunnel_message_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoyPing(v) => v6::ToEnvoy::ToEnvoyPing(convert_to_envoy_ping_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoySqliteGetPagesResponse(v) => v6::ToEnvoy::ToEnvoySqliteGetPagesResponse(convert_to_envoy_sqlite_get_pages_response_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoySqliteCommitResponse(v) => v6::ToEnvoy::ToEnvoySqliteCommitResponse(convert_to_envoy_sqlite_commit_response_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoySqliteExecResponse(v) => v6::ToEnvoy::ToEnvoySqliteExecResponse(convert_to_envoy_sqlite_exec_response_v5_to_v6(v)?), + v5::ToEnvoy::ToEnvoySqliteExecuteResponse(v) => v6::ToEnvoy::ToEnvoySqliteExecuteResponse(convert_to_envoy_sqlite_execute_response_v5_to_v6(v)?), + }) +} + +pub fn convert_to_envoy_conn_ping_v5_to_v6(x: v5::ToEnvoyConnPing) -> Result { + Ok(v6::ToEnvoyConnPing { + gateway_id: x.gateway_id, + request_id: x.request_id, + ts: x.ts, + }) +} + +pub fn convert_to_envoy_conn_v5_to_v6(x: v5::ToEnvoyConn) -> Result { + Ok(match x { + v5::ToEnvoyConn::ToEnvoyConnPing(v) => v6::ToEnvoyConn::ToEnvoyConnPing(convert_to_envoy_conn_ping_v5_to_v6(v)?), + v5::ToEnvoyConn::ToEnvoyConnClose => v6::ToEnvoyConn::ToEnvoyConnClose, + v5::ToEnvoyConn::ToEnvoyCommands(v) => v6::ToEnvoyConn::ToEnvoyCommands(convert_to_envoy_commands_v5_to_v6(v)?), + v5::ToEnvoyConn::ToEnvoyAckEvents(v) => v6::ToEnvoyConn::ToEnvoyAckEvents(convert_to_envoy_ack_events_v5_to_v6(v)?), + v5::ToEnvoyConn::ToEnvoyTunnelMessage(v) => v6::ToEnvoyConn::ToEnvoyTunnelMessage(convert_to_envoy_tunnel_message_v5_to_v6(v)?), + }) +} + +pub fn convert_to_gateway_pong_v5_to_v6(x: v5::ToGatewayPong) -> Result { + Ok(v6::ToGatewayPong { + request_id: x.request_id, + ts: x.ts, + }) +} + +pub fn convert_to_gateway_v5_to_v6(x: v5::ToGateway) -> Result { + Ok(match x { + v5::ToGateway::ToGatewayPong(v) => v6::ToGateway::ToGatewayPong(convert_to_gateway_pong_v5_to_v6(v)?), + v5::ToGateway::ToRivetTunnelMessage(v) => v6::ToGateway::ToRivetTunnelMessage(convert_to_rivet_tunnel_message_v5_to_v6(v)?), + }) +} + +pub fn convert_to_outbound_actor_start_v5_to_v6(x: v5::ToOutboundActorStart) -> Result { + Ok(v6::ToOutboundActorStart { + namespace_id: x.namespace_id, + pool_name: x.pool_name, + checkpoint: convert_actor_checkpoint_v5_to_v6(x.checkpoint)?, + actor_config: convert_actor_config_v5_to_v6(x.actor_config)?, + }) +} + +pub fn convert_to_outbound_v5_to_v6(x: v5::ToOutbound) -> Result { + Ok(match x { + v5::ToOutbound::ToOutboundActorStart(v) => v6::ToOutbound::ToOutboundActorStart(convert_to_outbound_actor_start_v5_to_v6(v)?), + }) +} diff --git a/engine/sdks/rust/envoy-protocol/src/versioned/v6_to_v5.rs b/engine/sdks/rust/envoy-protocol/src/versioned/v6_to_v5.rs new file mode 100644 index 0000000000..1307d4ac0e --- /dev/null +++ b/engine/sdks/rust/envoy-protocol/src/versioned/v6_to_v5.rs @@ -0,0 +1,776 @@ +// @generated initial scaffold by scripts/vbare-gen-converters +// from: v6.bare, to: v5.bare +// Replace each todo!() with the migration semantics, then drop the @generated marker. + +#![allow(dead_code, unused_variables)] + +use anyhow::Result; + +use crate::generated::{v6, v5}; + +pub fn convert_kv_metadata_v6_to_v5(x: v6::KvMetadata) -> Result { + Ok(v5::KvMetadata { + version: x.version, + update_ts: x.update_ts, + }) +} + +pub fn convert_kv_list_range_query_v6_to_v5(x: v6::KvListRangeQuery) -> Result { + Ok(v5::KvListRangeQuery { + start: x.start, + end: x.end, + exclusive: x.exclusive, + }) +} + +pub fn convert_kv_list_prefix_query_v6_to_v5(x: v6::KvListPrefixQuery) -> Result { + Ok(v5::KvListPrefixQuery { + key: x.key, + }) +} + +pub fn convert_kv_list_query_v6_to_v5(x: v6::KvListQuery) -> Result { + Ok(match x { + v6::KvListQuery::KvListAllQuery => v5::KvListQuery::KvListAllQuery, + v6::KvListQuery::KvListRangeQuery(v) => v5::KvListQuery::KvListRangeQuery(convert_kv_list_range_query_v6_to_v5(v)?), + v6::KvListQuery::KvListPrefixQuery(v) => v5::KvListQuery::KvListPrefixQuery(convert_kv_list_prefix_query_v6_to_v5(v)?), + }) +} + +pub fn convert_kv_get_request_v6_to_v5(x: v6::KvGetRequest) -> Result { + Ok(v5::KvGetRequest { + keys: x.keys, + }) +} + +pub fn convert_kv_list_request_v6_to_v5(x: v6::KvListRequest) -> Result { + Ok(v5::KvListRequest { + query: convert_kv_list_query_v6_to_v5(x.query)?, + reverse: x.reverse, + limit: x.limit, + }) +} + +pub fn convert_kv_put_request_v6_to_v5(x: v6::KvPutRequest) -> Result { + Ok(v5::KvPutRequest { + keys: x.keys, + values: x.values, + }) +} + +pub fn convert_kv_delete_request_v6_to_v5(x: v6::KvDeleteRequest) -> Result { + Ok(v5::KvDeleteRequest { + keys: x.keys, + }) +} + +pub fn convert_kv_delete_range_request_v6_to_v5(x: v6::KvDeleteRangeRequest) -> Result { + Ok(v5::KvDeleteRangeRequest { + start: x.start, + end: x.end, + }) +} + +pub fn convert_kv_error_response_v6_to_v5(x: v6::KvErrorResponse) -> Result { + Ok(v5::KvErrorResponse { + message: x.message, + }) +} + +pub fn convert_kv_get_response_v6_to_v5(x: v6::KvGetResponse) -> Result { + Ok(v5::KvGetResponse { + keys: x.keys, + values: x.values, + metadata: x.metadata.into_iter().map(|v| convert_kv_metadata_v6_to_v5(v)).collect::>>()?, + }) +} + +pub fn convert_kv_list_response_v6_to_v5(x: v6::KvListResponse) -> Result { + Ok(v5::KvListResponse { + keys: x.keys, + values: x.values, + metadata: x.metadata.into_iter().map(|v| convert_kv_metadata_v6_to_v5(v)).collect::>>()?, + }) +} + +pub fn convert_kv_request_data_v6_to_v5(x: v6::KvRequestData) -> Result { + Ok(match x { + v6::KvRequestData::KvGetRequest(v) => v5::KvRequestData::KvGetRequest(convert_kv_get_request_v6_to_v5(v)?), + v6::KvRequestData::KvListRequest(v) => v5::KvRequestData::KvListRequest(convert_kv_list_request_v6_to_v5(v)?), + v6::KvRequestData::KvPutRequest(v) => v5::KvRequestData::KvPutRequest(convert_kv_put_request_v6_to_v5(v)?), + v6::KvRequestData::KvDeleteRequest(v) => v5::KvRequestData::KvDeleteRequest(convert_kv_delete_request_v6_to_v5(v)?), + v6::KvRequestData::KvDeleteRangeRequest(v) => v5::KvRequestData::KvDeleteRangeRequest(convert_kv_delete_range_request_v6_to_v5(v)?), + v6::KvRequestData::KvDropRequest => v5::KvRequestData::KvDropRequest, + }) +} + +pub fn convert_kv_response_data_v6_to_v5(x: v6::KvResponseData) -> Result { + Ok(match x { + v6::KvResponseData::KvErrorResponse(v) => v5::KvResponseData::KvErrorResponse(convert_kv_error_response_v6_to_v5(v)?), + v6::KvResponseData::KvGetResponse(v) => v5::KvResponseData::KvGetResponse(convert_kv_get_response_v6_to_v5(v)?), + v6::KvResponseData::KvListResponse(v) => v5::KvResponseData::KvListResponse(convert_kv_list_response_v6_to_v5(v)?), + v6::KvResponseData::KvPutResponse => v5::KvResponseData::KvPutResponse, + v6::KvResponseData::KvDeleteResponse => v5::KvResponseData::KvDeleteResponse, + v6::KvResponseData::KvDropResponse => v5::KvResponseData::KvDropResponse, + }) +} + +pub fn convert_sqlite_dirty_page_v6_to_v5(x: v6::SqliteDirtyPage) -> Result { + Ok(v5::SqliteDirtyPage { + pgno: x.pgno, + bytes: x.bytes, + }) +} + +pub fn convert_sqlite_fetched_page_v6_to_v5(x: v6::SqliteFetchedPage) -> Result { + Ok(v5::SqliteFetchedPage { + pgno: x.pgno, + bytes: x.bytes, + }) +} + +pub fn convert_sqlite_get_pages_request_v6_to_v5(x: v6::SqliteGetPagesRequest) -> Result { + Ok(v5::SqliteGetPagesRequest { + actor_id: x.actor_id, + pgnos: x.pgnos, + expected_generation: x.expected_generation, + expected_head_txid: x.expected_head_txid, + }) +} + +pub fn convert_sqlite_get_pages_ok_v6_to_v5(x: v6::SqliteGetPagesOk) -> Result { + Ok(v5::SqliteGetPagesOk { + pages: x.pages.into_iter().map(|v| convert_sqlite_fetched_page_v6_to_v5(v)).collect::>>()?, + head_txid: x.head_txid, + }) +} + +pub fn convert_sqlite_error_response_v6_to_v5(x: v6::SqliteErrorResponse) -> Result { + Ok(v5::SqliteErrorResponse { + group: x.group, + code: x.code, + message: x.message, + }) +} + +pub fn convert_sqlite_get_pages_response_v6_to_v5(x: v6::SqliteGetPagesResponse) -> Result { + Ok(match x { + v6::SqliteGetPagesResponse::SqliteGetPagesOk(v) => v5::SqliteGetPagesResponse::SqliteGetPagesOk(convert_sqlite_get_pages_ok_v6_to_v5(v)?), + v6::SqliteGetPagesResponse::SqliteErrorResponse(v) => v5::SqliteGetPagesResponse::SqliteErrorResponse(convert_sqlite_error_response_v6_to_v5(v)?), + }) +} + +pub fn convert_sqlite_commit_request_v6_to_v5(x: v6::SqliteCommitRequest) -> Result { + Ok(v5::SqliteCommitRequest { + actor_id: x.actor_id, + dirty_pages: x.dirty_pages.into_iter().map(|v| convert_sqlite_dirty_page_v6_to_v5(v)).collect::>>()?, + db_size_pages: x.db_size_pages, + now_ms: x.now_ms, + expected_generation: x.expected_generation, + expected_head_txid: x.expected_head_txid, + }) +} + +pub fn convert_sqlite_commit_ok_v6_to_v5(x: v6::SqliteCommitOk) -> Result { + Ok(v5::SqliteCommitOk { + head_txid: x.head_txid, + }) +} + +pub fn convert_sqlite_commit_response_v6_to_v5(x: v6::SqliteCommitResponse) -> Result { + Ok(match x { + v6::SqliteCommitResponse::SqliteCommitOk(v) => v5::SqliteCommitResponse::SqliteCommitOk(convert_sqlite_commit_ok_v6_to_v5(v)?), + v6::SqliteCommitResponse::SqliteErrorResponse(v) => v5::SqliteCommitResponse::SqliteErrorResponse(convert_sqlite_error_response_v6_to_v5(v)?), + }) +} + +pub fn convert_sqlite_value_integer_v6_to_v5(x: v6::SqliteValueInteger) -> Result { + Ok(v5::SqliteValueInteger { + value: x.value, + }) +} + +pub fn convert_sqlite_value_float_v6_to_v5(x: v6::SqliteValueFloat) -> Result { + Ok(v5::SqliteValueFloat { + value: x.value, + }) +} + +pub fn convert_sqlite_value_text_v6_to_v5(x: v6::SqliteValueText) -> Result { + Ok(v5::SqliteValueText { + value: x.value, + }) +} + +pub fn convert_sqlite_value_blob_v6_to_v5(x: v6::SqliteValueBlob) -> Result { + Ok(v5::SqliteValueBlob { + value: x.value, + }) +} + +pub fn convert_sqlite_bind_param_v6_to_v5(x: v6::SqliteBindParam) -> Result { + Ok(match x { + v6::SqliteBindParam::SqliteValueNull => v5::SqliteBindParam::SqliteValueNull, + v6::SqliteBindParam::SqliteValueInteger(v) => v5::SqliteBindParam::SqliteValueInteger(convert_sqlite_value_integer_v6_to_v5(v)?), + v6::SqliteBindParam::SqliteValueFloat(v) => v5::SqliteBindParam::SqliteValueFloat(convert_sqlite_value_float_v6_to_v5(v)?), + v6::SqliteBindParam::SqliteValueText(v) => v5::SqliteBindParam::SqliteValueText(convert_sqlite_value_text_v6_to_v5(v)?), + v6::SqliteBindParam::SqliteValueBlob(v) => v5::SqliteBindParam::SqliteValueBlob(convert_sqlite_value_blob_v6_to_v5(v)?), + }) +} + +pub fn convert_sqlite_column_value_v6_to_v5(x: v6::SqliteColumnValue) -> Result { + Ok(match x { + v6::SqliteColumnValue::SqliteValueNull => v5::SqliteColumnValue::SqliteValueNull, + v6::SqliteColumnValue::SqliteValueInteger(v) => v5::SqliteColumnValue::SqliteValueInteger(convert_sqlite_value_integer_v6_to_v5(v)?), + v6::SqliteColumnValue::SqliteValueFloat(v) => v5::SqliteColumnValue::SqliteValueFloat(convert_sqlite_value_float_v6_to_v5(v)?), + v6::SqliteColumnValue::SqliteValueText(v) => v5::SqliteColumnValue::SqliteValueText(convert_sqlite_value_text_v6_to_v5(v)?), + v6::SqliteColumnValue::SqliteValueBlob(v) => v5::SqliteColumnValue::SqliteValueBlob(convert_sqlite_value_blob_v6_to_v5(v)?), + }) +} + +pub fn convert_sqlite_query_result_v6_to_v5(x: v6::SqliteQueryResult) -> Result { + Ok(v5::SqliteQueryResult { + columns: x.columns, + rows: x.rows.into_iter().map(|v| v.into_iter().map(|v| convert_sqlite_column_value_v6_to_v5(v)).collect::>>()).collect::>>()?, + }) +} + +pub fn convert_sqlite_execute_result_v6_to_v5(x: v6::SqliteExecuteResult) -> Result { + Ok(v5::SqliteExecuteResult { + columns: x.columns, + rows: x.rows.into_iter().map(|v| v.into_iter().map(|v| convert_sqlite_column_value_v6_to_v5(v)).collect::>>()).collect::>>()?, + changes: x.changes, + last_insert_row_id: x.last_insert_row_id, + }) +} + +pub fn convert_sqlite_exec_request_v6_to_v5(x: v6::SqliteExecRequest) -> Result { + Ok(v5::SqliteExecRequest { + namespace_id: x.namespace_id, + actor_id: x.actor_id, + generation: x.generation, + sql: x.sql, + }) +} + +pub fn convert_sqlite_execute_request_v6_to_v5(x: v6::SqliteExecuteRequest) -> Result { + Ok(v5::SqliteExecuteRequest { + namespace_id: x.namespace_id, + actor_id: x.actor_id, + generation: x.generation, + sql: x.sql, + params: x.params.map(|v| v.into_iter().map(|v| convert_sqlite_bind_param_v6_to_v5(v)).collect::>>()).transpose()?, + }) +} + +pub fn convert_sqlite_exec_ok_v6_to_v5(x: v6::SqliteExecOk) -> Result { + Ok(v5::SqliteExecOk { + result: convert_sqlite_query_result_v6_to_v5(x.result)?, + }) +} + +pub fn convert_sqlite_execute_ok_v6_to_v5(x: v6::SqliteExecuteOk) -> Result { + Ok(v5::SqliteExecuteOk { + result: convert_sqlite_execute_result_v6_to_v5(x.result)?, + }) +} + +pub fn convert_sqlite_exec_response_v6_to_v5(x: v6::SqliteExecResponse) -> Result { + Ok(match x { + v6::SqliteExecResponse::SqliteExecOk(v) => v5::SqliteExecResponse::SqliteExecOk(convert_sqlite_exec_ok_v6_to_v5(v)?), + v6::SqliteExecResponse::SqliteErrorResponse(v) => v5::SqliteExecResponse::SqliteErrorResponse(convert_sqlite_error_response_v6_to_v5(v)?), + }) +} + +pub fn convert_sqlite_execute_response_v6_to_v5(x: v6::SqliteExecuteResponse) -> Result { + Ok(match x { + v6::SqliteExecuteResponse::SqliteExecuteOk(v) => v5::SqliteExecuteResponse::SqliteExecuteOk(convert_sqlite_execute_ok_v6_to_v5(v)?), + v6::SqliteExecuteResponse::SqliteErrorResponse(v) => v5::SqliteExecuteResponse::SqliteErrorResponse(convert_sqlite_error_response_v6_to_v5(v)?), + }) +} + +pub fn convert_stop_code_v6_to_v5(x: v6::StopCode) -> Result { + Ok(match x { + v6::StopCode::Ok => v5::StopCode::Ok, + v6::StopCode::Error => v5::StopCode::Error, + }) +} + +pub fn convert_actor_name_v6_to_v5(x: v6::ActorName) -> Result { + Ok(v5::ActorName { + metadata: x.metadata, + }) +} + +pub fn convert_actor_config_v6_to_v5(x: v6::ActorConfig) -> Result { + Ok(v5::ActorConfig { + name: x.name, + key: x.key, + create_ts: x.create_ts, + input: x.input, + }) +} + +pub fn convert_actor_checkpoint_v6_to_v5(x: v6::ActorCheckpoint) -> Result { + Ok(v5::ActorCheckpoint { + actor_id: x.actor_id, + generation: x.generation, + index: x.index, + }) +} + +pub fn convert_actor_intent_v6_to_v5(x: v6::ActorIntent) -> Result { + Ok(match x { + v6::ActorIntent::ActorIntentSleep => v5::ActorIntent::ActorIntentSleep, + v6::ActorIntent::ActorIntentStop => v5::ActorIntent::ActorIntentStop, + }) +} + +pub fn convert_actor_state_stopped_v6_to_v5(x: v6::ActorStateStopped) -> Result { + Ok(v5::ActorStateStopped { + code: convert_stop_code_v6_to_v5(x.code)?, + message: x.message, + }) +} + +pub fn convert_actor_state_v6_to_v5(x: v6::ActorState) -> Result { + Ok(match x { + v6::ActorState::ActorStateRunning => v5::ActorState::ActorStateRunning, + v6::ActorState::ActorStateStopped(v) => v5::ActorState::ActorStateStopped(convert_actor_state_stopped_v6_to_v5(v)?), + }) +} + +pub fn convert_event_actor_intent_v6_to_v5(x: v6::EventActorIntent) -> Result { + Ok(v5::EventActorIntent { + intent: convert_actor_intent_v6_to_v5(x.intent)?, + }) +} + +pub fn convert_event_actor_state_update_v6_to_v5(x: v6::EventActorStateUpdate) -> Result { + Ok(v5::EventActorStateUpdate { + state: convert_actor_state_v6_to_v5(x.state)?, + }) +} + +pub fn convert_event_actor_set_alarm_v6_to_v5(x: v6::EventActorSetAlarm) -> Result { + Ok(v5::EventActorSetAlarm { + alarm_ts: x.alarm_ts, + }) +} + +pub fn convert_event_v6_to_v5(x: v6::Event) -> Result { + Ok(match x { + v6::Event::EventActorIntent(v) => v5::Event::EventActorIntent(convert_event_actor_intent_v6_to_v5(v)?), + v6::Event::EventActorStateUpdate(v) => v5::Event::EventActorStateUpdate(convert_event_actor_state_update_v6_to_v5(v)?), + v6::Event::EventActorSetAlarm(v) => v5::Event::EventActorSetAlarm(convert_event_actor_set_alarm_v6_to_v5(v)?), + }) +} + +pub fn convert_event_wrapper_v6_to_v5(x: v6::EventWrapper) -> Result { + Ok(v5::EventWrapper { + checkpoint: convert_actor_checkpoint_v6_to_v5(x.checkpoint)?, + inner: convert_event_v6_to_v5(x.inner)?, + }) +} + +pub fn convert_preloaded_kv_entry_v6_to_v5(x: v6::PreloadedKvEntry) -> Result { + Ok(v5::PreloadedKvEntry { + key: x.key, + value: x.value, + metadata: convert_kv_metadata_v6_to_v5(x.metadata)?, + }) +} + +pub fn convert_preloaded_kv_v6_to_v5(x: v6::PreloadedKv) -> Result { + Ok(v5::PreloadedKv { + entries: x.entries.into_iter().map(|v| convert_preloaded_kv_entry_v6_to_v5(v)).collect::>>()?, + requested_get_keys: x.requested_get_keys, + requested_prefixes: x.requested_prefixes, + }) +} + +pub fn convert_hibernating_request_v6_to_v5(x: v6::HibernatingRequest) -> Result { + Ok(v5::HibernatingRequest { + gateway_id: x.gateway_id, + request_id: x.request_id, + }) +} + +pub fn convert_command_start_actor_v6_to_v5(x: v6::CommandStartActor) -> Result { + Ok(v5::CommandStartActor { + config: convert_actor_config_v6_to_v5(x.config)?, + hibernating_requests: x.hibernating_requests.into_iter().map(|v| convert_hibernating_request_v6_to_v5(v)).collect::>>()?, + preloaded_kv: x.preloaded_kv.map(|v| convert_preloaded_kv_v6_to_v5(v)).transpose()?, + }) +} + +pub fn convert_stop_actor_reason_v6_to_v5(x: v6::StopActorReason) -> Result { + Ok(match x { + v6::StopActorReason::SleepIntent => v5::StopActorReason::SleepIntent, + v6::StopActorReason::StopIntent => v5::StopActorReason::StopIntent, + v6::StopActorReason::Destroy => v5::StopActorReason::Destroy, + v6::StopActorReason::GoingAway => v5::StopActorReason::GoingAway, + v6::StopActorReason::Lost => v5::StopActorReason::Lost, + }) +} + +pub fn convert_command_stop_actor_v6_to_v5(x: v6::CommandStopActor) -> Result { + Ok(v5::CommandStopActor { + reason: convert_stop_actor_reason_v6_to_v5(x.reason)?, + }) +} + +pub fn convert_command_v6_to_v5(x: v6::Command) -> Result { + Ok(match x { + v6::Command::CommandStartActor(v) => v5::Command::CommandStartActor(convert_command_start_actor_v6_to_v5(v)?), + v6::Command::CommandStopActor(v) => v5::Command::CommandStopActor(convert_command_stop_actor_v6_to_v5(v)?), + }) +} + +pub fn convert_command_wrapper_v6_to_v5(x: v6::CommandWrapper) -> Result { + Ok(v5::CommandWrapper { + checkpoint: convert_actor_checkpoint_v6_to_v5(x.checkpoint)?, + inner: convert_command_v6_to_v5(x.inner)?, + }) +} + +pub fn convert_actor_command_key_data_v6_to_v5(x: v6::ActorCommandKeyData) -> Result { + Ok(match x { + v6::ActorCommandKeyData::CommandStartActor(v) => v5::ActorCommandKeyData::CommandStartActor(convert_command_start_actor_v6_to_v5(v)?), + v6::ActorCommandKeyData::CommandStopActor(v) => v5::ActorCommandKeyData::CommandStopActor(convert_command_stop_actor_v6_to_v5(v)?), + }) +} + +pub fn convert_message_id_v6_to_v5(x: v6::MessageId) -> Result { + Ok(v5::MessageId { + gateway_id: x.gateway_id, + request_id: x.request_id, + message_index: x.message_index, + }) +} + +pub fn convert_to_envoy_request_start_v6_to_v5(x: v6::ToEnvoyRequestStart) -> Result { + Ok(v5::ToEnvoyRequestStart { + actor_id: x.actor_id, + method: x.method, + path: x.path, + headers: x.headers, + body: x.body, + stream: x.stream, + }) +} + +pub fn convert_to_envoy_request_chunk_v6_to_v5(x: v6::ToEnvoyRequestChunk) -> Result { + Ok(v5::ToEnvoyRequestChunk { + body: x.body, + finish: x.finish, + }) +} + +pub fn convert_to_rivet_response_start_v6_to_v5(x: v6::ToRivetResponseStart) -> Result { + Ok(v5::ToRivetResponseStart { + status: x.status, + headers: x.headers, + body: x.body, + stream: x.stream, + }) +} + +pub fn convert_to_rivet_response_chunk_v6_to_v5(x: v6::ToRivetResponseChunk) -> Result { + Ok(v5::ToRivetResponseChunk { + body: x.body, + finish: x.finish, + }) +} + +pub fn convert_to_envoy_web_socket_open_v6_to_v5(x: v6::ToEnvoyWebSocketOpen) -> Result { + Ok(v5::ToEnvoyWebSocketOpen { + actor_id: x.actor_id, + path: x.path, + headers: x.headers, + }) +} + +pub fn convert_to_envoy_web_socket_message_v6_to_v5(x: v6::ToEnvoyWebSocketMessage) -> Result { + Ok(v5::ToEnvoyWebSocketMessage { + data: x.data, + binary: x.binary, + }) +} + +pub fn convert_to_envoy_web_socket_close_v6_to_v5(x: v6::ToEnvoyWebSocketClose) -> Result { + Ok(v5::ToEnvoyWebSocketClose { + code: x.code, + reason: x.reason, + }) +} + +pub fn convert_to_rivet_web_socket_open_v6_to_v5(x: v6::ToRivetWebSocketOpen) -> Result { + Ok(v5::ToRivetWebSocketOpen { + can_hibernate: x.can_hibernate, + }) +} + +pub fn convert_to_rivet_web_socket_message_v6_to_v5(x: v6::ToRivetWebSocketMessage) -> Result { + Ok(v5::ToRivetWebSocketMessage { + data: x.data, + binary: x.binary, + }) +} + +pub fn convert_to_rivet_web_socket_message_ack_v6_to_v5(x: v6::ToRivetWebSocketMessageAck) -> Result { + Ok(v5::ToRivetWebSocketMessageAck { + index: x.index, + }) +} + +pub fn convert_to_rivet_web_socket_close_v6_to_v5(x: v6::ToRivetWebSocketClose) -> Result { + Ok(v5::ToRivetWebSocketClose { + code: x.code, + reason: x.reason, + hibernate: x.hibernate, + }) +} + +pub fn convert_to_rivet_tunnel_message_kind_v6_to_v5(x: v6::ToRivetTunnelMessageKind) -> Result { + Ok(match x { + v6::ToRivetTunnelMessageKind::ToRivetResponseStart(v) => v5::ToRivetTunnelMessageKind::ToRivetResponseStart(convert_to_rivet_response_start_v6_to_v5(v)?), + v6::ToRivetTunnelMessageKind::ToRivetResponseChunk(v) => v5::ToRivetTunnelMessageKind::ToRivetResponseChunk(convert_to_rivet_response_chunk_v6_to_v5(v)?), + v6::ToRivetTunnelMessageKind::ToRivetResponseAbort(_) => { + v5::ToRivetTunnelMessageKind::ToRivetResponseAbort + } + v6::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(v) => v5::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(convert_to_rivet_web_socket_open_v6_to_v5(v)?), + v6::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(v) => v5::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(convert_to_rivet_web_socket_message_v6_to_v5(v)?), + v6::ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(v) => v5::ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(convert_to_rivet_web_socket_message_ack_v6_to_v5(v)?), + v6::ToRivetTunnelMessageKind::ToRivetWebSocketClose(v) => v5::ToRivetTunnelMessageKind::ToRivetWebSocketClose(convert_to_rivet_web_socket_close_v6_to_v5(v)?), + }) +} + +pub fn convert_to_rivet_tunnel_message_v6_to_v5(x: v6::ToRivetTunnelMessage) -> Result { + Ok(v5::ToRivetTunnelMessage { + message_id: convert_message_id_v6_to_v5(x.message_id)?, + message_kind: convert_to_rivet_tunnel_message_kind_v6_to_v5(x.message_kind)?, + }) +} + +pub fn convert_to_envoy_tunnel_message_kind_v6_to_v5(x: v6::ToEnvoyTunnelMessageKind) -> Result { + Ok(match x { + v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(v) => v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestStart(convert_to_envoy_request_start_v6_to_v5(v)?), + v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(v) => v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestChunk(convert_to_envoy_request_chunk_v6_to_v5(v)?), + v6::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort(_) => { + v5::ToEnvoyTunnelMessageKind::ToEnvoyRequestAbort + } + v6::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(v) => v5::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketOpen(convert_to_envoy_web_socket_open_v6_to_v5(v)?), + v6::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(v) => v5::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketMessage(convert_to_envoy_web_socket_message_v6_to_v5(v)?), + v6::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(v) => v5::ToEnvoyTunnelMessageKind::ToEnvoyWebSocketClose(convert_to_envoy_web_socket_close_v6_to_v5(v)?), + }) +} + +pub fn convert_to_envoy_tunnel_message_v6_to_v5(x: v6::ToEnvoyTunnelMessage) -> Result { + Ok(v5::ToEnvoyTunnelMessage { + message_id: convert_message_id_v6_to_v5(x.message_id)?, + message_kind: convert_to_envoy_tunnel_message_kind_v6_to_v5(x.message_kind)?, + }) +} + +pub fn convert_to_envoy_ping_v6_to_v5(x: v6::ToEnvoyPing) -> Result { + Ok(v5::ToEnvoyPing { + ts: x.ts, + }) +} + +pub fn convert_to_rivet_metadata_v6_to_v5(x: v6::ToRivetMetadata) -> Result { + Ok(v5::ToRivetMetadata { + prepopulate_actor_names: x.prepopulate_actor_names.map(|v| v.into_iter().map(|(k, v)| -> Result<_> { Ok((k, convert_actor_name_v6_to_v5(v)?)) }).collect::>()).transpose()?, + metadata: x.metadata, + }) +} + +pub fn convert_to_rivet_events_v6_to_v5(x: v6::ToRivetEvents) -> Result { + Ok(x.into_iter().map(|v| convert_event_wrapper_v6_to_v5(v)).collect::>>()?) +} + +pub fn convert_to_rivet_ack_commands_v6_to_v5(x: v6::ToRivetAckCommands) -> Result { + Ok(v5::ToRivetAckCommands { + last_command_checkpoints: x.last_command_checkpoints.into_iter().map(|v| convert_actor_checkpoint_v6_to_v5(v)).collect::>>()?, + }) +} + +pub fn convert_to_rivet_pong_v6_to_v5(x: v6::ToRivetPong) -> Result { + Ok(v5::ToRivetPong { + ts: x.ts, + }) +} + +pub fn convert_to_rivet_kv_request_v6_to_v5(x: v6::ToRivetKvRequest) -> Result { + Ok(v5::ToRivetKvRequest { + actor_id: x.actor_id, + request_id: x.request_id, + data: convert_kv_request_data_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_get_pages_request_v6_to_v5(x: v6::ToRivetSqliteGetPagesRequest) -> Result { + Ok(v5::ToRivetSqliteGetPagesRequest { + request_id: x.request_id, + data: convert_sqlite_get_pages_request_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_commit_request_v6_to_v5(x: v6::ToRivetSqliteCommitRequest) -> Result { + Ok(v5::ToRivetSqliteCommitRequest { + request_id: x.request_id, + data: convert_sqlite_commit_request_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_exec_request_v6_to_v5(x: v6::ToRivetSqliteExecRequest) -> Result { + Ok(v5::ToRivetSqliteExecRequest { + request_id: x.request_id, + data: convert_sqlite_exec_request_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_rivet_sqlite_execute_request_v6_to_v5(x: v6::ToRivetSqliteExecuteRequest) -> Result { + Ok(v5::ToRivetSqliteExecuteRequest { + request_id: x.request_id, + data: convert_sqlite_execute_request_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_rivet_v6_to_v5(x: v6::ToRivet) -> Result { + Ok(match x { + v6::ToRivet::ToRivetMetadata(v) => v5::ToRivet::ToRivetMetadata(convert_to_rivet_metadata_v6_to_v5(v)?), + v6::ToRivet::ToRivetEvents(v) => v5::ToRivet::ToRivetEvents(convert_to_rivet_events_v6_to_v5(v)?), + v6::ToRivet::ToRivetAckCommands(v) => v5::ToRivet::ToRivetAckCommands(convert_to_rivet_ack_commands_v6_to_v5(v)?), + v6::ToRivet::ToRivetStopping => v5::ToRivet::ToRivetStopping, + v6::ToRivet::ToRivetPong(v) => v5::ToRivet::ToRivetPong(convert_to_rivet_pong_v6_to_v5(v)?), + v6::ToRivet::ToRivetKvRequest(v) => v5::ToRivet::ToRivetKvRequest(convert_to_rivet_kv_request_v6_to_v5(v)?), + v6::ToRivet::ToRivetTunnelMessage(v) => v5::ToRivet::ToRivetTunnelMessage(convert_to_rivet_tunnel_message_v6_to_v5(v)?), + v6::ToRivet::ToRivetSqliteGetPagesRequest(v) => v5::ToRivet::ToRivetSqliteGetPagesRequest(convert_to_rivet_sqlite_get_pages_request_v6_to_v5(v)?), + v6::ToRivet::ToRivetSqliteCommitRequest(v) => v5::ToRivet::ToRivetSqliteCommitRequest(convert_to_rivet_sqlite_commit_request_v6_to_v5(v)?), + v6::ToRivet::ToRivetSqliteExecRequest(v) => v5::ToRivet::ToRivetSqliteExecRequest(convert_to_rivet_sqlite_exec_request_v6_to_v5(v)?), + v6::ToRivet::ToRivetSqliteExecuteRequest(v) => v5::ToRivet::ToRivetSqliteExecuteRequest(convert_to_rivet_sqlite_execute_request_v6_to_v5(v)?), + }) +} + +pub fn convert_protocol_metadata_v6_to_v5(x: v6::ProtocolMetadata) -> Result { + Ok(v5::ProtocolMetadata { + envoy_lost_threshold: x.envoy_lost_threshold, + actor_stop_threshold: x.actor_stop_threshold, + max_response_payload_size: x.max_response_payload_size, + }) +} + +pub fn convert_to_envoy_init_v6_to_v5(x: v6::ToEnvoyInit) -> Result { + Ok(v5::ToEnvoyInit { + metadata: convert_protocol_metadata_v6_to_v5(x.metadata)?, + }) +} + +pub fn convert_to_envoy_commands_v6_to_v5(x: v6::ToEnvoyCommands) -> Result { + Ok(x.into_iter().map(|v| convert_command_wrapper_v6_to_v5(v)).collect::>>()?) +} + +pub fn convert_to_envoy_ack_events_v6_to_v5(x: v6::ToEnvoyAckEvents) -> Result { + Ok(v5::ToEnvoyAckEvents { + last_event_checkpoints: x.last_event_checkpoints.into_iter().map(|v| convert_actor_checkpoint_v6_to_v5(v)).collect::>>()?, + }) +} + +pub fn convert_to_envoy_kv_response_v6_to_v5(x: v6::ToEnvoyKvResponse) -> Result { + Ok(v5::ToEnvoyKvResponse { + request_id: x.request_id, + data: convert_kv_response_data_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_get_pages_response_v6_to_v5(x: v6::ToEnvoySqliteGetPagesResponse) -> Result { + Ok(v5::ToEnvoySqliteGetPagesResponse { + request_id: x.request_id, + data: convert_sqlite_get_pages_response_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_commit_response_v6_to_v5(x: v6::ToEnvoySqliteCommitResponse) -> Result { + Ok(v5::ToEnvoySqliteCommitResponse { + request_id: x.request_id, + data: convert_sqlite_commit_response_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_exec_response_v6_to_v5(x: v6::ToEnvoySqliteExecResponse) -> Result { + Ok(v5::ToEnvoySqliteExecResponse { + request_id: x.request_id, + data: convert_sqlite_exec_response_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_envoy_sqlite_execute_response_v6_to_v5(x: v6::ToEnvoySqliteExecuteResponse) -> Result { + Ok(v5::ToEnvoySqliteExecuteResponse { + request_id: x.request_id, + data: convert_sqlite_execute_response_v6_to_v5(x.data)?, + }) +} + +pub fn convert_to_envoy_v6_to_v5(x: v6::ToEnvoy) -> Result { + Ok(match x { + v6::ToEnvoy::ToEnvoyInit(v) => v5::ToEnvoy::ToEnvoyInit(convert_to_envoy_init_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoyCommands(v) => v5::ToEnvoy::ToEnvoyCommands(convert_to_envoy_commands_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoyAckEvents(v) => v5::ToEnvoy::ToEnvoyAckEvents(convert_to_envoy_ack_events_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoyKvResponse(v) => v5::ToEnvoy::ToEnvoyKvResponse(convert_to_envoy_kv_response_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoyTunnelMessage(v) => v5::ToEnvoy::ToEnvoyTunnelMessage(convert_to_envoy_tunnel_message_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoyPing(v) => v5::ToEnvoy::ToEnvoyPing(convert_to_envoy_ping_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoySqliteGetPagesResponse(v) => v5::ToEnvoy::ToEnvoySqliteGetPagesResponse(convert_to_envoy_sqlite_get_pages_response_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoySqliteCommitResponse(v) => v5::ToEnvoy::ToEnvoySqliteCommitResponse(convert_to_envoy_sqlite_commit_response_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoySqliteExecResponse(v) => v5::ToEnvoy::ToEnvoySqliteExecResponse(convert_to_envoy_sqlite_exec_response_v6_to_v5(v)?), + v6::ToEnvoy::ToEnvoySqliteExecuteResponse(v) => v5::ToEnvoy::ToEnvoySqliteExecuteResponse(convert_to_envoy_sqlite_execute_response_v6_to_v5(v)?), + }) +} + +pub fn convert_to_envoy_conn_ping_v6_to_v5(x: v6::ToEnvoyConnPing) -> Result { + Ok(v5::ToEnvoyConnPing { + gateway_id: x.gateway_id, + request_id: x.request_id, + ts: x.ts, + }) +} + +pub fn convert_to_envoy_conn_v6_to_v5(x: v6::ToEnvoyConn) -> Result { + Ok(match x { + v6::ToEnvoyConn::ToEnvoyConnPing(v) => v5::ToEnvoyConn::ToEnvoyConnPing(convert_to_envoy_conn_ping_v6_to_v5(v)?), + v6::ToEnvoyConn::ToEnvoyConnClose => v5::ToEnvoyConn::ToEnvoyConnClose, + v6::ToEnvoyConn::ToEnvoyCommands(v) => v5::ToEnvoyConn::ToEnvoyCommands(convert_to_envoy_commands_v6_to_v5(v)?), + v6::ToEnvoyConn::ToEnvoyAckEvents(v) => v5::ToEnvoyConn::ToEnvoyAckEvents(convert_to_envoy_ack_events_v6_to_v5(v)?), + v6::ToEnvoyConn::ToEnvoyTunnelMessage(v) => v5::ToEnvoyConn::ToEnvoyTunnelMessage(convert_to_envoy_tunnel_message_v6_to_v5(v)?), + }) +} + +pub fn convert_to_gateway_pong_v6_to_v5(x: v6::ToGatewayPong) -> Result { + Ok(v5::ToGatewayPong { + request_id: x.request_id, + ts: x.ts, + }) +} + +pub fn convert_to_gateway_v6_to_v5(x: v6::ToGateway) -> Result { + Ok(match x { + v6::ToGateway::ToGatewayPong(v) => v5::ToGateway::ToGatewayPong(convert_to_gateway_pong_v6_to_v5(v)?), + v6::ToGateway::ToRivetTunnelMessage(v) => v5::ToGateway::ToRivetTunnelMessage(convert_to_rivet_tunnel_message_v6_to_v5(v)?), + }) +} + +pub fn convert_to_outbound_actor_start_v6_to_v5(x: v6::ToOutboundActorStart) -> Result { + Ok(v5::ToOutboundActorStart { + namespace_id: x.namespace_id, + pool_name: x.pool_name, + checkpoint: convert_actor_checkpoint_v6_to_v5(x.checkpoint)?, + actor_config: convert_actor_config_v6_to_v5(x.actor_config)?, + }) +} + +pub fn convert_to_outbound_v6_to_v5(x: v6::ToOutbound) -> Result { + Ok(match x { + v6::ToOutbound::ToOutboundActorStart(v) => v5::ToOutbound::ToOutboundActorStart(convert_to_outbound_actor_start_v6_to_v5(v)?), + }) +} diff --git a/engine/sdks/rust/envoy-protocol/tests/remote_sql_compat.rs b/engine/sdks/rust/envoy-protocol/tests/remote_sql_compat.rs index 2fca661df7..a2b397ef8e 100644 --- a/engine/sdks/rust/envoy-protocol/tests/remote_sql_compat.rs +++ b/engine/sdks/rust/envoy-protocol/tests/remote_sql_compat.rs @@ -1,6 +1,6 @@ use anyhow::Result; use rivet_envoy_protocol::{ - generated::{v4, v5}, + generated::{v4, v6}, versioned::{ ProtocolCompatibilityDirection, ProtocolCompatibilityError, ProtocolCompatibilityFeature, ToEnvoy, ToRivet, @@ -8,10 +8,10 @@ use rivet_envoy_protocol::{ }; use vbare::OwnedVersionedData; -fn remote_sql_request_exec() -> v5::ToRivet { - v5::ToRivet::ToRivetSqliteExecRequest(v5::ToRivetSqliteExecRequest { +fn remote_sql_request_exec() -> v6::ToRivet { + v6::ToRivet::ToRivetSqliteExecRequest(v6::ToRivetSqliteExecRequest { request_id: 1, - data: v5::SqliteExecRequest { + data: v6::SqliteExecRequest { namespace_id: "namespace".into(), actor_id: "actor".into(), generation: 7, @@ -20,25 +20,25 @@ fn remote_sql_request_exec() -> v5::ToRivet { }) } -fn remote_sql_request_execute() -> v5::ToRivet { - v5::ToRivet::ToRivetSqliteExecuteRequest(v5::ToRivetSqliteExecuteRequest { +fn remote_sql_request_execute() -> v6::ToRivet { + v6::ToRivet::ToRivetSqliteExecuteRequest(v6::ToRivetSqliteExecuteRequest { request_id: 2, - data: v5::SqliteExecuteRequest { + data: v6::SqliteExecuteRequest { namespace_id: "namespace".into(), actor_id: "actor".into(), generation: 7, sql: "select ?".into(), - params: Some(vec![v5::SqliteBindParam::SqliteValueInteger( - v5::SqliteValueInteger { value: 1 }, + params: Some(vec![v6::SqliteBindParam::SqliteValueInteger( + v6::SqliteValueInteger { value: 1 }, )]), }, }) } -fn remote_sql_response_exec() -> v5::ToEnvoy { - v5::ToEnvoy::ToEnvoySqliteExecResponse(v5::ToEnvoySqliteExecResponse { +fn remote_sql_response_exec() -> v6::ToEnvoy { + v6::ToEnvoy::ToEnvoySqliteExecResponse(v6::ToEnvoySqliteExecResponse { request_id: 1, - data: v5::SqliteExecResponse::SqliteErrorResponse(v5::SqliteErrorResponse { + data: v6::SqliteExecResponse::SqliteErrorResponse(v6::SqliteErrorResponse { group: "sqlite".into(), code: "remote_unavailable".into(), message: "remote sql execution is unavailable".into(), @@ -46,10 +46,10 @@ fn remote_sql_response_exec() -> v5::ToEnvoy { }) } -fn remote_sql_response_execute() -> v5::ToEnvoy { - v5::ToEnvoy::ToEnvoySqliteExecuteResponse(v5::ToEnvoySqliteExecuteResponse { +fn remote_sql_response_execute() -> v6::ToEnvoy { + v6::ToEnvoy::ToEnvoySqliteExecuteResponse(v6::ToEnvoySqliteExecuteResponse { request_id: 2, - data: v5::SqliteExecuteResponse::SqliteErrorResponse(v5::SqliteErrorResponse { + data: v6::SqliteExecuteResponse::SqliteErrorResponse(v6::SqliteErrorResponse { group: "sqlite".into(), code: "remote_unavailable".into(), message: "remote sql execution is unavailable".into(), @@ -113,11 +113,11 @@ fn new_core_new_pegboard_envoy_allows_remote_sql_both_directions() -> Result<()> assert!(matches!( ToRivet::deserialize(&request, 4)?, - v5::ToRivet::ToRivetSqliteExecRequest(_) + v6::ToRivet::ToRivetSqliteExecRequest(_) )); assert!(matches!( ToEnvoy::deserialize(&response, 4)?, - v5::ToEnvoy::ToEnvoySqliteExecResponse(_) + v6::ToEnvoy::ToEnvoySqliteExecResponse(_) )); Ok(()) diff --git a/engine/sdks/rust/envoy-protocol/tests/stateless_sqlite_v3.rs b/engine/sdks/rust/envoy-protocol/tests/stateless_sqlite_v3.rs index 13dfdd105e..8f79d0e634 100644 --- a/engine/sdks/rust/envoy-protocol/tests/stateless_sqlite_v3.rs +++ b/engine/sdks/rust/envoy-protocol/tests/stateless_sqlite_v3.rs @@ -168,7 +168,7 @@ fn expected_generation_optional_present_and_absent() -> anyhow::Result<()> { #[test] fn protocol_version_constant_matches_schema_version() { - assert_eq!(PROTOCOL_VERSION, 5); + assert_eq!(PROTOCOL_VERSION, 6); } #[test] diff --git a/engine/sdks/schemas/envoy-protocol/v6.bare b/engine/sdks/schemas/envoy-protocol/v6.bare new file mode 100644 index 0000000000..fed130ef7e --- /dev/null +++ b/engine/sdks/schemas/envoy-protocol/v6.bare @@ -0,0 +1,663 @@ +# MARK: Core Primitives + +type Id str +type Json str + +type GatewayId data[4] +type RequestId data[4] +type MessageIndex u16 + +# MARK: KV + +# Basic types +type KvKey data +type KvValue data +type KvMetadata struct { + version: data + updateTs: i64 +} + +# Query types +type KvListAllQuery void +type KvListRangeQuery struct { + start: KvKey + end: KvKey + exclusive: bool +} + +type KvListPrefixQuery struct { + key: KvKey +} + +type KvListQuery union { + KvListAllQuery | + KvListRangeQuery | + KvListPrefixQuery +} + +# Request types +type KvGetRequest struct { + keys: list +} + +type KvListRequest struct { + query: KvListQuery + reverse: optional + limit: optional +} + +type KvPutRequest struct { + keys: list + values: list +} + +type KvDeleteRequest struct { + keys: list +} + +type KvDeleteRangeRequest struct { + start: KvKey + end: KvKey +} + +type KvDropRequest void + +# Response types +type KvErrorResponse struct { + message: str +} + +type KvGetResponse struct { + keys: list + values: list + metadata: list +} + +type KvListResponse struct { + keys: list + values: list + metadata: list +} + +type KvPutResponse void +type KvDeleteResponse void +type KvDropResponse void + +# Request/Response unions +type KvRequestData union { + KvGetRequest | + KvListRequest | + KvPutRequest | + KvDeleteRequest | + KvDeleteRangeRequest | + KvDropRequest +} + +type KvResponseData union { + KvErrorResponse | + KvGetResponse | + KvListResponse | + KvPutResponse | + KvDeleteResponse | + KvDropResponse +} + +# MARK: SQLite + +type SqlitePgno u32 +type SqliteGeneration u64 +type SqlitePageBytes data + +type SqliteDirtyPage struct { + pgno: SqlitePgno + bytes: SqlitePageBytes +} + +type SqliteFetchedPage struct { + pgno: SqlitePgno + bytes: optional +} + +type SqliteGetPagesRequest struct { + actorId: Id + pgnos: list + expectedGeneration: optional + expectedHeadTxid: optional +} + +type SqliteGetPagesOk struct { + pages: list + headTxid: optional +} + +type SqliteErrorResponse struct { + group: str + code: str + message: str +} + +type SqliteGetPagesResponse union { + SqliteGetPagesOk | + SqliteErrorResponse +} + +type SqliteCommitRequest struct { + actorId: Id + dirtyPages: list + dbSizePages: u32 + nowMs: i64 + expectedGeneration: optional + expectedHeadTxid: optional +} + +type SqliteCommitOk struct { + headTxid: optional +} + +type SqliteCommitResponse union { + SqliteCommitOk | + SqliteErrorResponse +} + +# MARK: SQLite Remote Execution + +type SqliteValueNull void + +type SqliteValueInteger struct { + value: i64 +} + +type SqliteValueFloat struct { + value: data[8] +} + +type SqliteValueText struct { + value: str +} + +type SqliteValueBlob struct { + value: data +} + +type SqliteBindParam union { + SqliteValueNull | + SqliteValueInteger | + SqliteValueFloat | + SqliteValueText | + SqliteValueBlob +} + +type SqliteColumnValue union { + SqliteValueNull | + SqliteValueInteger | + SqliteValueFloat | + SqliteValueText | + SqliteValueBlob +} + +type SqliteQueryResult struct { + columns: list + rows: list> +} + +type SqliteExecuteResult struct { + columns: list + rows: list> + changes: i64 + lastInsertRowId: optional +} + +type SqliteExecRequest struct { + namespaceId: Id + actorId: Id + generation: SqliteGeneration + sql: str +} + +type SqliteExecuteRequest struct { + namespaceId: Id + actorId: Id + generation: SqliteGeneration + sql: str + params: optional> +} + +type SqliteExecOk struct { + result: SqliteQueryResult +} + +type SqliteExecuteOk struct { + result: SqliteExecuteResult +} + +type SqliteExecResponse union { + SqliteExecOk | + SqliteErrorResponse +} + +type SqliteExecuteResponse union { + SqliteExecuteOk | + SqliteErrorResponse +} + +# MARK: Actor + +# Core +type StopCode enum { + OK + ERROR +} + +type ActorName struct { + metadata: Json +} + +type ActorConfig struct { + name: str + key: optional + createTs: i64 + input: optional +} + +type ActorCheckpoint struct { + actorId: Id + generation: u32 + index: i64 +} + +# Intent +type ActorIntentSleep void + +type ActorIntentStop void + +type ActorIntent union { + ActorIntentSleep | + ActorIntentStop +} + +# State +type ActorStateRunning void + +type ActorStateStopped struct { + code: StopCode + message: optional +} + +type ActorState union { + ActorStateRunning | + ActorStateStopped +} + +# MARK: Events +type EventActorIntent struct { + intent: ActorIntent +} + +type EventActorStateUpdate struct { + state: ActorState +} + +type EventActorSetAlarm struct { + alarmTs: optional +} + +type Event union { + EventActorIntent | + EventActorStateUpdate | + EventActorSetAlarm +} + +type EventWrapper struct { + checkpoint: ActorCheckpoint + inner: Event +} + +# MARK: Preloaded KV + +type PreloadedKvEntry struct { + key: KvKey + value: KvValue + metadata: KvMetadata +} + +type PreloadedKv struct { + entries: list + requestedGetKeys: list + requestedPrefixes: list +} + +# MARK: Commands + +type HibernatingRequest struct { + gatewayId: GatewayId + requestId: RequestId +} + +type CommandStartActor struct { + config: ActorConfig + hibernatingRequests: list + preloadedKv: optional +} + +type StopActorReason enum { + SLEEP_INTENT + STOP_INTENT + DESTROY + GOING_AWAY + LOST +} + +type CommandStopActor struct { + reason: StopActorReason +} + +type Command union { + CommandStartActor | + CommandStopActor +} + +type CommandWrapper struct { + checkpoint: ActorCheckpoint + inner: Command +} + +# We redeclare this so its top level +type ActorCommandKeyData union { + CommandStartActor | + CommandStopActor +} + +# MARK: Tunnel + +# Message ID + +type MessageId struct { + # Globally unique ID + gatewayId: GatewayId + # Unique ID to the gateway + requestId: RequestId + # Unique ID to the request + messageIndex: MessageIndex +} + +# HTTP +type ToEnvoyRequestStart struct { + actorId: Id + method: str + path: str + headers: map + body: optional + stream: bool +} + +type ToEnvoyRequestChunk struct { + body: data + finish: bool +} + +type HttpStreamAbortReasonKind enum { + UNKNOWN + CLIENT_DISCONNECT + HANDLER_ERROR + IDLE_TIMEOUT + OVERLOADED + BODY_TOO_LARGE + OUT_OF_MEMORY + SHUTDOWN + INTERNAL_ERROR +} + +type HttpStreamAbortReason struct { + kind: HttpStreamAbortReasonKind + detail: optional +} + +type ToEnvoyRequestAbort struct { + reason: HttpStreamAbortReason +} + +type ToRivetResponseStart struct { + status: u16 + headers: map + body: optional + stream: bool +} + +type ToRivetResponseChunk struct { + body: data + finish: bool +} + +type ToRivetResponseAbort struct { + reason: HttpStreamAbortReason +} + +# WebSocket +type ToEnvoyWebSocketOpen struct { + actorId: Id + path: str + headers: map +} + +type ToEnvoyWebSocketMessage struct { + data: data + binary: bool +} + +type ToEnvoyWebSocketClose struct { + code: optional + reason: optional +} + +type ToRivetWebSocketOpen struct { + canHibernate: bool +} + +type ToRivetWebSocketMessage struct { + data: data + binary: bool +} + +type ToRivetWebSocketMessageAck struct { + index: MessageIndex +} + +type ToRivetWebSocketClose struct { + code: optional + reason: optional + hibernate: bool +} + +# To Rivet +type ToRivetTunnelMessageKind union { + # HTTP + ToRivetResponseStart | + ToRivetResponseChunk | + ToRivetResponseAbort | + + # WebSocket + ToRivetWebSocketOpen | + ToRivetWebSocketMessage | + ToRivetWebSocketMessageAck | + ToRivetWebSocketClose +} + +type ToRivetTunnelMessage struct { + messageId: MessageId + messageKind: ToRivetTunnelMessageKind +} + +# To Envoy +type ToEnvoyTunnelMessageKind union { + # HTTP + ToEnvoyRequestStart | + ToEnvoyRequestChunk | + ToEnvoyRequestAbort | + + # WebSocket + ToEnvoyWebSocketOpen | + ToEnvoyWebSocketMessage | + ToEnvoyWebSocketClose +} + +type ToEnvoyTunnelMessage struct { + messageId: MessageId + messageKind: ToEnvoyTunnelMessageKind +} + +type ToEnvoyPing struct { + ts: i64 +} + +# MARK: To Rivet +type ToRivetMetadata struct { + prepopulateActorNames: optional> + metadata: optional +} + +type ToRivetEvents list + +type ToRivetAckCommands struct { + lastCommandCheckpoints: list +} + +type ToRivetStopping void + +type ToRivetPong struct { + ts: i64 +} + +type ToRivetKvRequest struct { + actorId: Id + requestId: u32 + data: KvRequestData +} + +type ToRivetSqliteGetPagesRequest struct { + requestId: u32 + data: SqliteGetPagesRequest +} + +type ToRivetSqliteCommitRequest struct { + requestId: u32 + data: SqliteCommitRequest +} + +type ToRivetSqliteExecRequest struct { + requestId: u32 + data: SqliteExecRequest +} + +type ToRivetSqliteExecuteRequest struct { + requestId: u32 + data: SqliteExecuteRequest +} + +type ToRivet union { + ToRivetMetadata | + ToRivetEvents | + ToRivetAckCommands | + ToRivetStopping | + ToRivetPong | + ToRivetKvRequest | + ToRivetTunnelMessage | + ToRivetSqliteGetPagesRequest | + ToRivetSqliteCommitRequest | + ToRivetSqliteExecRequest | + ToRivetSqliteExecuteRequest +} + +# MARK: To Envoy +type ProtocolMetadata struct { + envoyLostThreshold: i64 + actorStopThreshold: i64 + maxResponsePayloadSize: u64 +} + +type ToEnvoyInit struct { + metadata: ProtocolMetadata +} + +type ToEnvoyCommands list + +type ToEnvoyAckEvents struct { + lastEventCheckpoints: list +} + +type ToEnvoyKvResponse struct { + requestId: u32 + data: KvResponseData +} + +type ToEnvoySqliteGetPagesResponse struct { + requestId: u32 + data: SqliteGetPagesResponse +} + +type ToEnvoySqliteCommitResponse struct { + requestId: u32 + data: SqliteCommitResponse +} + +type ToEnvoySqliteExecResponse struct { + requestId: u32 + data: SqliteExecResponse +} + +type ToEnvoySqliteExecuteResponse struct { + requestId: u32 + data: SqliteExecuteResponse +} + +type ToEnvoy union { + ToEnvoyInit | + ToEnvoyCommands | + ToEnvoyAckEvents | + ToEnvoyKvResponse | + ToEnvoyTunnelMessage | + ToEnvoyPing | + ToEnvoySqliteGetPagesResponse | + ToEnvoySqliteCommitResponse | + ToEnvoySqliteExecResponse | + ToEnvoySqliteExecuteResponse +} + +# MARK: To Envoy Conn +type ToEnvoyConnPing struct { + gatewayId: GatewayId + requestId: RequestId + ts: i64 +} + +type ToEnvoyConnClose void + +type ToEnvoyConn union { + ToEnvoyConnPing | + ToEnvoyConnClose | + ToEnvoyCommands | + ToEnvoyAckEvents | + ToEnvoyTunnelMessage +} + +# MARK: To Gateway +type ToGatewayPong struct { + requestId: RequestId + ts: i64 +} + +type ToGateway union { + ToGatewayPong | + ToRivetTunnelMessage +} + +# MARK: To Outbound +type ToOutboundActorStart struct { + namespaceId: Id + poolName: str + checkpoint: ActorCheckpoint + actorConfig: ActorConfig +} + +type ToOutbound union { + ToOutboundActorStart +} diff --git a/engine/sdks/typescript/envoy-protocol/src/index.ts b/engine/sdks/typescript/envoy-protocol/src/index.ts index 5f186af919..8ccd3eaee0 100644 --- a/engine/sdks/typescript/envoy-protocol/src/index.ts +++ b/engine/sdks/typescript/envoy-protocol/src/index.ts @@ -1,5 +1,4 @@ // @generated - post-processed by build.rs - import * as bare from "@rivetkit/bare-ts" const DEFAULT_CONFIG = /* @__PURE__ */ bare.Config({}) @@ -2017,7 +2016,118 @@ export function writeToEnvoyRequestChunk(bc: bare.ByteCursor, x: ToEnvoyRequestC bare.writeBool(bc, x.finish) } -export type ToEnvoyRequestAbort = null +export enum HttpStreamAbortReasonKind { + Unknown = "Unknown", + ClientDisconnect = "ClientDisconnect", + HandlerError = "HandlerError", + IdleTimeout = "IdleTimeout", + Overloaded = "Overloaded", + BodyTooLarge = "BodyTooLarge", + OutOfMemory = "OutOfMemory", + Shutdown = "Shutdown", + InternalError = "InternalError", +} + +export function readHttpStreamAbortReasonKind(bc: bare.ByteCursor): HttpStreamAbortReasonKind { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return HttpStreamAbortReasonKind.Unknown + case 1: + return HttpStreamAbortReasonKind.ClientDisconnect + case 2: + return HttpStreamAbortReasonKind.HandlerError + case 3: + return HttpStreamAbortReasonKind.IdleTimeout + case 4: + return HttpStreamAbortReasonKind.Overloaded + case 5: + return HttpStreamAbortReasonKind.BodyTooLarge + case 6: + return HttpStreamAbortReasonKind.OutOfMemory + case 7: + return HttpStreamAbortReasonKind.Shutdown + case 8: + return HttpStreamAbortReasonKind.InternalError + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeHttpStreamAbortReasonKind(bc: bare.ByteCursor, x: HttpStreamAbortReasonKind): void { + switch (x) { + case HttpStreamAbortReasonKind.Unknown: { + bare.writeU8(bc, 0) + break + } + case HttpStreamAbortReasonKind.ClientDisconnect: { + bare.writeU8(bc, 1) + break + } + case HttpStreamAbortReasonKind.HandlerError: { + bare.writeU8(bc, 2) + break + } + case HttpStreamAbortReasonKind.IdleTimeout: { + bare.writeU8(bc, 3) + break + } + case HttpStreamAbortReasonKind.Overloaded: { + bare.writeU8(bc, 4) + break + } + case HttpStreamAbortReasonKind.BodyTooLarge: { + bare.writeU8(bc, 5) + break + } + case HttpStreamAbortReasonKind.OutOfMemory: { + bare.writeU8(bc, 6) + break + } + case HttpStreamAbortReasonKind.Shutdown: { + bare.writeU8(bc, 7) + break + } + case HttpStreamAbortReasonKind.InternalError: { + bare.writeU8(bc, 8) + break + } + } +} + +export type HttpStreamAbortReason = { + readonly kind: HttpStreamAbortReasonKind + readonly detail: string | null +} + +export function readHttpStreamAbortReason(bc: bare.ByteCursor): HttpStreamAbortReason { + return { + kind: readHttpStreamAbortReasonKind(bc), + detail: read15(bc), + } +} + +export function writeHttpStreamAbortReason(bc: bare.ByteCursor, x: HttpStreamAbortReason): void { + writeHttpStreamAbortReasonKind(bc, x.kind) + write15(bc, x.detail) +} + +export type ToEnvoyRequestAbort = { + readonly reason: HttpStreamAbortReason +} + +export function readToEnvoyRequestAbort(bc: bare.ByteCursor): ToEnvoyRequestAbort { + return { + reason: readHttpStreamAbortReason(bc), + } +} + +export function writeToEnvoyRequestAbort(bc: bare.ByteCursor, x: ToEnvoyRequestAbort): void { + writeHttpStreamAbortReason(bc, x.reason) +} export type ToRivetResponseStart = { readonly status: u16 @@ -2059,7 +2169,19 @@ export function writeToRivetResponseChunk(bc: bare.ByteCursor, x: ToRivetRespons bare.writeBool(bc, x.finish) } -export type ToRivetResponseAbort = null +export type ToRivetResponseAbort = { + readonly reason: HttpStreamAbortReason +} + +export function readToRivetResponseAbort(bc: bare.ByteCursor): ToRivetResponseAbort { + return { + reason: readHttpStreamAbortReason(bc), + } +} + +export function writeToRivetResponseAbort(bc: bare.ByteCursor, x: ToRivetResponseAbort): void { + writeHttpStreamAbortReason(bc, x.reason) +} /** * WebSocket @@ -2221,7 +2343,7 @@ export function readToRivetTunnelMessageKind(bc: bare.ByteCursor): ToRivetTunnel case 1: return { tag: "ToRivetResponseChunk", val: readToRivetResponseChunk(bc) } case 2: - return { tag: "ToRivetResponseAbort", val: null } + return { tag: "ToRivetResponseAbort", val: readToRivetResponseAbort(bc) } case 3: return { tag: "ToRivetWebSocketOpen", val: readToRivetWebSocketOpen(bc) } case 4: @@ -2251,6 +2373,7 @@ export function writeToRivetTunnelMessageKind(bc: bare.ByteCursor, x: ToRivetTun } case "ToRivetResponseAbort": { bare.writeU8(bc, 2) + writeToRivetResponseAbort(bc, x.val) break } case "ToRivetWebSocketOpen": { @@ -2319,7 +2442,7 @@ export function readToEnvoyTunnelMessageKind(bc: bare.ByteCursor): ToEnvoyTunnel case 1: return { tag: "ToEnvoyRequestChunk", val: readToEnvoyRequestChunk(bc) } case 2: - return { tag: "ToEnvoyRequestAbort", val: null } + return { tag: "ToEnvoyRequestAbort", val: readToEnvoyRequestAbort(bc) } case 3: return { tag: "ToEnvoyWebSocketOpen", val: readToEnvoyWebSocketOpen(bc) } case 4: @@ -2347,6 +2470,7 @@ export function writeToEnvoyTunnelMessageKind(bc: bare.ByteCursor, x: ToEnvoyTun } case "ToEnvoyRequestAbort": { bare.writeU8(bc, 2) + writeToEnvoyRequestAbort(bc, x.val) break } case "ToEnvoyWebSocketOpen": { @@ -3269,4 +3393,4 @@ function assert(condition: boolean, message?: string): asserts condition { if (!condition) throw new Error(message ?? "Assertion failed") } -export const VERSION = 5; \ No newline at end of file +export const VERSION = 6; \ No newline at end of file diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs index 2e2954658d..f0e7130cb4 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs @@ -1,8 +1,11 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; +use std::sync::Arc; use anyhow::Result; +use rivet_envoy_client::config::ResponseChunk; use serde::{Deserialize, Serialize}; +use tokio::sync::{Mutex, mpsc}; use crate::actor::connection::ConnHandle; use crate::actor::lifecycle_hooks::Reply; @@ -12,11 +15,17 @@ use crate::types::ConnId; use crate::websocket::WebSocket; #[derive(Clone, Debug)] -pub struct Request(http::Request>); +pub struct Request { + inner: http::Request>, + body_stream: Option>>>>>, +} impl Request { pub fn new(body: Vec) -> Self { - Self(http::Request::new(body)) + Self { + inner: http::Request::new(body), + body_stream: None, + } } pub fn from_parts( @@ -24,6 +33,16 @@ impl Request { uri: &str, headers: HashMap, body: Vec, + ) -> Result { + Self::from_parts_with_stream(method, uri, headers, body, None) + } + + pub fn from_parts_with_stream( + method: &str, + uri: &str, + headers: HashMap, + body: Vec, + body_stream: Option>>, ) -> Result { let method = method .parse::() @@ -46,7 +65,10 @@ impl Request { request.headers_mut().insert(header_name, header_value); } - Ok(Self(request)) + Ok(Self { + inner: request, + body_stream: body_stream.map(|rx| Arc::new(Mutex::new(Some(rx)))), + }) } pub fn to_parts(&self) -> (String, String, HashMap, Vec) { @@ -66,12 +88,36 @@ impl Request { ) } + pub fn has_body_stream(&self) -> bool { + self.body_stream.is_some() + } + + pub fn take_body_stream(&self) -> Option>> { + self.body_stream + .as_ref() + .and_then(|body_stream| body_stream.try_lock().ok()) + .and_then(|mut body_stream| body_stream.take()) + } + + pub async fn into_buffered(mut self) -> Self { + if let Some(body_stream) = &self.body_stream { + let mut body_stream = body_stream.lock().await.take(); + if let Some(mut body_stream) = body_stream.take() { + while let Some(chunk) = body_stream.recv().await { + self.inner.body_mut().extend_from_slice(&chunk); + } + } + } + self.body_stream = None; + self + } + pub fn into_inner(self) -> http::Request> { - self.0 + self.inner } pub fn into_body(self) -> Vec { - self.0.into_body() + self.inner.into_body() } } @@ -85,25 +131,28 @@ impl Deref for Request { type Target = http::Request>; fn deref(&self) -> &Self::Target { - &self.0 + &self.inner } } impl DerefMut for Request { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.inner } } impl From>> for Request { fn from(value: http::Request>) -> Self { - Self(value) + Self { + inner: value, + body_stream: None, + } } } impl From for http::Request> { fn from(value: Request) -> Self { - value.0 + value.inner } } @@ -211,6 +260,59 @@ impl From for http::Response> { } } +pub struct StreamingResponse { + status: u16, + headers: HashMap, + body_stream: mpsc::Receiver, +} + +impl StreamingResponse { + pub fn from_parts( + status: u16, + headers: HashMap, + body_stream: mpsc::Receiver, + ) -> Result { + let status_code: http::StatusCode = status + .try_into() + .map_err(|error| invalid_http_response("status", format!("{status}: {error}")))?; + for (name, value) in &headers { + let _: http::header::HeaderName = name.parse().map_err(|error| { + invalid_http_response("header name", format!("{name}: {error}")) + })?; + let _: http::header::HeaderValue = value.parse().map_err(|error| { + invalid_http_response("header value", format!("{name}: {error}")) + })?; + } + + Ok(Self { + status: status_code.as_u16(), + headers, + body_stream, + }) + } + + pub fn into_parts(self) -> (u16, HashMap, mpsc::Receiver) { + (self.status, self.headers, self.body_stream) + } +} + +pub enum ActorHttpResponse { + Buffered(Response), + Stream(StreamingResponse), +} + +impl From for ActorHttpResponse { + fn from(value: Response) -> Self { + Self::Buffered(value) + } +} + +impl From for ActorHttpResponse { + fn from(value: StreamingResponse) -> Self { + Self::Stream(value) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum StateDelta { ActorState(Vec), @@ -273,7 +375,7 @@ pub enum ActorEvent { }, HttpRequest { request: Request, - reply: Reply, + reply: Reply, }, QueueSend { name: String, diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs index c87602edc5..b7545ba90e 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs @@ -49,7 +49,7 @@ use crate::actor::factory::ActorFactory; use crate::actor::keys::{LAST_PUSHED_ALARM_KEY, PERSIST_DATA_KEY}; use crate::actor::lifecycle_hooks::{ActorEvents, ActorStart, Reply}; use crate::actor::messages::{ - ActorEvent, QueueSendResult, Request, Response, SerializeStateReason, StateDelta, + ActorEvent, ActorHttpResponse, QueueSendResult, Request, SerializeStateReason, StateDelta, }; use crate::actor::metrics::startup_phase::StartupPhase; use crate::actor::preload::{PreloadedKv, PreloadedPersistedActor}; @@ -65,7 +65,7 @@ use crate::types::{SaveStateOpts, format_actor_key}; use crate::websocket::WebSocket; pub type ActionDispatchResult = std::result::Result, ActionDispatchError>; -pub type HttpDispatchResult = Result; +pub type HttpDispatchResult = Result; const SERIALIZE_STATE_SHUTDOWN_SANITY_CAP: Duration = Duration::from_secs(15); #[cfg(test)] diff --git a/rivetkit-rust/packages/rivetkit-core/src/lib.rs b/rivetkit-rust/packages/rivetkit-core/src/lib.rs index 63e5be3cde..5b1ee9db44 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/lib.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/lib.rs @@ -124,8 +124,8 @@ pub use actor::factory::{ActorEntryFn, ActorFactory}; pub use actor::kv::Kv; pub use actor::lifecycle_hooks::{ActorEvents, ActorStart, Reply}; pub use actor::messages::{ - ActorEvent, QueueSendResult, QueueSendStatus, Request, Response, SerializeStateReason, - StateDelta, + ActorEvent, ActorHttpResponse, QueueSendResult, QueueSendStatus, Request, Response, + SerializeStateReason, StateDelta, StreamingResponse, }; pub use actor::queue::{ CompletableQueueMessage, EnqueueAndWaitOpts, QueueMessage, QueueNextBatchOpts, QueueNextOpts, @@ -139,6 +139,7 @@ pub use actor::task::{ ActionDispatchResult, ActorTask, DispatchCommand, HttpDispatchResult, LifecycleCommand, LifecycleEvent, LifecycleState, }; +pub use rivet_envoy_client::config::{HTTP_BODY_STREAM_CHANNEL_CAPACITY, ResponseChunk}; pub use actor::task_types::ShutdownKind; pub use actor::work_registry::{ActorWorkKind, ActorWorkPolicy}; pub use error::ActorLifecycle; diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs index 9839d79247..89b2c65d90 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/http.rs @@ -15,12 +15,20 @@ impl RegistryDispatcher { request: HttpRequest, ) -> Result { let original_path = request.path.clone(); - let request = build_http_request(request).await?; + let request = build_http_request(request)?; let route = RegistryHttpRoute::from_paths( &original_path, request.uri().path(), self.handle_inspector_http_in_runtime, )?; + let built_in_inspector_route = + request.uri().path().starts_with("/inspector/") && !self.handle_inspector_http_in_runtime; + let request = if matches!(route, RegistryHttpRoute::Framework(_)) || built_in_inspector_route + { + request.into_buffered().await + } else { + request + }; if matches!( route, RegistryHttpRoute::Framework(FrameworkHttpRoute::Metrics) @@ -70,6 +78,7 @@ impl RegistryDispatcher { request: Request, ) -> Result { let encoding = request_encoding(request.headers()); + let request_method = request.method().clone(); let (reply_tx, reply_rx) = oneshot::channel(); try_send_dispatch_command( &instance.dispatch, @@ -86,7 +95,7 @@ impl RegistryDispatcher { { Ok(response) => { rearm_sleep_after_request(instance.ctx.clone()); - build_envoy_response(response) + build_envoy_response(response, &request_method) } Err(error) => { tracing::error!( @@ -622,16 +631,16 @@ pub(super) fn authorization_bearer_token_map(headers: &HashMap) .and_then(|(_, value)| bearer_token_from_authorization(value)) } -pub(super) async fn build_http_request(request: HttpRequest) -> Result { - let mut body = request.body.unwrap_or_default(); - if let Some(mut body_stream) = request.body_stream { - while let Some(chunk) = body_stream.recv().await { - body.extend_from_slice(&chunk); - } - } - +pub(super) fn build_http_request(request: HttpRequest) -> Result { + let body = request.body.unwrap_or_default(); let request_path = normalize_actor_request_path(&request.path); - Request::from_parts(&request.method, &request_path, request.headers, body) + Request::from_parts_with_stream( + &request.method, + &request_path, + request.headers, + body, + request.body_stream, + ) .with_context(|| format!("build actor request for `{}`", request.path)) } @@ -663,15 +672,47 @@ pub(super) fn is_actor_request_path(path: &str) -> bool { .is_some_and(|byte| matches!(byte, b'/' | b'?')) } -pub(super) fn build_envoy_response(response: Response) -> Result { - let (status, headers, body) = response.to_parts(); +pub(super) fn build_envoy_response( + response: ActorHttpResponse, + request_method: &http::Method, +) -> Result { + match response { + ActorHttpResponse::Buffered(response) => { + let (status, headers, body) = response.to_parts(); + Ok(HttpResponse { + status, + headers, + body: Some(if response_body_forbidden(request_method, status) { + Vec::new() + } else { + body + }), + body_stream: None, + }) + } + ActorHttpResponse::Stream(response) => { + let (status, headers, body_stream) = response.into_parts(); + if response_body_forbidden(request_method, status) { + Ok(HttpResponse { + status, + headers, + body: Some(Vec::new()), + body_stream: None, + }) + } else { + Ok(HttpResponse { + status, + headers, + body: None, + body_stream: Some(body_stream), + }) + } + } + } +} - Ok(HttpResponse { - status, - headers, - body: Some(body), - body_stream: None, - }) +pub(super) fn response_body_forbidden(request_method: &http::Method, status: u16) -> bool { + *request_method == http::Method::HEAD || matches!(status, 100..=199 | 204 | 304) } fn actor_specifier_for_instance(instance: &ActorTaskHandle) -> ActorSpecifier { diff --git a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs index a9c23bf95f..27e2c2d19e 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs @@ -40,7 +40,9 @@ use crate::actor::context::{ActorContext, InspectorAttachGuard}; use crate::actor::factory::ActorFactory; use crate::actor::keys::PERSIST_DATA_KEY; use crate::actor::lifecycle_hooks::Reply; -use crate::actor::messages::{ActorEvent, QueueSendResult, Request, Response, StateDelta}; +use crate::actor::messages::{ + ActorEvent, ActorHttpResponse, QueueSendResult, Request, StateDelta, +}; use crate::actor::preload::{PreloadedKv, PreloadedPersistedActor}; use crate::actor::state::decode_persisted_actor; use crate::actor::task::{ diff --git a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs index 23e0c365af..dac476d31f 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs @@ -6,16 +6,20 @@ mod moved_tests { use super::{ HttpResponseEncoding, authorization_bearer_token, authorization_bearer_token_map, - framework_action_error_response, framework_anyhow_error_response_with_actor, - is_actor_request_path, message_boundary_error_response, + build_envoy_response, framework_action_error_response, + framework_anyhow_error_response_with_actor, is_actor_request_path, + message_boundary_error_response, message_boundary_error_response_with_actor, normalize_actor_request_path, request_encoding, workflow_dispatch_result, }; use crate::actor::action::ActionDispatchError; + use crate::actor::messages::{ActorHttpResponse, Response, StreamingResponse}; use crate::error::ActorLifecycle as ActorLifecycleError; use http::StatusCode; + use rivet_envoy_client::config::ResponseChunk; use rivet_error::{ActorSpecifier, MacroMarker, RivetError, RivetErrorSchema}; use serde_json::json; + use tokio::sync::mpsc; use vbare::OwnedVersionedData; #[derive(RivetError)] @@ -57,6 +61,76 @@ mod moved_tests { assert_eq!(error.code(), "destroying"); } + #[tokio::test] + async fn build_envoy_response_preserves_stream_body() { + let (tx, rx) = mpsc::channel(1); + let stream = StreamingResponse::from_parts( + StatusCode::OK.as_u16(), + HashMap::from([("content-type".to_owned(), "text/event-stream".to_owned())]), + rx, + ) + .expect("streaming response should build"); + + let mut response = + build_envoy_response(ActorHttpResponse::Stream(stream), &http::Method::GET) + .expect("envoy response should build"); + + assert_eq!(response.status, StatusCode::OK.as_u16()); + assert_eq!(response.body, None); + assert!(response.body_stream.is_some()); + + tx.send(ResponseChunk::Data { + data: b"data: ok\n\n".to_vec(), + finish: true, + }) + .await + .expect("send response chunk"); + let chunk = response + .body_stream + .as_mut() + .expect("body stream should exist") + .recv() + .await + .expect("stream chunk should arrive"); + match chunk { + ResponseChunk::Data { data, finish } => { + assert_eq!(data, b"data: ok\n\n"); + assert!(finish); + } + ResponseChunk::Error(error) => panic!("unexpected response chunk error: {error}"), + } + } + + #[test] + fn build_envoy_response_drops_stream_for_body_forbidden_status() { + let (_tx, rx) = mpsc::channel(1); + let stream = + StreamingResponse::from_parts(StatusCode::NO_CONTENT.as_u16(), HashMap::new(), rx) + .expect("streaming response should build"); + + let response = build_envoy_response(ActorHttpResponse::Stream(stream), &http::Method::GET) + .expect("envoy response should build"); + + assert_eq!(response.status, StatusCode::NO_CONTENT.as_u16()); + assert_eq!(response.body, Some(Vec::new())); + assert!(response.body_stream.is_none()); + } + + #[test] + fn build_envoy_response_drops_buffered_body_for_head() { + let response = + Response::from_parts(StatusCode::OK.as_u16(), HashMap::new(), b"body".to_vec()) + .expect("buffered response should build"); + + let response = + build_envoy_response(ActorHttpResponse::Buffered(response), &http::Method::HEAD) + .expect("envoy response should build"); + + assert_eq!(response.status, StatusCode::OK.as_u16()); + assert_eq!(response.body, Some(Vec::new())); + assert!(response.body_stream.is_none()); + } + #[test] fn inspector_error_status_maps_action_timeout_to_408() { assert_eq!( diff --git a/rivetkit-rust/packages/rivetkit/src/event.rs b/rivetkit-rust/packages/rivetkit/src/event.rs index 1c41d4852f..bf53c05600 100644 --- a/rivetkit-rust/packages/rivetkit/src/event.rs +++ b/rivetkit-rust/packages/rivetkit/src/event.rs @@ -5,8 +5,8 @@ use ciborium::Value; use rivetkit_core::actor::ShutdownKind; use rivetkit_core::error::ActorRuntime; use rivetkit_core::{ - ActorEvent, QueueSendResult, QueueSendStatus, Reply, Request, Response, SerializeStateReason, - StateDelta, WebSocket, + ActorEvent, ActorHttpResponse, QueueSendResult, QueueSendStatus, Reply, Request, Response, + SerializeStateReason, StateDelta, WebSocket, }; use serde::{ Serialize, @@ -956,7 +956,7 @@ impl de::Expected for Expected { #[must_use = "reply to the HTTP call or dropping it sends actor/dropped_reply"] pub struct HttpCall { pub(crate) request: Option, - pub(crate) reply: Option>, + pub(crate) reply: Option>, } impl Drop for HttpCall { @@ -975,7 +975,7 @@ impl Drop for HttpCall { #[derive(Debug)] #[must_use = "reply to the deferred HTTP call or dropping it sends actor/dropped_reply"] pub struct HttpReply { - reply: Option>, + reply: Option>, } impl Drop for HttpReply { @@ -989,7 +989,7 @@ impl Drop for HttpReply { impl HttpReply { pub fn reply(mut self, response: Response) { if let Some(reply) = self.reply.take() { - reply.send(Ok(response)); + reply.send(Ok(ActorHttpResponse::Buffered(response))); } } @@ -1034,7 +1034,7 @@ impl HttpCall { pub fn reply(mut self, response: Response) { if let Some(reply) = self.reply.take() { - reply.send(Ok(response)); + reply.send(Ok(ActorHttpResponse::Buffered(response))); } } @@ -1894,6 +1894,9 @@ mod tests { .block_on(reply_rx) .expect("receive http reply") .expect("http reply_status should succeed"); + let ActorHttpResponse::Buffered(response) = response else { + panic!("http reply_status should return buffered response"); + }; assert_eq!(response.status().as_u16(), 404); assert!(response.body().is_empty()); } diff --git a/rivetkit-rust/packages/rivetkit/src/start.rs b/rivetkit-rust/packages/rivetkit/src/start.rs index 4415267ef5..71f1973661 100644 --- a/rivetkit-rust/packages/rivetkit/src/start.rs +++ b/rivetkit-rust/packages/rivetkit/src/start.rs @@ -278,7 +278,13 @@ async fn handle_actor_event( } } ActorEvent::HttpRequest { request, reply } => { - reply.send(actor.on_fetch(ctx, request).await); + let request = request.into_buffered().await; + reply.send( + actor + .on_fetch(ctx, request) + .await + .map(rivetkit_core::ActorHttpResponse::Buffered), + ); } ActorEvent::QueueSend { name, @@ -894,6 +900,9 @@ mod tests { .expect("send http event"); let response = reply_rx.await.expect("http reply").expect("http response"); + let rivetkit_core::ActorHttpResponse::Buffered(response) = response else { + panic!("default fetch should return buffered response"); + }; assert_eq!(response.status().as_u16(), 404); request_sleep(&tx).await; diff --git a/rivetkit-typescript/packages/rivetkit-napi/index.d.ts b/rivetkit-typescript/packages/rivetkit-napi/index.d.ts index 6d99414903..e5287954cb 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/index.d.ts +++ b/rivetkit-typescript/packages/rivetkit-napi/index.d.ts @@ -39,6 +39,7 @@ export interface JsHttpResponse { status?: number headers?: Record body?: Buffer + stream?: boolean } export interface JsQueueSendResult { status: string @@ -280,6 +281,15 @@ export declare class ActorContext { runtimeState(): object clearRuntimeState(): void } +export declare class HttpResponseBodyStream { + write(chunk: Buffer): Promise + end(): Promise + error(message: string): Promise +} +export declare class HttpRequestBodyStream { + read(): Promise + cancel(): Promise +} export declare class NapiActorFactory { constructor(callbacks: object, config?: JsActorConfig | undefined | null) } diff --git a/rivetkit-typescript/packages/rivetkit-napi/index.js b/rivetkit-typescript/packages/rivetkit-napi/index.js index d5c3c616dc..afa005ce5f 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/index.js +++ b/rivetkit-typescript/packages/rivetkit-napi/index.js @@ -310,9 +310,11 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { ActorContext, NapiActorFactory, CancellationToken, ConnHandle, JsNativeDatabase, Kv, Queue, QueueMessage, CoreRegistry, Schedule, WebSocket } = nativeBinding +const { ActorContext, HttpResponseBodyStream, HttpRequestBodyStream, NapiActorFactory, CancellationToken, ConnHandle, JsNativeDatabase, Kv, Queue, QueueMessage, CoreRegistry, Schedule, WebSocket } = nativeBinding module.exports.ActorContext = ActorContext +module.exports.HttpResponseBodyStream = HttpResponseBodyStream +module.exports.HttpRequestBodyStream = HttpRequestBodyStream module.exports.NapiActorFactory = NapiActorFactory module.exports.CancellationToken = CancellationToken module.exports.ConnHandle = ConnHandle diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs b/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs index c25e8c92ce..8358e44049 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs @@ -11,9 +11,11 @@ use rivet_error::{ActorSpecifier, RivetError, RivetErrorKind}; use rivetkit_core::inspector::InspectorTabEntry; use rivetkit_core::{ ActionDefinition, ActorConfig, ActorConfigInput, ActorContext as CoreActorContext, - ActorFactory as CoreActorFactory, ConnHandle as CoreConnHandle, Request, Response, - WebSocket as CoreWebSocket, + ActorFactory as CoreActorFactory, ActorHttpResponse, ConnHandle as CoreConnHandle, Request, + Response, ResponseChunk, StreamingResponse, WebSocket as CoreWebSocket, + HTTP_BODY_STREAM_CHANNEL_CAPACITY, }; +use tokio::sync::{Mutex as TokioMutex, mpsc}; use crate::actor_context::{ActorContext, StateDeltaPayload}; use crate::cancellation_token::CancellationToken; @@ -45,6 +47,107 @@ pub struct JsHttpResponse { pub status: Option, pub headers: Option>, pub body: Option, + pub stream: Option, +} + +#[napi] +#[derive(Clone)] +pub struct HttpResponseBodyStream { + tx: Arc>>>, +} + +impl HttpResponseBodyStream { + fn new(tx: mpsc::Sender) -> Self { + Self { + tx: Arc::new(TokioMutex::new(Some(tx))), + } + } + + async fn take_sender(&self) -> napi::Result> { + self.tx.lock().await.take().ok_or_else(|| { + napi::Error::from_reason("http response body stream is already closed") + }) + } +} + +#[napi] +impl HttpResponseBodyStream { + #[napi] + pub async fn write(&self, chunk: Buffer) -> napi::Result<()> { + let tx = self.tx.lock().await.clone().ok_or_else(|| { + napi::Error::from_reason("http response body stream is already closed") + })?; + tx.send(ResponseChunk::Data { + data: chunk.to_vec(), + finish: false, + }) + .await + .map_err(|_| napi::Error::from_reason("http response body stream receiver dropped")) + } + + #[napi] + pub async fn end(&self) -> napi::Result<()> { + let tx = self.take_sender().await?; + tx.send(ResponseChunk::Data { + data: Vec::new(), + finish: true, + }) + .await + .map_err(|_| napi::Error::from_reason("http response body stream receiver dropped")) + } + + #[napi] + pub async fn error(&self, message: String) -> napi::Result<()> { + let tx = self.take_sender().await?; + tx.send(ResponseChunk::Error(message)) + .await + .map_err(|_| napi::Error::from_reason("http response body stream receiver dropped")) + } +} + +#[napi] +pub struct HttpRequestBodyStream { + initial_body: Arc>>>, + rx: Arc>>>>, +} + +impl HttpRequestBodyStream { + fn new(initial_body: Vec, rx: mpsc::Receiver>) -> Self { + Self { + initial_body: Arc::new(TokioMutex::new(Some(initial_body))), + rx: Arc::new(TokioMutex::new(Some(rx))), + } + } +} + +#[napi] +impl HttpRequestBodyStream { + #[napi] + pub async fn read(&self) -> napi::Result> { + if let Some(initial_body) = self.initial_body.lock().await.take() { + if !initial_body.is_empty() { + return Ok(Some(Buffer::from(initial_body))); + } + } + + let mut rx_guard = self.rx.lock().await; + let Some(rx) = rx_guard.as_mut() else { + return Ok(None); + }; + match rx.recv().await { + Some(chunk) => Ok(Some(Buffer::from(chunk))), + None => { + rx_guard.take(); + Ok(None) + } + } + } + + #[napi] + pub async fn cancel(&self) -> napi::Result<()> { + self.rx.lock().await.take(); + Ok(()) + } } #[napi(object)] @@ -144,6 +247,7 @@ pub(crate) struct HttpRequestPayload { pub(crate) ctx: CoreActorContext, pub(crate) request: Request, pub(crate) cancel_token: Option, + pub(crate) response_stream: Option, } #[derive(Clone)] @@ -552,8 +656,11 @@ where pub(crate) async fn call_request( callback_name: &str, callback: &CallbackTsfn, - payload: HttpRequestPayload, -) -> Result { + mut payload: HttpRequestPayload, +) -> Result { + let (body_tx, body_rx) = mpsc::channel(HTTP_BODY_STREAM_CHANNEL_CAPACITY); + payload.response_stream = Some(HttpResponseBodyStream::new(body_tx)); + log_tsfn_invocation(callback_name, &payload); let promise = callback .call_async::>(Ok(payload)) @@ -562,14 +669,21 @@ pub(crate) async fn call_request( let response = promise .await .map_err(|error| callback_error(callback_name, error))?; - Response::from_parts( - response.status.unwrap_or(200), - response.headers.unwrap_or_default(), - response - .body - .unwrap_or_else(|| Buffer::from(Vec::new())) - .to_vec(), - ) + let status = response.status.unwrap_or(200); + let headers = response.headers.unwrap_or_default(); + if response.stream.unwrap_or(false) { + StreamingResponse::from_parts(status, headers, body_rx).map(ActorHttpResponse::Stream) + } else { + Response::from_parts( + status, + headers, + response + .body + .unwrap_or_else(|| Buffer::from(Vec::new())) + .to_vec(), + ) + .map(ActorHttpResponse::Buffered) + } } #[allow(dead_code)] @@ -823,6 +937,10 @@ fn build_http_request_payload( let mut object = env.create_object()?; object.set("ctx", ActorContext::new(payload.ctx))?; object.set("request", build_request_object(env, payload.request)?)?; + match payload.response_stream { + Some(response_stream) => object.set("responseBodyStream", response_stream)?, + None => object.set("responseBodyStream", env.get_undefined()?)?, + } match payload.cancel_token { Some(cancel_token) => object.set("cancelToken", CancellationToken::new(cancel_token))?, None => object.set("cancelToken", env.get_undefined()?)?, @@ -958,11 +1076,21 @@ fn build_serialize_state_payload( fn build_request_object(env: &Env, request: Request) -> napi::Result { let (method, uri, headers, body) = request.to_parts(); + let body_stream = request.take_body_stream(); let mut request_object = env.create_object()?; request_object.set("method", method)?; request_object.set("uri", uri)?; request_object.set("headers", headers)?; - request_object.set("body", Buffer::from(body))?; + if let Some(body_stream) = body_stream { + request_object.set("body", env.get_undefined()?)?; + request_object.set( + "bodyStream", + HttpRequestBodyStream::new(body, body_stream), + )?; + } else { + request_object.set("body", Buffer::from(body))?; + request_object.set("bodyStream", env.get_undefined()?)?; + } Ok(request_object) } diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs index 34cf0b172b..74e983df52 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs @@ -1154,7 +1154,7 @@ async fn call_http_request( ctx: &ActorContext, request: rivetkit_core::Request, cancel_token: Option, -) -> Result { +) -> Result { call_request( "onRequest", callback, @@ -1162,6 +1162,7 @@ async fn call_http_request( ctx: ctx.inner().clone(), request, cancel_token, + response_stream: None, }, ) .await diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/raw-http.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/raw-http.ts index 5d22fa6ce5..c0ea3bb5f2 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/raw-http.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/raw-http.ts @@ -5,7 +5,7 @@ export const rawHttpActor = actor({ state: { requestCount: 0, }, - onRequest( + async onRequest( ctx: RequestContext, request: Request, ) { @@ -31,6 +31,50 @@ export const rawHttpActor = actor({ }); } + if (url.pathname === "/api/stream") { + const encoder = new TextEncoder(); + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue(encoder.encode("data: first\n\n")); + await new Promise((resolve) => + setTimeout(resolve, 150), + ); + controller.enqueue(encoder.encode("data: second\n\n")); + controller.close(); + }, + }), + { + headers: { "Content-Type": "text/event-stream" }, + }, + ); + } + + if (url.pathname === "/api/upload-stream" && method === "POST") { + const reader = request.body?.getReader(); + const sizes: number[] = []; + let totalBytes = 0; + if (reader) { + for (;;) { + const next = await reader.read(); + if (next.done) break; + sizes.push(next.value.byteLength); + totalBytes += next.value.byteLength; + } + } + return new Response( + JSON.stringify({ + chunkCount: sizes.length, + contentLength: request.headers.get("content-length"), + sizes, + totalBytes, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + } + if (url.pathname === "/api/state") { return new Response( JSON.stringify({ diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts index 5d8578980c..4a461fd54f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts @@ -17,8 +17,7 @@ export async function sendHttpRequestToGateway( actorRequest: Request, options: HttpGatewayRequestOptions = {}, ): Promise { - // Handle body properly based on method and presence - let bodyToSend: ArrayBuffer | null = null; + let bodyToSend: ReadableStream | null = null; const guardHeaders = buildGuardHeaders(runConfig, actorRequest, options); if (actorRequest.method !== "GET" && actorRequest.method !== "HEAD") { @@ -26,28 +25,22 @@ export async function sendHttpRequestToGateway( throw new Error("Request body has already been consumed"); } - // TODO: This buffers the entire request in memory every time. We - // need to properly implement streaming bodies. - const reqBody = await actorRequest.arrayBuffer(); - - if (reqBody.byteLength !== 0) { - bodyToSend = reqBody; - - // If this is a streaming request, we need to convert the headers - // for the basic array buffer. + if (actorRequest.body) { + bodyToSend = actorRequest.body; guardHeaders.delete("transfer-encoding"); guardHeaders.delete("content-length"); } } - const guardRequest = new Request(gatewayUrl, { - method: actorRequest.method, - headers: guardHeaders, - body: bodyToSend, - signal: actorRequest.signal, - }); - - return mutableResponse(await fetch(guardRequest)); + return mutableResponse( + await fetch(gatewayUrl, { + method: actorRequest.method, + headers: guardHeaders, + body: bodyToSend, + signal: actorRequest.signal, + ...(bodyToSend ? { duplex: "half" } : {}), + } as RequestInit), + ); } function mutableResponse(fetchRes: Response): Response { diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts index 3c94653463..e851f64638 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts @@ -89,6 +89,22 @@ import { createWriteThroughProxy } from "./write-through-proxy"; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); +const HTTP_BODY_CHUNK_SIZE = 64 * 1024; + +interface NativeHttpResponseBodyStream { + write(chunk: Uint8Array): Promise; + end(): Promise; + error(message: string): Promise; +} + +interface NativeHttpRequestBodyStream { + read(): Promise; + cancel(): Promise; +} + +type NativeReadableStreamReadResult = Awaited< + ReturnType["read"]> +>; type ResolvedRuntimeKind = Exclude; type RuntimeHostKind = "node-like" | "edge-like"; @@ -1119,33 +1135,222 @@ function buildRequest(init: { uri: string; headers?: Record; body?: RuntimeBytes; + bodyStream?: NativeHttpRequestBodyStream; + signal?: AbortSignal; }): Request { const url = init.uri.startsWith("http") ? init.uri : new URL(init.uri, "http://127.0.0.1").toString(); - const body = - init.body && init.body.length > 0 - ? runtimeBytesToArrayBuffer(init.body) - : undefined; + const method = init.method.toUpperCase(); + const bodyForbidden = method === "GET" || method === "HEAD"; + const body = bodyForbidden + ? undefined + : init.bodyStream + ? new ReadableStream({ + async pull(controller) { + try { + if (init.body && init.body.length > 0) { + controller.enqueue(new Uint8Array(init.body)); + init.body = undefined; + return; + } + const chunk = await init.bodyStream?.read(); + if (!chunk) { + controller.close(); + } else { + controller.enqueue(new Uint8Array(chunk)); + } + } catch (error) { + controller.error(error); + } + }, + async cancel() { + await init.bodyStream?.cancel(); + }, + }) + : init.body && init.body.length > 0 + ? runtimeBytesToArrayBuffer(init.body) + : undefined; + const streamInit = init.bodyStream && !bodyForbidden ? { duplex: "half" } : {}; return new Request(url, { - method: init.method, + method, headers: init.headers, body, - }); + signal: init.signal, + ...streamInit, + } as RequestInit); +} + +function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array { + const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const out = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + out.set(chunk, offset); + offset += chunk.byteLength; + } + return out; +} + +async function writeHttpResponseChunk( + stream: NativeHttpResponseBodyStream, + chunk: Uint8Array, +) { + for (let offset = 0; offset < chunk.byteLength; offset += HTTP_BODY_CHUNK_SIZE) { + await stream.write(chunk.subarray(offset, offset + HTTP_BODY_CHUNK_SIZE)); + } +} + +async function pumpRuntimeHttpResponseBody( + reader: ReadableStreamDefaultReader, + stream: NativeHttpResponseBodyStream, + initialChunks: Uint8Array[], + pendingRead?: Promise, +) { + try { + for (const chunk of initialChunks) { + await writeHttpResponseChunk(stream, chunk); + } + for (;;) { + const next = pendingRead ? await pendingRead : await reader.read(); + pendingRead = undefined; + if (next.done) { + await stream.end(); + return; + } + if (next.value?.byteLength) { + await writeHttpResponseChunk(stream, next.value); + } + } + } catch (error) { + try { + await stream.error(stringifyError(error)); + } catch (streamError) { + logger().debug({ + msg: "failed to report native http response stream error", + error: streamError, + }); + } + try { + await reader.cancel(error); + } catch { + // Reader may already be closed after a native-side disconnect. + } + } finally { + reader.releaseLock(); + } } async function toRuntimeHttpResponse( response: Response, + responseBodyStream?: NativeHttpResponseBodyStream, ): Promise { const headers = Object.fromEntries(response.headers.entries()); - const body = new Uint8Array(await response.arrayBuffer()); + if (!response.body) { + return { + status: response.status, + headers, + body: new Uint8Array(), + }; + } + + const reader = response.body.getReader(); + const first = await reader.read(); + if (first.done) { + reader.releaseLock(); + return { + status: response.status, + headers, + body: new Uint8Array(), + }; + } + + const firstChunk = first.value ?? new Uint8Array(); + const secondRead = reader.read(); + const secondResult = await Promise.race< + | { kind: "ready"; value: NativeReadableStreamReadResult } + | { kind: "pending" } + >([ + secondRead.then((value) => ({ kind: "ready", value }) as const), + Promise.resolve({ kind: "pending" } as const), + ]); + if (secondResult.kind === "pending") { + if (!responseBodyStream) { + const chunks = [firstChunk]; + for (;;) { + const next = await secondRead; + if (next.done) break; + if (next.value) chunks.push(next.value); + break; + } + for (;;) { + const next = await reader.read(); + if (next.done) break; + if (next.value) chunks.push(next.value); + } + reader.releaseLock(); + return { + status: response.status, + headers, + body: concatUint8Arrays(chunks), + }; + } + + void pumpRuntimeHttpResponseBody( + reader, + responseBodyStream, + [firstChunk], + secondRead, + ); + return { + status: response.status, + headers, + stream: true, + }; + } + + const second = secondResult.value; + if (second.done) { + reader.releaseLock(); + return { + status: response.status, + headers, + body: firstChunk, + }; + } + + const secondChunk = second.value ?? new Uint8Array(); + if (!responseBodyStream) { + const chunks = [firstChunk, secondChunk]; + for (;;) { + const next = await reader.read(); + if (next.done) break; + if (next.value) chunks.push(next.value); + } + reader.releaseLock(); + return { + status: response.status, + headers, + body: concatUint8Arrays(chunks), + }; + } + + void pumpRuntimeHttpResponseBody(reader, responseBodyStream, [ + firstChunk, + secondChunk, + ]); return { status: response.status, headers, - body, + stream: true, }; } +export const nativeRegistryTestInternals = { + buildRequest, + toRuntimeHttpResponse, +}; + function toActorKey( segments: Array<{ kind: string; @@ -3529,19 +3734,22 @@ export function buildNativeFactory( ); const maybeHandleNativeInspectorRequest = async ( ctx: ActorContextHandle, - _rawRequest: { + rawRequest: { method: string; uri: string; headers?: Record; body?: RuntimeBytes; }, - jsRequest: Request, ): Promise => { - const url = new URL(jsRequest.url); + const rawUrl = rawRequest.uri.startsWith("http") + ? rawRequest.uri + : new URL(rawRequest.uri, "http://127.0.0.1").toString(); + const url = new URL(rawUrl); if (!url.pathname.startsWith("/inspector/")) { return undefined; } + const jsRequest = buildRequest(rawRequest); const jsonResponse = (body: unknown, init?: ResponseInit) => new Response(JSON.stringify(body), { status: init?.status ?? 200, @@ -4424,34 +4632,41 @@ export function buildNativeFactory( uri: string; headers?: Record; body?: RuntimeBytes; + bodyStream?: NativeHttpRequestBodyStream; }; cancelToken?: CancellationTokenHandle; + responseBodyStream?: NativeHttpResponseBodyStream; }, ) => { try { - const { ctx, request, cancelToken } = unwrapTsfnPayload( - error, - payload, - ); - const jsRequest = buildRequest(request); + const { + ctx, + request, + cancelToken, + responseBodyStream, + } = unwrapTsfnPayload(error, payload); const inspectorResponse = await maybeHandleNativeInspectorRequest( ctx, request, - jsRequest, ); if (inspectorResponse) { - return await toRuntimeHttpResponse(inspectorResponse); + return await toRuntimeHttpResponse( + inspectorResponse, + responseBodyStream, + ); } if (typeof config.onRequest !== "function") { return await toRuntimeHttpResponse( new Response(null, { status: 404 }), + responseBodyStream, ); } + const handlerRequest = buildRequest(request); const rawConnParams = - jsRequest.headers.get(HEADER_CONN_PARAMS); + handlerRequest.headers.get(HEADER_CONN_PARAMS); let requestCtx: | ReturnType | undefined; @@ -4473,19 +4688,22 @@ export function buildNativeFactory( requestCtx = makeConnCtx( ctx, conn, - jsRequest, + handlerRequest, cancelToken, ); const response = await config.onRequest( requestCtx, - jsRequest, + handlerRequest, ); if (!(response instanceof Response)) { throw new Error( "onRequest handler must return a Response", ); } - return await toRuntimeHttpResponse(response); + return await toRuntimeHttpResponse( + response, + responseBodyStream, + ); } finally { await requestCtx?.dispose(); if (conn) { diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts b/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts index b8dca80bb8..f5de71c94f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts @@ -35,6 +35,7 @@ export interface RuntimeHttpResponse { status?: number; headers?: Record; body?: RuntimeBytes; + stream?: boolean; } export interface RuntimeStateDeltaPayload { diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver/streaming-http.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver/streaming-http.test.ts new file mode 100644 index 0000000000..536597d669 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/driver/streaming-http.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "vitest"; +import { describeDriverMatrix } from "./shared-matrix"; +import { setupDriverTest } from "./shared-utils"; + +function delay(ms: number): Promise<"timeout"> { + return new Promise((resolve) => setTimeout(() => resolve("timeout"), ms)); +} + +describeDriverMatrix( + "Streaming Http", + (driverTestConfig) => { + describe("streaming http", () => { + test("streams response chunks before the body completes", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = client.rawHttpActor.getOrCreate([ + "stream-response", + ]); + + const response = await actor.fetch("api/stream"); + expect(response.ok).toBe(true); + expect(response.headers.get("content-type")).toContain( + "text/event-stream", + ); + + const reader = response.body?.getReader(); + expect(reader).toBeDefined(); + const decoder = new TextDecoder(); + const first = await reader!.read(); + expect(first.done).toBe(false); + expect(decoder.decode(first.value)).toBe("data: first\n\n"); + + const secondRead = reader!.read(); + const earlySecond = await Promise.race([ + secondRead, + delay(50), + ]); + expect(earlySecond).toBe("timeout"); + + const second = await secondRead; + expect(second.done).toBe(false); + expect(decoder.decode(second.value)).toBe("data: second\n\n"); + expect(await reader!.read()).toEqual({ + done: true, + value: undefined, + }); + }); + + test("exposes gateway-chunked request bodies as Request streams", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = client.rawHttpActor.getOrCreate([ + "stream-upload", + ]); + const requestBody = new Uint8Array(80 * 1024); + requestBody.fill(1, 0, 40 * 1024); + requestBody.fill(2, 40 * 1024); + + const response = await actor.fetch("api/upload-stream", { + method: "POST", + body: Buffer.from(requestBody), + }); + + expect(response.ok).toBe(true); + const body = (await response.json()) as { + chunkCount: number; + contentLength: string | null; + sizes: number[]; + totalBytes: number; + }; + expect(body.totalBytes, JSON.stringify(body)).toBe(requestBody.byteLength); + expect(body.chunkCount).toBeGreaterThanOrEqual(2); + expect(Math.max(...body.sizes)).toBeLessThanOrEqual(64 * 1024); + }); + }); + }, + { + runtimes: ["native"], + encodings: ["bare"], + sqliteBackends: ["remote"], + config: { + useRealTimers: true, + }, + }, +); diff --git a/rivetkit-typescript/packages/rivetkit/tests/native-http-streaming.test.ts b/rivetkit-typescript/packages/rivetkit/tests/native-http-streaming.test.ts new file mode 100644 index 0000000000..f16a279126 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/native-http-streaming.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, test } from "vitest"; +import { nativeRegistryTestInternals } from "../src/registry/native"; + +describe("native http response streaming", () => { + test("constructs requests with streaming bodies and abort signals", async () => { + const chunks = [new Uint8Array([1]), new Uint8Array([2])]; + const controller = new AbortController(); + const request = nativeRegistryTestInternals.buildRequest({ + method: "POST", + uri: "/upload", + body: chunks.shift(), + bodyStream: { + async read() { + return chunks.shift() ?? null; + }, + async cancel() {}, + }, + signal: controller.signal, + }); + + controller.abort(); + + expect(request.signal.aborted).toBe(true); + expect(new Uint8Array(await request.arrayBuffer())).toEqual( + new Uint8Array([1, 2]), + ); + }); + + test("preserves native request stream chunks through Request bodies", async () => { + const chunkSizes = [13_093, 16_384, 32_768, 19_675]; + const chunks = chunkSizes.map((size, index) => { + const chunk = new Uint8Array(size); + chunk.fill(index + 1); + return chunk; + }); + const request = nativeRegistryTestInternals.buildRequest({ + method: "POST", + uri: "/upload", + bodyStream: { + async read() { + return chunks.shift() ?? null; + }, + async cancel() {}, + }, + }); + + const reader = request.body?.getReader(); + expect(reader).toBeDefined(); + + const sizes: number[] = []; + let totalBytes = 0; + for (;;) { + const next = await reader!.read(); + if (next.done) break; + sizes.push(next.value.byteLength); + totalBytes += next.value.byteLength; + } + + expect(sizes).toEqual(chunkSizes); + expect(totalBytes).toBe(80 * 1024); + }); + + test("streams multi-chunk responses through the native body stream", async () => { + const writes: Uint8Array[] = []; + let finish!: () => void; + const finished = new Promise((resolve) => { + finish = resolve; + }); + + const responseBodyStream = { + async write(chunk: Uint8Array) { + writes.push(new Uint8Array(chunk)); + }, + async end() { + finish(); + }, + async error(message: string) { + throw new Error(message); + }, + }; + const largeChunk = new Uint8Array(64 * 1024 + 1); + largeChunk.fill(7); + const response = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1])); + controller.enqueue(largeChunk); + controller.close(); + }, + }), + { + status: 202, + headers: { + "x-streamed": "yes", + }, + }, + ); + + const runtimeResponse = + await nativeRegistryTestInternals.toRuntimeHttpResponse( + response, + responseBodyStream, + ); + await finished; + + expect(runtimeResponse).toEqual({ + status: 202, + headers: { + "x-streamed": "yes", + }, + stream: true, + }); + expect(writes.map((chunk) => chunk.byteLength)).toEqual([ + 1, + 64 * 1024, + 1, + ]); + expect(writes[0]).toEqual(new Uint8Array([1])); + expect(writes[1][0]).toBe(7); + expect(writes[2][0]).toBe(7); + }); +}); diff --git a/website/src/content/docs/actors/lifecycle.mdx b/website/src/content/docs/actors/lifecycle.mdx index fa959b8fbf..748cdf3ceb 100644 --- a/website/src/content/docs/actors/lifecycle.mdx +++ b/website/src/content/docs/actors/lifecycle.mdx @@ -611,6 +611,8 @@ const chatRoom = actor({ The `onRequest` hook handles HTTP requests sent to your actor at `/actors/{actorName}/http/*` endpoints. Can be async. It receives the request context and a standard `Request` object, and should return a `Response` object. +HTTP bodies use standard Web Streams. In the native runtime, `request.body` can be read incrementally and `Response` bodies backed by `ReadableStream` are sent to the client chunk by chunk, which works for SSE and upload-style handlers without a Rivet-specific opt-in header. + See [Request Handler](/docs/actors/request-handler) for more details. ```typescript