{"record":{"id":"e0ee6cbc2081edaf","repo":"vectordotdev/vector","slug":"replacing-unknown-sink-from-fanout-id","errorCode":null,"errorMessage":"Replacing unknown sink from fanout: {id}","messagePattern":"Replacing unknown sink from fanout: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"lib/vector-core/src/fanout.rs","lineNumber":98,"sourceCode":"    fn remove(&mut self, id: &ComponentKey) {\n        assert!(\n            self.senders.shift_remove(id).is_some(),\n            \"Removing nonexistent sink from fanout: {id}\"\n        );\n    }\n\n    fn replace(&mut self, id: &ComponentKey, sink: BufferSender<EventArray>) {\n        match self.senders.get_mut(id) {\n            Some(sender) => {\n                // While a sink must be _known_ to be replaced, it must also be empty (previously\n                // paused or consumed when the `SendGroup` was created), otherwise an invalid\n                // sequence of control operations has been applied.\n                assert!(\n                    sender.replace(Sender::new(sink)).is_none(),\n                    \"Replacing existing sink is not valid: {id}\"\n                );\n            }\n            None => panic!(\"Replacing unknown sink from fanout: {id}\"),\n        }\n    }\n\n    fn pause(&mut self, id: &ComponentKey) {\n        match self.senders.get_mut(id) {\n            Some(sender) => {\n                // A sink must be known and present to be replaced, otherwise an invalid sequence of\n                // control operations has been applied.\n                assert!(\n                    sender.take().is_some(),\n                    \"Pausing nonexistent sink is not valid: {id}\"\n                );\n            }\n            None => panic!(\"Pausing unknown sink from fanout: {id}\"),\n        }\n    }\n\n    /// Waits for the next control message and applies it.","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/vectordotdev/vector/blob/3708c39b12a93212ed8b8d7510b4cc7769cb5864/lib/vector-core/src/fanout.rs#L80-L116","documentation":"Fanout is Vector's output bus: it fans one stream out to many sinks and is reconfigured at runtime through ControlMessages (Add/Remove/Pause/Replace) sent over its control channel during topology changes. replace() handles ControlMessage::Replace by looking up the sink's ComponentKey; if that key was never added (or was already removed), the lookup returns None and the fanout panics because the control-operation sequence is invalid.","triggerScenarios":"A ControlMessage::Replace(id, sender) arriving for a ComponentKey that is not in the fanout's senders map: the id was never Add-ed, or a Remove(id) was processed before the Replace. This happens during topology rebuild (config reload / healthcheck-driven replacement) when the orchestrator and the fanout disagree on which sinks exist.","commonSituations":"Hitting 'vector reload' (SIGHUP or config file watch) with configs that add/remove sinks; historically this class of panic has been an internal Vector topology-rebuild bug fixed across releases; embedding code that drives the fanout ControlChannel by hand and sends Replace without a prior Add+Pause.","solutions":["Upgrade Vector to the latest patch release and check the changelog for topology/reload fixes, since this panic indicates an internal control-sequence bug","Reproduce with a minimal config + reload sequence and capture RUST_LOG=trace logs of the ControlMessage order, then report it at github.com/vectordotdev/vector/issues","As a stopgap, restart Vector with the new config instead of live-reloading","If embedding vector-lib: only send Replace for an id previously Add-ed and Pause-d, and never after Remove"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Embedders driving the fanout control channel: only send Replace for an id\n// that was previously Add-ed and Pause-d, and never after Remove.\ncontrol_tx.send(ControlMessage::Add(id.clone(), tx));     // 1. register\n// ... later, during rebuild:\ncontrol_tx.send(ControlMessage::Pause(id.clone()));       // 2. pause\ncontrol_tx.send(ControlMessage::Replace(id.clone(), new_tx)); // 3. replace","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat the fanout control protocol as a per-sink state machine: Add -> (Pause -> Replace)* -> Remove","Keep a single owner for the ControlChannel to avoid interleaved/duplicate messages","Pin recent Vector releases; reload-orchestration races are actively fixed upstream","Capture RUST_LOG=debug logs during reloads in staging to verify message ordering before shipping config automation"],"tags":["rust","vector","fanout","topology","reload","panic"],"backgroundTag":"invalid-state-transition","analyzedSha":"3708c39b12a93212ed8b8d7510b4cc7769cb5864","analyzedAt":"2026-08-20T07:02:18.786Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T20:17:18.057Z"}