quickwit-oss/quickwit · error

`nodes` should not be empty

Error message

`nodes` should not be empty

What it means

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.

Source

Thrown at quickwit/quickwit-search/src/search_job_placer.rs:87

impl EventSubscriber<ReportSplitsRequest> for SearchJobPlacer {
    async fn handle_event(&mut self, evt: ReportSplitsRequest) {
        let mut nodes: HashMap<SocketAddr, SearcherNode> =
            self.searcher_pool.pairs().into_iter().collect();
        if nodes.is_empty() {
            return;
        }
        let mut splits_per_node: HashMap<SocketAddr, Vec<ReportSplit>> =
            HashMap::with_capacity(nodes.len().min(evt.report_splits.len()));
        for report_split in evt.report_splits {
            let node_addr = nodes
                .iter()
                .max_by_key(|(_node_addr, node)| {
                    node_affinity(&node.node_id, &report_split.split_id)
                })
                // This actually never happens thanks to the if-condition at the
                // top of this function.
                .map(|(node_addr, _node)| *node_addr)
                .expect("`nodes` should not be empty");
            splits_per_node
                .entry(node_addr)
                .or_default()
                .push(report_split);
        }
        for (node_addr, report_splits) in splits_per_node {
            if let Some(searcher_node) = nodes.get_mut(&node_addr) {
                let report_splits_req = ReportSplitsRequest { report_splits };
                let _ = searcher_node.client.report_splits(report_splits_req).await;
            }
        }
    }
}

impl fmt::Debug for SearchJobPlacer {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("SearchJobPlacer").finish()
    }

View on GitHub (pinned to a39730c5cd)

Solutions

  1. Verify the early-return guard at the top of handle_event covers the same `nodes` value used in the expect path; fix any divergence.
  2. If the cluster can legitimately be empty mid-event, handle it explicitly: return early or defer the split assignment instead of expecting non-emptiness.
  3. Add a unit test with an empty node list hitting the event path to lock in the guard behavior.

Example fix

// before
.map(|(node_addr, _node)| *node_addr)
.expect("`nodes` should not be empty");
// after
let Some(node_addr) = nodes
    .iter()
    .max_by_key(|(_node_addr, node)| node_affinity(&node.node_id, &report_split.split_id))
    .map(|(node_addr, _node)| *node_addr)
else { continue; };
Defensive patterns

Strategy: validation

Validate before calling

if nodes.is_empty() { return; } // ensure the early-return guard runs before assignment

Type guard

fn non_empty(nodes: &[Node]) -> Option<&[Node]> { if nodes.is_empty() { None } else { Some(nodes) } }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08). Data as JSON: /api/errors/7bcf4d60d9f53fc4. Report an issue: GitHub.