feat(uptime): Add ability to use queues to manage parallelism

getsentry/sentry#95633
Golden recall: 66.7% Extra findings: 9

Golden Comments (2/3 found)

# Comment Severity Status FriendlyReviewer Detail
#1 queue.shutdown(immediate=False) may not exist in standard queue.Queue HIGH ✓ Found FriendlyReviewer found more: not only does the call not wake blocked get() calls (guaranteed deadlock), but self.shutdown is a plain bool without memory barrier. Proposes threading.Event() + get(timeout=1.0).
#2 Magic number 50 for max_wait repeated 3× in tests LOW × Missed FriendlyReviewer did not flag the magic constant. However, it found a more critical test bug: send_result() hardcodes partition=1, offset=1 for all messages → invalidates all ordering/dedup/commit assertions.
#3 Docstring test_thread_queue_parallel_error_handling does not match implementation LOW ✓ Found FriendlyReviewer identifies precisely: "expects no commit even though complete_offset() is called in finally".

Supplementary Findings (15 findings)

Finding File Severity Legitimate?
Shutdown deadlock: get() without timeout + shutdown(immediate=False) → workers blocked, bool flag without memory barrier queue_consumer.py:85 HIGH ✓ Sibling of golden #1, more complete
Backpressure violation: submit() catches everything and never raises MessageRejected, queues unbounded (maxsize=0) queue_consumer.py:275 HIGH ✓ Unlimited memory + ignores downstream signals
assert isinstance(..., BrokerValue) stripped in -O mode → silent AttributeError queue_consumer.py:268 HIGH ✓ Replace with if not isinstance(...)
send_result() hardcodes partition=1, offset=1 for all messages → invalidates ordering tests test_results_consumer.py:129 HIGH ✓ All offset commit and ordering tests are invalid
Test error_handling contradictory: expects "no commit" but complete_offset() called in finally test_results_consumer.py:1900 MEDIUM ✓ Test cannot pass as described
terminate() does not join() the commit thread → lost offsets queue_consumer.py:310 MEDIUM ✓ Partially committed
_get_partition_lock: .get() then .setdefault() = TOCTOU, Lock objects wasted queue_consumer.py:61 MEDIUM ✓ Replace with defaultdict(threading.Lock)
Unit tests bypass the +1 offset conversion used in production test_queue_consumer.py:231 MEDIUM ✓ Don't test the real commit contract
Partition mismatch: messages partition 1 but consumer config partition 0 test_results_consumer.py:1686 MEDIUM ✓ Misleading for Kafka representation
CLI help --mode does not mention thread-queue-parallel consumers/__init__.py:124 LOW ✓ Documentation
num_queues=max_workers or 200 treated as falsy → 20 silently result_consumer.py:131 LOW ✓ Use is not None
else syntactically attached to last ifelif clearer result_consumer.py:206 LOW ✓ Clarity
Graceful shutdown test only checks queue_pool is None — incomplete test_results_consumer.py:2035 LOW ✓ Verify threads and commits
Kafka test uses private API _run_once() / _shutdown() test_results_consumer.py:2109 LOW ✓ Prefer public methods
Kafka test can leak consumer on assertion failure test_results_consumer.py:2137 LOW ✓ Missing try/finally

Honest Analysis

Golden #1 is a genuine win: the golden bug was about a missing queue.shutdown() method, but FriendlyReviewer correctly identified the deeper issue — the shutdown path doesn't wake blocked get() calls, so workers hang regardless. That's real value. Golden #3 (docstring mismatch) was also found and the severity adjustment from Low to Medium is fair — the test literally can't pass.

Golden #2 (magic number 50 repeated in tests) was missed entirely. FriendlyReviewer did catch something related — send_result() hardcoding partition=1, offset=1 — but that's a different bug, not the same finding. Treating it as "compensation" would be generous; it's a separate discovery in the same file.

The supplementary findings are a mixed bag. The deadlock diagnosis, backpressure analysis, and stripped-assert problem are solid — those are real concurrency bugs that deserve attention. The terminate() not joining the commit thread is also legitimate. But several findings are weak: the else/elif clarity nitpick, the 0 or 20 falsy trick, and the Kafka test private API usage are all near-noise. Out of 15 supplementary findings, about 6 are genuinely useful, 5 are acceptable but minor, and 4 are noise. That's a lower signal ratio than PR #7 (which had near-zero noise) and closer to PR #3's pattern.

The assert isinstance(...) finding (stripped in -O) deserves a caveat: in practice, most Python production deployments don't use -O, and even if they do, the resulting AttributeError would be caught quickly in tests. It's a real concern for hardened deployments but not a critical bug for most teams.

