Skip to content

perf(consumer): build processor chain once per consumer#50

Merged
iifawzi merged 1 commit into
mainfrom
perf/hoist-processor-chain
Jun 9, 2026
Merged

perf(consumer): build processor chain once per consumer#50
iifawzi merged 1 commit into
mainfrom
perf/hoist-processor-chain

Conversation

@iifawzi

@iifawzi iifawzi commented Jun 6, 2026

Copy link
Copy Markdown
Member

What

The processor decorator chain (the 8-deep RunMQExceptionLoggerProcessor → … → RunMQBaseProcessor stack plus a DefaultDeserializer) was being constructed inside the consume callback, on every delivery. Every processor and the deserializer are stateless across messages — the only per-message input is the RabbitMQMessage handed to consume() — so the chain can be built once per consumer and reused.

This moves the construction above the consumerChannel.consume(...) call. The per-message hot path now allocates only the unavoidable RabbitMQMessage.

Why

Removes 8 object allocations per consumed message. Behavior is identical:

  • chain order is unchanged (success flows inward, rejection flows outward)
  • the last-resort try/catch around consume() is preserved
  • one chain per consumer, and consumers don't share mutable state, so reuse is safe

Performance

Benchmarked before/after with the consumer-side scenarios (consume, concurrent, reliability, mean of 3 runs each). Throughput is unchanged within run-to-run noise — at 16–20k msg/s the bottleneck is broker round-trips / simulated work, and the removed objects are cheap short-lived young-gen allocations.

Scenario Before After
Consume throughput 15,881 ±421 15,673 ±759
Concurrent · 1 6,439 ±436 6,470 ±328
Concurrent · 8 19,337 ±384 19,942 ±168
Reliability · basic 18,891 ±91 19,239 ±354
Reliability · retries 19,724 ±398 19,137 ±187

So this is a cleanliness / GC-pressure reduction, not a throughput win — it shouldn't be sold as the latter. The value is fewer allocations per message under sustained high-throughput or memory-constrained loads.

Tests

Full suite green: 149 unit + 85 e2e passing.

The processor decorator chain (8 objects + the deserializer) was rebuilt
on every delivery inside the consume callback. Every processor and the
deserializer are stateless across messages — the only per-message input is
the RabbitMQMessage passed to consume() — so the chain can be constructed
once per consumer and reused.

Hoisting it out of the hot path removes 8 allocations per consumed message,
leaving only the unavoidable RabbitMQMessage allocation. Behavior is
unchanged; the last-resort try/catch around consume() is preserved.
@iifawzi iifawzi merged commit 18d62ea into main Jun 9, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant