Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -289,23 +289,34 @@ public override void DiscoverTopology(
return;
}

if (Transport.BindMode != MessagingBindMode.Implicit)
{
return;
}
// Under explicit binding, a target that is not declared is recorded in the topology
// but never provisioned. The sender does not own it.
var bindImplicitly = Transport.BindMode == MessagingBindMode.Implicit;

if (azureConfiguration.TopicName is not null)
{
_topology.GetOrAddTopic(
azureConfiguration.TopicName,
static _ => new AzureServiceBusTopicConfiguration());
bindImplicitly
? static _ => new AzureServiceBusTopicConfiguration()
: static _ => new AzureServiceBusTopicConfiguration
{
AutoProvision = false,
Origin = TopologyOrigin.Endpoint
});
}

if (azureConfiguration.QueueName is not null)
{
_topology.GetOrAddQueue(
azureConfiguration.QueueName,
static _ => new AzureServiceBusQueueConfiguration());
bindImplicitly
? static _ => new AzureServiceBusQueueConfiguration()
: static _ => new AzureServiceBusQueueConfiguration
{
AutoProvision = false,
Origin = TopologyOrigin.Endpoint
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ public interface IRabbitMQDispatchEndpointDescriptor
{
/// <summary>
/// Sets the endpoint to dispatch messages to the specified queue, clearing any exchange target.
/// A queue that is not declared in the transport topology is addressed by name and never provisioned.
/// </summary>
/// <param name="name">The target queue name.</param>
/// <returns>The descriptor for method chaining.</returns>
IRabbitMQDispatchEndpointDescriptor ToQueue(string name);

/// <summary>
/// Sets the endpoint to dispatch messages to the specified exchange, clearing any queue target.
/// An exchange that is not declared in the transport topology is addressed by name and never provisioned.
/// </summary>
/// <param name="name">The target exchange name.</param>
/// <returns>The descriptor for method chaining.</returns>
Expand Down
64 changes: 25 additions & 39 deletions src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQDispatchEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,17 @@ namespace Mocha.Transport.RabbitMQ;
/// RabbitMQ dispatch endpoint that publishes outbound messages to a target queue or exchange
/// using pooled channels from the transport's dispatcher.
/// </summary>
/// <remarks>
/// The target is addressed by name. A target that is part of the transport topology is provisioned
/// according to its auto-provision setting; a target outside the topology is published to by name
/// and is never declared, bound, or consumed.
/// </remarks>
/// <param name="transport">The owning RabbitMQ transport instance.</param>
public sealed class RabbitMQDispatchEndpoint(RabbitMQMessagingTransport transport)
: DispatchEndpoint<RabbitMQDispatchEndpointConfiguration>(transport)
{
private CachedString _exchangeName = CachedString.Empty;
private CachedString _routingKey = CachedString.Empty;

/// <summary>
/// Gets the name of the target queue, or <c>null</c> if the endpoint targets an exchange.
/// </summary>
public string? QueueName { get; private set; }

/// <summary>
/// Gets the name of the target exchange, or <c>null</c> if the endpoint targets a queue.
/// </summary>
public string? ExchangeName { get; private set; }

/// <summary>
/// Gets the topology queue this endpoint targets, or <c>null</c> if the endpoint targets an
/// exchange or a queue that is not part of the transport topology.
/// Gets the target queue for this endpoint, or <c>null</c> if the endpoint targets an exchange.
/// </summary>
public RabbitMQQueue? Queue { get; private set; }

/// <summary>
/// Gets the topology exchange this endpoint targets, or <c>null</c> if the endpoint targets a
/// queue or an exchange that is not part of the transport topology.
/// Gets the target exchange for this endpoint, or <c>null</c> if the endpoint targets a queue.
/// </summary>
public RabbitMQExchange? Exchange { get; private set; }

Expand Down Expand Up @@ -127,14 +107,19 @@ private async ValueTask DispatchAsync(
}
else
{
exchangeName = _exchangeName;
routingKey = _routingKey;
if (Exchange is not null)
{
exchangeName = Exchange.CachedName;

if (ExchangeName is not null
&& envelope.Headers is not null
&& envelope.Headers.TryGet(RabbitMQMessageHeaders.RoutingKey, out var rk))
if (envelope.Headers is not null
&& envelope.Headers.TryGet(RabbitMQMessageHeaders.RoutingKey, out var rk))
{
routingKey = new CachedString(rk);
}
}
else if (Queue is not null)
{
routingKey = new CachedString(rk);
routingKey = Queue.CachedName;
}
}

Expand Down Expand Up @@ -182,21 +167,22 @@ protected override void OnComplete(
RabbitMQDispatchEndpointConfiguration configuration)
{
var topology = (RabbitMQMessagingTopology)Transport.Topology;

if (configuration.ExchangeName is { } exchangeName)
if (configuration.ExchangeName is not null)
{
ExchangeName = exchangeName;
Exchange = topology.Exchanges.FirstOrDefault(e => e.Name == exchangeName);
_exchangeName = Exchange?.CachedName ?? new CachedString(exchangeName);
Exchange =
topology.Exchanges.FirstOrDefault(e => e.Name == configuration.ExchangeName)
?? throw new InvalidOperationException("Exchange not found");
}
else if (configuration.QueueName is { } queueName)
else if (configuration.QueueName is not null)
{
QueueName = queueName;
Queue = topology.Queues.FirstOrDefault(q => q.Name == queueName);
_routingKey = Queue?.CachedName ?? new CachedString(queueName);
Queue =
topology.Queues.FirstOrDefault(q => q.Name == configuration.QueueName)
?? throw new InvalidOperationException("Queue not found");
}

// The destination stays unset for a target outside the topology.
Destination = ((TopologyResource?)Exchange ?? Queue)!;
Destination =
Exchange as TopologyResource
?? Queue as TopologyResource
?? throw new InvalidOperationException("Destination is not set");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,56 +240,64 @@ public override bool TryGetDispatchEndpoint(Uri address, [NotNullWhen(true)] out
{
foreach (var candidate in DispatchEndpoints)
{
if (candidate.IsCompleted && candidate.Address == address)
if (!candidate.IsCompleted)
{
continue;
}

if (candidate.Address == address)
{
endpoint = candidate;
return true;
}
}
}

string? queueName = null;
string? exchangeName = null;

if (address.TryParseTopologyAddress(Topology.Address, out var kind, out var name))
if (Topology.Address.IsBaseOf(address))
{
if (kind is 'q')
{
queueName = name;
}
else if (kind is 'e')
foreach (var candidate in DispatchEndpoints)
{
exchangeName = name;
if (!candidate.IsCompleted)
{
continue;
}

if (candidate.Destination.Address == address)
{
endpoint = candidate;
return true;
}
}
}
else if (TryGetResourceName(address, "queue", out var resourceQueueName))
{
queueName = resourceQueueName;
}
else if (TryGetResourceName(address, "exchange", out var resourceExchangeName))
{
exchangeName = resourceExchangeName;
}

if (queueName is not null)
if (TryGetResourceName(address, "queue", out var queueName))
{
foreach (var candidate in DispatchEndpoints)
{
if (candidate is RabbitMQDispatchEndpoint { IsCompleted: true } rabbitCandidate
&& rabbitCandidate.QueueName == queueName)
if (!candidate.IsCompleted)
{
continue;
}

if (candidate.Destination is RabbitMQQueue queue && queue.Name == queueName)
{
endpoint = candidate;
return true;
}
}
}

if (exchangeName is not null)
if (TryGetResourceName(address, "exchange", out var exchangeName))
{
foreach (var candidate in DispatchEndpoints)
{
if (candidate is RabbitMQDispatchEndpoint { IsCompleted: true } rabbitCandidate
&& rabbitCandidate.ExchangeName == exchangeName)
if (!candidate.IsCompleted)
{
continue;
}

if (candidate.Destination is RabbitMQExchange exchange
&& exchange.Name == exchangeName)
{
endpoint = candidate;
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public sealed class RabbitMQRoutingStrategy : RoutingStrategy<RabbitMQMessagingT
}

if (configuration is null
&& address.TryParseTopologyAddress(Transport.Topology.Address, out var topologyKind, out var resourceName))
&& TryParseTopologyAddress(address, out var topologyKind, out var resourceName))
{
if (topologyKind is 'e')
{
Expand Down Expand Up @@ -335,25 +335,34 @@ public override void DiscoverTopology(
return;
}

// Under BindExplicitly, the dispatch endpoint must not materialize a destination
// exchange the user never declared: the user retains full ownership of the topology,
// and a default-config (fanout) shadow can collide with a previously-declared exchange
// on the broker (PRECONDITION_FAILED on declare). The user is expected to either
// DeclareExchange it explicitly or rely on a pre-existing broker entity.
// Under explicit binding, a target that is not declared is recorded in the topology
// but never provisioned or bound. The sender does not own it.
var bindImplicitly = Transport.BindMode == MessagingBindMode.Implicit;

if (rabbitConfiguration.ExchangeName is not null && bindImplicitly)
if (rabbitConfiguration.ExchangeName is not null)
{
_topology.GetOrAddExchange(
rabbitConfiguration.ExchangeName,
static _ => new RabbitMQExchangeConfiguration());
bindImplicitly
? static _ => new RabbitMQExchangeConfiguration()
: static _ => new RabbitMQExchangeConfiguration
{
AutoProvision = false,
Origin = TopologyOrigin.Endpoint
});
}

if (rabbitConfiguration.QueueName is not null && bindImplicitly)
if (rabbitConfiguration.QueueName is not null)
{
_topology.GetOrAddQueue(
rabbitConfiguration.QueueName,
_ => new RabbitMQQueueConfiguration { AutoProvision = rabbitConfiguration.AutoProvision });
bindImplicitly
? _ => new RabbitMQQueueConfiguration { AutoProvision = rabbitConfiguration.AutoProvision }
: static _ => new RabbitMQQueueConfiguration
{
AutoProvision = false,
Origin = TopologyOrigin.Endpoint
});
}

var schema = Transport.Schema;
Expand Down Expand Up @@ -531,7 +540,7 @@ private bool TryGetQueueName(
}
}

if (address.TryParseTopologyAddress(Transport.Topology.Address, out var topologyKind, out var topologyName)
if (TryParseTopologyAddress(address, out var topologyKind, out var topologyName)
&& topologyKind is 'q')
{
queueName = topologyName;
Expand All @@ -547,6 +556,75 @@ private bool TryGetQueueName(
queueName = string.Empty;
return false;
}

/// <summary>
/// Parses an address under the transport topology into its kind and resource name.
/// Everything after the kind segment is the name, because a queue or exchange name may
/// contain a slash.
/// </summary>
private bool TryParseTopologyAddress(
Uri address,
out char kind,
out string name)
{
kind = default;
name = string.Empty;

// A valid resource path is not enough to identify this transport. For example,
// rabbitmq://other-host:5672/tenant-a/q/orders must not be claimed by this topology.
var topologyAddress = Transport.Topology.Address;
if (!address.Scheme.EqualsOrdinalIgnoreCase(topologyAddress.Scheme)
|| !address.Host.EqualsOrdinalIgnoreCase(topologyAddress.Host)
|| address.Port != topologyAddress.Port)
{
return false;
}

// Normalize only the topology path: "/" becomes empty and "/tenant-a/" becomes
// "/tenant-a". Keep the address path unchanged while removing the topology prefix below.
var basePath = topologyAddress.AbsolutePath.AsSpan().TrimEnd('/');
var path = address.AbsolutePath.AsSpan();
ReadOnlySpan<char> relativePath;

if (basePath.IsEmpty)
{
// A root topology consumes exactly one structural slash:
// "/q/orders" becomes "q/orders". The shorthand "rabbitmq:q/orders" has no
// leading slash and is handled by the transport-specific branch above.
if (path.IsEmpty || path[0] is not '/')
{
return false;
}

relativePath = path[1..];
}
else
{
// Remove the exact virtual-host path and its separator:
// "/tenant-a/q/orders" becomes "q/orders". Requiring the separator prevents
// "/tenant-a" from matching "/tenant-a-other/q/orders".
if (path.Length <= basePath.Length
|| !path.StartsWith(basePath, StringComparison.Ordinal)
|| path[basePath.Length] is not '/')
{
return false;
}

relativePath = path[(basePath.Length + 1)..];
}

// Require "{kind}/{name}", where kind is one character. Everything after the first
// slash is the name, so a name containing a slash, such as "q/nested/queue", stays intact.
if (relativePath.Length < 3 || relativePath[1] is not '/')
{
return false;
}

kind = relativePath[0];
// AbsolutePath is escaped, so "q/space%20name" must produce "space name".
name = Uri.UnescapeDataString(relativePath[2..].ToString());
return true;
}
}

file static class Extensions
Expand Down
Loading
Loading