reddb_server/wire/redwire/
output_stream.rs1use std::collections::HashMap;
27use std::sync::Arc;
28
29use tokio::sync::{oneshot, Mutex};
30
31use crate::application::OperationContext;
32use crate::auth::Role;
33use crate::runtime::RedDBRuntime;
34use crate::serde_json::Value as JsonValue;
35use crate::server::output_stream::{
36 self as outs, Clock, OpenStreamError, StreamConfig, SystemClock,
37};
38pub use reddb_wire::redwire::stream::{
39 OpenStreamParseError, OpenStreamRequest, StreamCancelRequest,
40};
41use reddb_wire::redwire::{encode_frame, Frame};
42
43pub fn parse_open_stream(payload: &[u8]) -> Result<OpenStreamRequest, OpenStreamParseError> {
44 reddb_wire::redwire::stream::parse_open_stream(payload)
45}
46
47pub fn parse_stream_cancel(payload: &[u8]) -> StreamCancelRequest {
48 reddb_wire::redwire::stream::parse_stream_cancel(payload)
49}
50
51pub fn build_open_ack_frame(
52 correlation_id: u64,
53 stream_id: u16,
54 lease_id: u64,
55 snapshot_lsn: u64,
56 resumable: bool,
57) -> Result<Frame, reddb_wire::BuildError> {
58 reddb_wire::redwire::stream::build_open_ack_frame(
59 correlation_id,
60 stream_id,
61 lease_id,
62 snapshot_lsn,
63 resumable,
64 )
65}
66
67#[derive(Default)]
72pub struct StreamRegistry {
73 inner: Mutex<HashMap<u16, oneshot::Sender<()>>>,
74}
75
76impl StreamRegistry {
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub async fn register(&self, stream_id: u16) -> Result<oneshot::Receiver<()>, RegisterError> {
85 if stream_id == 0 {
86 return Err(RegisterError::ReservedStreamId);
87 }
88 let mut guard = self.inner.lock().await;
89 if guard.contains_key(&stream_id) {
90 return Err(RegisterError::StreamInUse);
91 }
92 let (tx, rx) = oneshot::channel();
93 guard.insert(stream_id, tx);
94 Ok(rx)
95 }
96
97 pub async fn cancel(&self, stream_id: u16) -> bool {
101 let mut guard = self.inner.lock().await;
102 match guard.remove(&stream_id) {
103 Some(tx) => {
104 let _ = tx.send(());
105 true
106 }
107 None => false,
108 }
109 }
110
111 pub async fn unregister(&self, stream_id: u16) {
114 let mut guard = self.inner.lock().await;
115 guard.remove(&stream_id);
116 }
117
118 pub async fn active_count(&self) -> usize {
119 self.inner.lock().await.len()
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum RegisterError {
125 ReservedStreamId,
126 StreamInUse,
127}
128
129impl RegisterError {
130 pub fn code(&self) -> &'static str {
131 match self {
132 Self::ReservedStreamId => "open_stream_reserved_id",
133 Self::StreamInUse => "open_stream_id_in_use",
134 }
135 }
136 pub fn message(&self) -> &'static str {
137 match self {
138 Self::ReservedStreamId => {
139 "OpenStream cannot use stream_id 0 (reserved for unsolicited)"
140 }
141 Self::StreamInUse => "OpenStream stream_id already has an active stream",
142 }
143 }
144}
145
146pub fn build_stream_error_frame(
150 correlation_id: u64,
151 stream_id: u16,
152 code: &str,
153 message: &str,
154) -> std::io::Result<Frame> {
155 reddb_wire::redwire::stream::build_stream_error_frame(
156 correlation_id,
157 stream_id,
158 None,
159 code,
160 message,
161 )
162 .map_err(|e| std::io::Error::other(format!("build StreamError: {e}")))
163}
164
165pub async fn run_output_stream(
175 runtime: Arc<RedDBRuntime>,
176 correlation_id: u64,
177 stream_id: u16,
178 request: OpenStreamRequest,
179 in_transaction: bool,
180 mut cancel_rx: oneshot::Receiver<()>,
181 send: FrameTx,
182 operation_context: OperationContext,
183 role: Role,
184) {
185 let clock = SystemClock;
186 let config = StreamConfig::load(&runtime);
187 let snapshot_lsn = runtime.cdc_current_lsn();
188
189 let lease = match outs::open_stream(config, snapshot_lsn, in_transaction, &clock) {
190 Ok(l) => l,
191 Err(OpenStreamError::TransactionActive) => {
192 let err = OpenStreamError::TransactionActive;
193 let frame = match build_stream_error_frame(
194 correlation_id,
195 stream_id,
196 err.code(),
197 err.message(),
198 ) {
199 Ok(f) => f,
200 Err(_) => return,
201 };
202 send.send_frame(frame).await;
203 return;
204 }
205 };
206
207 let ack = match reddb_wire::redwire::stream::build_open_ack_frame(
209 correlation_id,
210 stream_id,
211 lease.id,
212 lease.snapshot_lsn,
213 false,
214 ) {
215 Ok(f) => f,
216 Err(_) => return,
217 };
218 send.send_frame(ack).await;
219
220 let result = super::session::execute_with_redwire_context(&operation_context, role, || {
222 runtime.execute_query(&request.sql)
223 });
224
225 let mut seq: u64 = 0;
227 let mut row_count: u64 = 0;
228 let mut cancelled = false;
229 let mut had_error: Option<(String, String)> = None;
230
231 match result {
232 Ok(qr) => {
233 let columns = qr.result.columns.clone();
234 let rows: Vec<JsonValue> = qr
235 .result
236 .records
237 .iter()
238 .map(|r| crate::presentation::query_result_json::unified_record_json(r, &columns))
239 .collect();
240
241 for row in rows {
249 if let Ok(()) = cancel_rx.try_recv() {
251 cancelled = true;
252 break;
253 }
254 if lease.snapshot_expired(clock.now_ms()) {
255 had_error = Some((
256 "snapshot_expired".to_string(),
257 "stream snapshot pin TTL elapsed".to_string(),
258 ));
259 break;
260 }
261 let row_bytes = row.to_string_compact().into_bytes();
262 let frame =
263 match reddb_wire::redwire::stream::build_stream_chunk_frame_from_json_bytes(
264 correlation_id,
265 stream_id,
266 seq,
267 vec![row_bytes],
268 false,
269 ) {
270 Ok(f) => f,
271 Err(_) => break,
272 };
273 send.send_frame(frame).await;
274 seq += 1;
275 row_count += 1;
276 }
277 let _ = config;
281 }
282 Err(err) => {
283 had_error = Some(("query_failed".to_string(), err.to_string()));
284 }
285 }
286
287 if let Some((code, message)) = had_error {
288 if let Ok(frame) = reddb_wire::redwire::stream::build_stream_error_frame(
289 correlation_id,
290 stream_id,
291 Some(seq),
292 &code,
293 &message,
294 ) {
295 send.send_frame(frame).await;
296 }
297 }
298
299 if let Ok(frame) = reddb_wire::redwire::stream::build_stream_end_frame(
303 correlation_id,
304 stream_id,
305 row_count,
306 lease.id,
307 lease.snapshot_lsn,
308 cancelled,
309 ) {
310 send.send_frame(frame).await;
311 }
312}
313
314#[derive(Clone)]
320pub struct FrameTx {
321 tx: tokio::sync::mpsc::Sender<Vec<u8>>,
322}
323
324impl FrameTx {
325 pub fn new(tx: tokio::sync::mpsc::Sender<Vec<u8>>) -> Self {
326 Self { tx }
327 }
328
329 pub async fn send_frame(&self, frame: Frame) {
333 let bytes = encode_frame(&frame);
334 let _ = self.tx.send(bytes).await;
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use reddb_wire::redwire::MessageKind;
342
343 #[test]
344 fn parse_open_stream_accepts_minimal_payload() {
345 let req = parse_open_stream(br#"{"sql":"SELECT 1"}"#).unwrap();
346 assert_eq!(req.sql, "SELECT 1");
347 assert!(req.opts_raw.is_empty());
348 }
349
350 #[test]
351 fn parse_open_stream_captures_opts_opaque() {
352 let req =
353 parse_open_stream(br#"{"sql":"SELECT 1","opts":{"resume_after_rid":42}}"#).unwrap();
354 assert_eq!(req.sql, "SELECT 1");
355 assert!(!req.opts_raw.is_empty());
356 }
357
358 #[test]
359 fn parse_open_stream_rejects_non_object() {
360 assert!(matches!(
361 parse_open_stream(b"\"sql\""),
362 Err(OpenStreamParseError::NotObject)
363 ));
364 }
365
366 #[test]
367 fn parse_open_stream_rejects_missing_sql() {
368 assert!(matches!(
369 parse_open_stream(b"{}"),
370 Err(OpenStreamParseError::MissingSql)
371 ));
372 }
373
374 #[test]
375 fn parse_open_stream_rejects_empty_sql() {
376 assert!(matches!(
377 parse_open_stream(br#"{"sql":""}"#),
378 Err(OpenStreamParseError::EmptySql)
379 ));
380 }
381
382 #[test]
383 fn parse_open_stream_rejects_invalid_json() {
384 assert!(matches!(
385 parse_open_stream(b"{not json"),
386 Err(OpenStreamParseError::NotJson)
387 ));
388 }
389
390 #[test]
391 fn parse_stream_cancel_with_reason() {
392 let r = parse_stream_cancel(br#"{"reason":"client-abort"}"#);
393 assert_eq!(r.reason.as_deref(), Some("client-abort"));
394 }
395
396 #[test]
397 fn parse_stream_cancel_empty_payload_is_default() {
398 assert_eq!(parse_stream_cancel(b""), StreamCancelRequest::default());
399 assert_eq!(parse_stream_cancel(b"{}"), StreamCancelRequest::default());
400 }
401
402 #[tokio::test]
403 async fn registry_rejects_reserved_id_and_duplicates() {
404 let r = StreamRegistry::new();
405 assert!(matches!(
406 r.register(0).await,
407 Err(RegisterError::ReservedStreamId)
408 ));
409 let _rx = r.register(1).await.unwrap();
410 assert!(matches!(
411 r.register(1).await,
412 Err(RegisterError::StreamInUse)
413 ));
414 assert_eq!(r.active_count().await, 1);
415 }
416
417 #[tokio::test]
418 async fn registry_cancel_signals_named_stream_only() {
419 let r = StreamRegistry::new();
421 let rx1 = r.register(1).await.unwrap();
422 let mut rx2 = r.register(2).await.unwrap();
423 assert!(r.cancel(1).await);
424 assert!(rx1.await.is_ok());
426 match rx2.try_recv() {
428 Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {}
429 other => panic!("stream 2 should not be cancelled: {other:?}"),
430 }
431 assert_eq!(r.active_count().await, 1);
432 }
433
434 #[tokio::test]
435 async fn registry_cancel_unknown_returns_false() {
436 let r = StreamRegistry::new();
437 assert!(!r.cancel(99).await);
438 }
439
440 #[tokio::test]
441 async fn registry_unregister_is_idempotent() {
442 let r = StreamRegistry::new();
443 let _rx = r.register(1).await.unwrap();
444 r.unregister(1).await;
445 r.unregister(1).await;
446 assert_eq!(r.active_count().await, 0);
447 }
448
449 #[test]
450 fn build_stream_error_frame_carries_stream_id_and_correlation() {
451 let frame = build_stream_error_frame(99, 7, "unknown_stream", "no such stream").unwrap();
452 assert_eq!(frame.kind, MessageKind::StreamError);
453 assert_eq!(frame.stream_id, 7);
454 assert_eq!(frame.correlation_id, 99);
455 }
456}