{"record":{"id":"a87e7adfa3c49057","repo":"GitoxideLabs/gitoxide","slug":"rebase-worker-stopped-unexpectedly","errorCode":null,"errorMessage":"rebase worker stopped unexpectedly","messagePattern":"rebase worker stopped unexpectedly","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gix-tix/src/lib.rs","lineNumber":5840,"sourceCode":"        let started = Instant::now();\n        let mut last_draw = started;\n        let mut latest = None;\n        let mut rendered = None;\n        let result = loop {\n            let now = Instant::now();\n            let timeout = if now.duration_since(started) < TODO_PROGRESS_DELAY {\n                Some(TODO_PROGRESS_DELAY.saturating_sub(now.duration_since(started)))\n            } else if latest != rendered {\n                Some(FRAME_INTERVAL.saturating_sub(now.duration_since(last_draw)))\n            } else {\n                None\n            };\n            let event = match timeout {\n                Some(timeout) => match receiver.recv_timeout(timeout) {\n                    Ok(event) => Some(event),\n                    Err(mpsc::RecvTimeoutError::Timeout) => None,\n                    Err(mpsc::RecvTimeoutError::Disconnected) => {\n                        break Err(anyhow::anyhow!(\"rebase worker stopped unexpectedly\"));\n                    }\n                },\n                None => match receiver.recv() {\n                    Ok(event) => Some(event),\n                    Err(_) => break Err(anyhow::anyhow!(\"rebase worker stopped unexpectedly\")),\n                },\n            };\n            match event {\n                Some(RebaseWorkerEvent::Progress(progress)) => latest = Some(progress),\n                Some(RebaseWorkerEvent::Complete(result)) => break result,\n                None => {}\n            }\n            let now = Instant::now();\n            if todo_progress_visible(now.duration_since(started))\n                && latest != rendered\n                && now.duration_since(last_draw) >= FRAME_INTERVAL\n            {\n                let progress = latest.expect(\"a changed progress snapshot is available\");","sourceCodeStart":5822,"sourceCodeEnd":5858,"githubUrl":"https://github.com/GitoxideLabs/gitoxide/blob/e73179060badf27222d790981fac3f84c1830a7e/gix-tix/src/lib.rs#L5822-L5858","documentation":"During interactive rebase rendering, the driver thread waits on a bounded `recv_timeout` for events from the rebase worker thread. A `mpsc::RecvTimeoutError::Disconnected` means every sender handle to the worker's event channel has been dropped, i.e. the worker thread exited without reporting `Complete`. The library raises this because a silent worker shutdown would otherwise look like a missing timeout event.","triggerScenarios":"Calling the interactive rebase-with-TUI flow (the function driving `RebaseWorkerEvent`s at gix-tix/src/lib.rs:5840) when the worker thread returns early or its spawning/pump loop drops the `mpsc::Sender` before sending `Complete`.","commonSituations":"The worker hit an early `return`/`break` on an unrecoverable rebase error and dropped its sender; the user-initiated abort path closed the channel without sending a completion event; a bug in the worker loop exited without `result_tx.send(...)`.","solutions":["Inspect the worker closure for early `return`/`break`/`?` paths that exit without sending `RebaseWorkerEvent::Complete`; make every exit path send a terminal event or propagate the error before dropping the sender.","Keep a clone of the `mpsc::Sender` alive for the worker's full lifetime (e.g. in a guard that sends `Complete`/error on drop).","Check whether the rebase script/todos are malformed, causing the worker to bail out before producing events.","Reproduce with logging inside the worker to see why the thread exits early."],"exampleFix":"// before\nmatch receiver.recv_timeout(timeout) {\n    Ok(event) => Some(event),\n    Err(mpsc::RecvTimeoutError::Timeout) => None,\n    Err(mpsc::RecvTimeoutError::Disconnected) => {\n        break Err(anyhow::anyhow!(\"rebase worker stopped unexpectedly\"))\n    }\n}\n// after\n// In the worker: ensure the terminal event is always sent,\n// even on error paths:\nlet result = run_rebase_steps(&mut progress_tx);\nlet _ = result_tx.send(RebaseWorkerEvent::Complete(result));","handlingStrategy":"try-catch","validationCode":"// Before driving the worker, verify the channel contract:\n// keep a Sender clone alive and assert the worker emits a terminal event\nlet keepalive = result_tx.clone(); // drop only after worker.join()\nlet _ = keepalive;","typeGuard":"fn worker_terminated(result: &Result<(), anyhow::Error>) -> bool {\n    result.is_ok() || !result.as_ref().unwrap_err().to_string().contains(\"stopped unexpectedly\")\n}","tryCatchPattern":"match run_rebase_with_tui(...) {\n    Err(e) if e.to_string().contains(\"rebase worker stopped unexpectedly\") => {\n        // worker exited early without Complete; inspect worker logs / retry\n    }\n    Err(e) => return Err(e),\n    Ok(v) => return Ok(v),\n}","preventionTips":["Always send a terminal `Complete` event on every worker exit path, including errors","Use a drop guard in the worker so a Sender is never silently dropped","Test the worker with malformed rebase todos to exercise early-exit paths","Keep a Sender clone alive until after `join()`"],"tags":["rebase","channel-disconnected","worker-thread","concurrency"],"backgroundTag":"channel-closed","analyzedSha":"e73179060badf27222d790981fac3f84c1830a7e","analyzedAt":"2026-09-08T11:26:50.865Z","contentChangedAt":"2026-09-08T11:26:50.865Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}