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 @@ -8,7 +8,6 @@

namespace Wolverine.AmazonSns.Tests;

[Trait("Category", "Flaky")]
public class send_to_topic_and_receive_in_queue_in_aws : IAsyncLifetime
{
private IHost _host = null!;
Expand Down Expand Up @@ -53,7 +52,11 @@ public async ValueTask DisposeAsync()
_host.Dispose();
}

[Fact]
// Line-for-line the same test as send_to_topic_and_receive_in_queue, except that it points at a
// real AWS account instead of LocalStack. It is here to be run by hand when SNS fidelity is in
// question; CI has no credentials, so it can only ever fail there. Skipped rather than tagged
// Flaky (#3763) because there is nothing unstable about it.
[Fact(Skip = "Requires real AWS credentials; the LocalStack twin is send_to_topic_and_receive_in_queue")]
public async Task send_to_topic_and_receive_in_queue_a_single_message()
{
var message = new SnsMessage("Josh Allen");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,83 @@ public void findEndpointByUri_should_correctly_create_endpoint_if_it_doesnt_exis
transport.Queues.Count.ShouldBe(2);
result2.EndpointName.ShouldBe(queueName2);
}
}
}
// GH-3763. Conventional routing derives queue names from message type names, so it can hand SQS a
// name it rejects outright -- and because broker initialization provisions every queue together,
// one bad name fails startup for every conventionally-routed host in the assembly. See GH-3786 for
// the same defect on Azure Service Bus.
public class sanitizing_sqs_queue_names
{
// "Can only include alphanumeric characters, hyphens, or underscores. 1 to 80 in length"
[Theory]
[InlineData("Wolverine.Bugs.BatchedItem[]", "Wolverine-Bugs-BatchedItem__")]
[InlineData("Wolverine.Envelope`1[System.String]", "Wolverine-Envelope_1_System-String_")]
[InlineData("Outer+Inner", "Outer_Inner")]
[InlineData("has spaces", "has_spaces")]
public void illegal_characters_are_substituted(string identifier, string expected)
{
AmazonSqsTransport.SanitizeSqsName(identifier).ShouldBe(expected);
}

// Substituting rather than stripping is what keeps distinct type names distinct.
[Fact]
public void sanitizing_does_not_collide_an_array_type_with_its_element_type()
{
AmazonSqsTransport.SanitizeSqsName("BatchedItem[]")
.ShouldNotBe(AmazonSqsTransport.SanitizeSqsName("BatchedItem"));
}

// No name that works today may change: a name SQS rejects could never have been provisioned in
// the first place, so this must stay a pure no-op for legal names of legal length.
[Theory]
[InlineData("Wolverine.Bugs.BatchedItem", "Wolverine-Bugs-BatchedItem")]
[InlineData("two-dead-letter-queue", "two-dead-letter-queue")]
[InlineData("wolverine_retries_MyService", "wolverine_retries_MyService")]
[InlineData("wolverine.retries.MyService.fifo", "wolverine-retries-MyService.fifo")]
public void legal_identifiers_are_left_alone(string identifier, string expected)
{
AmazonSqsTransport.SanitizeSqsName(identifier).ShouldBe(expected);
}

// The case that actually broke CI: "shazaam-" + a namespace-qualified type name is 81 characters.
[Fact]
public void an_overlong_name_is_brought_under_the_limit()
{
var name = AmazonSqsTransport.SanitizeSqsName(
"shazaam-Wolverine.AmazonSqs.Tests.ConventionalRouting.SqsHandlerTypeNamingMessage");

name.Length.ShouldBe(AmazonSqsTransport.MaximumQueueNameLength);
name.ShouldStartWith("shazaam-Wolverine-AmazonSqs-Tests-ConventionalRouting-SqsHandlerType");
}

// Truncation alone would collide two long names that share a prefix -- and namespace-qualified
// type names very often do.
[Fact]
public void two_overlong_names_sharing_a_prefix_stay_distinct()
{
var prefix = new string('a', AmazonSqsTransport.MaximumQueueNameLength);

AmazonSqsTransport.SanitizeSqsName(prefix + "One")
.ShouldNotBe(AmazonSqsTransport.SanitizeSqsName(prefix + "Two"));
}

// The digest has to be stable across processes and machines, which rules out GetHashCode().
[Fact]
public void truncation_is_deterministic()
{
var identifier = new string('b', 200);

AmazonSqsTransport.SanitizeSqsName(identifier)
.ShouldBe("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-aaebc35c");
}

// AWS requires the .fifo suffix, and it counts against the 80 character budget.
[Fact]
public void the_fifo_suffix_survives_truncation()
{
var name = AmazonSqsTransport.SanitizeSqsName(new string('c', 200) + ".fifo");

name.Length.ShouldBe(AmazonSqsTransport.MaximumQueueNameLength);
name.ShouldEndWith(".fifo");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

// https://github.com/JasperFx/wolverine/issues/3633
public class Bug_3633_conventional_routing_respects_named_broker : IDisposable
public class Bug_3633_conventional_routing_respects_named_broker : IAsyncLifetime
{
private static readonly BrokerName theBrokerName = new("other");
private readonly IHost _host;
private IHost _host = null!;

public Bug_3633_conventional_routing_respects_named_broker()
public async ValueTask InitializeAsync()
{
_host = Host.CreateDefaultBuilder()
_host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// A default, unnamed broker is also registered so that conventional
Expand All @@ -25,11 +25,13 @@ public Bug_3633_conventional_routing_respects_named_broker()
.UseConventionalRouting(x => x.IncludeTypes(t => t == typeof(RoutedMessage)))
.AutoProvision()
.AutoPurgeOnStartup();
}).Start();
}).StartAsync();
}

