1use std::collections::HashMap;
51use std::sync::atomic::{AtomicUsize, Ordering};
52use std::sync::Arc;
53use std::time::Duration;
54
55use schemars::JsonSchema;
56use serde::{Deserialize, Serialize};
57
58use crate::execution::{
59 Delivery, EventPayload, ExecutionEvent, ExecutionId, ExecutionStore, Filter, PROTOCOL_VERSION,
60};
61
62use super::router::{Request, Response};
63use super::ws;
64
65pub const PATH: &str = "/events";
68
69pub const HEARTBEAT: Duration = Duration::from_secs(20);
71
72pub const MAX_CONNECTIONS: usize = 64;
75
76pub const MAX_REPLAY: usize = 1_000;
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(tag = "type", content = "data")]
86pub enum StreamMessage {
87 #[serde(rename = "stream.ready")]
90 Ready {
91 protocol_version: String,
93 scope: Scope,
95 #[serde(skip_serializing_if = "Vec::is_empty")]
97 executions: Vec<String>,
98 replayed: usize,
101 truncated: bool,
105 heartbeat_seconds: u64,
107 },
108 #[serde(rename = "stream.heartbeat")]
112 Heartbeat {
113 at: String,
115 },
116 #[serde(rename = "stream.lagged")]
119 Lagged {
120 dropped: u64,
122 },
123 #[serde(rename = "stream.closing")]
125 Closing {
126 reason: String,
128 },
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133#[serde(rename_all = "snake_case")]
134pub enum Scope {
135 All,
137 Executions,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
161pub struct Withheld {
162 pub withheld: String,
164 pub bytes: usize,
166 pub limit: usize,
168 #[serde(rename = "self")]
170 pub itself: String,
171}
172
173static OPEN: AtomicUsize = AtomicUsize::new(0);
175
176pub fn open_connections() -> usize {
178 OPEN.load(Ordering::Relaxed)
179}
180
181pub struct Accepted {
183 pub accept: String,
185 stream: LiveStream,
186}
187
188impl Accepted {
189 pub fn serve(self, socket: Box<dyn std::io::Write + Send>) {
192 self.stream.run(socket, || false);
193 }
194
195 pub fn serve_until(self, socket: Box<dyn std::io::Write + Send>, hung_up: impl Fn() -> bool) {
222 self.stream.run(socket, hung_up);
223 }
224}
225
226pub const LOOK: Duration = Duration::from_secs(1);
229
230struct LiveStream {
231 store: Arc<ExecutionStore>,
232 filter: Filter,
233 after: u64,
234 heartbeat: Duration,
236 snapshot: String,
238}
239
240pub fn accept(
250 store: &Arc<ExecutionStore>,
251 req: &Request,
252 snapshot: &str,
253) -> Result<Accepted, Response> {
254 if !ws::is_upgrade(req) {
255 return Err(Response::error(
256 426,
257 "upgrade_required",
258 &format!(
259 "{PATH} is a WebSocket. Open it with a WebSocket client; the same events are readable over HTTP at {}executions/events",
260 crate::capability::model::HttpExposure::PREFIX
261 ),
262 ));
263 }
264 if let Some(origin) = req.header("origin") {
265 let host = req.header("host").unwrap_or_default();
266 let ours = [format!("http://{host}"), format!("https://{host}")];
267 if !ours.iter().any(|a| a == origin) {
268 tracing::warn!(
269 origin = origin,
270 "a live channel from another origin was refused"
271 );
272 return Err(Response::error(
273 403,
274 "forbidden",
275 &format!("a WebSocket from origin '{origin}' is refused: this server accepts one from its own origin only"),
276 ));
277 }
278 }
279 let accept = match ws::handshake(req) {
280 ws::Handshake::Accept(value) => value,
281 ws::Handshake::Refuse { status, reason } => {
282 return Err(Response::error(status, "invalid_input", &reason))
283 }
284 };
285 if OPEN.load(Ordering::Relaxed) >= MAX_CONNECTIONS {
286 return Err(Response::error(
287 503,
288 "unavailable",
289 &format!("this process serves {MAX_CONNECTIONS} live channels at once and they are all in use"),
290 ));
291 }
292 let mut executions = Vec::new();
293 let mut after = 0u64;
294 for (key, value) in &req.query {
295 match key.as_str() {
296 "execution" => match ExecutionId::parse(value) {
297 Some(id) if executions.contains(&id) => {}
299 Some(id) => executions.push(id),
300 None => {
301 return Err(Response::error(
302 400,
303 "invalid_input",
304 &format!("'{value}' is not an execution id"),
305 ))
306 }
307 },
308 "after" => match value.parse::<u64>() {
309 Ok(n) => after = n,
310 Err(_) => {
311 return Err(Response::error(
312 400,
313 "invalid_input",
314 &format!("after '{value}' is not a sequence number"),
315 ))
316 }
317 },
318 other => {
319 return Err(Response::error(
320 400,
321 "invalid_input",
322 &format!("unknown parameter '{other}'; {PATH} takes 'execution' and 'after'"),
323 ))
324 }
325 }
326 }
327 let filter = if executions.is_empty() {
328 Filter::All
329 } else {
330 Filter::Only(executions)
331 };
332 Ok(Accepted {
333 accept,
334 stream: LiveStream {
335 store: Arc::clone(store),
336 filter,
337 after,
338 heartbeat: HEARTBEAT,
339 snapshot: snapshot.to_string(),
340 },
341 })
342}
343
344impl LiveStream {
345 fn run(self, mut socket: Box<dyn std::io::Write + Send>, hung_up: impl Fn() -> bool) {
346 let open = OPEN.fetch_add(1, Ordering::Relaxed) + 1;
347 tracing::debug!(connections = open, "a live channel opened");
348 let (subscriber, rx) = self.store.subscribe(self.filter.clone());
349 let mut written: HashMap<ExecutionId, u64> = HashMap::new();
356 let mut replayed = Vec::new();
357 let mut truncated = false;
358 match &self.filter {
359 Filter::Only(ids) => {
360 for id in ids {
364 let (events, cut) = self.replay(id);
365 truncated |= cut;
366 let cursor = events.iter().map(|e| e.sequence).fold(self.after, u64::max);
367 replayed.extend(events);
368 written.insert(id.clone(), cursor);
369 }
370 }
371 Filter::All => {}
374 }
375 let ready = StreamMessage::Ready {
376 protocol_version: PROTOCOL_VERSION.to_string(),
377 scope: match self.filter {
378 Filter::All => Scope::All,
379 Filter::Only(_) => Scope::Executions,
380 },
381 executions: match &self.filter {
382 Filter::All => Vec::new(),
383 Filter::Only(ids) => ids.iter().map(|i| i.to_string()).collect(),
384 },
385 replayed: replayed.len(),
386 truncated,
387 heartbeat_seconds: HEARTBEAT.as_secs(),
388 };
389 let mut alive = write(&mut socket, &ready);
390 for event in replayed {
391 if !alive {
392 break;
393 }
394 alive = self.write_event(&mut socket, &event);
395 }
396 let mut quiet = std::time::Instant::now();
399 while alive {
400 let until_heartbeat = self.heartbeat.saturating_sub(quiet.elapsed());
401 match rx.recv_timeout(until_heartbeat.min(LOOK)) {
402 Ok(Delivery::Event(event)) => {
403 if let Some(cursor) = written.get_mut(&event.execution_id) {
405 if event.sequence <= *cursor {
406 continue; }
408 *cursor = event.sequence;
409 }
410 alive = self.write_event(&mut socket, &event);
411 quiet = std::time::Instant::now();
412 }
413 Ok(Delivery::Lagged { dropped }) => {
414 alive = write(&mut socket, &StreamMessage::Lagged { dropped });
415 quiet = std::time::Instant::now();
416 }
417 Err(std::sync::mpsc::RecvTimeoutError::Timeout) if hung_up() => {
418 tracing::debug!("the client of a live channel hung up");
419 break;
420 }
421 Err(std::sync::mpsc::RecvTimeoutError::Timeout)
422 if quiet.elapsed() < self.heartbeat => {}
423 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
424 alive = ws::write_ping(&mut socket).is_ok()
427 && write(
428 &mut socket,
429 &StreamMessage::Heartbeat {
430 at: crate::peers::rfc3339(std::time::SystemTime::now()),
431 },
432 );
433 quiet = std::time::Instant::now();
434 }
435 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
436 let _ = write(
437 &mut socket,
438 &StreamMessage::Closing {
439 reason: "this server is shutting down".into(),
440 },
441 );
442 let _ = ws::write_close(&mut socket, 1001, "going away");
443 break;
444 }
445 }
446 }
447 self.store.unsubscribe(subscriber);
448 let open = OPEN.fetch_sub(1, Ordering::Relaxed) - 1;
449 tracing::debug!(connections = open, "a live channel closed");
450 }
451
452 fn replay(&self, id: &ExecutionId) -> (Vec<Arc<ExecutionEvent>>, bool) {
460 let Some(first) = self.store.events(id, self.after, MAX_REPLAY) else {
461 return (Vec::new(), false);
462 };
463 let truncated = first.truncated;
464 let mut more = first.more;
465 let mut cursor = first.last_sequence;
466 let mut events = first.events;
467 while more {
468 let Some(page) = self.store.events(id, cursor, MAX_REPLAY) else {
469 break;
470 };
471 if page.events.is_empty() {
472 break;
473 }
474 more = page.more;
475 cursor = page.last_sequence;
476 events.extend(page.events);
477 }
478 (events, truncated)
479 }
480
481 fn write_event(
484 &self,
485 socket: &mut Box<dyn std::io::Write + Send>,
486 event: &ExecutionEvent,
487 ) -> bool {
488 let Ok(text) = serde_json::to_string(event) else {
489 tracing::error!("an execution event could not be rendered as JSON");
490 return true;
491 };
492 if text.len() <= ws::MAX_FRAME_BYTES {
493 return write_frame(socket, &text);
494 }
495 match self.withhold(event) {
496 Some(lighter) => write(socket, &lighter),
497 None => {
498 tracing::warn!(
502 event = %event.event_id,
503 bytes = text.len(),
504 "an execution event too large for one frame was not written"
505 );
506 true
507 }
508 }
509 }
510
511 fn withhold(&self, event: &ExecutionEvent) -> Option<ExecutionEvent> {
513 let reference = |value: &serde_json::Value| {
514 let bytes = serde_json::to_string(value).map_or(0, |t| t.len());
515 serde_json::to_value(Withheld {
516 withheld: "too_large".into(),
517 bytes,
518 limit: ws::MAX_FRAME_BYTES,
519 itself: format!(
520 "{}?id={}",
521 self.snapshot,
522 super::router::percent_encode(event.execution_id.as_str())
523 ),
524 })
525 .unwrap_or_default()
526 };
527 let payload = match &event.payload {
528 EventPayload::Completed { output } => EventPayload::Completed {
529 output: reference(output),
530 },
531 EventPayload::Created {
532 capability,
533 title,
534 input,
535 } => EventPayload::Created {
536 capability: capability.clone(),
537 title: title.clone(),
538 input: reference(input),
539 },
540 _ => return None,
541 };
542 Some(ExecutionEvent {
543 payload,
544 ..event.clone()
545 })
546 }
547}
548
549fn write<T: Serialize>(socket: &mut Box<dyn std::io::Write + Send>, value: &T) -> bool {
551 let Ok(text) = serde_json::to_string(value) else {
552 tracing::error!("an execution event could not be rendered as JSON");
553 return true;
554 };
555 write_frame(socket, &text)
556}
557
558fn write_frame(socket: &mut Box<dyn std::io::Write + Send>, text: &str) -> bool {
560 match ws::write_text(socket.as_mut(), text) {
561 Ok(()) => true,
562 Err(e) => {
563 tracing::debug!("a live channel went away: {e}");
564 false
565 }
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use crate::execution::{
573 Actor, ActorKind, EventPayload, ExecutionState, Limits, ProgressView, RepositoryRef,
574 };
575
576 const SNAPSHOT: &str = "/api/v1/executions/get";
579
580 fn accept(store: &Arc<ExecutionStore>, req: &Request) -> Result<Accepted, Response> {
582 super::accept(store, req, SNAPSHOT)
583 }
584
585 fn upgrade_request(target: &str) -> Request {
586 Request::parse_target("GET", target, vec![]).with_headers(vec![
587 ("Upgrade".into(), "websocket".into()),
588 ("Connection".into(), "Upgrade".into()),
589 (
590 "Sec-WebSocket-Key".into(),
591 "dGhlIHNhbXBsZSBub25jZQ==".into(),
592 ),
593 ("Sec-WebSocket-Version".into(), "13".into()),
594 ])
595 }
596
597 fn create(store: &ExecutionStore, id: &ExecutionId) {
599 store.create(
600 id.clone(),
601 "demo.x",
602 "Demo",
603 serde_json::json!({}),
604 true,
605 Actor::of(ActorKind::Internal),
606 RepositoryRef {
607 name: "r".into(),
608 id: "i".into(),
609 branch: None,
610 },
611 );
612 }
613
614 fn store_with_one() -> (Arc<ExecutionStore>, ExecutionId) {
615 let store = Arc::new(ExecutionStore::new(Limits::default()));
616 let id = ExecutionId::fresh();
617 create(&store, &id);
618 store.publish(&id, EventPayload::Started);
619 store.publish(
620 &id,
621 EventPayload::Progress(ProgressView {
622 current: 1,
623 total: Some(1),
624 message: None,
625 }),
626 );
627 store.publish(
628 &id,
629 EventPayload::Completed {
630 output: serde_json::json!({}),
631 },
632 );
633 (store, id)
634 }
635
636 #[test]
637 fn a_plain_get_is_told_what_this_path_is_and_where_the_same_events_are() {
638 let (store, _) = store_with_one();
639 let response = accept(&store, &Request::parse_target("GET", PATH, vec![]))
640 .err()
641 .expect("a plain GET is not a connection");
642 assert_eq!(response.status, 426);
643 assert!(response.body.text().contains("executions/events"));
644 }
645
646 #[test]
647 fn a_foreign_origin_is_refused_before_the_handshake() {
648 let (store, _) = store_with_one();
649 let req = upgrade_request(PATH).with_headers({
650 let mut h = upgrade_request(PATH).headers;
651 h.push(("Host".into(), "127.0.0.1:8741".into()));
652 h.push(("Origin".into(), "https://evil.example".into()));
653 h
654 });
655 let response = accept(&store, &req).err().expect("refused");
656 assert_eq!(response.status, 403);
657 let ours = upgrade_request(PATH).with_headers({
658 let mut h = upgrade_request(PATH).headers;
659 h.push(("Host".into(), "127.0.0.1:8741".into()));
660 h.push(("Origin".into(), "http://127.0.0.1:8741".into()));
661 h
662 });
663 assert!(accept(&store, &ours).is_ok(), "our own page is not foreign");
664 }
665
666 #[test]
667 fn the_target_is_the_whole_subscription_and_is_validated() {
668 let (store, id) = store_with_one();
669 assert!(
670 accept(&store, &upgrade_request(PATH)).is_ok(),
671 "every execution"
672 );
673 assert!(accept(
674 &store,
675 &upgrade_request(&format!("{PATH}?execution={id}&after=2"))
676 )
677 .is_ok());
678 for bad in [
679 format!("{PATH}?execution=../../etc/passwd"),
680 format!("{PATH}?execution={id}&after=soon"),
681 format!("{PATH}?follow=everything"),
682 ] {
683 let response = accept(&store, &upgrade_request(&bad)).err().expect(&bad);
684 assert_eq!(response.status, 400, "{bad}");
685 }
686 }
687
688 #[test]
693 fn a_connection_replays_from_its_cursor_and_ends_when_the_client_goes() {
694 let (store, id) = store_with_one();
695 let accepted = accept(
696 &store,
697 &upgrade_request(&format!("{PATH}?execution={id}&after=2")),
698 )
699 .expect("accepted");
700 assert_eq!(accepted.accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
701 let sink = SharedSink::new(3);
703 let writer = Box::new(sink.clone());
704 let handle = std::thread::spawn(move || accepted.serve(writer));
705 nudge(&store, &id);
708 handle
709 .join()
710 .expect("the connection thread ended by itself");
711
712 let frames = sink.frames();
713 let types: Vec<String> = frames
714 .iter()
715 .filter_map(|f| serde_json::from_str::<serde_json::Value>(f).ok())
716 .map(|v| v["type"].as_str().unwrap_or_default().to_string())
717 .collect();
718 assert_eq!(
719 types,
720 ["stream.ready", "execution.progress", "execution.completed"],
721 "the first frame says what was subscribed to, then the cursor is honoured"
722 );
723 let ready: serde_json::Value = serde_json::from_str(&frames[0]).unwrap();
724 assert_eq!(ready["data"]["protocol_version"], PROTOCOL_VERSION);
725 assert_eq!(ready["data"]["scope"], "executions");
726 assert_eq!(
727 ready["data"]["replayed"], 2,
728 "sequences 3 and 4, not 1 and 2"
729 );
730 assert_eq!(ready["data"]["truncated"], false);
731 assert_eq!(ready["data"]["heartbeat_seconds"], HEARTBEAT.as_secs());
732 assert_eq!(store.subscribers(), 0, "its subscription was ended");
733 assert_eq!(
735 store.get(&id).map(|e| e.state),
736 Some(ExecutionState::Succeeded)
737 );
738 }
739
740 #[test]
743 fn an_unscoped_connection_replays_nothing_and_says_so() {
744 let (store, _) = store_with_one();
745 let accepted = accept(&store, &upgrade_request(PATH)).expect("accepted");
746 let sink = SharedSink::new(1);
747 let writer = Box::new(sink.clone());
748 let handle = std::thread::spawn(move || accepted.serve(writer));
749 let id = store.list(None, None, 1)[0].id.clone();
750 nudge(&store, &id);
751 handle.join().expect("ended");
752 let ready: serde_json::Value = serde_json::from_str(&sink.frames()[0]).unwrap();
753 assert_eq!(ready["type"], "stream.ready");
754 assert_eq!(ready["data"]["scope"], "all");
755 assert_eq!(ready["data"]["replayed"], 0);
756 assert!(ready["data"]["executions"].is_null(), "it names none");
757 }
758
759 #[test]
764 fn a_connection_to_two_executions_delivers_every_event_of_each() {
765 let store = Arc::new(ExecutionStore::new(Limits::default()));
766 let (a, b) = (ExecutionId::fresh(), ExecutionId::fresh());
767 let accepted = accept(
768 &store,
769 &upgrade_request(&format!("{PATH}?execution={a}&execution={b}")),
770 )
771 .expect("accepted");
772 let sink = SharedSink::new(6);
774 let writer = Box::new(sink.clone());
775 let handle = std::thread::spawn(move || accepted.serve(writer));
776 wait_subscribed(&store);
777 create(&store, &a);
778 store.publish(&a, EventPayload::Started);
779 store.publish(
780 &a,
781 EventPayload::Progress(ProgressView {
782 current: 1,
783 total: Some(2),
784 message: None,
785 }),
786 );
787 create(&store, &b);
788 store.publish(&b, EventPayload::Started);
789 until_gone(&store, &a, &handle);
790 handle.join().expect("ended");
791
792 let written = sequences(&sink.frames());
793 assert_eq!(
794 of(&written, &a)[..3],
795 [1, 2, 3],
796 "A's events, in order: {written:?}"
797 );
798 assert_eq!(
799 of(&written, &b),
800 [1, 2],
801 "every event of B arrives although A has gone further: {written:?}"
802 );
803 }
804
805 #[test]
809 fn a_connection_to_two_executions_replays_each_from_the_cursor() {
810 let (store, a) = store_with_one(); let b = ExecutionId::fresh();
812 create(&store, &b);
813 store.publish(&b, EventPayload::Started); let accepted = accept(
815 &store,
816 &upgrade_request(&format!(
817 "{PATH}?execution={a}&execution={b}&execution={a}&after=1"
818 )),
819 )
820 .expect("accepted");
821 let sink = SharedSink::new(5);
823 let writer = Box::new(sink.clone());
824 let handle = std::thread::spawn(move || accepted.serve(writer));
825 wait_subscribed(&store);
826 until_gone(&store, &a, &handle);
827 handle.join().expect("ended");
828
829 let frames = sink.frames();
830 let ready: serde_json::Value = serde_json::from_str(&frames[0]).unwrap();
831 assert_eq!(ready["type"], "stream.ready");
832 assert_eq!(
833 ready["data"]["executions"],
834 serde_json::json!([a.to_string(), b.to_string()]),
835 "each execution is named once"
836 );
837 assert_eq!(ready["data"]["replayed"], 4, "A's 2..4 and B's 2");
838 assert_eq!(ready["data"]["truncated"], false);
839 let written = sequences(&frames);
840 assert_eq!(of(&written, &a)[..3], [2, 3, 4], "{written:?}");
841 assert_eq!(of(&written, &b), [2], "{written:?}");
842 }
843
844 fn sequences(frames: &[String]) -> Vec<(String, u64)> {
846 frames
847 .iter()
848 .filter_map(|f| serde_json::from_str::<serde_json::Value>(f).ok())
849 .filter_map(|v| {
850 Some((
851 v["execution_id"].as_str()?.to_string(),
852 v["sequence"].as_u64()?,
853 ))
854 })
855 .collect()
856 }
857
858 fn of(written: &[(String, u64)], id: &ExecutionId) -> Vec<u64> {
860 let id = id.to_string();
861 written
862 .iter()
863 .filter(|(e, _)| *e == id)
864 .map(|(_, s)| *s)
865 .collect()
866 }
867
868 fn wait_subscribed(store: &Arc<ExecutionStore>) {
870 for _ in 0..200 {
871 if store.subscribers() > 0 {
872 return;
873 }
874 std::thread::sleep(Duration::from_millis(5));
875 }
876 panic!("the connection never subscribed");
877 }
878
879 fn until_gone(
882 store: &Arc<ExecutionStore>,
883 id: &ExecutionId,
884 handle: &std::thread::JoinHandle<()>,
885 ) {
886 for _ in 0..500 {
887 if handle.is_finished() {
888 return;
889 }
890 store.publish(
891 id,
892 EventPayload::Log {
893 stream: crate::execution::LogStream::Handler,
894 message: "is anyone there".into(),
895 },
896 );
897 std::thread::sleep(Duration::from_millis(10));
898 }
899 panic!("the connection never noticed its client had gone");
900 }
901
902 #[test]
906 fn a_reconnect_further_behind_than_one_page_is_replayed_without_a_gap() {
907 let (store, id) = store_with_one();
908 for n in 0..(MAX_REPLAY + 300) {
909 store.publish(
910 &id,
911 EventPayload::Log {
912 stream: crate::execution::LogStream::Handler,
913 message: format!("line {n}"),
914 },
915 );
916 }
917 let last = store.get(&id).expect("retained").last_sequence;
918 let after = 2u64;
919 let behind = usize::try_from(last - after).unwrap();
920 assert!(
921 behind > MAX_REPLAY,
922 "the cursor is more than one page behind"
923 );
924 let mut accepted = accept(
925 &store,
926 &upgrade_request(&format!("{PATH}?execution={id}&after={after}")),
927 )
928 .expect("accepted");
929 accepted.stream.heartbeat = Duration::from_millis(1);
932 let sink = SharedSink::new(1 + behind);
934 let writer = Box::new(sink.clone());
935 let handle = std::thread::spawn(move || accepted.serve(writer));
936 nudge(&store, &id);
937 handle.join().expect("ended");
938
939 let frames: Vec<serde_json::Value> = sink
940 .frames()
941 .iter()
942 .map(|f| serde_json::from_str(f).unwrap())
943 .collect();
944 assert_eq!(frames[0]["type"], "stream.ready");
945 assert_eq!(
946 frames[0]["data"]["replayed"], behind,
947 "the whole gap, not one page"
948 );
949 let sequences: Vec<u64> = frames[1..]
950 .iter()
951 .map(|f| f["sequence"].as_u64().expect("an event"))
952 .collect();
953 let expected: Vec<u64> = (after + 1..=last).collect();
954 assert_eq!(
955 sequences, expected,
956 "every retained sequence after the cursor, densely"
957 );
958 }
959
960 #[test]
963 fn a_quiet_channel_writes_a_heartbeat_a_page_can_see() {
964 let (store, id) = store_with_one();
965 let last = store.get(&id).expect("retained").last_sequence;
966 let mut accepted = accept(
967 &store,
968 &upgrade_request(&format!("{PATH}?execution={id}&after={last}")),
969 )
970 .expect("accepted");
971 accepted.stream.heartbeat = Duration::from_millis(20);
972 let sink = SharedSink::new(3);
974 let writer = Box::new(sink.clone());
975 std::thread::spawn(move || accepted.serve(writer))
976 .join()
977 .expect("the connection ended by itself at the next quiet interval");
978 let frames: Vec<serde_json::Value> = sink
979 .frames()
980 .iter()
981 .map(|f| serde_json::from_str(f).unwrap())
982 .collect();
983 let types: Vec<&str> = frames
984 .iter()
985 .map(|f| f["type"].as_str().unwrap_or_default())
986 .collect();
987 assert_eq!(types, ["stream.ready", "stream.heartbeat"]);
988 assert!(
989 frames[1]["data"]["at"]
990 .as_str()
991 .is_some_and(|at| at.ends_with('Z')),
992 "{}",
993 frames[1]
994 );
995 }
996
997 #[test]
1001 fn a_quiet_channel_whose_client_hangs_up_ends_within_a_look_without_a_write() {
1002 let (store, id) = store_with_one();
1003 let last = store.get(&id).expect("retained").last_sequence;
1004 let accepted = accept(
1005 &store,
1006 &upgrade_request(&format!("{PATH}?execution={id}&after={last}")),
1007 )
1008 .expect("accepted");
1009 let sink = SharedSink::new(usize::MAX);
1011 let writer = Box::new(sink.clone());
1012 let gone = Arc::new(std::sync::atomic::AtomicBool::new(false));
1013 let asked = Arc::clone(&gone);
1014 let handle = std::thread::spawn(move || {
1015 accepted.serve_until(writer, move || asked.load(Ordering::SeqCst))
1016 });
1017 std::thread::sleep(Duration::from_millis(300));
1018 assert!(
1019 !handle.is_finished(),
1020 "a client that is there keeps its channel"
1021 );
1022 gone.store(true, Ordering::SeqCst);
1023 let hung_up = std::time::Instant::now();
1024 handle.join().expect("the channel ended by itself");
1025 assert!(
1026 hung_up.elapsed() < LOOK + Duration::from_millis(500),
1027 "the hang-up was noticed after {:?}, not within a look",
1028 hung_up.elapsed()
1029 );
1030 assert!(
1031 LOOK < HEARTBEAT,
1032 "a look is sooner than the heartbeat it replaces"
1033 );
1034 let types: Vec<String> = sink
1035 .frames()
1036 .iter()
1037 .map(|f| serde_json::from_str::<serde_json::Value>(f).unwrap()["type"].to_string())
1038 .collect();
1039 assert_eq!(
1040 types,
1041 ["\"stream.ready\""],
1042 "nothing was written to a client that left"
1043 );
1044 assert_eq!(store.subscribers(), 0, "its subscription was ended");
1045 }
1046
1047 #[test]
1051 fn an_output_too_large_for_a_frame_is_named_not_fatal() {
1052 let store = Arc::new(ExecutionStore::new(Limits::default()));
1053 let id = ExecutionId::fresh();
1054 store.create(
1055 id.clone(),
1056 "demo.x",
1057 "Demo",
1058 serde_json::json!({}),
1059 true,
1060 Actor::of(ActorKind::Internal),
1061 RepositoryRef {
1062 name: "r".into(),
1063 id: "i".into(),
1064 branch: None,
1065 },
1066 );
1067 store.publish(&id, EventPayload::Started);
1068 let big = "x".repeat(ws::MAX_FRAME_BYTES + 1024);
1069 store.publish(
1070 &id,
1071 EventPayload::Completed {
1072 output: serde_json::json!({ "text": big }),
1073 },
1074 );
1075 let mut accepted = accept(
1076 &store,
1077 &upgrade_request(&format!("{PATH}?execution={id}&after=2")),
1078 )
1079 .expect("accepted");
1080 accepted.stream.heartbeat = Duration::from_millis(20);
1081 let sink = SharedSink::new(3);
1083 let writer = Box::new(sink.clone());
1084 let handle = std::thread::spawn(move || accepted.serve(writer));
1085 nudge(&store, &id);
1086 handle.join().expect("ended");
1087
1088 let raw = sink.frames();
1089 assert!(raw.iter().all(|f| f.len() <= ws::MAX_FRAME_BYTES));
1090 let frames: Vec<serde_json::Value> = raw
1091 .iter()
1092 .map(|f| serde_json::from_str(f).unwrap())
1093 .collect();
1094 let types: Vec<&str> = frames
1095 .iter()
1096 .map(|f| f["type"].as_str().unwrap_or_default())
1097 .collect();
1098 assert_eq!(
1099 types,
1100 ["stream.ready", "execution.completed", "execution.log"],
1101 "the event is written, and the channel is still there for the next one"
1102 );
1103 let completed = &frames[1];
1104 assert_eq!(
1105 completed["sequence"], 3,
1106 "the same event, at its own sequence"
1107 );
1108 let output = &completed["data"]["output"];
1109 assert_eq!(output["withheld"], "too_large", "{output}");
1110 assert!(output["bytes"].as_u64().unwrap() > ws::MAX_FRAME_BYTES as u64);
1111 assert_eq!(output["limit"], ws::MAX_FRAME_BYTES);
1112 assert_eq!(output["self"], format!("{SNAPSHOT}?id={id}"));
1113 assert_eq!(
1115 store.get(&id).and_then(|e| e.output).expect("an output")["text"]
1116 .as_str()
1117 .map(str::len),
1118 Some(ws::MAX_FRAME_BYTES + 1024)
1119 );
1120 }
1121
1122 fn nudge(store: &Arc<ExecutionStore>, id: &crate::execution::ExecutionId) {
1125 for _ in 0..20 {
1127 std::thread::sleep(Duration::from_millis(10));
1128 if store.subscribers() > 0 {
1129 break;
1130 }
1131 }
1132 store.publish(
1133 id,
1134 EventPayload::Log {
1135 stream: crate::execution::LogStream::Handler,
1136 message: "the client is gone by now".into(),
1137 },
1138 );
1139 }
1140
1141 #[derive(Clone)]
1145 struct SharedSink {
1146 bytes: Arc<std::sync::Mutex<Vec<u8>>>,
1147 accepts: Arc<AtomicUsize>,
1150 }
1151
1152 impl SharedSink {
1153 fn new(accepts: usize) -> Self {
1154 SharedSink {
1155 bytes: Arc::new(std::sync::Mutex::new(Vec::new())),
1156 accepts: Arc::new(AtomicUsize::new(accepts)),
1157 }
1158 }
1159
1160 fn frames(&self) -> Vec<String> {
1161 let bytes = self.bytes.lock().unwrap();
1162 let mut out = Vec::new();
1163 let mut i = 0usize;
1164 while i + 2 <= bytes.len() {
1165 let opcode = bytes[i] & 0x0f;
1166 let short = bytes[i + 1] as usize;
1167 let (len, header) = match short {
1168 126 => (u16::from_be_bytes([bytes[i + 2], bytes[i + 3]]) as usize, 4),
1169 127 => (
1170 u64::from_be_bytes(bytes[i + 2..i + 10].try_into().unwrap()) as usize,
1171 10,
1172 ),
1173 n => (n, 2),
1174 };
1175 let start = i + header;
1176 let end = start + len;
1177 if end > bytes.len() {
1178 break;
1179 }
1180 if opcode == 0x1 {
1181 out.push(String::from_utf8_lossy(&bytes[start..end]).into_owned());
1182 }
1183 i = end;
1184 }
1185 out
1186 }
1187 }
1188
1189 impl std::io::Write for SharedSink {
1190 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1191 if self.accepts.load(Ordering::SeqCst) == 0 {
1192 return Err(std::io::Error::new(
1193 std::io::ErrorKind::BrokenPipe,
1194 "the client went away",
1195 ));
1196 }
1197 self.accepts.fetch_sub(1, Ordering::SeqCst);
1198 self.bytes.lock().unwrap().extend_from_slice(buf);
1199 Ok(buf.len())
1200 }
1201 fn flush(&mut self) -> std::io::Result<()> {
1202 Ok(())
1203 }
1204 }
1205}