{"record":{"id":"ac2c6d3cf7baa338","repo":"vectordotdev/vector","slug":"every-containerloginfo-has-it-s-containerstate","errorCode":null,"errorMessage":"Every ContainerLogInfo has it's ContainerState","messagePattern":"Every ContainerLogInfo has it's ContainerState","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/sources/docker_logs/mod.rs","lineNumber":597,"sourceCode":"                }\n\n                let id = ContainerId::new(id);\n                self.containers.insert(id.clone(), self.esb.start(id, None));\n            });\n\n        Ok(self)\n    }\n\n    async fn run(mut self) {\n        loop {\n            tokio::select! {\n                value = self.main_recv.recv() => {\n                    match value {\n                        Some(Ok(info)) => {\n                            let state = self\n                                .containers\n                                .get_mut(&info.id)\n                                .expect(\"Every ContainerLogInfo has it's ContainerState\");\n                            if state.return_info(info) {\n                                self.esb.restart(state);\n                            }\n                        },\n                        Some(Err((id,persistence))) => {\n                            let state = self\n                                .containers\n                                .remove(&id)\n                                .expect(\"Every started ContainerId has it's ContainerState\");\n                            match persistence{\n                                ErrorPersistence::Transient => if state.is_running() {\n                                    let backoff= Some(self.backoff_duration);\n                                    self.containers.insert(id.clone(), self.esb.start(id, backoff));\n                                }\n                                // Forget the container since the error is permanent.\n                                ErrorPersistence::Permanent => (),\n                            }\n                        }","sourceCodeStart":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/vectordotdev/vector/blob/3708c39b12a93212ed8b8d7510b4cc7769cb5864/src/sources/docker_logs/mod.rs#L579-L615","documentation":"docker_logs runs an internal actor that owns containers: HashMap<ContainerId, ContainerState>. When a container's log-stream future finishes it returns its ContainerLogInfo to the actor, which resolves the entry with containers.get_mut(&info.id).expect(\"Every ContainerLogInfo has it's ContainerState\"). The expect encodes the bookkeeping invariant that every started id has a live entry; it panics when an info message arrives for an id that is not (or no longer) in the map, i.e. the start/finish bookkeeping desynchronized inside Vector.","triggerScenarios":"A ContainerLogInfo delivered after the error path already removed the state for that id, or a future returning its info twice - both are internal ordering bugs between esb.start/restart, return_info, and the map mutations, not a Docker-side condition.","commonSituations":"Docker engine restarts and rapid container churn stressing the restart bookkeeping, or regressions after docker_logs refactors in specific Vector versions.","solutions":["Capture a repro (Vector version, docker events around the panic, debug logs for docker_logs) and file an upstream issue - this is an internal invariant break","Move to a Vector release where docker_logs is known stable for your churn pattern","Patch locally: replace the expect with a match that warns and skips the stale info","Run the source under a supervisor/restart policy until patched"],"exampleFix":"// before\nlet state = self\n    .containers\n    .get_mut(&info.id)\n    .expect(\"Every ContainerLogInfo has it's ContainerState\");\n\n// after\nlet Some(state) = self.containers.get_mut(&info.id) else {\n    warn!(message = \"ContainerLogInfo without ContainerState, skipping\", id = %info.id);\n    continue;\n};","handlingStrategy":"validation","validationCode":"// before accessing the map in the actor loop:\nmatch self.containers.get_mut(&info.id) {\n    Some(state) => { /* return_info / restart logic */ }\n    None => {\n        warn!(message = \"stale ContainerLogInfo\", id = %info.id);\n        continue;\n    }\n}","typeGuard":"fn state_of<'a>(\n    containers: &'a mut HashMap<ContainerId, ContainerState>,\n    id: &ContainerId,\n) -> Option<&'a mut ContainerState> {\n    containers.get_mut(id)\n}","tryCatchPattern":"// Rust panics in async tasks are not catchable per-message; guard instead:\nif let Some(state) = self.containers.get_mut(&info.id) {\n    if state.return_info(info) {\n        self.esb.restart(state);\n    }\n} else {\n    warn!(id = %info.id, \"container info without state\");\n}","preventionTips":["Model actor maps as fallible lookups; never expect() on message-driven state access","Log id + current map size when a lookup misses to make desyncs diagnosable","Add stress tests with rapid container start/stop churn to CI"],"tags":["rust","panic","invariant","docker-logs","hashmap","actor"],"backgroundTag":"invariant-assertion-failed","analyzedSha":"3708c39b12a93212ed8b8d7510b4cc7769cb5864","analyzedAt":"2026-08-20T07:02:18.786Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}