Optimize spans buffer insertion with eviction during insert

getsentry/sentry#92393
Golden recall: 100% Extra findings: 9

Golden Comments (3/3 found)

# Comment Severity Status FriendlyReviewer Detail
#1 OptimizedCursorPaginator negative-offset branch slices QuerySet with a negative start index HIGH ✓ Found "Negative-offset 'advanced feature' uses Django negative slicing, which the ORM does not support (ValueError). The branch is effectively dead since cursors never produce negative offsets; when triggered via a hand-crafted cursor it 500s" (paginator.py L877)
#2 BasePaginator negative-offset branch slices QuerySet with a negative start index HIGH ✓ Found Dataset duplicate: the two classes share the same buggy branch; a single FriendlyReviewer candidate (paginator.py L877) covers both goldens #1 and #2 — counted found by generosity of the judge
#3 OptimizedCursorPaginator.get_item_key uses floor/ceil on a datetime key (order_by='-datetime'), causing TypeError HIGH ✓ Found "OptimizedCursorPaginator crashes on the audit-log '-datetime' order key: it is a copy of the datetime-agnostic Paginator, so get_item_key uses math.floor/ceil on datetime objects (TypeError) and value_from_cursor returns a numeric cursor compared against the datetime column (type mismatch)" (organization_auditlogs.py L79)

Supplementary Findings (9 findings)

Finding File Severity Legitimate?
Eviction via zpopmin is score-ordered and ignores the span tree — parent/root spans can be evicted while children remain, producing corrupt segments spans/buffer.py:197 HIGH ✓ The true subject of the PR; genuine correctness bug — real but unscorable (no golden covers the spans-buffer area)
organization_context.member is None-able and dereferenced unconditionally on every audit-log request organization_auditlogs.py:71 HIGH AttributeError/500 for non-superusers without a member
Shared BasePaginator.get_result modified unconditionally (128 dependents, asymmetric clamping for forward vs prev cursors) paginator.py:182 MEDIUM ✓ Mostly a no-op in practice but an untested behavior-changing edit on shared code
Comment-only change claims "Allow negative offsets for advanced pagination" without any functional change cursors.py:26 MEDIUM ✓ Borderline: misleading comments only, no functional impact
~90 lines of get_result duplicated from BasePaginator; no tests for the new paginator or the optimized_pagination branch paginator.py:845 MEDIUM ✓ The crash went uncaught precisely because of the missing coverage
max_segment_spans is now dead config (set but never read), inconsistent with the hardcoded 1000 cap in the Lua script spans/buffer.py:447 MEDIUM ✓ Dead config plus divergence with the hardcoded cap
end_timestamp_precise read with a hard subscript while sibling fields use .get() — KeyError or None-score crash risk spans/consumers/process/factory.py:141 MEDIUM ✓ Missing/None value crashes the batch (KeyError or zadd TypeError/DataError)
No test covers the eviction mechanism: no >1000-span insert, no root-not-evicted/orphan assertion, no TTL/leak check, no tie-break coverage tests/sentry/spans/test_buffer.py:120 MEDIUM ✓ Given the corruption risk of the eviction path, this is a real coverage gap
Migration from Redis set to sorted set unhandled — pre-existing set-typed keys hit WRONGTYPE with zadd/zscan/zpopmin spans/buffer.py:197 MEDIUM ✓ Can crash the flusher into a restart loop during rollout

Honest Analysis

Recall is 100% (3/3) but the score is generous: goldens #1 and #2 are the same bug described twice in the dataset — negative slicing on OptimizedCursorPaginator and on BasePaginator, two classes sharing the same buggy branch. A single FriendlyReviewer candidate (paginator.py L877) covers both, so the per-golden recall overstates the unique-bug recall (2/2).

More importantly, the dataset entry is misaligned: titled "Optimize spans buffer insertion with eviction during insert", its 3 goldens all concern the pagination zone — the fork branch is composite (pagination + spans buffer). The actual subject of the PR therefore has zero goldens, and FriendlyReviewer's most valuable findings on this PR are unscored: the eviction mechanism (L197) is score-ordered via zpopmin and ignores the span tree, so it can evict parent/root spans while children remain and silently produce corrupt segments; the set → sorted-set migration is unhandled (WRONGTYPE risk, flusher restart loop); and max_segment_spans is dead config inconsistent with the hardcoded 1000 cap. These are exactly the findings a human reviewer of this PR would care about most, and the Martian Code dataset cannot credit them.

