influxdata/influxdb · error

If you call `drop_last_value`, the tag buffer must contain a

Error message

If you call `drop_last_value`, the tag buffer must contain at least one value, but it contained zero

What it means

DictionaryBuffer::drop_last_value() removes the most recently pushed dictionary-encoded value; it is the rollback primitive used when recovering from a partial line-protocol write. The .expect fires when self.encoded (the tag->dictionary-ID buffer) is empty, i.e. more values were dropped than were ever pushed. It is pub(crate), so it can only be triggered by code inside the table_batch builder, not by external callers.

Source

Thrown at core/table_batch/src/builder/column_writer/dictionary.rs:74

    /// Remove the last wrote value.
    ///
    /// # Reference Leak
    ///
    /// This call removes the encoded value from the buffer.
    ///
    /// If this call removes the sole reference to a value in this buffer, the value will not appear
    /// in the decoded result, and is removed from the buffer containing all previously-pushed
    /// strings. This allows us to avoid storing and transmitting unnecessary data, at the cost of
    /// a slightly more expensive operation when we have to remove a value due to recovering from a
    /// partial write.
    ///
    /// # Panics
    ///
    /// Panics if no values remain in `self`.
    pub(crate) fn drop_last_value(&mut self) {
        let last_pushed = self.encoded.pop()
            .expect("If you call `drop_last_value`, the tag buffer must contain at least one value, but it contained zero");

        // And we should remove it from the packed strings as well if it failed, just to get more
        // predictable behavior and avoid sending some data if possible.
        if !self.encoded.contains(&last_pushed) {
            let removed = self.keys.pop()
                .expect("If we can remove a value from the interned strings, we must also be able to remove a value from the packed strings.");

            self.id_map.remove(&removed);
        }
    }

    pub(crate) fn finish(self) -> Option<InternedStrings> {
        let dictionary = self.keys.finish()?;

        // Invariant: the key -> ID map and the buffer containing the encoded
        // keys must always agree on the number of unique keys observed.
        debug_assert_eq!(
            self.id_map.len(),

View on GitHub (pinned to d28e26e048)

Solutions

  1. Check the buffer is non-empty before dropping: DictionaryBuffer exposes len(), so guard with len() > 0.
  2. Audit the caller: count pushes per column and only drop that many — make undo symmetric with the successful writes of the failed row.
  3. Reproduce with the failing line-protocol payload in a unit test (the module has proptest tests for exactly this push/drop behavior) and fix the off-by-one.
  4. If the rollback order across columns can vary, switch the caller to snapshot/restore of buffer lengths instead of repeated pops.

Example fix

// before
col.drop_last_value(); // panics: 'the tag buffer must contain at least one value, but it contained zero'

// after
if col.len() > 0 {
    col.drop_last_value();
}
Defensive patterns

Strategy: type-guard

Validate before calling

// drop_last_value is pub(crate): guard inside the builder rollback code.
if column.dictionary.len() > 0 {
    column.dictionary.drop_last_value();
} else {
    debug!("skip drop_last_value: buffer already empty");
}

Type guard

// Language-appropriate guard: use the existing len() accessor.
impl DictionaryBuffer {
    pub(crate) fn can_drop_last(&self) -> bool {
        self.len() > 0
    }
}

Prevention

When it happens

Trigger: Builder rollback logic calling drop_last_value() on a column that had zero successful pushes, or calling the rollback twice for the same row (double-undo), or undoing a row after the buffer was already drained by a previous undo. Any code path in core/table_batch that recovers from a partial write and unconditionally drops N values without tracking how many were actually written.

Common situations: Refactoring the partial-write recovery logic so the number of drop_last_value calls no longer matches the number of push_str calls; a malformed line-protocol payload where some columns of a row parsed and others errored, combined with an off-by-one in the undo loop; test code exercising rollback on an empty buffer.

Related errors


AI-assisted analysis of influxdata/influxdb@d28e26e048 (2026-08-16). Data as JSON: /api/errors/a10050c0e85bd038. Report an issue: GitHub.