Overall: 2/3 golden recall is decent but not exceptional. The depth on the shutdown deadlock is the standout finding. The noise ratio is higher than ideal — FriendlyReviewer casts a wide net, and this PR shows the downside.

FriendlyReviewer's full review

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

friendly-reviewer bot commented now

Hi there 👋

🌥️ Tech
5 high, 4 medium, 6 low

The merge request introduces a thread-queue-parallel processing mode for remote subscriptions, with a new queue consumer, result consumer wiring, and integration tests. Several critical issues must be addressed before merging, including a shutdown deadlock due to Queue.shutdown(immediate=False) not waking blocked workers, lack of backpressure (unbounded queues and swallowed MessageRejected), use of assert for runtime validation (stripped in -O mode), and test flaws that invalidate ordering and error-handling coverage. Additional medium-priority issues involve thread signaling, TOCTOU patterns, missing commit thread join, and inconsistent offset handling in tests.


These might need a close look

  • 🔴 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L85)
    Shutdown deadlock: OrderedQueueWorker.run() calls self.work_queue.get() without timeout; FixedQueuePool.shutdown() uses immediate=False which does not wake blocked get() calls, causing threads to hang. Additionally, self.shutdown is a plain bool without memory barrier. Use threading.Event() and get(timeout=1.0) with a sentinel or immediate=True.
  • 🔴 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L275)
    Backpressure violation: SimpleQueueProcessingStrategy.submit() catches all exceptions and never raises MessageRejected. Queues are unbounded (maxsize=0). Memory can grow unbounded and downstream backpressure signals are ignored. Re-raise MessageRejected and consider bounding queue depth.
  • 🔴 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L268)
    assert isinstance(message.value, BrokerValue) is stripped with -O. Replace with explicit if not isinstance(...): raise TypeError(...) to avoid confusing AttributeError.
  • 🔴 tests/sentry/uptime/consumers/test_results_consumer.py (L129)
    Critical test issue: send_result() hardcodes partition=1 and offset=1 for every message, collapsing all offsets in the OffsetTracker set. This invalidates ordering, deduplication, and commit verification in all tests using it. Generate unique offsets per message.
  • 🔴 tests/sentry/uptime/consumers/test_results_consumer.py (L1900)
    Contradictory test: test_thread_queue_parallel_error_handling expects no commit even though complete_offset() is called in finally. The test should expect a commit, or its name/assertion must be corrected.

Worth checking

  • 🟡 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L310)
    SimpleQueueProcessingStrategy.terminate() does not join the commit thread, risking partially committed offsets on abrupt termination. Add self.commit_thread.join(timeout=5.0) after shutdown signal.
  • 🟡 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L61)
    _get_partition_lock has a TOCTOU pattern: .get() then .setdefault(). Use .setdefault() directly or defaultdict(threading.Lock) to avoid wasted Lock objects.
  • 🟡 tests/sentry/remote_subscriptions/consumers/test_queue_consumer.py (L231)
    Unit tests test_offset_committing and test_offset_gaps_block_commits bypass the +1 offset conversion used in production, so they do not test the real commit contract. Either test through the production wrapper or document the discrepancy.
  • 🟡 tests/sentry/uptime/consumers/test_results_consumer.py (L1686)
    Partition mismatch: messages use partition 1 but consumer config uses partition 0. While not runtime-failing, it misrepresents Kafka behavior. Align partitions.

Small things (take or leave)

  • 🔵 src/sentry/consumers/__init__.py (L124)
    CLI help text for --mode does not mention thread-queue-parallel. Update to list all supported modes.
  • 🔵 src/sentry/remote_subscriptions/consumers/result_consumer.py (L131)
    num_queues=max_workers or 20 treats 0 as falsy, silently defaulting to 20. Use explicit None check: num_queues=max_workers if max_workers is not None else 20.
  • 🔵 src/sentry/remote_subscriptions/consumers/result_consumer.py (L206)
    The else clause in create_with_partitions is syntactically attached only to the last if; convert to elif chain for clarity.
  • 🔵 tests/sentry/uptime/consumers/test_results_consumer.py (L2035)
    Graceful shutdown test only checks queue_pool is None. Verify message processing, thread termination, and offset commits.
  • 🔵 tests/sentry/uptime/consumers/test_results_consumer.py (L2109)
    Kafka integration test uses private API _run_once() and _shutdown(). Prefer public methods or time-bounded approach.
  • 🔵 tests/sentry/uptime/consumers/test_results_consumer.py (L2137)
    Kafka test may leak consumer on assertion failure; wrap in try/finally for cleanup.