{"record":{"id":"3106ae0f52153fbe","repo":"quickwit-oss/quickwit","slug":"receiver-should-live-longer-than-sender","errorCode":null,"errorMessage":"Receiver should live longer than sender","messagePattern":"Receiver should live longer than sender","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"quickwit/quickwit-search/src/search_permit_provider.rs","lineNumber":484,"sourceCode":"    pub fn free_warmup_slot(&mut self) {\n        if self.warmup_slot_freed {\n            return;\n        }\n        self.warmup_slot_freed = true;\n        self.send_if_still_running(SearchPermitMessage::FreeWarmupSlot);\n    }\n\n    pub fn memory_allocation(&self) -> ByteSize {\n        ByteSize(self.memory_allocation)\n    }\n\n    fn send_if_still_running(&self, msg: SearchPermitMessage) {\n        if let Some(sender) = self.msg_sender.upgrade() {\n            sender\n                .send(msg)\n                // Receiver instance in the event loop is never dropped or\n                // closed as long as there is a strong sender reference.\n                .expect(\"Receiver should live longer than sender\");\n        }\n    }\n}\n\nimpl Drop for SearchPermit {\n    fn drop(&mut self) {\n        let prev = self\n            .total_job_cost\n            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {\n                Some(v.saturating_sub(self.job_cost))\n            })\n            .expect(\"closure always returns Some\");\n        if self.job_cost > prev {\n            warn!(\n                job_cost = self.job_cost,\n                total_job_cost = prev,\n                \"job cost underflow: more job cost released than allocated\"\n            );","sourceCodeStart":466,"sourceCodeEnd":502,"githubUrl":"https://github.com/quickwit-oss/quickwit/blob/a39730c5cdcd1a4fe798403737ae293999ea21f8/quickwit/quickwit-search/src/search_permit_provider.rs#L466-L502","documentation":"send_if_still_running sends a message to the permit provider's event loop through a Weak sender. The expect asserts the receiver (the actor event loop) is still alive whenever a strong sender exists. If the channel is closed despite the sender being upgradable, the lifecycle assumption is broken and the code panics.","triggerScenarios":"update_memory_usage, free_warmup_slot, or SearchPermit::drop attempt to send a message after the event loop's receiver was dropped or its channel closed, while the sender is still upgradeable.","commonSituations":"Shutting down the search service while in-flight requests still hold SearchPermits; ordering issues during actor teardown where the receiver dies before all permit holders drop.","solutions":["Ensure the permit provider event loop outlives all SearchPermit holders (hold its mailbox in the service root)","Audit shutdown ordering: stop sources/requests before dropping the receiver","Replace expect with graceful handling of SendError when a deliberate shutdown is in progress"],"exampleFix":"// before\nsender.send(msg).expect(\"Receiver should live longer than sender\");\n// after\nif sender.send(msg).is_err() {\n    debug!(\"permit provider receiver already closed; dropping message\");\n}","handlingStrategy":"type-guard","validationCode":"// Check liveness before sending:\nif search_permit_provider_msg_sender.strong_count() == 0 { /* provider gone */ }","typeGuard":"fn receiver_alive(sender: &Weak<UnboundedSender<SearchPermitMessage>>) -> bool {\n    sender.upgrade().map(|s| !s.is_closed()).unwrap_or(false)\n}","tryCatchPattern":"// Replace expect for shutdown tolerance:\nif let Some(sender) = self.msg_sender.upgrade() {\n    let _ = sender.send(msg); // swallow SendError during shutdown\n}","preventionTips":["Hold a strong reference to the permit provider's receiver for the whole service lifetime","Drop SearchPermits before stopping the search service","Test shutdown paths with in-flight permits"],"tags":["rust","channel","panic","actor-lifecycle"],"backgroundTag":"internal-invariant-violation","analyzedSha":"a39730c5cdcd1a4fe798403737ae293999ea21f8","analyzedAt":"2026-09-08T13:19:37.784Z","contentChangedAt":"2026-09-08T13:19:37.784Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}