{"record":{"id":"24e8eb28cedfd872","repo":"embassy-rs/embassy","slug":"rpcservice-run-must-not-be-called-concurrently","errorCode":null,"errorMessage":"RpcService::run() must not be called concurrently","messagePattern":"RpcService::run\\(\\) must not be called concurrently","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"embassy-sync/src/rpc_service.rs","lineNumber":519,"sourceCode":"    /// This future is cancel-safe. A subsequent call to `run()` will recover the\n    /// previous state and resume processing any in-flight call.\n    pub async fn run(&self, state: &mut T) -> ! {\n        struct RunGuard<'a, M: RawMutex> {\n            runner_state: &'a Mutex<M, Cell<RunnerState>>,\n        }\n        impl<M: RawMutex> Drop for RunGuard<'_, M> {\n            fn drop(&mut self) {\n                self.runner_state.lock(|cell| {\n                    let mut s = cell.get();\n                    s.running = false;\n                    cell.set(s);\n                });\n            }\n        }\n\n        let needs_recovery = self.with_runner_state(|s| {\n            if s.running {\n                panic!(\"RpcService::run() must not be called concurrently\")\n            }\n            s.running = true;\n            s.needs_recovery\n        });\n        let _guard = RunGuard {\n            runner_state: &self.runner_state,\n        };\n\n        // If the previous runner was cancelled mid-job the caller might still\n        // be interacting with the slot. We must wait for it to finish (the caller\n        // always acks, either explicitly or via its Drop) and then clean up.\n        if needs_recovery {\n            self.slot.wait_ack_and_finish(&self.runner_state).await;\n        }\n\n        loop {\n            // Wait for a caller to submit a closure.\n            // This is a clean cancellation point because no job in flight","sourceCodeStart":501,"sourceCodeEnd":537,"githubUrl":"https://github.com/embassy-rs/embassy/blob/463a07b963419a1bfe61d5d597c44acb810afb8b/embassy-sync/src/rpc_service.rs#L501-L537","documentation":"RpcService::run() is a single-consumer runner loop: the runner_state tracks a `running` flag and run() panics if invoked while a previous run() is still active. Only one run loop may drive the service at a time, since recovery and call dispatch state are singular.","triggerScenarios":"Calling service.run(...) from two tasks/spawns concurrently, calling run() again before the first loop returns, or spawning run() in a loop without awaiting/joining the previous invocation.","commonSituations":"Accidentally spawning the RPC runner in two Embassy tasks; restarting the runner after a hot path without awaiting shutdown; firmware update code that re-inits the service while the old task still runs.","solutions":["Spawn run() exactly once and keep that task alive for the service lifetime.","Await the existing run() future (or shut it down via RunGuard/Drop) before calling run() again.","Use a supervisor task that owns run() so no other code path can start it."],"exampleFix":"// before\nspawner.spawn(run_rpc(service.clone()).unwrap());\nspawner.spawn(run_rpc(service.clone()).unwrap()); // panics\n// after\nspawner.spawn(run_rpc(service).unwrap()); // single runner","handlingStrategy":"validation","validationCode":"// Wrap the runner so run() can only ever be spawned once:\nuse embassy_sync::once_lock::OnceLock;\nstatic SPAWNED: OnceLock<()> = OnceLock::new();\nfn spawn_runner_once(spawner, service) {\n    if SPAWNED.try_get().is_none() {\n        SPAWNED.init(());\n        spawner.must_spawn(service.run());\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Spawn run() in exactly one dedicated supervisor task","Never call run() again before the previous future completes","Use OnceLock/OnceCell style guards for one-shot spawns","Review task spawn sites for duplicated runner tasks"],"tags":["embedded","rpc","concurrency","panic"],"backgroundTag":"invalid-state-transition","analyzedSha":"463a07b963419a1bfe61d5d597c44acb810afb8b","analyzedAt":"2026-09-10T13:38:26.660Z","contentChangedAt":"2026-09-10T13:38:26.660Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}