vectordotdev/vector · error

node exists

Error message

node exists

What it means

`remove_inputs` calls `self.config.inputs_for_node(key).expect("node exists")` (src/topology/running.rs:1009). The running config is expected to contain every node being removed; if the node is absent from the current config's graph, the lookup returns None and panics. The new config lookup is tolerated via `unwrap_or_default`, but the old config lookup is not.

Solutions

  1. Ensure each component id in the config is unique and removed only once per diff.
  2. Check that shutdown_diff's removal list is derived from the same config snapshot stored in self.config.
  3. Use `unwrap_or_default()` symmetry: treat a missing old node as no-op rather than panicking if this state can legitimately occur.
Defensive patterns

Strategy: validation

Validate before calling

// ensure unique component ids before building the config
let ids: HashSet<_> = config.components().map(|c| c.id()).collect();
assert_eq!(ids.len(), config.component_count(), "duplicate component keys");

Prevention

When it happens

Trigger: `shutdown_diff` removing a component key that is missing from `self.config` — e.g. a key removed twice in one diff, or a key whose config entry was already mutated away by an earlier reload step.

Common situations: A config where a component was deleted but its key still appears in the removal set (duplicate removal), or hand-edited/duplicated component ids causing inconsistent diff bookkeeping.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of vectordotdev/vector@bdb87aeaa4 (2026-09-16). Data as JSON: /api/errors/916dde4fdae36b7a. Report an issue: GitHub.

Appendix: source

Thrown at src/topology/running.rs:1009

            }
        }

        self.inputs.insert(key.clone(), tx);
        new_pieces
            .detach_triggers
            .remove(key)
            .map(|trigger| self.detach_triggers.insert(key.clone(), trigger.into()));
    }

    fn remove_outputs(&mut self, key: &ComponentKey) {
        self.outputs.retain(|id, _output| &id.component != key);
    }

    fn remove_inputs(&mut self, key: &ComponentKey, diff: &ConfigDiff, new_config: &Config) {
        self.inputs.remove(key);
        self.detach_triggers.remove(key);

        let old_inputs = self.config.inputs_for_node(key).expect("node exists");
        let new_inputs = new_config
            .inputs_for_node(key)
            .unwrap_or_default()
            .iter()
            .collect::<HashSet<_>>();

        for input in old_inputs {
            if let Some(output) = self.outputs.get_mut(input) {
                if diff.contains(&input.component)
                    || diff.is_removed(key)
                    || !new_inputs.contains(input)
                {
                    // 3 cases to remove the input:
                    //
                    // Case 1: If the input we're removing ourselves from is changing, that means its
                    // outputs will be recreated, so instead of pausing the sink, we just delete it
                    // outright to ensure things are clean.
                    //

View on GitHub (pinned to bdb87aeaa4)