{"record":{"id":"ab706b7b8269a286","repo":"nautechsystems/nautilus_trader","slug":"window-limit-must-be-non-zero","errorCode":null,"errorMessage":"window limit must be non-zero","messagePattern":"window limit must be non-zero","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/derive/src/common/rate_limit.rs","lineNumber":427,"sourceCode":"        RateBucket::PerInstrument(instrument_name) => Ustr::from(\n            format!(\n                \"{DERIVE_PER_INSTRUMENT_RATE_KEY_PREFIX}{}\",\n                instrument_name.as_str(),\n            )\n            .as_str(),\n        ),\n        RateBucket::CancelAll => Ustr::from(DERIVE_CANCEL_ALL_RATE_KEY),\n        RateBucket::CancelByLabel => Ustr::from(DERIVE_CANCEL_BY_LABEL_RATE_KEY),\n    }\n}\n\nfn resolve_tps(configured: Option<u32>, default_tps: u32) -> u32 {\n    configured.filter(|&v| v > 0).unwrap_or(default_tps)\n}\n\nfn window_limit(tps: u32) -> NonZeroU32 {\n    NonZeroU32::new(tps.saturating_mul(DERIVE_RATE_BURST_MULTIPLIER))\n        .expect(\"window limit must be non-zero\")\n}\n\nfn window_index(elapsed_nanos: u64) -> u32 {\n    u32::try_from(elapsed_nanos / RATE_WINDOW_NANOS).expect(\"window index fits u32\")\n}\n\n/// Packs `(window index, consumed)` into one atomic word; the window index in\n/// the high half so the default zero value reads as a stale window.\nfn pack(window: u32, consumed: u32) -> u64 {\n    (u64::from(window) << 32) | u64::from(consumed)\n}\n\nfn unpack(packed: u64) -> (u32, u32) {\n    (\n        u32::try_from(packed >> 32).expect(\"window index fits u32\"),\n        packed as u32,\n    )\n}","sourceCodeStart":409,"sourceCodeEnd":445,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/derive/src/common/rate_limit.rs#L409-L445","documentation":"`window_limit` builds the rate-limiter's burst window limit as `tps * DERIVE_RATE_BURST_MULTIPLIER` and converts to `NonZeroU32`. The panic fires only if the product is zero — `saturating_mul` overflow clamps to u32::MAX (non-zero), so zero requires tps == 0 AND the multiplier == 0, or a zero multiplier constant. It enforces that the limiter never gets a zero-capacity window.","triggerScenarios":"Constructing the rate limiter with a configured/default TPS of 0 while the burst multiplier is also 0 — practically only from a mis-edited constant or a `resolve_tps` regression, since `resolve_tps` already filters non-positive values to a default.","commonSituations":"Hand-crafted configuration bypassing `resolve_tps`, e.g. constructing the limiter directly with `window_limit(0)` in tests or a fork that changed `DERIVE_RATE_BURST_MULTIPLIER`.","solutions":["Ensure TPS is resolved through `resolve_tps` (which substitutes the default for 0) before calling `window_limit`.","Verify `DERIVE_RATE_BURST_MULTIPLIER` is non-zero in your build.","If constructing the limiter manually, pass a positive tps value."],"exampleFix":"// before\nlet limiter = RateLimiter::new(window_limit(configured_tps));\n// after\nlet tps = resolve_tps(configured_tps, DEFAULT_TPS);\nlet limiter = RateLimiter::new(window_limit(tps));","handlingStrategy":"validation","validationCode":"// before constructing the limiter\nlet tps = configured.unwrap_or(DEFAULT_TPS);\nassert!(tps > 0, \"tps must be positive\");","typeGuard":"fn tps_ok(tps: u32) -> bool { tps > 0 }","tryCatchPattern":null,"preventionTips":["Always resolve TPS through resolve_tps so zero falls back to the default.","Keep DERIVE_RATE_BURST_MULTIPLIER non-zero.","Don't construct the limiter with raw user config without the resolve step."],"tags":["rust","panic","rate-limit","config"],"backgroundTag":"invalid-config-value","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}