Data loss in agg plugin #20
Labels
No labels
agent
blocked
agent
new
agent
review
agent
working
complexity
high
complexity
low
priority
high
priority
low
priority
medium
risk
high
risk
low
risk
medium
type
bug
type
chore
type
feature
type
security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tfks/logbus#20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
When agg releases a bucket because of max-size or the max-real-seconds timer, it removes the bucket entirely. The next event for that key then hits the "no bucket yet" branch and must satisfy start() to open a new one. If it doesn't, the event is discarded — no error, no log line, no counter, exit code 0.
For any group that is cut mid-flight, this means the entire remainder of the group is thrown away rather than emitted in a second bucket.
The cut itself is triggered by defaults the operator never set (max-size: 1000, max-real-seconds: 300), and the timer variant is wall-clock driven, so the same input can produce different output depending on how long the process has been running.
Reproduction
Input — 10 events, one key, only the first carrying the start marker:
Config (max-size: 3 stands in for the default 1000 to keep the repro small; the other two triggers are pinned out of reach so the size trigger is the only variable):
Observed — 3 of 10 events survive; the process exits 0 with nothing on ERRORS:
emitted bucket: n=1,2,3
Expected — every event accounted for, e.g. 1,2,3 / 4,5,6 / 7,8,9 / 10.
Evidence
Three runs of the same 10 events, varying only how the bucket is cut:
The third row is the important one: the same plugin already implements the correct behaviour on a different trigger.
Root cause
handle_event has two exits that release a bucket, and they are not consistent.
max-size (and stop) remove the bucket outright — agg.rs:173:
max-event-seconds cuts the bucket but re-seeds the key, bypassing start() — agg.rs:196:
flush_all — driven by the max-real-seconds ticker and by shutdown — drains every key (agg.rs:235), so after one tick every open group is bucket-less.
The loss then happens here, agg.rs:203:
There is no distinction between "a group ended and a new one has not begun" — where dropping is the intended start/stop semantic — and "a group was cut by a size or time limit and is still in progress" — where dropping is data loss.
Second silent-drop path, same expression
call_bool(...).unwrap_or(false) means a JS error inside start() — a typo, a null deref on an unexpected event shape — also drops the event silently, and drops every subsequent event for that key. Unlike filter, key and view, which all emit to ERRORS on failure, start has no error path at all.
Suggested fix
Treat only stop and end-of-input as terminal. Size and time limits are cuts, and a cut group should continue:
A narrower alternative, if changing the semantics is unwelcome: log at warn whenever an event is dropped because start() returned false while a cut was outstanding — turning silent loss into something an operator can see. This is strictly weaker; the group is still lost.
Acceptance criteria
Notes
The shipped examples/seim/demo.yml uses start: () => true, which is immune — every event opens a bucket — and is likely why this has not surfaced. Configs using a real start predicate (a request/response pair, a multi-line trace, a session boundary) are exposed, and the exposure grows with runtime because the max-real-seconds timer fires every 5 minutes by default.
Found while building examples/agent-ops/demo.yml; that config omits start: entirely and is not affected.