Skip to content

SendInline() on a shared endpoint silently overwrites the listener's EndpointMode #4059

Description

@jeremydmiller

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:

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions