Hi there 👋
| 🌥️ Tech |
🌤️ Feat |
| 3 high, 3 medium, 5 low |
1 mitigated |
This MR implements a queue-based parallelism strategy (SimpleQueueProcessingStrategy) that replaces batched processing with a fixed number of per-group FIFO queues and a background contiguous-offset commit loop, matching the overall intent of the PR (isolating slow items so they don't clog the entire batch). However, the implementation has several blocking correctness and lifecycle issues: messages that raise during processing are still acknowledged (offset committed), leading to silent data loss; the queues are unbounded, so the claimed backpressure does not exist and memory can grow without limit; the shared FixedQueuePool is torn down from multiple call sites, causing a race and double shutdown; and group→queue assignment uses Python's randomized hash(), so it is not deterministic across restarts. Several tests are flaky or validate a different contract than production (e.g., the +1 offset wrapper is bypassed in unit tests). While the design direction is sound, these defects must be resolved before the MR is ready to merge.
These might need a close look
- 🔴 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L149)
In OrderedQueueWorker.run, exceptions from result_processor are swallowed and the finally block unconditionally calls complete_offset, committing offsets for failed messages. This violates the documented 'Only commits offsets after successful processing' contract and causes silent data loss (a failed subscription update is acknowledged). It also contradicts the error-handling test which expects the failed partition not to be committed.
- 🔴 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L182)
queue.Queue() is created with no maxsize, so it is unbounded and work_queue.put() never blocks. The docstring claims 'Natural backpressure when queues fill up', but no backpressure exists; the consumer can drain the whole topic into memory whenever workers are slower than the input. Bound the queues (e.g. maxsize=...) and block in submit to apply real backpressure.
- 🔴 src/sentry/remote_subscriptions/consumers/result_consumer.py (L242)
FixedQueuePool is created once in ResultsStrategyFactory.__init__ and shared across every strategy instance, but each SimpleQueueProcessingStrategy starts its own commit thread over the same OffsetTracker, and close()/terminate() calls queue_pool.shutdown() which destroys workers still needed by other strategies. Also, ResultsStrategyFactory.shutdown() sets queue_pool = None, so a subsequent create_with_partitions hits the assert self.queue_pool is not None and raises. Tie the commit loop to the pool (single commit thread per pool) and coordinate teardown to avoid races and double-commit scenarios.
Worth checking
- 🟡 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L199)
hash(group_key) % self.num_queues relies on Python's built-in str hash, which is randomized per interpreter run (PYTHONHASHSEED). The FixedQueuePool docstring calls this 'consistent hashing', but the mapping is not deterministic across processes/restarts. Use a stable hash such as int(hashlib.md5(group_key.encode()).hexdigest(), 16) % num_queues or zlib.crc32.
- 🟡 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L225)
FixedQueuePool.shutdown sets worker.shutdown = True then calls q.shutdown(immediate=False), which leaves remaining queued items unprocessed and never complete_offset-ed — these are silently re-delivered on restart. The pool is also shut down from two call sites (strategy.close() and factory.shutdown()), causing a double Queue.shutdown(). If graceful semantics are intended, consolidate shutdown to a single owner and drain workers before terminating.
- 🟡 tests/sentry/uptime/consumers/test_results_consumer.py (L1874)
test_thread_queue_parallel_error_handling asserts that a message whose processing raised is not committed, but the implementation commits the failed offset anyway. The test only passes when the commit thread hasn't fired within its 0.2s observation window, making it flaky and encoding the opposite of the actual behavior. Rewrite after fixing the error-handling semantics to be deterministic.
Small things (take or leave)
- 🔵 tests/sentry/remote_subscriptions/consumers/test_queue_consumer.py (L285)
TestSimpleQueueProcessingStrategy passes its raw commit_function directly into the strategy, bypassing the production +1 offset wrapper (commit_offsets). The unit tests therefore assert the highest processed offset (104/100/103) while production commits 105/101/104, so they validate a different contract. Apply the same +1 in the unit commit function or add an assertion that exercises the production wrapper.
- 🔵 tests/sentry/uptime/consumers/test_results_consumer.py (L116)
send_result always uses Partition(Topic("test"), 1) and offset=1, so test_thread_queue_parallel, test_thread_queue_parallel_preserves_order, and test_thread_queue_parallel_concurrent_subscriptions all feed identical partition/offset, meaning they only exercise the worker FIFO path and never verify commit behavior or multi-offset contiguity. Consider allowing an explicit offset to widen coverage.
- 🔵 src/sentry/remote_subscriptions/consumers/queue_consumer.py (L89)
for offset in range(start, max_offset + 1) iterates over every integer between the last committed offset and the max seen offset, potentially walking huge spans (e.g. offsets 100 and 100_000) on every commit tick while holding the partition lock. Consider walking the known offsets set (sorted) instead of a numeric range, and/or capping the scan.
- 🔵 src/sentry/consumers/__init__.py (L121)
uptime_options() always includes --max-batch-size and --max-batch-time, which are silently ignored in the new thread-queue-parallel mode since SimpleQueueProcessingStrategy does not use batching. Operators may believe these settings still apply. Document that they are ignored in queue mode or gate/hide them when that mode is selected.
- 🔵 src/sentry/consumers/__init__.py (L141)
--max-workers defaults to None, and in thread-queue-parallel mode the factory translates this to num_queues=max_workers or 20. The new help text does not convey that the option selects the number of queues in queue mode or that omitting it yields 20, so the CLI contract and runtime default disagree. Include the default (e.g. 'defaults to 20 queues') in the help text.
✅ Feature-level checklist
Partially covered
- ⚠️ Implement a queueing method instead of batch processing, with N queues each having their own workers, to prevent any one slow item from clogging the whole batch and use increased concurrency to reduce the chance of backlog.
The MR introduces a queue-based strategy with per-group FIFO queues and one worker per queue, which partially fulfills the goal. However, it is not fully compliant: the queues are unbounded (no real backpressure, risking unbounded memory growth), failed messages are acknowledged (offset committed) causing silent data loss, and the shared pool lifecycle race/double shutdown can drop work or crash during rebalances. These issues undermine the intended reliability and parallelism benefits.
Recall is 2/3 (66.7%), and both hits are genuine — with one important nuance on golden #1. FR criticizes the exact
q.shutdown(immediate=False)call at the right location (queue_consumer.py L225) — items left unprocessed and nevercomplete_offset-ed, plus a double shutdown from two call sites — but describes the behavior as if the call worked. The golden's diagnosis is different:queue.Queuein the standard library has noshutdownmethod at all, so this raisesAttributeError. The golden is counted found on the strength of the shared call site; that is generous, and the golden may itself be misaligned if the fork's queue implementation does haveshutdown.Strengths: the review's three high findings are correctness bugs that would be goldens in a better-curated dataset — offsets committed for failed messages (silent data loss, L149), unbounded queues contradicting the claimed backpressure (L182), and the shared
FixedQueuePooldouble-shutdown race (result_consumer.py L242). The second hit (the error-handling test at L1874) matches golden #2's essence: the test encodes the opposite of the actual behavior, though FR frames it as a flaky test rather than a docstring mismatch.Weaknesses: the miss — the repeated magic number 50 for
max_wait— is a maintainability nit, and missing it is defensible: FR prioritized real correctness bugs over it. The nuance on golden #1 cuts the other way: on the highest-severity golden, FR's diagnosis was a different bug on the same line, and FR's own severity for that finding (medium) sits below the dataset's HIGH. This page's score is thus both generous (golden #1 counted) and earned (two real catches).All 9 supplementary findings are legitimate. The
hash()randomization finding (queue_consumer.py L199) is a notable catch: group→queue assignment relies on Python's randomizedstrhash, so the mapping is not deterministic across restarts despite the pool's 'consistent hashing' claim. The weakest supplementary items are the CLI help-text gaps (consumers/__init__.py L121/L141): real but cosmetic.