Span Buffer Multiprocess Enhancement with Health Monitoring - #6
Conversation
There was a problem hiding this comment.
Greptile Summary
This PR transforms the span processing system from single-process to multi-process architecture with configurable process limits. The core change refactors SpanFlusher to distribute shards across multiple processes using round-robin allocation, enabling parallel processing of spans from Redis to Kafka. Key components include:
Architecture Changes:
- Modified
SpanFlusherto create multiple worker processes instead of a single process - Added shard-to-process mapping logic that distributes shards evenly across available processes
- Implemented per-process health monitoring and backpressure tracking using
multiprocessing.Valueobjects - Each process group gets its own
SpansBufferinstance for independent operation
Configuration Interface:
- Added
--flusher-processesCLI option to the process-spans consumer (defaults to 1 for backward compatibility) - Extended
ProcessSpansStrategyFactorywithflusher_processesparameter for programmatic control - When
max_processes< number of shards, shards are distributed across available processes
Monitoring Enhancements:
- Per-process health tracking with automatic restart capability for failed processes
- Enhanced metrics with shard-level tagging for better observability
- Backpressure detection now checks across all processes rather than a single global state
Documentation:
- Added anti-pattern guidance in
CLAUDE.mdabout proper type checking with union types (isinstance()vshasattr())
The integration fits into Sentry's existing span processing pipeline by maintaining the same external interface while parallelizing the internal work distribution, allowing better resource utilization on multi-core systems.
Confidence score: 2/5
- This PR introduces significant architectural complexity with potential for race conditions and resource management issues
- Multiple concerning implementation details including inconsistent metric naming, potential race conditions in process restart logic, and incomplete shutdown procedures
- Critical files
src/sentry/spans/consumers/process/flusher.pyneeds thorough review for process synchronization, cleanup logic, and the shard distribution algorithm
6 files reviewed, 3 comments
| with metrics.timer("spans.buffer.flusher.produce", tags={"shard": shard_tag}): | ||
| for flushed_segment in flushed_segments.values(): | ||
| if not flushed_segment.spans: | ||
| continue | ||
|
|
||
| spans = [span.payload for span in flushed_segment.spans] | ||
| kafka_payload = KafkaPayload(None, orjson.dumps({"spans": spans}), []) | ||
| metrics.timing("spans.buffer.segment_size_bytes", len(kafka_payload.value)) | ||
| metrics.timing( | ||
| "spans.buffer.segment_size_bytes", | ||
| len(kafka_payload.value), | ||
| tags={"shard": shard_tag}, | ||
| ) | ||
| produce(kafka_payload) | ||
|
|
||
| with metrics.timer("spans.buffer.flusher.wait_produce"): | ||
| with metrics.timer("spans.buffer.flusher.wait_produce", tags={"shards": shard_tag}): |
There was a problem hiding this comment.
syntax: Inconsistent metric tag naming: line 185 uses 'shard' but line 199 uses 'shards' for the same shard_tag value
| with metrics.timer("spans.buffer.flusher.produce", tags={"shard": shard_tag}): | |
| for flushed_segment in flushed_segments.values(): | |
| if not flushed_segment.spans: | |
| continue | |
| spans = [span.payload for span in flushed_segment.spans] | |
| kafka_payload = KafkaPayload(None, orjson.dumps({"spans": spans}), []) | |
| metrics.timing("spans.buffer.segment_size_bytes", len(kafka_payload.value)) | |
| metrics.timing( | |
| "spans.buffer.segment_size_bytes", | |
| len(kafka_payload.value), | |
| tags={"shard": shard_tag}, | |
| ) | |
| produce(kafka_payload) | |
| with metrics.timer("spans.buffer.flusher.wait_produce"): | |
| with metrics.timer("spans.buffer.flusher.wait_produce", tags={"shards": shard_tag}): | |
| with metrics.timer("spans.buffer.flusher.produce", tags={"shard": shard_tag}): | |
| for flushed_segment in flushed_segments.values(): | |
| if not flushed_segment.spans: | |
| continue | |
| spans = [span.payload for span in flushed_segment.spans] | |
| kafka_payload = KafkaPayload(None, orjson.dumps({"spans": spans}), []) | |
| metrics.timing( | |
| "spans.buffer.segment_size_bytes", | |
| len(kafka_payload.value), | |
| tags={"shard": shard_tag}, | |
| ) | |
| produce(kafka_payload) | |
| with metrics.timer("spans.buffer.flusher.wait_produce", tags={"shard": shard_tag}): |
| while process.is_alive() and (deadline is None or deadline > time.time()): | ||
| time.sleep(0.1) | ||
|
|
||
| if isinstance(self.process, multiprocessing.Process): | ||
| self.process.terminate() | ||
| if isinstance(process, multiprocessing.Process): | ||
| process.terminate() |
There was a problem hiding this comment.
logic: Process termination only handles multiprocessing.Process but not threading.Thread cleanup
| flusher = fac._flusher | ||
| assert len(flusher.processes) == 2 | ||
| assert flusher.max_processes == 2 | ||
| assert flusher.num_processes == 2 |
There was a problem hiding this comment.
style: These assertions test the same value (flusher.max_processes and flusher.num_processes both equal 2). Consider removing the redundant assertion or add a comment explaining why both are needed.
Review Summary🏷️ Draft Comments (6)
|
Test 6