{"record":{"id":"1e5eb09da7c9eec8","repo":"databendlabs/databend","slug":"transform-aggregator-invalid-hash-table-state-be","errorCode":null,"errorMessage":"[TRANSFORM-AGGREGATOR] Invalid hash table state before spill","messagePattern":"\\[TRANSFORM-AGGREGATOR\\] Invalid hash table state before spill","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/query/service/src/pipelines/processors/transforms/aggregator/transform_aggregate_final.rs","lineNumber":331,"sourceCode":"        } else {\n            unreachable!(\"[TRANSFORM-AGGREGATOR] Invalid hash table state during spill check\")\n        }\n        self.reset_hashtable(self.current_partition_depth);\n        Ok(())\n    }\n\n    fn finish(\n        &mut self,\n        task_id: Option<u64>,\n        spilled_depth: usize,\n        tx: Sender<FinalAggregateTask>,\n    ) -> Result<()> {\n        if self.spilled_occurred {\n            let (output_rows, hash_index_resizes) = match &self.hashtable {\n                HashTable::AggregateHashTable(ht) => {\n                    (ht.payload.len(), ht.hash_index_resize_count())\n                }\n                _ => unreachable!(\"[TRANSFORM-AGGREGATOR] Invalid hash table state before spill\"),\n            };\n            self.spill_finish(spilled_depth, tx)?;\n            if let Some(task_id) = task_id {\n                self.statistics.log_task_finish_statistics(\n                    task_id,\n                    self._id,\n                    spilled_depth,\n                    output_rows,\n                    hash_index_resizes,\n                    true,\n                );\n            } else {\n                self.statistics.reset();\n            }\n\n            self.spilled_occurred = false;\n            let _ = mem::take(&mut self.hashtable);\n            return Ok(());","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/databendlabs/databend/blob/288d84d76e20a2f8f7173bda9691eb6ece301aa9/src/query/service/src/pipelines/processors/transforms/aggregator/transform_aggregate_final.rs#L313-L349","documentation":"TransformFinalAggregate::finish, when spilled_occurred is true, reads statistics (payload length, resize count) from the hash table, requiring HashTable::AggregateHashTable. If the table is in another state (e.g. MovedOut), unreachable! fires, meaning the finish path executed after the hash table was already consumed.","triggerScenarios":"finish() is called with spilled_occurred set but self.hashtable no longer holds AggregateHashTable — from the table being taken earlier by on_finish/spill paths, double process/finish invocation, or pipeline driver bugs.","commonSituations":"Query finalization racing with spill-out; executor state-machine regressions; custom pipelines calling finish more than once.","solutions":["Retry on a fixed/updated version; ensure single invocation of the final transform's finish","Check custom pipeline event wiring so spill_out/finish ordering matches AccumulatingTransform contract","File a bug with stack trace if reproducible on a single version"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn as_aggregate_ht(ht: &HashTable) -> Option<&AggregateHashTable> {\n    match ht { HashTable::AggregateHashTable(t) => Some(t), _ => None }\n}","tryCatchPattern":"match res {\n    Err(e) if e.message().contains(\"Invalid hash table state before spill\") => {\n        // abort, retry on fixed version, capture stack trace\n    }\n    ...\n}","preventionTips":["Ensure finish is invoked exactly once per transform instance","Test spill-then-finish ordering in pipeline integration tests","Avoid custom executors that double-drive AccumulatingTransform::process/finish"],"tags":["internal","aggregation","spill","unreachable"],"backgroundTag":"internal-invariant-violation","analyzedSha":"288d84d76e20a2f8f7173bda9691eb6ece301aa9","analyzedAt":"2026-09-11T11:29:36.208Z","contentChangedAt":"2026-09-11T11:29:36.208Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}