Summary
On any transport where publishing and listening resolve to the same Endpoint object — a RabbitMQ queue, a Redis stream, a Pulsar topic — calling SendInline() on the publishing side silently overwrites the listening side's EndpointMode. No warning, no error; the listener just quietly runs in a different mode than it was configured for.
opts.ListenToRabbitQueue("orders")
.ProcessInParallelWithNativeAcks() // asks for native acks + parallel lanes
.PartitionProcessingByGroupId(PartitionSlots.Five);
opts.PublishMessage<Order>()
.ToRabbitQueue("orders")
.SendInline(); // silently resets Mode to Inline
Mechanism
Mode is a single property on Endpoint governing both directions, and the sending-side fluent methods assign it unconditionally (src/Wolverine/Configuration/SubscriberConfiguration.cs):
public T SendInline()
{
add(e => e.Mode = EndpointMode.Inline);
return this.As<T>();
}
UseDurableOutbox() and BufferedInMemory() on the same class do the same thing (:95, :101). Because these are delayed configuration, the outcome depends on which fluent block Wolverine applies last — so the same two calls in the other order produce different endpoint state. That is the identical order-dependence class GH-3712 fixed for ProcessInline() + MaximumParallelMessages().
Why this is worth fixing now
It was hit independently, by two separate people, within an hour, while implementing #4046 (Redis Streams) and #4047 (Pulsar) — in both cases costing a debugging round on a test that failed for a reason nowhere near where it looked. It is pre-existing and not specific to EndpointMode.NativeAck.
But NativeAck raises the stakes. Previously the silent overwrite downgraded throughput. Now it can silently discard a processing guarantee: ProcessInParallelWithNativeAcks() + PartitionProcessingByGroupId() reset to Inline means partitioned processing is gone — and GH-3712's validator, which exists precisely to reject Inline + partitioning, sees the endpoint after the overwrite and cannot tell "the user asked for Inline" from "the user's listener mode was clobbered by a publish rule".
Suggested direction
The pieces to decide between:
- Warn or throw when a sending-side mode assignment changes the mode of an endpoint that is also a listener — the cheapest fix, and it makes the collision observable rather than silent.
- Separate the concerns. The mode genuinely means different things per direction (
buildSendingAgent vs buildReceiverAsync switch on the same value). A distinct SendingMode would remove the collision at the root, at the cost of a larger change and a migration story.
- Whatever is chosen, apply it to
UseDurableOutbox() and BufferedInMemory() too — SendInline() is just the loudest instance.
Note that option 1's check belongs in ListenerConfigurationValidator, not in the Mode setter: both sides are delayed configuration, so a setter-based guard catches one ordering and misses the reverse. Same reasoning as GH-3712, and the same place #4049 wants the ASB-sessions rejection to live.
Related: #3712, #3708, #4046, #4047.
Summary
On any transport where publishing and listening resolve to the same
Endpointobject — a RabbitMQ queue, a Redis stream, a Pulsar topic — callingSendInline()on the publishing side silently overwrites the listening side'sEndpointMode. No warning, no error; the listener just quietly runs in a different mode than it was configured for.Mechanism
Modeis a single property onEndpointgoverning both directions, and the sending-side fluent methods assign it unconditionally (src/Wolverine/Configuration/SubscriberConfiguration.cs):UseDurableOutbox()andBufferedInMemory()on the same class do the same thing (:95,:101). Because these are delayed configuration, the outcome depends on which fluent block Wolverine applies last — so the same two calls in the other order produce different endpoint state. That is the identical order-dependence class GH-3712 fixed forProcessInline()+MaximumParallelMessages().Why this is worth fixing now
It was hit independently, by two separate people, within an hour, while implementing #4046 (Redis Streams) and #4047 (Pulsar) — in both cases costing a debugging round on a test that failed for a reason nowhere near where it looked. It is pre-existing and not specific to
EndpointMode.NativeAck.But NativeAck raises the stakes. Previously the silent overwrite downgraded throughput. Now it can silently discard a processing guarantee:
ProcessInParallelWithNativeAcks()+PartitionProcessingByGroupId()reset toInlinemeans partitioned processing is gone — and GH-3712's validator, which exists precisely to reject Inline + partitioning, sees the endpoint after the overwrite and cannot tell "the user asked for Inline" from "the user's listener mode was clobbered by a publish rule".Suggested direction
The pieces to decide between:
buildSendingAgentvsbuildReceiverAsyncswitch on the same value). A distinctSendingModewould remove the collision at the root, at the cost of a larger change and a migration story.UseDurableOutbox()andBufferedInMemory()too —SendInline()is just the loudest instance.Note that option 1's check belongs in
ListenerConfigurationValidator, not in theModesetter: both sides are delayed configuration, so a setter-based guard catches one ordering and misses the reverse. Same reasoning as GH-3712, and the same place #4049 wants the ASB-sessions rejection to live.Related: #3712, #3708, #4046, #4047.