{"record":{"id":"2abebbdda79e70f3","repo":"nautechsystems/nautilus_trader","slug":"rate-limiter-decision-lock-poisoned","errorCode":null,"errorMessage":"rate limiter decision lock poisoned","messagePattern":"rate limiter decision lock poisoned","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/network/src/ratelimiter/mod.rs","lineNumber":247,"sourceCode":"        self.clock.advance(by);\n    }\n}\n\nimpl<K, C> RateLimiter<K, C>\nwhere\n    K: Hash + Eq + Clone,\n    C: Clock,\n{\n    /// Adds or updates a quota for a specific key.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the rate limiter decision mutex is poisoned.\n    pub fn add_quota_for_key(&self, key: K, value: Quota) {\n        let _guard = self\n            .decision_lock\n            .lock()\n            .expect(\"rate limiter decision lock poisoned\");\n        self.gcra.insert(key, Gcra::new(value));\n    }\n\n    /// Checks if the given key is allowed under the rate limit.\n    ///\n    /// # Errors\n    ///\n    /// Returns `Err(NotUntil)` if the key is rate-limited, indicating when it will be allowed.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the rate limiter decision mutex is poisoned.\n    pub fn check_key(&self, key: &K) -> Result<(), NotUntil<C::Instant>> {\n        let _guard = self\n            .decision_lock\n            .lock()\n            .expect(\"rate limiter decision lock poisoned\");\n","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/d1527c24afdf475115785557f89a55c3e336c51f/crates/network/src/ratelimiter/mod.rs#L229-L265","documentation":"The generic rate limiter (governor-style GCRA) serializes decisions behind decision_lock. add_quota_for_key inserts a new quota for a key and panics on a poisoned decision_lock — i.e. some thread panicked while holding it, most commonly inside quota.test_and_update or plan/commit logic.","triggerScenarios":"Calling add_quota_for_key (per-connection/per-domain quota registration) after another thread panicked in check_key/await_keys_ready while holding decision_lock — for example a clock or GCRA arithmetic panic.","commonSituations":"Mostly seen in tests (test_custom_key_quota, test_multiple_keys) when a shared limiter is reused after a failing test poisoned it; in production, a malformed Quota (zero/negative period) causing arithmetic panic under the lock.","solutions":["Find the first panic under decision_lock (often a Quota with zero capacity/period passed to Gcra::new) and fix/validate inputs","Validate Quota parameters before inserting (period > 0, burst > 0)","Treat lock poisoning as recoverable: decision state is best-effort rate limiting, so into_inner() or error-mapping is preferable to a panic"],"exampleFix":"// before\nlet _guard = self.decision_lock.lock().expect(\"rate limiter decision lock poisoned\");\n// after\nlet _guard = self.decision_lock.lock().unwrap_or_else(|e| e.into_inner());","handlingStrategy":"validation","validationCode":"// reject degenerate quotas before registering\nfn assert_quota_ok(q: &Quota) {\n    assert!(q.replenish_interval_ns() > 0, \"quota period must be > 0\");\n    assert!(q.burst_size().get() > 0, \"quota burst must be > 0\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate Quota parameters at construction so GCRA math cannot panic under decision_lock","Don't share limiter instances across independent tests that can panic","Consider fail-open recovery: rate limiting should degrade, not crash the process"],"tags":["rust","mutex-poisoned","rate-limiter","network","panic"],"backgroundTag":"mutex-poisoned","analyzedSha":"d1527c24afdf475115785557f89a55c3e336c51f","analyzedAt":"2026-08-27T04:01:12.327Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}