Skip to content

frequent_items_sketch: fix is_empty() after purge clears all counters - #527

Open
FluorineDog wants to merge 1 commit into
apache:masterfrom
FluorineDog:fix-frequent-items-empty-after-purge
Open

FluorineDog wants to merge 1 commit into
apache:masterfrom
FluorineDog:fix-frequent-items-empty-after-purge

Conversation

@FluorineDog

Copy link
Copy Markdown
Contributor

Problem (AI generated)

frequent_items_sketch::is_empty() is defined as map.get_num_active() == 0, conflating "no retained items" with "no observations".

A purge subtracts the median of all counters and keeps only positive ones. When every counter equals the median — typical for streams with many low-frequency items — the purge wipes all counters, leaving num_active == 0 while offset and total_weight remain non-zero. Such a state is then misreported as empty, which corrupts two operations:

  • serialize() emits the empty short format, dropping total_weight and offset on the wire. After a round-trip the sketch reads total_weight == 0 and get_maximum_error() == 0 — falsely looking like a fresh, exact sketch.
  • merge() early-returns on other.is_empty(), silently discarding the other sketch's total_weight and offset.

Minimal reproducer

See unittest in this PR. After serde, data is lost.

Fix

Use the following to implement is_empty instead.

bool is_empty() const { return total_weight == 0; }

A purge subtracts the median from all counters and keeps only positive
ones, so when all counters equal the median (typical for many low-frequency
items), every counter is wiped and num_active drops to 0 while offset and
total_weight remain non-zero. is_empty() was defined as num_active == 0,
conflating "no retained items" with "no observations", which caused:

- serialize() to emit the empty short format, dropping total_weight and
  offset on the wire;
- merge() to early-return, silently discarding the other sketch's
  total_weight and offset.

Reproducer: 193 distinct items at lg_k=8 (capacity 192). The 193rd update
triggers a purge with median 1 that clears all counters; the state then
reports is_empty()==true, round-trips to total_weight=0/max_error=0
(falsely looking exact), and contributes nothing when merged.

Fix: define emptiness as "no observations" (total_weight == 0). The
non-empty wire format already supports num_items == 0, so purged states
now round-trip with weight and error intact, and merge absorbs them.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant