Skip to content

feat(rabbitmq): pool RabbitMQ channels in RabbitMqMessageTransport - #675

Merged
samtrion merged 5 commits into
mainfrom
feature/241-rabbitmq-channel-pool
Aug 3, 2026
Merged

feat(rabbitmq): pool RabbitMQ channels in RabbitMqMessageTransport#675
samtrion merged 5 commits into
mainfrom
feature/241-rabbitmq-channel-pool

Conversation

@samtrion

@samtrion samtrion commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add IRabbitMqChannelPool / RabbitMqChannelPool in NetEvolve.Pulse.Internals, backed by a ConcurrentQueue<IRabbitMqChannelAdapter> of idle channels and a SemaphoreSlim capped at MaxChannelPoolSize concurrent rentals. RentAsync dequeues an open idle channel, disposing and skipping any closed one, or creates a new channel via IRabbitMqConnectionAdapter when none are available; Return re-queues an open channel or disposes a closed one, always releasing the rental slot exactly once (including when the pool itself has been disposed while the channel was rented out).
  • Add RabbitMqTransportOptions.MaxChannelPoolSize (default 10).
  • Rework RabbitMqMessageTransport to rent a channel from the pool per SendAsync call and to rent a single channel for the whole SendBatchAsync batch (published sequentially on it), always returning the channel in a finally block. This removes the old single shared _channel field, _initializationLock, and _publishLock — per-channel publish safety is now inherent because each rental is exclusive. IsHealthyAsync now delegates to the pool's own health check.
  • Register IRabbitMqChannelPoolRabbitMqChannelPool as a singleton via TryAddSingleton in UseRabbitMqTransport, so calling it more than once does not duplicate the registration.
  • Update unit tests to the new pool-based design (constructor now takes IRabbitMqChannelPool instead of IRabbitMqConnectionAdapter) and add RabbitMqChannelPoolTests covering: concurrent rent capped at MaxChannelPoolSize, returning an open channel makes it available for reuse, returning a closed channel disposes it and the next rent creates a fresh one, pool exhaustion causing a rent to wait until a return happens, and dispose (idempotent, disposes all idle channels, Return/RentAsync behave safely after dispose).
  • Update the RabbitMQ integration test fixture and one behavior test (IsHealthyAsync before any channel exists now correctly reports healthy based on the connection/pool, not on the previous single-field-existence quirk).

Closes #241

Deviations from the issue text

  • The issue's out-of-the-box constructor code snippets were adapted to match the actual codebase (existing IRabbitMqChannelAdapter / IRabbitMqConnectionAdapter types were reused as-is, matching the codebase context already given).
  • SendBatchAsync rents one channel for the entire batch and publishes its messages sequentially on it, rather than renting a channel per message. This was called out as an explicit choice in the issue ("pick whichever keeps the implementation simplest and correct"); renting once per batch avoids rent/return churn for what is inherently a single-threaded, ordered sequence of publishes.
  • RabbitMqMessageTransport's constructor now takes IRabbitMqChannelPool instead of IRabbitMqConnectionAdapter, since the channel pool fully owns channel creation/health against the connection. This is a necessary consequence of removing the transport's private channel-management, and is reflected in the DI registration and the updated unit tests.
  • IsHealthyAsync no longer requires that a channel has already been created before reporting healthy (the old code returned false until the first SendAsync/SendBatchAsync call because a _channel field had not been set yet). With pooling there is no single persistent "the channel" to check, so health now reflects the pool/connection state directly. One integration test (IsHealthyAsync_Before_first_send_returns_false) was updated to IsHealthyAsync_Before_first_send_returns_true_when_connection_open to reflect this intentional, documented behavior change.

Test plan

  • dotnet build Pulse.slnx — builds clean (net8.0/net9.0/net10.0)
  • dotnet test Tests/NetEvolve.Pulse.Tests.Unit --treenode-filter "/*/*/*RabbitMq*/*" — 156 passed, 0 failed, 0 skipped (net8.0/net9.0/net10.0)
  • csharpier format . run before committing
  • Integration tests (NetEvolve.Pulse.Tests.Integration) require Docker/Testcontainers and were not run in this environment, but the project builds successfully with the updated pool-based CreateTransport helper.