Honesty about the caveats: this review is marked partial by FriendlyReviewer itself — the pagination area was produced from a prose pass and is less verified than the span-buffer area. The datetime golden (#3) was nonetheless found within that less-verified zone (organization_auditlogs.py L79), and the negative-slicing pair was found as well. The main weakness is a cluster of overlapping findings around the paginator (L182, L845, cursors.py L26) that re-analyzes the same code region from several angles — legitimate but redundant.

The extra findings are all defensible. The None-dereference on organization_context.member (L71) is a real crash outside the reference set; the hard-subscript read of end_timestamp_precise (factory.py L141) and the missing eviction tests (test_buffer.py L120) are concrete and localizable. The comment-only finding on cursors.py L26 is the most minor — a documentation-accuracy nit — and is the acceptable noise in an otherwise high-value review.

FriendlyReviewer's full review

As posted on the PR — the complete output from FriendlyReviewer.

friendly-reviewer bot commented now

Hi there 👋

🌥️ Tech 🌤️ Feat
4 high, 7 medium 1 mitigated

This MR covers two distinct areas. The span-buffer changes migrate segment storage from Redis sets to sorted sets and introduce eviction-during-insert via a Lua zpopmin when the segment exceeds 1000 spans. The plumbing is internally consistent, but the eviction mechanism is score-ordered and ignores the span tree, so it can evict parent/root spans and silently produce corrupt segments; there is also no test coverage for the new eviction path, existing set-typed Redis keys are not migrated (WRONGTYPE risk), and end_timestamp_precise is read non-defensively. The pagination changes add an OptimizedCursorPaginator that crashes on the audit-log endpoint's -datetime order key (numeric-vs-datetime mismatches), a broken negative-offset branch that relies on unsupported Django negative slicing, and a None-dereference on organization_context.member. Note that the review is partial: the pagination area was produced from a prose pass and may be less verified than the span-buffer area.


These might need a close look

  • 🔴 src/sentry/api/endpoints/organization_auditlogs.py (L79)
    OptimizedCursorPaginator crashes on the audit-log '-datetime' order key: it is a copy of the datetime-agnostic Paginator, so get_item_key uses math.floor/ceil on datetime objects (TypeError) and value_from_cursor returns a numeric cursor compared against the datetime column (type mismatch). The endpoint previously used DateTimePaginator for this reason. Make the paginator datetime-aware or don't wire it to this endpoint.
  • 🔴 src/sentry/api/paginator.py (L877)
    Negative-offset 'advanced feature' uses Django negative slicing, which the ORM does not support (ValueError). The branch is effectively dead since cursors never produce negative offsets; when triggered via a hand-crafted cursor it 500s. Remove the branch and enable_advanced_features flag or implement correctly.
  • 🔴 src/sentry/api/endpoints/organization_auditlogs.py (L71)
    organization_context.member is None-able and dereferenced unconditionally on every audit-log request, causing AttributeError/500 for non-superusers without a member. Guard with 'is not None'.
  • 🔴 src/sentry/spans/buffer.py (L197)
    Eviction via zpopmin removes lowest-score spans by end_timestamp_precise, ignoring tree structure, so parent/root spans can be evicted while children remain, producing broken segments (missing parent_span_id or no root). Evicted spans are silently dropped without metric. Make eviction tree-aware and add an incr metric.

Worth checking

  • 🟡 src/sentry/api/paginator.py (L182)
    Unconditional modification of shared BasePaginator.get_result affects 128 dependents with asymmetric clamping for forward vs prev cursors; offsets are never negative under normal construction, so this is mostly a no-op but an untested behavior-changing edit. Move it to the new subclass or add explicit tests.
  • 🟡 src/sentry/utils/cursors.py (L26)
    Comment-only change claims 'Allow negative offsets for advanced pagination' but no functional change was made; the advertised feature is not implemented and the comments are misleading.
  • 🟡 src/sentry/api/paginator.py (L845)
    ~90 lines of get_result duplicated from BasePaginator and get_item_key/value_from_cursor duplicate Paginator; no tests added for the new paginator or the optimized_pagination branch, so the crash is uncaught. Prefer composing DateTimePaginator over a parallel class.
  • 🟡 src/sentry/spans/buffer.py (L447)
    max_segment_spans is now dead config (set but never read) and inconsistent with the hardcoded 1000 cap in the Lua script; either thread the config as ARGV or remove the parameter and update docstring.
  • 🟡 src/sentry/spans/consumers/process/factory.py (L141)
    end_timestamp_precise is read with a hard subscript while sibling fields use .get(); missing/None value causes KeyError or a None score to zadd (TypeError/DataError), crashing the batch. Use .get and define None-score handling.
  • 🟡 tests/sentry/spans/test_buffer.py (L120)
    No test covers the new eviction mechanism: no >1000-span insert, no assertion that root is not evicted/orphans, no TTL/leak check post-eviction, no tie-break coverage. Given the corruption risk, this must be tested.
  • 🟡 src/sentry/spans/buffer.py (L197)
    Migration from set to sorted set is unhandled: pre-existing set-typed keys hit WRONGTYPE with zadd/zscan/zpopmin and can crash the flusher (restart loop). Drain/convert keys during rollout or add tolerant handling (detect WRONGTYPE, unlink, continue).

✅ Feature-level checklist

Partially covered

  • ⚠️ Optimize spans buffer insertion with eviction during insert
    Eviction-during-insert is implemented via zpopmin in the Lua script and wired into the new zadd-based writes, but the implementation is not tree-aware and can evict parent/root spans causing corrupt segments, lacks test coverage, and does not handle migration of existing set-typed keys. The optimization is present but not safe/complete.