Benchmark: sentry PR 95633 - #11
Conversation
One potential problem we have with batch processing is that any one slow item will clog up the whole batch. This pr implements a queueing method instead, where we keep N queues that each have their own workers. There's still a chance of individual items backlogging a queue, but we can try increased concurrency here to reduce the chances of that happening
celmis-codereviewer
left a comment
There was a problem hiding this comment.
💬 COMMENT — findings to consider
Full findings and scope are in the review summary comment on this pull request — one persistent comment, updated in place on every run.
celmis-codereviewer
left a comment
There was a problem hiding this comment.
❌ CHANGES REQUESTED — blocking findings
Full findings and scope are in the review summary comment on this pull request — one persistent comment, updated in place on every run.
| min_offset = min(all_offsets) | ||
| max_offset = max(all_offsets) | ||
|
|
||
| start = max(last_committed + 1, min_offset) |
There was a problem hiding this comment.
Why: When min_offset exceeds last_committed + 1 on line 86, start skips unrecorded preceding offsets, causing get_committable_offsets to prematurely return and commit higher offsets while earlier messages are still in flight.
🔴 OffsetTracker skips missing intermediate offsets when calculating committable offsets
In OffsetTracker.get_committable_offsets, line 86 calculates start = max(last_committed + 1, min_offset). If all_offsets contains gaps (for instance, if message 101 is delayed in submission while message 102 has already been added to all_offsets), min_offset will be 102 while last_committed + 1 is 101.
Setting start to min_offset (102) skips checking whether offset 101 was processed or recorded, advancing highest_committable to 102 and committing past offset 101. If the consumer rebalances or restarts, unconsumed or in-flight messages at earlier offsets will be skipped.
| start = max(last_committed + 1, min_offset) | |
| start = last_committed + 1 if partition in self.last_committed else min_offset |
agent: defect · rule: defect.off-by-one · confidence: 0.95
| while not self.shutdown: | ||
| try: | ||
| work_item = self.work_queue.get() | ||
| except queue.ShutDown: |
There was a problem hiding this comment.
Why: queue.ShutDown and Queue.shutdown are Python 3.13 features not available on Python 3.11/3.12, raising AttributeError when exception handlers run on line 132 or when shutdown() is called on line 237.
🔴 Usage of Python 3.13 queue.ShutDown and Queue.shutdown APIs
queue.ShutDown and queue.Queue.shutdown() were introduced in Python 3.13. On Python 3.11 and 3.12 (which Sentry runs on), referencing queue.ShutDown in except queue.ShutDown: raises AttributeError: module 'queue' has no attribute 'ShutDown'. Similarly, calling q.shutdown() on line 237 raises AttributeError: 'Queue' object has no attribute 'shutdown', causing worker thread shutdown to fail and worker threads waiting on work_queue.get() (line 131) without a timeout to block indefinitely.
| except queue.ShutDown: | |
| except Exception: | |
| break |
agent: defect · rule: defect.compatibility · confidence: 0.95
| op="queue_worker.process", | ||
| name=f"monitors.{self.identifier}.worker_{self.worker_id}", | ||
| ): | ||
| self.result_processor(self.identifier, work_item.result) |
There was a problem hiding this comment.
Why: self.result_processor holds an instance of ResultProcessor passed from ResultsStrategyFactory, which does not implement call, raising a TypeError when invoked on line 140 during message processing.
🔴 Result processor object called directly as a function
In ResultsStrategyFactory.__init__, self.result_processor is initialized as an instance of ResultProcessor (self.result_processor_cls()) and passed to FixedQueuePool. In OrderedQueueWorker.run, line 140 executes self.result_processor(self.identifier, work_item.result). Because ResultProcessor instances do not implement __call__, processing any message will raise a TypeError: 'ResultProcessor' object is not callable.
| self.result_processor(self.identifier, work_item.result) | |
| self.result_processor.handle_result(self.identifier, work_item.result) |
agent: defect · rule: defect.type-error · confidence: 0.95
| self.multiprocessing_pool = MultiprocessingPool(num_processes) | ||
| if mode == "thread-queue-parallel": | ||
| self.thread_queue_parallel = True | ||
| self.queue_pool = FixedQueuePool( |
There was a problem hiding this comment.
Why: self.result_processor is passed to FixedQueuePool on line 133 of src/sentry/remote_subscriptions/consumers/result_consumer.py, but OrderedQueueWorker calls it with two arguments on line 140 of src/sentry/remote_subscriptions/consumers/queue_consumer.py, raising a TypeError at runtime.
🟠 Signature mismatch between ResultProcessor and OrderedQueueWorker invocation
In src/sentry/remote_subscriptions/consumers/result_consumer.py, ResultsStrategyFactory instantiates FixedQueuePool by passing self.result_processor directly:
self.queue_pool = FixedQueuePool(
result_processor=self.result_processor,
identifier=self.identifier,
num_queues=max_workers or 20, # Number of parallel queues
)However, in src/sentry/remote_subscriptions/consumers/queue_consumer.py, OrderedQueueWorker.run invokes self.result_processor with two arguments (self.identifier and work_item.result):
self.result_processor(self.identifier, work_item.result)Since ResultProcessor.__call__ only accepts a single argument (result), processing any queued work item will raise a TypeError at runtime.
| self.queue_pool = FixedQueuePool( | |
| self.queue_pool = FixedQueuePool( | |
| result_processor=lambda _id, result: self.result_processor(result), | |
| identifier=self.identifier, | |
| num_queues=max_workers or 20, # Number of parallel queues | |
| ) |
agent: contract · rule: contract.broken-caller · confidence: 0.95
| For each partition, finds the highest contiguous offset that has been processed. | ||
| """ | ||
| committable = {} | ||
| for partition in list(self.all_offsets.keys()): |
There was a problem hiding this comment.
Why: get_committable_offsets iterates over list(self.all_offsets.keys()) on line 74 without holding a lock on self.all_offsets, raising RuntimeError if add_offset inserts a new partition key concurrently from the strategy thread.
🟠 Unsynchronized dictionary key iteration during concurrent partition insertion
add_offset is called from SimpleQueueProcessingStrategy.submit on the Arroyo consumer thread and mutates self.all_offsets (a defaultdict) when a new partition is encountered. Concurrent execution of list(self.all_offsets.keys()) in get_committable_offsets on commit_thread without holding a global lock around self.all_offsets can raise RuntimeError: dictionary changed size during iteration.
| for partition in list(self.all_offsets.keys()): | |
| # Protect dict key access across threads | |
| with threading.Lock(): | |
| partitions = list(self.all_offsets.keys()) | |
| for partition in partitions: |
agent: defect · rule: defect.concurrency · confidence: 0.85
| while not self.shutdown: | ||
| try: | ||
| work_item = self.work_queue.get() | ||
| except queue.ShutDown: |
There was a problem hiding this comment.
🟡 Bare except: catches everything including KeyboardInterrupt
Use except Exception: so SystemExit and KeyboardInterrupt still propagate as the user expects.
except queue.ShutDown: ⏎ break
Also at line 142.
| except queue.ShutDown: | |
| except Exception: |
agent: structural · rule: structural.py.bare-except · confidence: 1.00
| finally: | ||
| try: | ||
| admin_client.delete_topics([test_topic]) | ||
| except Exception: |
There was a problem hiding this comment.
🟡 Empty except clause silently swallows errors
Catching an exception with only pass hides bugs and makes debugging harder. At minimum log the exception or re-raise.
except Exception: ⏎ pass
| except Exception: | |
| logger.exception('...') or raise |
agent: structural · rule: structural.py.empty-except · confidence: 1.00
🤖 Code Review for PR #11❌ CHANGES REQUESTED — blocking findings Findings
Scope
Performance
Powered by Code Analyzer · context: tree-sitter graph + cve, structural, security, contract, defect |
Benchmark reproduction of getsentry#95633