← All Pull Requests

The busy gate waits out the bridge's own quick reads instead of refusing the next verb #23

Merged Released in 1.13.5 on 28 Oct 2026 Approved opened by Drew Owens 2026-10-28
Merges fix/busy-gate-short-reads → master

What this fixes

With many documents open, the bridge refused a verb because of its own document snapshot.

  • The snapshot. After every window-raising verb (run_modeling_script included), the window etiquette snapshots the open documents with document_info. It waits only 3 s for the answer (etiquette.rs, quick).
  • The overrun. With about 90 documents open, that document_info can run past the 3 s. The exe returns, Fusion keeps working on it, and the caller's next verb arrives while it is still running.
  • The refusal. The busy gate refuses anything that has been running for 2 s or more, so that next verb gets main_thread_busy for work the bridge started itself.

Evidence from the assembly campaign on Drew's laptop (94 documents open), two consecutive calls:

0448 fusion_run_modeling_script   ok     _timing: pre 0.08 s, add-in 0.97 s, post 4.90 s
0449 fusion_mechanical {op: inspect}  main_thread_busy  busyCommand: document_info, elapsedSeconds: 4.9

It happened once in about 450 calls. It depends on load: on a quiet Fusion with the same 93 documents, the snapshot took about 1 s and I could not reproduce it on demand. A strict harness (Astra's campaign) stops on it.

What changed

busy_gate (addin.rs) now waits out the bridge's own quick reads, document_info and get_app_state:

  • It re-probes /status every 250 ms, for up to 15 s, before judging the add-in.
  • Real work in front of a verb is still refused exactly as before: a script, an import, a cloud walk, or a quick read that is still running after 15 s.
  • Unit test: the_gate_waits_out_the_bridges_own_quick_reads.

Verified

  • tools/rust-ship.sh is green: 140 Rust tests, the Windows build and the guards. The Python suites pass.
  • Not reproduced live, for the load-dependence reason above.

Two things on master for you, Colby (not in this PR)

  • The vendored engine is missing a directory. rust/vendor/adom-eda-checks/src/writers/ was not pushed: lib.rs declares pub mod writers; and master 1.13.2 does not build from source without it. I copied it from adom/eda-bridge-standard at e8a33cc (the commit VENDOR.md names) to build locally. Everything else in the vendored copy matches that commit.
  • rust/Cargo.lock on master is stale. It still says adom-eda-checks 0.1.4 and the bridge 1.13.0, without the writers' dependencies (roxmltree, svgtypes).

FYI for the 1.13.0 changelog: joint_origin_* and motion_link_* have now run live. All 15 assembly campaign groups pass on the dev pin: nested_context, origins, redefinition, links_two, link_single and asbuilt added 133 checks, with no failures. So the "not yet run live" line can go.

Diff Skip to comments

CHANGELOG.md+8
@@ -3,6 +3,14 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.13.3 (unreleased)++- The busy gate waits out the bridge's own quick reads (`document_info`, `get_app_state`) for up to 15 s instead+  of refusing the next verb. With about 90 documents open, the post-verb document snapshot can outlive the 3 s+  the window etiquette waits for it, and the caller's next verb then found it still running and got+  `main_thread_busy` (a mechanical `inspect` in the assembly campaign, behind a 4.9 s `document_info`). Real work+  in front of a verb is refused as before.+ ## 1.13.2 (2026-10-28, insiders)  The standard's shared writers, for files every bridge writes the same way.
rust/bridge/src/addin.rs+27−1
@@ -167,6 +167,18 @@ pub fn loaded_evidence(pids: &[u32]) -> Option<Value> {     loaded_evidence_from(None, Some(ago), port_refused()) } +/// The bridge's own quick reads. With ~90 documents open a document_info takes ~5 s, longer than+/// the 3 s the post-verb document snapshot waits, so it is still running when the caller's next+/// verb lands, and that verb was refused as main_thread_busy (2026-10-28, a mechanical inspect in+/// the assembly campaign). Such a read ends in seconds: the gate waits it out instead.+const SHORT_READS: &[&str] = &["document_info", "get_app_state"];+const SHORT_READ_WAIT: Duration = Duration::from_secs(15);++/// Is the add-in busy with one of the bridge's own quick reads, which the gate should wait out?+fn busy_with_short_read(st: &Value) -> bool {+    st["busy"].as_bool() == Some(true) && SHORT_READS.contains(&st["busyCommand"].as_str().unwrap_or(""))+}+ /// The two busy gates, in server.py's order: our own long command, then the add-in's /status. fn busy_gate(cmd: &str) -> Option<Value> {     if LONG_RUNNING.contains(&cmd) || BUSY_EXEMPT.contains(&cmd) { return None; }@@ -183,7 +195,12 @@ fn busy_gate(cmd: &str) -> Option<Value> {             "errorCode": "main_thread_busy", "busyCommand": bc, "elapsedSeconds": elapsed, "progress": progress,             "_hint": format!("A cloud search ({bc}) is in progress{}, running for {elapsed}s. Do NOT retry add-in commands (get_app_state, document_info, etc.) — they will all be rejected until the search finishes. Commands that still work right now: fusion_window_info, fusion_screenshot_fusion, fusion_click_fusion, fusion_send_key, fusion_close_window. Wait for the search to complete, then retry.", pct.map(|p| format!(" (~{p}% done)")).unwrap_or_default())}));     }-    if let Some(st) = addin_status_probe(Duration::from_secs(3)) {+    if let Some(mut st) = addin_status_probe(Duration::from_secs(3)) {+        let deadline = Instant::now() + SHORT_READ_WAIT;+        while busy_with_short_read(&st) && Instant::now() < deadline {+            std::thread::sleep(Duration::from_millis(250));+            match addin_status_probe(Duration::from_secs(3)) { Some(next) => st = next, None => break }+        }         // the palette (activity_banner) repaints in a fraction of a second and is advisory: a verb         // that lands while it paints waits its turn in the add-in instead of being refused         // a sub-second probe from another caller (the dashboard polls get_app_state every half@@ -471,6 +488,15 @@ mod tests {         assert!(loaded_evidence_from(None, None, false).is_none());     }     #[test]+    fn the_gate_waits_out_the_bridges_own_quick_reads() {+        // 2026-10-28: the post-verb document_info ran ~5 s with 94 documents open and the next+        // verb was refused; a quick read is waited out, real work still is not+        assert!(busy_with_short_read(&json!({"busy": true, "busyCommand": "document_info", "elapsedSeconds": 4.9})));+        assert!(busy_with_short_read(&json!({"busy": true, "busyCommand": "get_app_state", "elapsedSeconds": 2.5})));+        assert!(!busy_with_short_read(&json!({"busy": true, "busyCommand": "run_modeling_script", "elapsedSeconds": 4.9})));+        assert!(!busy_with_short_read(&json!({"busy": false, "busyCommand": "document_info"})));+    }+    #[test]     fn requests_leave_the_in_flight_list_however_they_end() {         assert!(IN_FLIGHT.lock().unwrap().iter().all(|(_, c, _)| c != "slow_one"));         let a = InFlight::begin("slow_one");

Reviews

Colby Knox approved · 14d ago

Reviewed and approved. The gate change is right: only the bridge's own quick reads (document_info, get_app_state) are waited out, re-probed every 250 ms for up to 15 s. A script, an import, a cloud walk or a read still running after 15 s is refused exactly as before, and a lost probe falls back to the old judgement. Applied to master (1.13.2): cargo test --workspace is green (65 bridge tests, with the_gate_waits_out_the_bridges_own_quick_reads), the Windows build is clean, the contract is unchanged at 298 verbs, the Python suites pass, and back-to-back document_info, get_app_state and routing_state calls on the test laptop all succeed on this build. Both of your master catches were right: the page lacked rust/vendor/adom-eda-checks/src/writers/ and had a stale Cargo.lock, because release.sh pushed a glob list that missed them. Both are on the page now (a fresh clone builds with --locked), and release.sh pushes the whole tracked rust/ tree and refuses a release that would leave a tracked source off the page. The changelog line about joint_origin_* and motion_link_* not having run live is corrected. This ships in 1.13.3.

Comments

No comments yet.

Log in to comment.