{"record":{"id":"c36fa2f0b8eac43d","repo":"nautechsystems/nautilus_trader","slug":"subscription-state-lock-poisoned","errorCode":null,"errorMessage":"subscription state lock poisoned","messagePattern":"subscription state lock poisoned","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/network/src/websocket/subscription.rs","lineNumber":475,"sourceCode":"\n            // Add symbol-level subscriptions (skip marker)\n            for symbol in symbols {\n                if *symbol != marker {\n                    topics.push(format!(\"{channel}{}{symbol}\", self.delimiter));\n                }\n            }\n        }\n\n        // Sort so resubscription after a reconnect replays topics in the same sequence\n        // across runs; both the outer DashMap and the inner symbol sets are unordered.\n        topics.sort();\n        topics\n    }\n\n    fn lock_state_read(&self) -> RwLockReadGuard<'_, ()> {\n        self.state_lock\n            .read()\n            .expect(\"subscription state lock poisoned\")\n    }\n\n    fn lock_state_write(&self) -> RwLockWriteGuard<'_, ()> {\n        self.state_lock\n            .write()\n            .expect(\"subscription state lock poisoned\")\n    }\n}\n\n/// Splits a topic into channel and optional symbol using the specified delimiter.\n#[must_use]\npub fn split_topic(topic: &str, delimiter: char) -> (&str, Option<&str>) {\n    topic\n        .split_once(delimiter)\n        .map_or((topic, None), |(channel, symbol)| (channel, Some(symbol)))\n}\n\nfn snapshot(map: &DashMap<Ustr, AHashSet<Ustr>>) -> SubscriptionSnapshot {","sourceCodeStart":457,"sourceCodeEnd":493,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/d1527c24afdf475115785557f89a55c3e336c51f/crates/network/src/websocket/subscription.rs#L457-L493","documentation":"Subscription state (pending/subscribed topics) is guarded by an RwLock; read-side helpers (confirmed, pending_subscribe, len, is_empty, is_subscribed) take a read guard via lock_state_read and panic if the RwLock is poisoned by a panic in a write-side critical section.","triggerScenarios":"Any read of subscription state after a thread panicked while holding the write lock — e.g. mark_subscribe/confirm_subscribe mutating topic maps hit an inconsistent state or a downstream callback panicked under the guard.","commonSituations":"A WS user handler or metrics callback panicking during confirm_subscribe poisons the lock; afterwards every status query (is_subscribed etc.) panics, crashing the actor's message loop.","solutions":["Fix the panic inside the write-side critical sections (keep callbacks/user code outside the guard)","Read guards on a poisoned RwLock can safely recover with into_inner() since reads don't mutate — switch lock_state_read to unwrap_or_else(|e| e.into_inner())","Add tests that panic in a write path and assert reads still work"],"exampleFix":"// before\nself.state_lock.read().expect(\"subscription state lock poisoned\")\n// after\nself.state_lock.read().unwrap_or_else(|e| e.into_inner())","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Move user callbacks and logging outside the write guard","Recover reads from poison with into_inner(); reads cannot worsen torn state","Assert in tests that a write-path panic leaves queries functional"],"tags":["rust","mutex-poisoned","rwlock","websocket","subscriptions"],"backgroundTag":"mutex-poisoned","analyzedSha":"d1527c24afdf475115785557f89a55c3e336c51f","analyzedAt":"2026-08-27T04:01:12.327Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}