{"record":{"id":"dac89b6ecfb76b5a","repo":"quickwit-oss/quickwit","slug":"assign-jobs-should-return-at-least-one-client-or","errorCode":null,"errorMessage":"`assign_jobs` should return at least one client or fail.","messagePattern":"`assign_jobs` should return at least one client or fail\\.","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"quickwit/quickwit-search/src/search_job_placer.rs","lineNumber":367,"sourceCode":"                .or_insert_with(|| (chosen_node.client.clone(), Vec::new()))\n                .1\n                .push(job);\n        }\n        Ok(job_assignments.into_values())\n    }\n\n    /// Assigns a single job to a client.\n    pub async fn assign_job<J: Job>(\n        &self,\n        job: J,\n        excluded_addrs: &HashSet<SocketAddr>,\n    ) -> anyhow::Result<SearchServiceClient> {\n        let client = self\n            .assign_jobs(vec![job], excluded_addrs)\n            .await?\n            .next()\n            .map(|(client, _jobs)| client)\n            .expect(\"`assign_jobs` should return at least one client or fail.\");\n        Ok(client)\n    }\n}\n\n#[derive(Debug, Clone)]\nstruct CandidateNode {\n    affinity_id: NodeId,\n    pub grpc_addr: SocketAddr,\n    pub client: SearchServiceClient,\n    /// Current load of this node in job-cost units. `None` means the node\n    /// could not be reached and should only be used as a last resort.\n    pub load: Option<usize>,\n}\n\nimpl Hash for CandidateNode {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        self.affinity_id.hash(state);\n    }","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/quickwit-oss/quickwit/blob/a39730c5cdcd1a4fe798403737ae293999ea21f8/quickwit/quickwit-search/src/search_job_placer.rs#L349-L385","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure at least one eligible search node is running and not in the excluded set; check cluster membership health.","If retrying on failures, bound excluded_addrs growth so the last attempt can reuse a previously failed node rather than excluding everything.","If implementing a custom placement strategy, return an explicit error (e.g. anyhow::bail!(\"no available node\")) instead of an empty Ok iterator."],"exampleFix":"// before\n.next()\n.map(|(client, _jobs)| client)\n.expect(\"`assign_jobs` should return at least one client or fail.\");\n// after\n.next()\n.map(|(client, _jobs)| client)\n.ok_or_else(|| anyhow::anyhow!(\"no candidate node available for job placement\"))?","handlingStrategy":"retry","validationCode":"let eligible = cluster_nodes.iter().filter(|n| !excluded_addrs.contains(&n.advertise_addr)).count();\nassert!(eligible > 0, \"no eligible node outside excluded_addrs\");","typeGuard":null,"tryCatchPattern":"// caller pattern\nmatch placer.assign_job(&job, &excluded_addrs).await {\n    Ok(client) => client,\n    Err(e) if e.to_string().contains(\"no candidate\") => retry_with_relaxed_exclusions().await,\n    Err(e) => return Err(e),\n}","preventionTips":["Keep at least one search node reachable; monitor cluster membership.","Cap excluded_addrs growth across retries so the final attempt can reuse a failed node.","Custom placement strategies must return an error, never Ok with an empty iterator."],"tags":["rust","search","scheduling","cluster","no-candidates"],"backgroundTag":"empty-result-set","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"}