Tapped copies {type:tapped, message:{id,from,to,reply_to,body}, ts} are
delivered directly (never tapped themselves), dropped when the tapper's
mailbox is full, and the tap is saved in actor.json like subscriptions.
This commit is contained in:
@@ -742,3 +742,114 @@ async fn running_actors_have_sockets_and_candidates_do_not() {
|
||||
assert_eq!(answer.body, json!({"type": "tcp", "line": "echo: hi"}));
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
/// Every message that arrives until the mailbox is quiet for `quiet`.
|
||||
async fn drain(mailbox: &mut mpsc::Receiver<Envelope>, quiet: Duration) -> Vec<Envelope> {
|
||||
let mut messages = Vec::new();
|
||||
while let Ok(Some(message)) = tokio::time::timeout(quiet, mailbox.recv()).await {
|
||||
messages.push(message);
|
||||
}
|
||||
messages
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tap_copies_every_routed_message_once() {
|
||||
let world = create_world("tap");
|
||||
let wasm = toolbox_wasm().clone();
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
let source = passing_source(&world, "source");
|
||||
spawn_type(&host, &mut mailbox, "tapped", &wasm, &source).await;
|
||||
let subscribe = json!({"type": "subscribe", "actor": "tapped"});
|
||||
assert_eq!(request(&host, &mut mailbox, subscribe).await.body["type"], "subscribed");
|
||||
assert_eq!(request(&host, &mut mailbox, json!({"type": "tap"})).await.body["type"], "tapping");
|
||||
|
||||
let echo = json!({"type": "echo", "value": "hi"});
|
||||
let echo_id = host.broker.send("console", Some("tapped"), None, None, echo.clone());
|
||||
let publish = json!({"type": "publish", "value": "news"});
|
||||
host.broker.send("console", Some("tapped"), None, None, publish);
|
||||
let messages = drain(&mut mailbox, Duration::from_secs(2)).await;
|
||||
let copies: Vec<&Value> = messages
|
||||
.iter()
|
||||
.filter(|m| m.from == "broker" && m.body["type"] == "tapped")
|
||||
.map(|m| &m.body)
|
||||
.collect();
|
||||
// 4 copies (2 requests, 1 reply, 1 publish) + the reply + the publish itself.
|
||||
assert_eq!((copies.len(), messages.len()), (4, 6), "{messages:?}");
|
||||
for copy in &copies {
|
||||
assert!(copy["ts"].as_u64().unwrap() > 0, "{copy}");
|
||||
}
|
||||
let count = |want: Value| copies.iter().filter(|c| c["message"] == want).count();
|
||||
let request_copy = json!({"id": echo_id, "from": "console", "to": "tapped", "reply_to": null, "body": echo});
|
||||
assert_eq!(count(request_copy), 1, "{copies:?}");
|
||||
let reply_copies = copies
|
||||
.iter()
|
||||
.filter(|c| c["message"]["reply_to"] == echo_id.as_str() && c["message"]["from"] == "tapped" && c["message"]["to"] == "console")
|
||||
.count();
|
||||
assert_eq!(reply_copies, 1, "{copies:?}");
|
||||
let publish_copy = copies.iter().find(|c| c["message"]["body"]["type"] == "published").expect("publish not tapped");
|
||||
assert_eq!(publish_copy["message"]["from"], "tapped");
|
||||
assert_eq!(publish_copy["message"]["to"], Value::Null);
|
||||
|
||||
assert_eq!(request(&host, &mut mailbox, json!({"type": "untap"})).await.body["type"], "untapped");
|
||||
let echo_id = host.broker.send("console", Some("tapped"), None, None, json!({"type": "echo", "value": "quiet"}));
|
||||
let messages = drain(&mut mailbox, Duration::from_secs(1)).await;
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert_eq!(messages[0].reply_to.as_deref(), Some(echo_id.as_str()));
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tap_survives_a_restart() {
|
||||
let world = create_world("tap-restart");
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
assert_eq!(request(&host, &mut mailbox, json!({"type": "tap"})).await.body["type"], "tapping");
|
||||
host.stop().await;
|
||||
assert!(world.read_meta("console").tapped);
|
||||
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
let id = host.broker.send("console", Some("nobody"), None, None, json!({"type": "hello"}));
|
||||
let copy = tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
let m = mailbox.recv().await.expect("console mailbox closed");
|
||||
if m.body["type"] == "tapped" && m.body["message"]["id"] == id.as_str() {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("no tapped copy after the restart");
|
||||
assert_eq!(copy.body["message"]["to"], "nobody");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_full_tapper_mailbox_drops_copies_without_blocking() {
|
||||
let world = create_world("tap-full");
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
// "ghost" taps and never reads its mailbox.
|
||||
let mut ghost = host.mailbox("ghost");
|
||||
let mut sink = host.mailbox("sink");
|
||||
host.broker.send("ghost", Some("broker"), None, None, json!({"type": "tap"}));
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
while !world.read_meta("ghost").tapped {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("tap was not saved");
|
||||
|
||||
let total = sausage::broker::MAILBOX_SIZE + 500;
|
||||
for n in 0..total {
|
||||
host.broker.send("console", Some("sink"), None, None, json!({"type": "n", "n": n}));
|
||||
assert_eq!(sink.try_recv().unwrap().body["n"], n);
|
||||
}
|
||||
assert!(mailbox.try_recv().is_err(), "a dropped copy reported something to the sender");
|
||||
let mut held = 0;
|
||||
while ghost.try_recv().is_ok() {
|
||||
held += 1;
|
||||
}
|
||||
assert_eq!(held, sausage::broker::MAILBOX_SIZE);
|
||||
host.broker.send("console", Some("sink"), None, None, json!({"type": "after"}));
|
||||
assert_eq!(ghost.try_recv().unwrap().body["type"], "tapped");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user