public void Dispose()
// StopAsync, not just Dispose -- see the note in ConventionalRoutingContext (GH-3763).
public async ValueTask DisposeAsync()
{
await _host.StopAsync();
_host.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,65 @@

namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

public abstract class ConventionalRoutingContext : IDisposable
/// <summary>
/// Every class in this namespace listens to the same small set of LocalStack queues -- most of them
/// to <c>sqs://routed</c>. A host that is disposed but never stopped keeps long-polling that queue,
/// so it steals messages from whichever class runs next and the theft looks like flakiness. See
/// GH-3763.
///
/// This type owns disposal for that reason: derived classes override <see cref="InitializeAsync"/>
/// rather than re-declaring <see cref="IAsyncLifetime"/> themselves. Re-declaring it is what let a
/// no-op <c>DisposeAsync</c> shadow the real one here and in the Azure Service Bus fixtures
/// (GH-3758) -- an explicit interface implementation on a derived class silently wins.
/// </summary>
public abstract class ConventionalRoutingContext : IAsyncLifetime
{
private IHost _host = null!;

public virtual ValueTask InitializeAsync() => ValueTask.CompletedTask;

internal async Task<IWolverineRuntime> theRuntime()
{
_host ??= await WolverineHost.ForAsync(opts =>
opts.UseAmazonSqsTransport().UseConventionalRouting().AutoProvision().AutoPurgeOnStartup());
opts.UseAmazonSqsTransportLocally().UseConventionalRouting(leaveTheEndToEndQueueAlone).AutoProvision()
.AutoPurgeOnStartup());

return _host.Services.GetRequiredService<IWolverineRuntime>();
}

public void Dispose()
/// <summary>
/// These classes only assert on configuration, but they still stand up real listeners for every
/// handler in the assembly -- which would include the queue end_to_end_with_conventional_routing
/// is trying to receive on, in a different worker process. An ExcludeTypes rather than an
/// IncludeTypes: excludes are ANDed and includes are ORed, so an include would silently widen a
/// test's own filter in ConfigureConventions. See GH-3763.
/// </summary>
private static void leaveTheEndToEndQueueAlone(AmazonSqsMessageRoutingConvention convention)
{
_host?.Dispose();
convention.ExcludeTypes(t => t == typeof(EndToEndRoutedMessage));
}

public async ValueTask DisposeAsync()
{
if (_host == null) return;

// StopAsync, not just Dispose: IHost.Dispose() tears down the container without ever
// running IHostedService.StopAsync, which leaves the SQS listeners polling.
await _host.StopAsync();
_host.Dispose();
_host = null!;
}

internal async Task ConfigureConventions(Action<AmazonSqsMessageRoutingConvention> configure)
{
_host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseAmazonSqsTransport().UseConventionalRouting(configure).AutoProvision()
opts.UseAmazonSqsTransportLocally().UseConventionalRouting(c =>
{
leaveTheEndToEndQueueAlone(c);
configure(c);
}).AutoProvision()
.AutoPurgeOnStartup();
}).StartAsync();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Wolverine.Attributes;

namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

/// <summary>
/// end_to_end_with_conventional_routing needs a queue nothing else in this namespace listens to.
///
/// The CI shard runs this project across three worker PROCESSES partitioned by test class, so
/// CollectionPerAssembly only buys serialization inside one process. Every other conventional
/// routing class here stands up a host listening at <c>sqs://routed</c> for
/// <see cref="RoutedMessage"/>; a concurrent worker holding that listener receives the end-to-end
/// message and the tracked session times out waiting for a delivery that already happened
/// somewhere else. Its own message type gives it its own queue. See GH-3763.
/// </summary>
[MessageIdentity("end-to-end-routed")]
public class EndToEndRoutedMessage;

