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
- Check the buffer is non-empty before dropping: DictionaryBuffer exposes len(), so guard with len() > 0.
- Audit the caller: count pushes per column and only drop that many — make undo symmetric with the successful writes of the failed row.
- 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.
- 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
- Make undo symmetric with push: track successful pushes per column and drop exactly that many.
- Prefer snapshot/restore of buffer lengths over repeated pops when rollback order can vary.
- Keep property tests covering interleaved push_str/drop_last_value sequences in CI.
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
- num_columns_in_parallel should be above zero
- sharder mapped input to non-existant bucket
- mapped to out-of-bounds shard
- If we can remove a value from the interned strings, we must
- must have offset
AI-assisted analysis of influxdata/influxdb@d28e26e048 (2026-08-16).
Data as JSON: /api/errors/a10050c0e85bd038.
Report an issue: GitHub.