Repository navigation
fix(bridge): keep wait-graph marks across slow dispatches and cancels (#537) - #561
Merged
Merged
Conversation
…cross slow dispatches (#537)
Contributor
Author
|
审核结论:需要修改,暂不能合并(会让 #228 的卡死回来)。
CI: |
Contributor
Author
|
复审:代码方向对了,还需改说明、测试与文档。 第二个提交回到“调用方被标记即为 hop”,与 #535 的模型( 仍需:
注意 (a) 的写法:函数要在更早的一次调用里取到。如果在 另: (a) 的测试补丁diff --git a/crates/rutis-loader/tests/sync_cycles.rs b/crates/rutis-loader/tests/sync_cycles.rs
index c9dd2ebf..16c70417 100644
--- a/crates/rutis-loader/tests/sync_cycles.rs
+++ b/crates/rutis-loader/tests/sync_cycles.rs
@@ -939,3 +939,86 @@ async fn an_emit_listener_run_while_a_call_waits() {
fixture.root.shutdown().await.unwrap();
}
}
+
+// REVIEW-ONLY: function references relayed between two Node runtimes.
+const NODE_REF_WORK: &str = r#"
+import { definePlugin } from 'PLUGIN'
+export default definePlugin({
+ provides: { SELF: { fn: 'sync' } },
+ apply(ctx) {
+ ctx.provide('SELF', { fn: () => () => 'SELF works' })
+ },
+})
+"#;
+const NODE_REF_GO: &str = r#"
+import { definePlugin } from 'PLUGIN'
+export default definePlugin({
+ inject: ['gate', 'OTHER'],
+ provides: { SELF: { prepare: 'sync', go: 'sync' } },
+ apply(ctx) {
+ const gate = ctx.use('gate')
+ let f
+ ctx.provide('SELF', {
+ prepare() { f = ctx.use('OTHER').fn(); return 'ready' },
+ go() {
+ gate.arrive('SELF')
+ try { return `SELF got ${f()}` } catch (error) { return `SELF: ${error.name}` }
+ },
+ })
+ },
+})
+"#;
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
+async fn review_function_references_calling_each_other() {
+ let dir = tempfile::tempdir().unwrap();
+ let work = |me: &str| NODE_REF_WORK.replace("SELF", me);
+ let go = |me: &str, other: &str| NODE_REF_GO.replace("SELF", me).replace("OTHER", other);
+ let files = [
+ ("aw", "node", write(dir.path(), "aw.mjs", &work("a_work"))),
+ ("bw", "nb", write(dir.path(), "bw.mjs", &work("b_work"))),
+ ("a", "node", write(dir.path(), "a.mjs", &go("a_svc", "b_work"))),
+ ("b", "nb", write(dir.path(), "b.mjs", &go("b_svc", "a_work"))),
+ ];
+ let (gate, _arrivals) = Gate::new(2);
+ let fixture = fixture(
+ vec![(node("node"), true), (node("nb"), false)],
+ false,
+ false,
+ &["a_work", "b_work", "a_svc", "b_svc"],
+ gate,
+ )
+ .await;
+ let rows_of = files
+ .iter()
+ .map(|(id, runtime, file)| match *runtime {
+ "node" => json!({ "id": id, "name": file.to_string_lossy() }),
+ runtime => json!({ "id": id, "name": format!("{runtime}:{}", file.display()) }),
+ })
+ .collect();
+ fixture.loader.reconcile(rows(rows_of), None).await.unwrap();
+ let (loader, root) = (fixture.loader.clone(), fixture.root.clone());
+ COLD.until(
+ || {
+ ["aw", "bw", "a", "b"].iter().all(|id| active(&loader, id))
+ && ["a_work", "b_work", "a_svc", "b_svc"]
+ .iter()
+ .all(|name| service(&root, name).is_some())
+ },
+ "the rows and their services",
+ )
+ .await;
+ answer(&call(&fixture.root, "a_svc", "prepare"), "a.prepare");
+ answer(&call(&fixture.root, "b_svc", "prepare"), "b.prepare");
+ let started = std::time::Instant::now();
+ let a_go = call(&fixture.root, "a_svc", "go");
+ let b_go = call(&fixture.root, "b_svc", "go");
+ let answers = [answer(&a_go, "a.go"), answer(&b_go, "b.go")];
+ eprintln!("REVIEW answers {answers:?} in {:?}", started.elapsed());
+ let refused = answers
+ .iter()
+ .filter(|a| a.as_str().unwrap().ends_with(": SyncWaitCycle"))
+ .count();
+ assert_eq!(refused, 1, "{answers:?}");
+ fixture.root.shutdown().await.unwrap();
+} |
This was referenced Oct 22, 2026
This was referenced Oct 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #537. This fixes the slow-path chain loss and cancellation; the host-root call and the remaining async-control classification are still open.
with_callercaptures the invocation path when the future is created and enters it on every poll, so a filter or listener that suspends before its first poll still forwards from the caller's chain.Frame::Cancelremoves a call from the wait graph when an incoming call or an awaiting relay is removed. A synchronous call already running keeps its mark until its reply, so a later synchronous forward still participates in cycle detection.unchainednow also marks the thread as sending a notification;waits::forwardleaves such sends out of the wait graph.events.elsewhereand the link notifications use it, so they no longer enter the table.request_async; a notification nobody waits for is not.Tests:
function_references_calling_each_otherinsync_cycles.rs: two Node runtimes take each other's function reference, then call them while both wait; one call is refused withSyncWaitCycle.with_caller_captures_the_chain_when_it_is_made: the path is kept when the future is created, before its first poll.an_emit_listener_called_after_its_filter_answeredinsync_cycles.rs: a Node emitter emits through a filter, so the dispatch suspends before any listener is called; the listener, run in Python on top of a call C waits for, calls C back and is refused withSyncWaitCycle, and the rest completes. It locks the current behaviour and does not exercisewith_caller: an emit's filter and listener calls run on the bus's own task (Bus::emit_erased,crates/rutis/src/bus.rs), with an empty path and no caller, so it passes with or without the change above. The eager capture inwith_callermatters for theparallel/serial/waterfalldispatch futures,events/link.rsandnext_function;with_caller_captures_the_chain_when_it_is_madecovers it.a_running_synchronous_call_cancelled_keeps_its_mark(rpc/tests.rs): a synchronous call cancelled while it runs keeps its mark; its later forward that closes a cycle with another session is refused withSyncWaitCycle. Without the cancel condition the forward is sent and waits for ever.a_cancelled_awaited_call_leaves_no_mark(rpc/tests.rs): a synchronousawaitof a future, cancelled through a realFrame::Cancelwhile it is awaited, leaves the session's mark count at 0. Without theawaitingbranch the mark stays.a_notification_sent_unchained_is_not_in_the_graph,an_answered_call_leaves_no_mark, and the existingsync_cyclessuite (4 passed).cargo test -p rutis-bridge --all-features --lib(139 passed),cargo clippy -p rutis-bridge --all-features --all-targets -- -D warnings.Still open under #537: a host-originated synchronous call is recorded as an entry (no caller session id), and an async notification that does not go through
unchainedon a marked chain is still a hop. An emit's chain is not carried into the bus's emit task. On an application thread with no tokio runtime, an emit's reply panics (no reactor running) andrun_emitdrops a pending run: #567.