quickwit-oss/quickwit · error

`assign_jobs` should return at least one client or fail.

Error message

`assign_jobs` should return at least one client or fail.

What it means

assign_job is a convenience wrapper that asks the placement strategy to place exactly one search job and expects at least one (client, jobs) pair back. The contract is that assign_jobs either returns candidates or fails with an error; the expect documents that returning an empty iterator is a strategy bug. It fires if the placement strategy (e.g. round-robin or load-based with fallbacks) yields no candidate node for the job while reporting success.

Source

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

                .or_insert_with(|| (chosen_node.client.clone(), Vec::new()))
                .1
                .push(job);
        }
        Ok(job_assignments.into_values())
    }

    /// Assigns a single job to a client.
    pub async fn assign_job<J: Job>(
        &self,
        job: J,
        excluded_addrs: &HashSet<SocketAddr>,
    ) -> anyhow::Result<SearchServiceClient> {
        let client = self
            .assign_jobs(vec![job], excluded_addrs)
            .await?
            .next()
            .map(|(client, _jobs)| client)
            .expect("`assign_jobs` should return at least one client or fail.");
        Ok(client)
    }
}

#[derive(Debug, Clone)]
struct CandidateNode {
    affinity_id: NodeId,
    pub grpc_addr: SocketAddr,
    pub client: SearchServiceClient,
    /// Current load of this node in job-cost units. `None` means the node
    /// could not be reached and should only be used as a last resort.
    pub load: Option<usize>,
}

impl Hash for CandidateNode {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.affinity_id.hash(state);
    }

View on GitHub (pinned to a39730c5cd)

Solutions

  1. Ensure at least one eligible search node is running and not in the excluded set; check cluster membership health.
  2. If retrying on failures, bound excluded_addrs growth so the last attempt can reuse a previously failed node rather than excluding everything.
  3. If implementing a custom placement strategy, return an explicit error (e.g. anyhow::bail!("no available node")) instead of an empty Ok iterator.

Example fix

// before
.next()
.map(|(client, _jobs)| client)
.expect("`assign_jobs` should return at least one client or fail.");
// after
.next()
.map(|(client, _jobs)| client)
.ok_or_else(|| anyhow::anyhow!("no candidate node available for job placement"))?
Defensive patterns

Strategy: retry

Validate before calling

let eligible = cluster_nodes.iter().filter(|n| !excluded_addrs.contains(&n.advertise_addr)).count();
assert!(eligible > 0, "no eligible node outside excluded_addrs");

Try / catch

// caller pattern
match placer.assign_job(&job, &excluded_addrs).await {
    Ok(client) => client,
    Err(e) if e.to_string().contains("no candidate") => retry_with_relaxed_exclusions().await,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling assign_job when every candidate node is excluded (excluded_addrs covers the whole cluster), all nodes are unreachable/overloaded and the fallback logic yields nothing, or a custom SearchJobPlacer strategy returns Ok with an empty iterator.

Common situations: Single-node clusters where the only node's address is already in excluded_addrs after repeated retries; all search nodes marked unavailable during rolling restarts; retries exhausted (see retry_client tests) leaving no candidates.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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