{"record":{"id":"7bcf4d60d9f53fc4","repo":"quickwit-oss/quickwit","slug":"nodes-should-not-be-empty","errorCode":null,"errorMessage":"`nodes` should not be empty","messagePattern":"`nodes` should not be empty","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"quickwit/quickwit-search/src/search_job_placer.rs","lineNumber":87,"sourceCode":"impl EventSubscriber<ReportSplitsRequest> for SearchJobPlacer {\n    async fn handle_event(&mut self, evt: ReportSplitsRequest) {\n        let mut nodes: HashMap<SocketAddr, SearcherNode> =\n            self.searcher_pool.pairs().into_iter().collect();\n        if nodes.is_empty() {\n            return;\n        }\n        let mut splits_per_node: HashMap<SocketAddr, Vec<ReportSplit>> =\n            HashMap::with_capacity(nodes.len().min(evt.report_splits.len()));\n        for report_split in evt.report_splits {\n            let node_addr = nodes\n                .iter()\n                .max_by_key(|(_node_addr, node)| {\n                    node_affinity(&node.node_id, &report_split.split_id)\n                })\n                // This actually never happens thanks to the if-condition at the\n                // top of this function.\n                .map(|(node_addr, _node)| *node_addr)\n                .expect(\"`nodes` should not be empty\");\n            splits_per_node\n                .entry(node_addr)\n                .or_default()\n                .push(report_split);\n        }\n        for (node_addr, report_splits) in splits_per_node {\n            if let Some(searcher_node) = nodes.get_mut(&node_addr) {\n                let report_splits_req = ReportSplitsRequest { report_splits };\n                let _ = searcher_node.client.report_splits(report_splits_req).await;\n            }\n        }\n    }\n}\n\nimpl fmt::Debug for SearchJobPlacer {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        f.debug_struct(\"SearchJobPlacer\").finish()\n    }","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/quickwit-oss/quickwit/blob/a39730c5cdcd1a4fe798403737ae293999ea21f8/quickwit/quickwit-search/src/search_job_placer.rs#L69-L105","documentation":"In the search job placer's handling of a coverage/refresh report event, the code picks the node with the best affinity for a split and expects the candidate node list to be non-empty. The preceding if-condition already returns early when nodes is empty, so the expect asserts an internal invariant; a panic means that guard was bypassed or the max_by_key iterator logic changed.","triggerScenarios":"handle_event processing a report for a split when the `nodes` map/vec is empty and the early-return guard at the top of the function did not fire — i.e. only after modifying the guard logic or calling this path with an empty node set through new code paths.","commonSituations":"Seen during cluster changes: all indexing/search nodes left the cluster while a control-plane event was in flight, exposing a race if the empty-check happens before node list shrinks; or during development refactors of search_job_placer.rs.","solutions":["Verify the early-return guard at the top of handle_event covers the same `nodes` value used in the expect path; fix any divergence.","If the cluster can legitimately be empty mid-event, handle it explicitly: return early or defer the split assignment instead of expecting non-emptiness.","Add a unit test with an empty node list hitting the event path to lock in the guard behavior."],"exampleFix":"// before\n.map(|(node_addr, _node)| *node_addr)\n.expect(\"`nodes` should not be empty\");\n// after\nlet Some(node_addr) = nodes\n    .iter()\n    .max_by_key(|(_node_addr, node)| node_affinity(&node.node_id, &report_split.split_id))\n    .map(|(node_addr, _node)| *node_addr)\nelse { continue; };","handlingStrategy":"validation","validationCode":"if nodes.is_empty() { return; } // ensure the early-return guard runs before assignment","typeGuard":"fn non_empty(nodes: &[Node]) -> Option<&[Node]> { if nodes.is_empty() { None } else { Some(nodes) } }","tryCatchPattern":null,"preventionTips":["Keep the empty-nodes guard in the same function/scope as the max_by_key selection.","Add tests for cluster events with zero live nodes.","Prefer if-let/let-else over expect when emptiness is a runtime possibility."],"tags":["rust","cluster","scheduling","empty-collection"],"backgroundTag":"empty-required-field","analyzedSha":"a39730c5cdcd1a4fe798403737ae293999ea21f8","analyzedAt":"2026-09-08T13:19:37.784Z","contentChangedAt":"2026-09-08T13:19:37.784Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}