Replace the single lazily-created, publish-serialized channel in
RabbitMqMessageTransport with a pooled IRabbitMqChannelPool /
RabbitMqChannelPool backed by a ConcurrentQueue of idle channels and a
SemaphoreSlim capped at the new RabbitMqTransportOptions.MaxChannelPoolSize
(default 10). SendAsync rents a channel per call; SendBatchAsync rents a
single channel for the whole batch and publishes sequentially on it, since
only one thread ever touches that channel. Both always return the channel
in a finally block. IsHealthyAsync now delegates to the pool. The channel
pool is registered as a singleton via TryAddSingleton in
UseRabbitMqTransport so repeated calls do not duplicate it.
@samtrion
samtrion requested a review from a team as a code owner August 3, 2026 08:53
@samtrion
samtrion requested a review from Hnogared August 3, 2026 08:53
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • state:ready for merge

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2968dc44-a782-4b00-a18c-afdd4acef152

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…sk conversion

RentAsync_ConcurrentCalls_AreCappedAtMaxChannelPoolSize converts a fresh
ValueTask returned by RentAsync to a Task exactly once per loop iteration,
but SonarAnalyzer's cross-iteration analysis cannot tell the instances
apart and flags a false double-consumption.
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.83721% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.05%. Comparing base (c82da38) to head (703b098).

Files with missing lines Patch % Lines
...ve.Pulse.RabbitMQ/Internals/RabbitMqChannelPool.cs 92.85% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #675      +/-   ##
==========================================
- Coverage   96.07%   96.05%   -0.02%     
==========================================
  Files         172      188      +16     
  Lines        7179     7373     +194     
  Branches      661      679      +18     
==========================================
+ Hits         6897     7082     +185     
- Misses        136      145       +9     
  Partials      146      146              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

samtrion and others added 3 commits August 3, 2026 12:44
Adds coverage for RentAsync releasing its rental slot when channel
creation fails, and for resolving IRabbitMqChannelPool from a built
service provider, raising patch coverage to the required threshold.
…lasses (#676)

* feat: add IValidateOptions and ValidateOnStart for existing options classes

Add IValidateOptions<T> validators for TimeoutRequestInterceptorOptions,
QueryCachingOptions, OutboxOptions, OutboxProcessorOptions,
AzureServiceBusTransportOptions, RabbitMqTransportOptions, and
DaprMessageTransportOptions, each registered with AddOptions<T>().ValidateOnStart()
so misconfiguration is caught at startup instead of at first use.

The AzureServiceBus transport's imperative ValidateOptions() check is replaced
by AzureServiceBusTransportOptionsValidator with equivalent behavior, now
surfaced as an OptionsValidationException instead of an InvalidOperationException.

LoggingInterceptorOptions is intentionally left untouched: it already has a
validator but is out of scope for this change. SQLiteOutboxOptions does not
exist as a distinct type; the SQLite provider reuses the shared OutboxOptions,
which is already covered by OutboxOptionsValidator (TableName not empty).
ConnectionString remains unvalidated there since it is legitimately null for
EF Core-based outbox usage.

* fix(outbox): reword test comment to avoid false-positive S125 match

SonarAnalyzer flagged the explanatory comment as commented-out code
because it contained a code-like fragment; reworded in prose only.

* feat: IConfigureOptions with IConfiguration binding for existing options classes (#677)

* feat: bind options from configuration via IConfigureOptions

Add IConfigureOptions<TOptions> implementations that bind LoggingInterceptorOptions,
TimeoutRequestInterceptorOptions, QueryCachingOptions, OutboxOptions,
OutboxProcessorOptions, AzureServiceBusTransportOptions, RabbitMqTransportOptions,
and DaprMessageTransportOptions from documented Pulse:* configuration sections,
registered inside the respective existing Add*/Use* extension methods so
IConfiguration-backed values are validated at startup by the #238 validators.

AddRequestTimeout only applies its explicit globalTimeout parameter when a value
is provided, so a configuration-bound GlobalTimeout is no longer unconditionally
overwritten by the method's default null argument.

* fix(rabbitmq): register IConfiguration in channel pool resolution test

Resolving IRabbitMqChannelPool now requires IConfiguration to be
resolvable, since RabbitMqTransportOptionsConfiguration (added by the
IConfigureOptions binding work) depends on it.
Resolving IOptions<RabbitMqTransportOptions> now requires IConfiguration
to be resolvable, since RabbitMqTransportOptionsConfiguration depends
on it; the raw ServiceCollection built by this integration test did not
register one.
@samtrion
samtrion merged commit 351b7f0 into main Aug 3, 2026
11 checks passed
@samtrion
samtrion deleted the feature/241-rabbitmq-channel-pool branch August 3, 2026 12:13
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.

feat: RabbitMQ channel pooling in RabbitMqMessageTransport

1 participant