public class EndToEndRoutedMessageHandler
{
public void Handle(EndToEndRoutedMessage message)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

[Trait("Category", "Flaky")]
public class conventional_listener_discovery : ConventionalRoutingContext
{
[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@
using Xunit;
namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

[Trait("Category", "Flaky")]
public class discover_with_naming_prefix : IDisposable
public class discover_with_naming_prefix : IAsyncLifetime
{
private readonly IHost _host;
private readonly ITestOutputHelper _output;
private IHost _host = null!;

public discover_with_naming_prefix(ITestOutputHelper output)
public async ValueTask InitializeAsync()
{
_output = output;
_host = Host.CreateDefaultBuilder()
_host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseAmazonSqsTransport().PrefixIdentifiers("zztop").UseConventionalRouting().AutoProvision()
opts.UseAmazonSqsTransportLocally().PrefixIdentifiers("zztop").UseConventionalRouting().AutoProvision()
.AutoPurgeOnStartup();
}).Start();
}).StartAsync();
}

public void Dispose()
// StopAsync, not just Dispose -- see the note in ConventionalRoutingContext (GH-3763).
public async ValueTask DisposeAsync()
{
await _host.StopAsync();
_host.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@

namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

[Trait("Category", "Flaky")]
public class end_to_end_with_conventional_routing : IAsyncLifetime, IDisposable
public class end_to_end_with_conventional_routing : IAsyncLifetime
{
private IHost _receiver = null!;
private IHost _sender = null!;
Expand All @@ -16,24 +15,28 @@ public async ValueTask InitializeAsync()
{
_sender = await WolverineHost.ForAsync(opts =>
{
opts.UseAmazonSqsTransport().UseConventionalRouting().AutoProvision().AutoPurgeOnStartup();
opts.UseAmazonSqsTransportLocally().UseConventionalRouting().AutoProvision().AutoPurgeOnStartup();
opts.DisableConventionalDiscovery();
opts.ServiceName = "Sender";
});

_receiver = await WolverineHost.ForAsync(opts =>
{
opts.UseAmazonSqsTransport().UseConventionalRouting().AutoProvision().AutoPurgeOnStartup();
opts.UseAmazonSqsTransportLocally().UseConventionalRouting().AutoProvision().AutoPurgeOnStartup();
opts.ServiceName = "Receiver";
});
}

ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;

public void Dispose()
// StopAsync, not just Dispose: IHost.Dispose() tears down the container without ever running
// IHostedService.StopAsync, so the SQS listeners keep polling and steal messages from the next
// class in this namespace. See GH-3763.
public async ValueTask DisposeAsync()
{
_sender?.Dispose();
_receiver?.Dispose();
await _sender.StopAsync();
_sender.Dispose();

await _receiver.StopAsync();
_receiver.Dispose();
}

[Fact]
Expand All @@ -43,11 +46,11 @@ public async Task send_from_one_node_to_another_all_with_conventional_routing()
.AlsoTrack(_receiver)
.IncludeExternalTransports()
.Timeout(30.Seconds())
.SendMessageAndWaitAsync(new RoutedMessage());
.SendMessageAndWaitAsync(new EndToEndRoutedMessage());

var received = session
.AllRecordsInOrder()
.Where(x => x.Envelope!.Message!.GetType() == typeof(RoutedMessage))
.Where(x => x.Envelope!.Message!.GetType() == typeof(EndToEndRoutedMessage))
.Single(x => x.MessageEventType == MessageEventType.Received);

received
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@

namespace Wolverine.AmazonSqs.Tests.ConventionalRouting;

[Trait("Category", "Flaky")]
public class end_to_end_with_conventional_routing_with_prefix : IAsyncLifetime, IDisposable
public class end_to_end_with_conventional_routing_with_prefix : IAsyncLifetime
{
private IHost _receiver = null!;
private IHost _sender = null!;
Expand All @@ -16,7 +15,7 @@ public async ValueTask InitializeAsync()
{
_sender = await WolverineHost.ForAsync(opts =>
{
opts.UseAmazonSqsTransport()
opts.UseAmazonSqsTransportLocally()
.PrefixIdentifiers("shazaam")
.UseConventionalRouting().AutoProvision().AutoPurgeOnStartup();
opts.DisableConventionalDiscovery();
Expand All @@ -25,19 +24,23 @@ public async ValueTask InitializeAsync()

_receiver = await WolverineHost.ForAsync(opts =>
{
opts.UseAmazonSqsTransport()
opts.UseAmazonSqsTransportLocally()
.PrefixIdentifiers("shazaam")
.UseConventionalRouting().AutoProvision().AutoPurgeOnStartup();
opts.ServiceName = "Receiver";
});
}

ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;

public void Dispose()
// StopAsync, not just Dispose: IHost.Dispose() tears down the container without ever running
// IHostedService.StopAsync, so the SQS listeners keep polling and steal messages from the next
// class in this namespace. See GH-3763.
public async ValueTask DisposeAsync()
{
_sender?.Dispose();
_receiver?.Dispose();
await _sender.StopAsync();
_sender.Dispose();

await _receiver.StopAsync();
_receiver.Dispose();
}

[Fact]
Expand Down
Loading
Loading