{"record":{"id":"354b149db6c42c71","repo":"pydantic/monty","slug":"gather-id-doesn-t-point-to-a-gatherfuture","errorCode":null,"errorMessage":"gather_id doesn't point to a GatherFuture","messagePattern":"gather_id doesn't point to a GatherFuture","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/monty/src/bytecode/vm/scheduler.rs","lineNumber":513,"sourceCode":"            None => None,\n            Some(Awaiter::Task(task_id)) => {\n                // A task's own awaiter is the `GatherSlot` its gather gave it;\n                // borrow that gather's id without taking the task's ref, which\n                // stays until the task is cancelled.\n                let gather_id = match self.tasks.get(&task_id).and_then(|t| t.awaiter.as_ref()) {\n                    Some(Awaiter::GatherSlot { gather, .. }) => Some(*gather),\n                    Some(Awaiter::Task(_)) | None => None,\n                };\n                match gather_id {\n                    // The task that was awaiting the future is itself in a\n                    // gather. Settle that gather here so the failure anchors\n                    // at the same site as the resolution; return the gather's\n                    // awaiter for the caller to chain. A gather that has\n                    // already settled takes nothing more, and the failure is\n                    // the parked task's own to raise.\n                    Some(gather_id) => {\n                        let HeapReadOutput::GatherFuture(mut gather_rd) = heap.read(gather_id) else {\n                            panic!(\"gather_id doesn't point to a GatherFuture\")\n                        };\n                        let outer_awaiter = gather_rd.fail(heap, error);\n                        // Dropped before the cancel below: that releases the\n                        // task's `GatherSlot` inc_ref on this gather, which\n                        // must not run under a live reader on it.\n                        drop(gather_rd);\n                        match outer_awaiter {\n                            // The gather handed the failure outwards, so\n                            // nothing will ever deliver it to `task_id`. Its\n                            // `await` raised, so it is finished — drop it, or\n                            // it stays `Blocked` forever on a future that was\n                            // just failed and unregistered, holding its\n                            // coroutine and this gather alive for the rest of\n                            // the session. Siblings are untouched: the task is\n                            // blocked on an external future, so the cancel\n                            // walk finds no gather to cascade into.\n                            Some(outer) => {\n                                self.cancel_task(task_id, heap);","sourceCodeStart":495,"sourceCodeEnd":531,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/bytecode/vm/scheduler.rs#L495-L531","documentation":"When a task awaiting a gather future fails, the scheduler looks up the gather future on the heap; this panic fires if the stored HeapId is not a `GatherFuture`. Gather ids come from the scheduler's own allocation, so this means heap corruption, premature free of the gather, or a scheduler bug — not a Python-level error.","triggerScenarios":"`fail_for_call` unwinding an awaiter chain that reaches an `Awaiter::Task` whose linked gather HeapId resolves to a non-GatherFuture heap entry; via heap corruption or a gather-lifecycle bug.","commonSituations":"Developing/fuzzing asyncio-style gather support; patches that free a GatherFuture while tasks still hold `GatherSlot` references to it.","solutions":["File a bug with the async reproducer","Audit GatherFuture free/dec_ref ordering (the code deliberately drops the reader before releasing the GatherSlot inc_ref — preserve that ordering in edits)","Run async/gather tests with memory-model-checks enabled"],"exampleFix":"// not applicable — internal scheduler invariant","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Internal panic — no user-side catch; isolate and report.\nmatch scheduler.fail_for_call(call_id, &err, heap) {\n    Some(awaiter) => chain(awaiter),\n    None => (),\n}","preventionTips":["Keep GatherSlot inc_ref/dec_ref bookkeeping balanced","Drop heap readers before releasing gather inc_refs (as the surrounding code does)","Run gather tests with memory-model-checks after lifecycle edits"],"tags":["panic","internal","async","gather","scheduler"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}