From b7df847f4f4349277f025616d0559785cd8dbb7d Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:56:53 +0200 Subject: [PATCH 01/16] Initial GCP Pub/Sub transporter --- .gitignore | 3 + docker-compose.yml | 25 ++- .../BufferedSendingAndReceivingCompliance.cs | 73 +++++++ .../Internal/PubsubSubscriptionTests.cs | 74 ++++++++ .../Internal/PubsubTopicTests.cs | 57 ++++++ .../NoParallelization.cs | 3 + .../PubsubTransportTests.cs | 54 ++++++ .../TestPubsubEnvelopeMapper.cs | 26 +++ .../TestingExtensions.cs | 9 + .../Wolverine.Pubsub.Tests.csproj | 38 ++++ .../GCP/Wolverine.Pubsub.Tests/end_to_end.cs | 91 +++++++++ .../Wolverine.Pubsub/AssemblyAttributes.cs | 3 + .../Wolverine.Pubsub/IPubsubEnvelopeMapper.cs | 9 + .../Internal/BatchedPubsubListener.cs | 56 ++++++ .../Internal/InlinePubsubListener.cs | 25 +++ .../Internal/InlinePubsubSender.cs | 37 ++++ .../Internal/PubsubEndpoint.cs | 59 ++++++ .../Internal/PubsubEnvelope.cs | 9 + .../Internal/PubsubEnvelopeMapper.cs | 83 ++++++++ .../Internal/PubsubListener.cs | 178 ++++++++++++++++++ .../Internal/PubsubSenderProtocol.cs | 62 ++++++ .../Internal/PubsubSubscription.cs | 135 +++++++++++++ .../Wolverine.Pubsub/Internal/PubsubTopic.cs | 106 +++++++++++ .../Wolverine.Pubsub/PubsubConfiguration.cs | 77 ++++++++ .../PubsubMessageRoutingConvention.cs | 38 ++++ .../PubsubSubscriptionConfiguration.cs | 77 ++++++++ .../PubsubSubscriptionOptions.cs | 22 +++ ...ubsubTopicBroadcastingRoutingConvention.cs | 61 ++++++ .../PubsubTopicConfiguration.cs | 19 ++ .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 128 +++++++++++++ .../PubsubTransportExtensions.cs | 92 +++++++++ .../Wolverine.Pubsub/Wolverine.Pubsub.csproj | 16 ++ .../WolverinePubsubTransportException.cs | 5 + ...inePubsubTransportNotConnectedException.cs | 5 + wolverine.sln | 22 +++ 35 files changed, 1776 insertions(+), 1 deletion(-) create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/NoParallelization.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/AssemblyAttributes.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj create mode 100644 src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportException.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs diff --git a/.gitignore b/.gitignore index a63b702ee..112b37b77 100644 --- a/.gitignore +++ b/.gitignore @@ -166,6 +166,9 @@ ClientBin/ *.publishsettings node_modules/ bower_components/ +.vscode/ +.editorconfig +omnisharp.json # RIA/Silverlight projects Generated_Code/ diff --git a/docker-compose.yml b/docker-compose.yml index 1e190ac6e..2964e32a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,12 +13,35 @@ services: - POSTGRES_DATABASE=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres + + gcp-pubsub: + image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators + ports: + - "8085:8085" + command: + [ + "gcloud", + "--quiet", + "beta", + "emulators", + "pubsub", + "start", + "--host-port", + "0.0.0.0:8085", + "--project", + "wolverine", + "--verbosity", + "debug", + "--log-http", + "--user-output-enabled", + ] rabbitmq: image: "rabbitmq:management" ports: - "5672:5672" - "15672:15672" + sqlserver: image: "mcr.microsoft.com/azure-sql-edge" ports: @@ -27,6 +50,7 @@ services: - "ACCEPT_EULA=Y" - "SA_PASSWORD=P@55w0rd" - "MSSQL_PID=Developer" + pulsar: image: "apachepulsar/pulsar:latest" ports: @@ -34,7 +58,6 @@ services: - "8080:8080" command: bin/pulsar standalone - localstack: image: localstack/localstack:stable ports: diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs new file mode 100644 index 000000000..6191d7f7b --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Wolverine.ComplianceTests.Compliance; +using Wolverine.Runtime; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class BufferedComplianceFixture : TransportComplianceFixture, IAsyncLifetime { + public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://buffered-receiver"), 120) { } + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + var topicName = Guid.NewGuid().ToString(); + + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://" + topicName); + + await SenderIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); + opts + .PublishAllMessages() + .ToPubsubTopic(topicName); + }); + + await ReceiverIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); + opts + .ListenToPubsubTopic(topicName) + .BufferedInMemory(); + }); + } + + public new async Task DisposeAsync() { + await DisposeAsync(); + } +} + +[Collection("acceptance")] +public class BufferedSendingAndReceivingCompliance : TransportCompliance { + [Fact] + public virtual async Task dl_mechanics() { + throwOnAttempt(1); + throwOnAttempt(2); + throwOnAttempt(3); + + await shouldMoveToErrorQueueOnAttempt(1); + + var runtime = theReceiver.Services.GetRequiredService(); + + var transport = runtime.Options.Transports.GetOrCreate(); + var dlSubscription = transport.Topics[PubsubTransport.DeadLetterName].FindOrCreateSubscription(); + + await dlSubscription.InitializeAsync(NullLogger.Instance); + + var pullResponse = await transport.SubscriberApiClient!.PullAsync( + dlSubscription.Name, + maxMessages: 5 + ); + + pullResponse.ReceivedMessages.ShouldNotBeEmpty(); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs new file mode 100644 index 000000000..081f40aaa --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs @@ -0,0 +1,74 @@ +using Google.Api.Gax; +using Google.Cloud.PubSub.V1; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; +using Wolverine.Configuration; +using Wolverine.Pubsub.Internal; +using Xunit; + +namespace Wolverine.Pubsub.Tests.Internal; + +public class PubsubSubscriptionTests { + private PubsubTransport createTransport() => new("wolverine") { + PublisherApiClient = Substitute.For(), + SubscriberApiClient = Substitute.For(), + EmulatorDetection = EmulatorDetection.EmulatorOnly, + }; + + [Fact] + public void default_dead_letter_name_is_transport_default() { + new PubsubTopic("foo", createTransport()).FindOrCreateSubscription("bar") + .DeadLetterName.ShouldBe(PubsubTransport.DeadLetterName); + } + + [Fact] + public void default_mode_is_buffered() { + new PubsubTopic("foo", createTransport()).FindOrCreateSubscription("bar") + .Mode.ShouldBe(EndpointMode.BufferedInMemory); + } + + [Fact] + public void create_uri() { + var topic = new PubsubTopic("top1", createTransport()); + var subscription = topic.FindOrCreateSubscription("sub1"); + + subscription.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://top1/sub1")); + } + + [Fact] + public void endpoint_name_is_subscription_name_without_prefix() { + var topic = new PubsubTopic("top1", createTransport()); + var subscription = topic.FindOrCreateSubscription("sub1"); + + subscription.EndpointName.ShouldBe("sub1"); + } + + [Fact] + public async Task initialize_with_no_auto_provision() { + var transport = createTransport(); + var topic = new PubsubTopic("foo", transport); + var subscription = topic.FindOrCreateSubscription("bar"); + + await subscription.InitializeAsync(NullLogger.Instance); + + await transport.SubscriberApiClient!.DidNotReceive().CreateSubscriptionAsync(Arg.Any()); + } + + [Fact] + public async Task initialize_with_auto_provision() { + var transport = createTransport(); + + transport.AutoProvision = true; + + var topic = new PubsubTopic("foo", transport); + var subscription = topic.FindOrCreateSubscription("bar"); + + await subscription.InitializeAsync(NullLogger.Instance); + + await transport.SubscriberApiClient!.Received().CreateSubscriptionAsync(Arg.Is(x => + x.SubscriptionName == subscription.Name && + x.TopicAsTopicName == topic.Name + )); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs new file mode 100644 index 000000000..38cbb776c --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs @@ -0,0 +1,57 @@ +using Google.Api.Gax; +using Google.Cloud.PubSub.V1; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Shouldly; +using Wolverine.Pubsub.Internal; +using Xunit; + +namespace Wolverine.Pubsub.Tests.Internal; + +public class PubsubTopicTests { + private PubsubTransport createTransport() => new("wolverine") { + PublisherApiClient = Substitute.For(), + SubscriberApiClient = Substitute.For(), + EmulatorDetection = EmulatorDetection.EmulatorOnly + }; + + [Fact] + public void create_uri() { + var topic = new PubsubTopic("top1", createTransport()); + + topic.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://top1")); + } + + [Fact] + public void endpoint_name_is_topic_name_without_prefix() { + var topic = new PubsubTopic("top1", createTransport()); + + topic.EndpointName.ShouldBe("top1"); + } + + [Fact] + public async Task initialize_with_no_auto_provision() { + var transport = createTransport(); + var topic = new PubsubTopic("foo", transport); + + await topic.InitializeAsync(NullLogger.Instance); + + await transport.PublisherApiClient!.DidNotReceive().CreateTopicAsync(Arg.Any()); + } + + [Fact] + public async Task initialize_with_auto_provision() { + var transport = createTransport(); + + transport.AutoProvision = true; + + var topic = new PubsubTopic("foo", transport); + + transport.PublisherApiClient!.GetTopicAsync(Arg.Is(topic.Name)).Throws(); + + await topic.InitializeAsync(NullLogger.Instance); + + await transport.PublisherApiClient!.Received().CreateTopicAsync(Arg.Is(topic.Name)); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/NoParallelization.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/NoParallelization.cs new file mode 100644 index 000000000..b1fa88422 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/NoParallelization.cs @@ -0,0 +1,3 @@ +using Xunit; + +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs new file mode 100644 index 000000000..c4d3dea90 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs @@ -0,0 +1,54 @@ +using Shouldly; +using Wolverine.Pubsub.Internal; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class PubsubTransportTests { + [Fact] + public void find_topic_by_uri() { + var transport = new PubsubTransport("wolverine"); + var topic = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://one")).ShouldBeOfType(); + + topic.Name.TopicId.ShouldBe(PubsubTransport.SanitizePubsubName("one")); + } + + [Fact] + public void find_subscription_by_uri() { + var transport = new PubsubTransport("wolverine"); + var subscription = transport + .GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://one/red")) + .ShouldBeOfType(); + + subscription.Name.SubscriptionId.ShouldBe(PubsubTransport.SanitizePubsubName("red")); + } + + [Fact] + public void response_subscriptions_are_disabled_by_default() { + var transport = new PubsubTransport("wolverine"); + + transport.SystemEndpointsEnabled.ShouldBeFalse(); + } + + [Fact] + public void return_all_endpoints_gets_dead_letter_subscription_too() { + var transport = new PubsubTransport("wolverine") { + EnableDeadLettering = true + }; + var one = transport.Topics["one"].FindOrCreateSubscription(); + var two = transport.Topics["two"].FindOrCreateSubscription(); + var three = transport.Topics["three"].FindOrCreateSubscription(); + + one.DeadLetterName = null; + two.DeadLetterName = "two-dead-letter"; + + var endpoints = transport.Endpoints().OfType().ToArray(); + + endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("one")); + endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("two")); + endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("three")); + + endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName(PubsubTransport.DeadLetterName)); + endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("two-dead-letter")); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs new file mode 100644 index 000000000..20b97b36d --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs @@ -0,0 +1,26 @@ +using JasperFx.Core; +using Wolverine.Configuration; +using Wolverine.Pubsub.Internal; +using Wolverine.Runtime.Serialization; + +namespace Wolverine.Pubsub.Tests; + +internal class TestPubsubEnvelopeMapper : PubsubEnvelopeMapper { + private SystemTextJsonSerializer _serializer = new(SystemTextJsonSerializer.DefaultOptions()); + + public TestPubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { + MapProperty( + x => x.Message!, + (e, m) => { + if (e.Data is null || e.MessageType.IsEmpty()) return; + + var type = Type.GetType(e.MessageType); + + if (type is null) return; + + e.Message = _serializer.ReadFromData(type, e); + }, + (e, m) => { } + ); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs new file mode 100644 index 000000000..890563969 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs @@ -0,0 +1,9 @@ +using Google.Api.Gax; + +namespace Wolverine.Pubsub.Tests; + +public static class TestingExtensions { + public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) => options.UsePubsub("wolverine", opts => { + opts.EmulatorDetection = EmulatorDetection.EmulatorOnly; + }); +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj b/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj new file mode 100644 index 000000000..36518cbff --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj @@ -0,0 +1,38 @@ + + + + false + + + + + + + + + + + + + + + + + + + Servers.cs + + + + + + + + + + Always + Always + + + diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs new file mode 100644 index 000000000..7cf4f2833 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs @@ -0,0 +1,91 @@ +using JasperFx.Core; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Wolverine.Pubsub.Internal; +using Wolverine.Configuration; +using Wolverine.Tracking; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class end_to_end : IAsyncLifetime { + private IHost _host = default!; + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + _host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => { + opts.UsePubsubTesting().AutoProvision(); + opts.ListenToPubsubTopic("send_and_receive").ConfigureSubscription(x => x.Mapper = new TestPubsubEnvelopeMapper(x)); + opts.PublishMessage().ToPubsubTopic("send_and_receive"); + }).StartAsync(); + } + + public async Task DisposeAsync() { + await _host.StopAsync(); + } + + [Fact] + public void system_endpoints_disabled_by_default() { + var transport = _host.GetRuntime().Options.Transports.GetOrCreate(); + var endpoints = transport + .Endpoints() + .Where(x => x.Role == EndpointRole.System) + .OfType().ToArray(); + + endpoints.Any().ShouldBeFalse(); + } + + [Fact] + public async Task builds_system_endpoints() { + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => { + opts.UsePubsubTesting() + .AutoProvision() + .SystemEndpointsAreEnabled(true); + opts.ListenToPubsubTopic("send_and_receive"); + opts.PublishAllMessages().ToPubsubTopic("send_and_receive"); + }).StartAsync(); + var transport = host.GetRuntime().Options.Transports.GetOrCreate(); + var endpoints = transport + .Endpoints() + .Where(x => x.Role == EndpointRole.System); + var topics = endpoints.OfType().ToArray(); + var subscriptions = endpoints.OfType().ToArray(); + + topics.ShouldContain(x => x.Name.TopicId.StartsWith(PubsubTransport.ResponseName)); + subscriptions.ShouldContain(x => x.Name.SubscriptionId.StartsWith(PubsubTransport.ResponseName)); + } + + [Fact] + public async Task send_and_receive_a_single_message() { + var message = new PubsubMessage1("Josh Allen"); + var session = await _host.TrackActivity() + .IncludeExternalTransports() + .Timeout(1.Minutes()) + .SendMessageAndWaitAsync(message); + + session.Received.SingleMessage().Name.ShouldBe(message.Name); + } + + [Fact] + public async Task send_and_receive_multiple_messages_concurreently() { + var session = await _host.TrackActivity() + .IncludeExternalTransports() + .Timeout(1.Minutes()) + .ExecuteAndWaitAsync(ctx => Task.Run(async () => { + await ctx.SendAsync(new PubsubMessage1("Red")); + await ctx.SendAsync(new PubsubMessage1("Green")); + await ctx.SendAsync(new PubsubMessage1("Refactor")); + })); + var received = session.Received.MessagesOf().Select(x => x.Name).ToArray(); + + received.ShouldContain("Red"); + received.ShouldContain("Green"); + received.ShouldContain("Refactor"); + } +} + +public record PubsubMessage1(string Name); diff --git a/src/Transports/GCP/Wolverine.Pubsub/AssemblyAttributes.cs b/src/Transports/GCP/Wolverine.Pubsub/AssemblyAttributes.cs new file mode 100644 index 000000000..0db32af81 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Wolverine.Pubsub.Tests")] diff --git a/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs new file mode 100644 index 000000000..d155ce26e --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs @@ -0,0 +1,9 @@ +using Google.Cloud.PubSub.V1; +using Wolverine.Transports; + +namespace Wolverine.Pubsub; + +/// +/// Pluggable strategy for reading and writing data to Google Cloud Pub/Sub +/// +public interface IPubsubEnvelopeMapper : IEnvelopeMapper; diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs new file mode 100644 index 000000000..51fc85212 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs @@ -0,0 +1,56 @@ +using Google.Api.Gax.Grpc; +using JasperFx.Core; +using Microsoft.Extensions.Logging; +using Wolverine.Runtime; +using Wolverine.Transports; +using Wolverine.Util.Dataflow; + +namespace Wolverine.Pubsub.Internal; + +public class BatchedPubsubListener : PubsubListener { + public BatchedPubsubListener( + PubsubSubscription endpoint, + PubsubTransport transport, + IReceiver receiver, + IWolverineRuntime runtime + ) : base(endpoint, transport, receiver, runtime) { } + + public override async Task StartAsync() { + if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + using var streamingPull = _transport.SubscriberApiClient.StreamingPull(CallSettings.FromCancellationToken(_cancellation.Token)); + + await streamingPull.WriteAsync(new() { + SubscriptionAsSubscriptionName = _endpoint.Name, + StreamAckDeadlineSeconds = 20, + MaxOutstandingMessages = _endpoint.PubsubOptions.MaxOutstandingMessages, + MaxOutstandingBytes = _endpoint.PubsubOptions.MaxOutstandingByteCount, + }); + + await using var stream = streamingPull.GetResponseStream(); + + _complete = new RetryBlock( + async (envelopes, _) => { + await streamingPull.WriteAsync(new() { AckIds = { envelopes.Select(x => x.AckId).ToArray() } }); + }, + _logger, + _cancellation.Token + ); + + try { + await listenForMessagesAsync(async () => { + while (await stream.MoveNextAsync(_cancellation.Token)) { + await handleMessagesAsync(stream.Current.ReceivedMessages); + } + }); + } + finally { + try { + await streamingPull.WriteCompleteAsync(); + } + catch (Exception ex) { + _logger.LogError(ex, "{Uri}: Error while completing the Google Cloud Pub/Sub streaming pull.", _endpoint.Uri); + } + } + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs new file mode 100644 index 000000000..825397f9a --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs @@ -0,0 +1,25 @@ +using Wolverine.Runtime; +using Wolverine.Transports; + +namespace Wolverine.Pubsub.Internal; + +public class InlinePubsubListener : PubsubListener { + public InlinePubsubListener( + PubsubSubscription endpoint, + PubsubTransport transport, + IReceiver receiver, + IWolverineRuntime runtime + ) : base(endpoint, transport, receiver, runtime) { } + + public override Task StartAsync() => listenForMessagesAsync(async () => { + if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + var response = await _transport.SubscriberApiClient.PullAsync( + _endpoint.Name, + maxMessages: 1, + _cancellation.Token + ); + + await handleMessagesAsync(response.ReceivedMessages); + }); +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs new file mode 100644 index 000000000..c93836155 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs @@ -0,0 +1,37 @@ +using Google.Cloud.PubSub.V1; +using Microsoft.Extensions.Logging; +using Wolverine.Runtime; +using Wolverine.Transports.Sending; + +namespace Wolverine.Pubsub.Internal; + +public class InlinePubsubSender : ISender { + private readonly PubsubTopic _topic; + private readonly ILogger _logger; + + public bool SupportsNativeScheduledSend => false; + public Uri Destination => _topic.Uri; + + public InlinePubsubSender( + PubsubTopic topic, + IWolverineRuntime runtime + ) { + _topic = topic; + _logger = runtime.LoggerFactory.CreateLogger(); + } + + public async Task PingAsync() { + var envelope = Envelope.ForPing(Destination); + + try { + await SendAsync(envelope); + + return true; + } + catch (Exception) { + return false; + } + } + + public async ValueTask SendAsync(Envelope envelope) => await _topic.SendMessageAsync(envelope, _logger); +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs new file mode 100644 index 000000000..217738c36 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Logging; +using Wolverine.Configuration; +using Wolverine.Transports; + +namespace Wolverine.Pubsub.Internal; + +public abstract class PubsubEndpoint : Endpoint, IBrokerEndpoint { + private IPubsubEnvelopeMapper? _mapper; + protected readonly PubsubTransport _transport; + + protected bool _hasInitialized = false; + + /// + /// Pluggable strategy for interoperability with non-Wolverine systems. Customizes how the incoming Google Cloud Pub/Sub messages + /// are read and how outgoing messages are written to Google Cloud Pub/Sub. + /// + public IPubsubEnvelopeMapper Mapper { + get { + if (_mapper is not null) return _mapper; + + var mapper = new PubsubEnvelopeMapper(this); + + // Important for interoperability + if (MessageType != null) mapper.ReceivesMessage(MessageType); + + _mapper = mapper; + + return _mapper; + } + set => _mapper = value; + } + + public PubsubEndpoint( + Uri uri, + PubsubTransport transport, + EndpointRole role = EndpointRole.Application + ) : base(uri, role) { + _transport = transport; + } + + public override async ValueTask InitializeAsync(ILogger logger) { + if (_hasInitialized) return; + + try { + if (_transport.AutoProvision) await SetupAsync(logger); + } + catch (Exception ex) { + throw new WolverinePubsubTransportException($"{Uri}: Error trying to initialize Google Cloud Pub/Sub endpoint", ex); + } + + _hasInitialized = true; + } + + public abstract ValueTask SetupAsync(ILogger logger); + public abstract ValueTask CheckAsync(); + public abstract ValueTask TeardownAsync(ILogger logger); + + protected override bool supportsMode(EndpointMode mode) => true; +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs new file mode 100644 index 000000000..397b5cc9d --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs @@ -0,0 +1,9 @@ +namespace Wolverine.Pubsub.Internal; + +public class PubsubEnvelope : Envelope { + public string AckId { get; set; } = string.Empty; + + public PubsubEnvelope(string ackId) { + AckId = ackId; + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs new file mode 100644 index 000000000..de76dbc1e --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs @@ -0,0 +1,83 @@ +using Google.Cloud.PubSub.V1; +using Google.Protobuf; +using Wolverine.Configuration; +using Wolverine.Transports; + +namespace Wolverine.Pubsub.Internal; + +internal class PubsubEnvelopeMapper : EnvelopeMapper, IPubsubEnvelopeMapper { + public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { + MapProperty( + x => x.ContentType!, + (e, m) => e.ContentType = m.Attributes["content-type"], + (e, m) => { + if (e.ContentType is null) return; + + m.Attributes["content-type"] = e.ContentType; + } + ); + MapProperty( + x => x.Data!, + (e, m) => e.Data = m.Data.ToByteArray(), + (e, m) => { + if (e.Data is null) return; + + m.Data = ByteString.CopyFrom(e.Data); + } + ); + MapProperty( + x => x.Id, + (e, m) => { + if (Guid.TryParse(m.Attributes["wolverine-id"], out var id)) { + e.Id = id; + } + }, + (e, m) => m.Attributes["wolverine-id"] = e.Id.ToString() + ); + MapProperty( + x => x.CorrelationId!, + (e, m) => e.CorrelationId = m.Attributes["wolverine-correlation-id"], + (e, m) => { + if (e.CorrelationId is null) return; + + m.Attributes["wolverine-correlation-id"] = e.CorrelationId; + } + ); + MapProperty( + x => x.MessageType!, + (e, m) => e.MessageType = m.Attributes["wolverine-message-type"], + (e, m) => { + if (e.MessageType is null) return; + + m.Attributes["wolverine-message-type"] = e.MessageType; + } + ); + MapProperty( + x => x.GroupId!, + (e, m) => e.GroupId = m.OrderingKey, + (e, m) => m.OrderingKey = e.GroupId ?? string.Empty + ); + } + + protected override void writeOutgoingHeader(PubsubMessage outgoing, string key, string value) { + outgoing.Attributes[key] = value; + } + + protected override void writeIncomingHeaders(PubsubMessage incoming, Envelope envelope) { + if (incoming.Attributes is null) return; + + foreach (var pair in incoming.Attributes) envelope.Headers[pair.Key] = pair.Value?.ToString(); + } + + protected override bool tryReadIncomingHeader(PubsubMessage incoming, string key, out string? value) { + if (incoming.Attributes.TryGetValue(key, out var header)) { + value = header.ToString(); + + return true; + } + + value = null; + + return false; + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs new file mode 100644 index 000000000..fac2ee21c --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -0,0 +1,178 @@ +using Google.Cloud.PubSub.V1; +using Google.Protobuf.Collections; +using Grpc.Core; +using JasperFx.Core; +using Microsoft.Extensions.Logging; +using Wolverine.Runtime; +using Wolverine.Transports; +using Wolverine.Util.Dataflow; + +namespace Wolverine.Pubsub.Internal; + +public abstract class PubsubListener : IListener, ISupportDeadLetterQueue { + protected readonly PubsubSubscription _endpoint; + protected readonly PubsubTransport _transport; + protected readonly IReceiver _receiver; + protected readonly ILogger _logger; + protected readonly PubsubTopic? _deadLetterTopic; + protected readonly RetryBlock _resend; + protected readonly RetryBlock? _deadLetter; + protected readonly CancellationTokenSource _cancellation = new(); + + protected RetryBlock _complete; + protected Task _task; + + public bool NativeDeadLetterQueueEnabled { get; } = false; + public Uri Address => _endpoint.Uri; + + public PubsubListener( + PubsubSubscription endpoint, + PubsubTransport transport, + IReceiver receiver, + IWolverineRuntime runtime + ) { + if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + _endpoint = endpoint; + _transport = transport; + _receiver = receiver; + _logger = runtime.LoggerFactory.CreateLogger(); + + _resend = new RetryBlock(async (envelope, _) => { + await _endpoint.Topic.SendMessageAsync(envelope, _logger); + }, _logger, runtime.Cancellation); + + if (_endpoint.DeadLetterName.IsNotEmpty() && !transport.EnableDeadLettering) { + NativeDeadLetterQueueEnabled = true; + _deadLetterTopic = _transport.Topics[_endpoint.DeadLetterName]; + } + + _deadLetter = new RetryBlock(async (e, _) => { + if (_deadLetterTopic is null) return; + + await _deadLetterTopic.SendMessageAsync(e, _logger); + }, _logger, runtime.Cancellation); + + _complete = new RetryBlock( + async (e, _) => { + if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + await transport.SubscriberApiClient.AcknowledgeAsync(_endpoint.Name, e.Select(x => x.AckId)); + }, + _logger, + _cancellation.Token + ); + + _task = StartAsync(); + } + + public abstract Task StartAsync(); + + public virtual ValueTask CompleteAsync(Envelope envelope) => ValueTask.CompletedTask; + + public async ValueTask DeferAsync(Envelope envelope) { + if (envelope is PubsubEnvelope e) await _resend.PostAsync(e); + } + + public Task MoveToErrorsAsync(Envelope envelope, Exception exception) => _deadLetter?.PostAsync(envelope) ?? Task.CompletedTask; + + public async Task TryRequeueAsync(Envelope envelope) { + if (envelope is PubsubEnvelope) { + await _resend.PostAsync(envelope); + + return true; + } + + return false; + } + + public ValueTask StopAsync() { + _cancellation.Cancel(); + + return new ValueTask(_task); + } + + public ValueTask DisposeAsync() { + _cancellation.Cancel(); + _task.SafeDispose(); + _complete.SafeDispose(); + _resend.SafeDispose(); + _deadLetter?.SafeDispose(); + + return ValueTask.CompletedTask; + } + + protected async Task listenForMessagesAsync(Func listenAsync) { + int retryCount = 0; + + while (!_cancellation.IsCancellationRequested) { + try { + await listenAsync(); + } + catch (TaskCanceledException) when (_cancellation.IsCancellationRequested) { + _logger.LogInformation("{Uri}: Listener canceled, shutting down listener...", _endpoint.Uri); + + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled) { + _logger.LogInformation("{Uri}: Listener canceled, shutting down listener...", _endpoint.Uri); + + break; + } + catch (Exception ex) { + retryCount++; + + if (retryCount > _endpoint.MaxRetryCount) { + _logger.LogError(ex, "{Uri}: Max retry attempts reached, unable to restart listener.", _endpoint.Uri); + + throw; + } + + _logger.LogError( + ex, + "{Uri}: Error while trying to retrieve messages from Google Cloud Pub/Sub, attempting to restart stream ({RetryCount}/{MaxRetryCount})...", + _endpoint.Uri, + retryCount, + _endpoint.MaxRetryCount + ); + + int retryDelay = (int) Math.Pow(2, retryCount) * _endpoint.RetryDelay; + + await Task.Delay(retryDelay, _cancellation.Token); + } + } + } + + protected async Task handleMessagesAsync(RepeatedField messages) { + var envelopes = new List(messages.Count); + + foreach (var message in messages) { + try { + var envelope = new PubsubEnvelope(message.AckId); + + _endpoint.Mapper.MapIncomingToEnvelope(envelope, message.Message); + + if (envelope.IsPing()) { + try { + await _complete.PostAsync([envelope]); + } + catch (Exception ex) { + _logger.LogError(ex, "{Uri}: Error while acknowledging Google Cloud Pub/Sub ping message \"{AckId}\".", _endpoint.Uri, message.AckId); + } + + continue; + } + + envelopes.Add(envelope); + } + catch (Exception ex) { + _logger.LogError(ex, "{Uri}: Error while mapping Google Cloud Pub/Sub message {AckId}.", _endpoint.Uri, message.AckId); + } + } + + if (envelopes.Any()) { + await _receiver.ReceivedAsync(this, envelopes.ToArray()); + await _complete.PostAsync(envelopes.ToArray()); + } + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs new file mode 100644 index 000000000..41bf2e6a5 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs @@ -0,0 +1,62 @@ +using Google.Cloud.PubSub.V1; +using Microsoft.Extensions.Logging; +using Wolverine.Runtime; +using Wolverine.Transports; +using Wolverine.Transports.Sending; + +namespace Wolverine.Pubsub.Internal; + +internal class PubsubSenderProtocol : ISenderProtocol { + private readonly PubsubTopic _topic; + private readonly PublisherServiceApiClient _client; + private readonly ILogger _logger; + + public PubsubSenderProtocol( + PubsubTopic topic, + PublisherServiceApiClient client, + IWolverineRuntime runtime + ) { + _topic = topic; + _client = client; + _logger = runtime.LoggerFactory.CreateLogger(); + } + + public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch batch) { + await _topic.InitializeAsync(_logger); + + var messages = new List(); + var successes = new List(); + var fails = new List(); + + foreach (var envelope in batch.Messages) { + try { + var message = new PubsubMessage(); + + _topic.Mapper.MapEnvelopeToOutgoing(envelope, message); + + messages.Add(message); + successes.Add(envelope); + } + catch (Exception ex) { + _logger.LogError(ex, "{Uril}: Error while mapping envelope \"{Envelope}\" to a PubsubMessage object.", _topic.Uri, envelope); + + fails.Add(envelope); + } + } + + try { + await _client.PublishAsync(new() { + TopicAsTopicName = _topic.Name, + Messages = { messages } + }); + + await callback.MarkSuccessfulAsync(new OutgoingMessageBatch(batch.Destination, successes)); + + if (fails.Any()) + await callback.MarkProcessingFailureAsync(new OutgoingMessageBatch(batch.Destination, fails)); + } + catch (Exception ex) { + await callback.MarkProcessingFailureAsync(batch, ex); + } + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs new file mode 100644 index 000000000..91da102d3 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs @@ -0,0 +1,135 @@ +using Google.Cloud.PubSub.V1; +using Grpc.Core; +using JasperFx.Core; +using Microsoft.Extensions.Logging; +using Wolverine.Configuration; +using Wolverine.Runtime; +using Wolverine.Transports; +using Wolverine.Transports.Sending; + +namespace Wolverine.Pubsub.Internal; + +public class PubsubSubscription : PubsubEndpoint, IBrokerQueue { + public readonly SubscriptionName Name; + public readonly PubsubTopic Topic; + + public int MaxRetryCount = 5; + public int RetryDelay = 1000; + + /// + /// Name of the dead letter queue for this SQS queue where failed messages will be moved + /// + public string? DeadLetterName = PubsubTransport.DeadLetterName; + + public PubsubSubscriptionOptions PubsubOptions = new PubsubSubscriptionOptions(); + + public PubsubSubscription( + string subscriptionName, + PubsubTopic topic, + PubsubTransport transport, + EndpointRole role = EndpointRole.Application + ) : base(new($"{transport.Protocol}://{topic.EndpointName}/{subscriptionName}"), transport, role) { + Name = new(transport.ProjectId, $"{PubsubTransport.SanitizePubsubName(subscriptionName)}"); + Topic = topic; + EndpointName = subscriptionName; + IsListener = true; + } + + public override async ValueTask SetupAsync(ILogger logger) { + if (_transport.SubscriberApiClient is null || _transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + try { + var request = new Subscription { + SubscriptionName = Name, + TopicAsTopicName = Topic.Name, + AckDeadlineSeconds = PubsubOptions.AckDeadlineSeconds, + EnableExactlyOnceDelivery = PubsubOptions.EnableExactlyOnceDelivery, + EnableMessageOrdering = PubsubOptions.EnableMessageOrdering, + MessageRetentionDuration = PubsubOptions.MessageRetentionDuration, + RetainAckedMessages = PubsubOptions.RetainAckedMessages, + RetryPolicy = PubsubOptions.RetryPolicy + }; + + if (PubsubOptions.DeadLetterPolicy is not null) request.DeadLetterPolicy = PubsubOptions.DeadLetterPolicy; + if (PubsubOptions.ExpirationPolicy is not null) request.ExpirationPolicy = PubsubOptions.ExpirationPolicy; + if (PubsubOptions.Filter is not null) request.Filter = PubsubOptions.Filter; + + await _transport.SubscriberApiClient.CreateSubscriptionAsync(request); + } + catch (RpcException ex) { + if (ex.StatusCode != StatusCode.AlreadyExists) { + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Name, Topic.Name); + + throw; + } + + logger.LogInformation("{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", Uri, Name); + } + catch (Exception ex) { + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Name, Topic.Name); + + throw; + } + } + + public ValueTask PurgeAsync(ILogger logger) => ValueTask.CompletedTask; + + public override async ValueTask CheckAsync() { + if (_transport.SubscriberApiClient is null) return false; + + try { + await _transport.SubscriberApiClient.GetSubscriptionAsync(Name); + + return true; + } + catch { + return false; + } + } + + public ValueTask> GetAttributesAsync() => ValueTask.FromResult(new Dictionary()); + + public override async ValueTask TeardownAsync(ILogger logger) { + if (_transport.SubscriberApiClient is null) return; + + await _transport.SubscriberApiClient.DeleteSubscriptionAsync(Name); + } + + public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { + if (Mode == EndpointMode.Inline) return ValueTask.FromResult(new InlinePubsubListener( + this, + _transport, + receiver, + runtime + )); + + return ValueTask.FromResult(new BatchedPubsubListener( + this, + _transport, + receiver, + runtime + )); + } + + public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISender? deadLetterSender) { + if (DeadLetterName.IsNotEmpty() && _transport.EnableDeadLettering) { + var dl = _transport.Topics[DeadLetterName]; + + deadLetterSender = new InlinePubsubSender(dl, runtime); + + return true; + } + + deadLetterSender = default; + + return false; + } + + protected override ISender CreateSender(IWolverineRuntime runtime) => throw new NotSupportedException(); + + internal void ConfigureDeadLetter(Action configure) { + if (DeadLetterName.IsEmpty()) return; + + configure(_transport.Topics[DeadLetterName].FindOrCreateSubscription()); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs new file mode 100644 index 000000000..6a6bfdddd --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs @@ -0,0 +1,106 @@ +using Google.Cloud.PubSub.V1; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Wolverine.Configuration; +using Wolverine.Runtime; +using Wolverine.Transports; +using Wolverine.Transports.Sending; + +namespace Wolverine.Pubsub.Internal; + +public class PubsubTopic : PubsubEndpoint { + public TopicName Name { get; } + + public PubsubTopic( + string topicName, + PubsubTransport transport, + EndpointRole role = EndpointRole.Application + ) : base(new($"{transport.Protocol}://{topicName}"), transport, role) { + Name = new(transport.ProjectId, $"{PubsubTransport.SanitizePubsubName(topicName)}"); + EndpointName = topicName; + IsListener = false; + } + + public PubsubSubscription FindOrCreateSubscription(string? subscriptionName = null) { + var existing = _transport.Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == $"{Uri.OriginalString}/{subscriptionName ?? EndpointName}"); + + if (existing != null) return existing; + + var subscription = new PubsubSubscription(subscriptionName ?? EndpointName, this, _transport); + + _transport.Subscriptions.Add(subscription); + + return subscription; + } + + public override async ValueTask SetupAsync(ILogger logger) { + if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + try { + await _transport.PublisherApiClient.CreateTopicAsync(Name); + } + catch (RpcException ex) { + if (ex.StatusCode != StatusCode.AlreadyExists) { + logger.LogError(ex, "Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Name); + + throw; + } + + logger.LogInformation("Google Cloud Pub/Sub topic \"{Topic}\" already exists", Name); + } + catch (Exception ex) { + logger.LogError(ex, "Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Name); + + throw; + } + } + + public override async ValueTask CheckAsync() { + if (_transport.PublisherApiClient is null) return false; + + try { + await _transport.PublisherApiClient.GetTopicAsync(Name); + + return true; + } + catch { + return false; + } + } + + public override async ValueTask TeardownAsync(ILogger logger) { + if (_transport.PublisherApiClient is null) return; + + await _transport.PublisherApiClient.DeleteTopicAsync(Name); + } + + public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) => throw new NotSupportedException(); + + protected override ISender CreateSender(IWolverineRuntime runtime) { + if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + if (Mode == EndpointMode.Inline) return new InlinePubsubSender(this, runtime); + + return new BatchedSender( + this, + new PubsubSenderProtocol(this, _transport.PublisherApiClient, runtime), + runtime.DurabilitySettings.Cancellation, + runtime.LoggerFactory.CreateLogger() + ); + } + + internal async Task SendMessageAsync(Envelope envelope, ILogger logger) { + if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + if (!_hasInitialized) await InitializeAsync(logger); + + var message = new PubsubMessage(); + + Mapper.MapEnvelopeToOutgoing(envelope, message); + + await _transport.PublisherApiClient.PublishAsync(new() { + TopicAsTopicName = Name, + Messages = { message } + }); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs new file mode 100644 index 000000000..acfd54eda --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs @@ -0,0 +1,77 @@ +using Wolverine.Pubsub.Internal; +using Wolverine.Transports; + +namespace Wolverine.Pubsub; + +public class PubsubConfiguration : BrokerExpression< + PubsubTransport, + PubsubSubscription, + PubsubTopic, + PubsubSubscriptionConfiguration, + PubsubTopicConfiguration, + PubsubConfiguration +> { + public PubsubConfiguration(PubsubTransport transport, WolverineOptions options) : base(transport, options) { } + + /// + /// Opt into using conventional message routing using topics and + /// subscriptions based on message type names + /// + /// + /// + public PubsubConfiguration UseTopicAndSubscriptionConventionalRouting( + Action? configure = null + ) { + var routing = new PubsubTopicBroadcastingRoutingConvention(); + + configure?.Invoke(routing); + + Options.RouteWith(routing); + + return this; + } + + /// + /// Opt into using conventional message routing using + /// queues based on message type names + /// + /// + /// + public PubsubConfiguration UseConventionalRouting( + Action? configure = null + ) { + var routing = new PubsubMessageRoutingConvention(); + + configure?.Invoke(routing); + + Options.RouteWith(routing); + + return this; + } + + /// + /// Is Wolverine enabled to create system endpoints automatically for responses and retries? This + /// should probably be set to false if the application does not have permissions to create topcis and subscriptions + /// + /// + /// + public PubsubConfiguration SystemEndpointsAreEnabled(bool enabled) { + Transport.SystemEndpointsEnabled = enabled; + + return this; + } + + /// + /// Globally enable all native dead lettering with Google Cloud Pub/Sub within this entire + /// application + /// + /// + public PubsubConfiguration EnableAllNativeDeadLettering() { + Transport.EnableDeadLettering = true; + + return this; + } + + protected override PubsubSubscriptionConfiguration createListenerExpression(PubsubSubscription subscriberEndpoint) => new(subscriberEndpoint); + protected override PubsubTopicConfiguration createSubscriberExpression(PubsubTopic topicEndpoint) => new(topicEndpoint); +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs new file mode 100644 index 000000000..2c335983f --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs @@ -0,0 +1,38 @@ +using Wolverine.Configuration; +using Wolverine.Transports; + +namespace Wolverine.Pubsub; + +public class PubsubMessageRoutingConvention : MessageRoutingConvention< + PubsubTransport, + PubsubSubscriptionConfiguration, + PubsubTopicConfiguration, + PubsubMessageRoutingConvention +> { + protected override (PubsubSubscriptionConfiguration, Endpoint) FindOrCreateListenerForIdentifier( + string identifier, + PubsubTransport transport, + Type messageType + ) { + var topic = transport.Topics[identifier]; + var subscription = topic.FindOrCreateSubscription(); + + return (new PubsubSubscriptionConfiguration(subscription), subscription); + } + + protected override (PubsubTopicConfiguration, Endpoint) FindOrCreateSubscriber( + string identifier, + PubsubTransport transport + ) { + var topic = transport.Topics[identifier]; + + return (new PubsubTopicConfiguration(topic), topic); + } + + /// + /// Specify naming rules for the subscribing topic for message types + /// + /// + /// + public PubsubMessageRoutingConvention TopicNameForSender(Func namingRule) => IdentifierForSender(namingRule); +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs new file mode 100644 index 000000000..b544cb96a --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs @@ -0,0 +1,77 @@ +using Wolverine.Pubsub.Internal; +using Wolverine.Configuration; +using Wolverine.ErrorHandling; + +namespace Wolverine.Pubsub; + +public class PubsubSubscriptionConfiguration : ListenerConfiguration { + public PubsubSubscriptionConfiguration(PubsubSubscription endpoint) : base(endpoint) { } + + /// + /// Add circuit breaker exception handling to this listener + /// + /// + /// + public PubsubSubscriptionConfiguration CircuitBreaker(Action? configure = null) { + add(e => { + e.CircuitBreakerOptions = new CircuitBreakerOptions(); + + configure?.Invoke(e.CircuitBreakerOptions); + }); + + return this; + } + + /// + /// Configure the underlying Google Cloud Pub/Sub subscription. This is only applicable when + /// Wolverine is creating the subscriptions + /// + /// + /// + public PubsubSubscriptionConfiguration ConfigureSubscription(Action configure) { + add(s => configure(s)); + + return this; + } + + /// + /// Completely disable all Google Cloud Pub/Sub dead lettering for just this subscription + /// + /// + public PubsubSubscriptionConfiguration DisableDeadLettering() { + add(e => e.DeadLetterName = null); + + return this; + } + + /// + /// Customize the dead lettering for just this subscription + /// + /// + /// Optionally configure properties of the dead lettering itself + /// + /// + public PubsubSubscriptionConfiguration ConfigureDeadLettering( + string deadLetterName, + Action? configure = null + ) { + add(e => { + e.DeadLetterName = deadLetterName; + + if (configure is not null) e.ConfigureDeadLetter(configure); + }); + + return this; + } + + /// + /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// + /// + /// + public PubsubSubscriptionConfiguration InteropWith(IPubsubEnvelopeMapper mapper) { + add(e => e.Mapper = mapper); + + return this; + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs new file mode 100644 index 000000000..2b9d3a351 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs @@ -0,0 +1,22 @@ +using Google.Cloud.PubSub.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Wolverine.Pubsub; + +public class PubsubSubscriptionOptions { + public long MaxOutstandingMessages = 1000; + public long MaxOutstandingByteCount = 100 * 1024 * 1024; + + public int AckDeadlineSeconds = 10; + public DeadLetterPolicy? DeadLetterPolicy = null; + public bool EnableExactlyOnceDelivery = false; + public bool EnableMessageOrdering = false; + public ExpirationPolicy? ExpirationPolicy = null; + public string? Filter = null; + public Duration MessageRetentionDuration = Duration.FromTimeSpan(TimeSpan.FromDays(7)); + public bool RetainAckedMessages = false; + public RetryPolicy RetryPolicy = new RetryPolicy { + MinimumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)), + MaximumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(600)) + }; +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs new file mode 100644 index 000000000..3b8db6a76 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs @@ -0,0 +1,61 @@ +using Wolverine.Pubsub.Internal; +using Wolverine.Configuration; +using Wolverine.Transports; + +namespace Wolverine.Pubsub; + +public class PubsubTopicBroadcastingRoutingConvention : MessageRoutingConvention< + PubsubTransport, + PubsubSubscriptionConfiguration, + PubsubTopicConfiguration, + PubsubTopicBroadcastingRoutingConvention +> { + private Func? _subscriptionNameSource; + + protected override (PubsubSubscriptionConfiguration, Endpoint) FindOrCreateListenerForIdentifier( + string identifier, + PubsubTransport transport, + Type messageType + ) { + var topic = transport.Topics[identifier]; + var subscriptionName = _subscriptionNameSource == null ? identifier : _subscriptionNameSource(messageType); + var subscription = topic.FindOrCreateSubscription(subscriptionName); + + return (new PubsubSubscriptionConfiguration(subscription), subscription); + } + + protected override (PubsubTopicConfiguration, Endpoint) FindOrCreateSubscriber( + string identifier, + PubsubTransport transport + ) { + var topic = transport.Topics[identifier]; + + return (new PubsubTopicConfiguration(topic), topic); + } + + /// + /// Override the naming convention for topics. Identical in functionality to IdentifierForSender() + /// + /// + /// + public PubsubTopicBroadcastingRoutingConvention TopicNameForSender(Func nameSource) => IdentifierForSender(nameSource); + + /// + /// Override the subscription name for a message type. By default this would be the same as the topic + /// + /// + /// + /// + public PubsubTopicBroadcastingRoutingConvention SubscriptionNameForListener(Func nameSource) { + _subscriptionNameSource = nameSource; + + return this; + } + + /// + /// Override the topic name by message type for listeners. This has the same functionality as IdentifierForListener() + /// + /// + /// + public PubsubTopicBroadcastingRoutingConvention TopicNameForListener(Func nameSource) => IdentifierForListener(nameSource); +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs new file mode 100644 index 000000000..46d629172 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs @@ -0,0 +1,19 @@ +using Wolverine.Pubsub.Internal; +using Wolverine.Configuration; + +namespace Wolverine.Pubsub; + +public class PubsubTopicConfiguration : SubscriberConfiguration { + public PubsubTopicConfiguration(PubsubTopic endpoint) : base(endpoint) { } + + /// + /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// + /// + /// + public PubsubTopicConfiguration InteropWith(IPubsubEnvelopeMapper mapper) { + add(e => e.Mapper = mapper); + + return this; + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs new file mode 100644 index 000000000..5b8446d78 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -0,0 +1,128 @@ +using Wolverine.Transports; +using Wolverine.Pubsub.Internal; +using Wolverine.Runtime; +using Google.Cloud.PubSub.V1; +using JasperFx.Core; +using Wolverine.Configuration; +using Spectre.Console; +using Google.Api.Gax; + +namespace Wolverine.Pubsub; + +public class PubsubTransport : BrokerTransport, IAsyncDisposable { + public const char Separator = '-'; + public const string ProtocolName = "pubsub"; + public const string ResponseName = "wlvrn-responses"; + public const string DeadLetterName = "wlvrn-dead-letter"; + + public static string SanitizePubsubName(string identifier) => (!identifier.StartsWith("wlvrn-") ? $"wlvrn-{identifier}" : identifier).ToLowerInvariant().Replace('.', Separator); + + internal PublisherServiceApiClient? PublisherApiClient = null; + internal SubscriberServiceApiClient? SubscriberApiClient = null; + + + public readonly LightweightCache Topics; + public readonly List Subscriptions = new(); + + public string ProjectId = string.Empty; + public EmulatorDetection EmulatorDetection = EmulatorDetection.None; + public bool EnableDeadLettering = false; + + /// + /// Is this transport connection allowed to build and use response and retry queues + /// for just this node? + /// + public bool SystemEndpointsEnabled = false; + + public PubsubTransport() : base(ProtocolName, "Google Cloud Pub/Sub") { + IdentifierDelimiter = "-"; + Topics = new(name => new(name, this)); + } + + public PubsubTransport(string projectId) : this() { + ProjectId = projectId; + } + + public override async ValueTask ConnectAsync(IWolverineRuntime runtime) { + var pubBuilder = new PublisherServiceApiClientBuilder { + EmulatorDetection = EmulatorDetection + }; + var subBuilder = new SubscriberServiceApiClientBuilder { + EmulatorDetection = EmulatorDetection, + }; + + if (string.IsNullOrWhiteSpace(ProjectId)) throw new InvalidOperationException("Google Cloud Pub/Sub project id must be set before connecting"); + + PublisherApiClient = await pubBuilder.BuildAsync(); + SubscriberApiClient = await subBuilder.BuildAsync(); + } + + public override Endpoint? ReplyEndpoint() { + var replies = base.ReplyEndpoint(); + + if (replies is PubsubTopic) return replies; + + return null; + } + + public override IEnumerable DiagnosticColumns() { + yield return new PropertyColumn("Subscription name", "name"); + yield return new PropertyColumn("Messages", "count", Justify.Right); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + protected override IEnumerable explicitEndpoints() { + foreach (var topic in Topics) yield return topic; + foreach (var subscription in Subscriptions) yield return subscription; + } + + protected override IEnumerable endpoints() { + if (EnableDeadLettering) { + var dlNames = Subscriptions.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); + + foreach (var dlName in dlNames) Topics[dlName!].FindOrCreateSubscription(); + } + + foreach (var topic in Topics) yield return topic; + foreach (var subscription in Subscriptions) yield return subscription; + } + + protected override PubsubEndpoint findEndpointByUri(Uri uri) { + if (uri.Scheme != Protocol) throw new ArgumentOutOfRangeException(nameof(uri)); + + var topicName = uri.Host; + + if (uri.Segments.Length == 2) { + var subscription = Subscriptions.FirstOrDefault(x => x.Uri == uri); + + if (subscription != null) return subscription; + + var subscriptionName = uri.Segments.Last().TrimEnd('/'); + var topic = Topics[topicName]; + + subscription = new PubsubSubscription(subscriptionName, topic, this); + + Subscriptions.Add(subscription); + + return subscription; + } + + return Topics[topicName]; + } + + protected override void tryBuildSystemEndpoints(IWolverineRuntime runtime) { + if (!SystemEndpointsEnabled) return; + + var responseName = SanitizeIdentifier($"{ResponseName}-{Math.Abs(runtime.DurabilitySettings.AssignedNodeNumber)}"); + var responseTopic = new PubsubTopic(responseName, this, EndpointRole.System); + var responseSubscription = new PubsubSubscription(responseName, responseTopic, this, EndpointRole.System); + + responseSubscription.Mode = EndpointMode.BufferedInMemory; + responseSubscription.EndpointName = ResponseName; + responseSubscription.IsUsedForReplies = true; + + Topics[responseName] = responseTopic; + Subscriptions.Add(responseSubscription); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs new file mode 100644 index 000000000..2a9a69a9d --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs @@ -0,0 +1,92 @@ +using JasperFx.Core.Reflection; +using Wolverine.Configuration; +using Wolverine.Pubsub.Internal; + +namespace Wolverine.Pubsub; + +public static class PubsubTransportExtensions { + + /// + /// Quick access to the Google Cloud Pub/Sub Transport within this application. + /// This is for advanced usage + /// + /// + /// + internal static PubsubTransport PubsubTransport(this WolverineOptions endpoints) { + var transports = endpoints.As().Transports; + + return transports.GetOrCreate(); + } + + /// + /// Additive configuration to the Google Cloud Pub/Sub integration for this Wolverine application + /// + /// + /// + public static PubsubConfiguration ConfigurePubsub(this WolverineOptions endpoints) => new PubsubConfiguration(endpoints.PubsubTransport(), endpoints); + + /// + /// Connect to Google Cloud Pub/Sub with a prject id + /// + /// + /// + /// + /// + /// + public static PubsubConfiguration UsePubsub(this WolverineOptions endpoints, string projectId, Action? configure = null) { + var transport = endpoints.PubsubTransport(); + + transport.ProjectId = projectId ?? throw new ArgumentNullException(nameof(projectId)); + + configure?.Invoke(transport); + + return new PubsubConfiguration(transport, endpoints); + } + + /// + /// Listen for incoming messages at the designated Google Cloud Pub/Sub topic by name + /// + /// + /// The name of the Google Cloud Pub/Sub topic + /// + /// Optional configuration for this Google Cloud Pub/Sub subscription if being initialized by Wolverine + /// + public static PubsubSubscriptionConfiguration ListenToPubsubTopic( + this WolverineOptions endpoints, + string topicName, + Action? configure = null + ) { + var transport = endpoints.PubsubTransport(); + var topic = transport.Topics[transport.MaybeCorrectName(topicName)]; + + topic.EndpointName = topicName; + + var subscription = topic.FindOrCreateSubscription(); + + configure?.Invoke(subscription); + + return new PubsubSubscriptionConfiguration(subscription); + } + + /// + /// Publish the designated messages to a Google Cloud Pub/Sub topic + /// + /// + /// + /// + public static PubsubTopicConfiguration ToPubsubTopic( + this IPublishToExpression publishing, + string topicName + ) { + var transports = publishing.As().Parent.Transports; + var transport = transports.GetOrCreate(); + var topic = transport.Topics[transport.MaybeCorrectName(topicName)]; + + topic.EndpointName = topicName; + + // This is necessary unfortunately to hook up the subscription rules + publishing.To(topic.Uri); + + return new PubsubTopicConfiguration(topic); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj b/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj new file mode 100644 index 000000000..5dcace3a7 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj @@ -0,0 +1,16 @@ + + + + WolverineFx.Pubsub + Google Cloud Pub/Sub transport for Wolverine applications + + + + + + + + + + + diff --git a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportException.cs b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportException.cs new file mode 100644 index 000000000..dcc88527a --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportException.cs @@ -0,0 +1,5 @@ +namespace Wolverine.Pubsub; + +public class WolverinePubsubTransportException : Exception { + public WolverinePubsubTransportException(string? message, Exception? innerException) : base(message, innerException) { } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs new file mode 100644 index 000000000..9136d4f68 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs @@ -0,0 +1,5 @@ +namespace Wolverine.Pubsub; + +public class WolverinePubsubTransportNotConnectedException : Exception { + public WolverinePubsubTransportNotConnectedException(string message = "Google Cloud Pub/Sub transport has not been connected", Exception? innerException = null) : base(message, innerException) { } +} diff --git a/wolverine.sln b/wolverine.sln index 7de67de5a..17bb8015a 100644 --- a/wolverine.sln +++ b/wolverine.sln @@ -243,6 +243,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RavenDbTests", "src\Persist EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatchMessaging", "src\Transports\Kafka\BatchMessaging\BatchMessaging.csproj", "{B035801D-E786-4AAA-858A-0770D88116D6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EBA73DEC-CDC8-4234-8BF3-D50B27E9C7F4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Transports", "Transports", "{572BED90-AD27-498E-9985-3085F4DBD715}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GCP", "GCP", "{0D320722-7CED-41C0-A914-11AC223320AA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Pubsub", "src\Transports\GCP\Wolverine.Pubsub\Wolverine.Pubsub.csproj", "{579CD7E7-216A-4A68-A338-663FC3D031B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Pubsub.Tests", "src\Transports\GCP\Wolverine.Pubsub.Tests\Wolverine.Pubsub.Tests.csproj", "{619D927F-D1B3-4A40-8B6C-AD136D569FE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -616,6 +626,14 @@ Global {B035801D-E786-4AAA-858A-0770D88116D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {B035801D-E786-4AAA-858A-0770D88116D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {B035801D-E786-4AAA-858A-0770D88116D6}.Release|Any CPU.Build.0 = Release|Any CPU + {579CD7E7-216A-4A68-A338-663FC3D031B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {579CD7E7-216A-4A68-A338-663FC3D031B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {579CD7E7-216A-4A68-A338-663FC3D031B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {579CD7E7-216A-4A68-A338-663FC3D031B7}.Release|Any CPU.Build.0 = Release|Any CPU + {619D927F-D1B3-4A40-8B6C-AD136D569FE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {619D927F-D1B3-4A40-8B6C-AD136D569FE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {619D927F-D1B3-4A40-8B6C-AD136D569FE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {619D927F-D1B3-4A40-8B6C-AD136D569FE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {24497E6A-D6B1-4C80-ABFB-57FFAD5070C4} = {96119B5E-B5F0-400A-9580-B342EBE26212} @@ -724,5 +742,9 @@ Global {AAFFC067-D110-45FF-9FA0-8E02F77D9D14} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} {71B152DD-7A0B-4935-B8B1-1060E674D23D} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} {B035801D-E786-4AAA-858A-0770D88116D6} = {63E9B289-95E8-4F2B-A064-156971A6853C} + {572BED90-AD27-498E-9985-3085F4DBD715} = {EBA73DEC-CDC8-4234-8BF3-D50B27E9C7F4} + {0D320722-7CED-41C0-A914-11AC223320AA} = {572BED90-AD27-498E-9985-3085F4DBD715} + {579CD7E7-216A-4A68-A338-663FC3D031B7} = {0D320722-7CED-41C0-A914-11AC223320AA} + {619D927F-D1B3-4A40-8B6C-AD136D569FE1} = {0D320722-7CED-41C0-A914-11AC223320AA} EndGlobalSection EndGlobal From 15765b895b3ab3d8caab233c5c8340c38cd8768a Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:28:09 +0200 Subject: [PATCH 02/16] findEndpointByUri updated to use Uri.OriginalString instead, to preserve casing --- .../PubsubTransportTests.cs | 16 ++++++++++++++++ .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 14 +++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs index c4d3dea90..9a3f87fe9 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs @@ -51,4 +51,20 @@ public void return_all_endpoints_gets_dead_letter_subscription_too() { endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName(PubsubTransport.DeadLetterName)); endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("two-dead-letter")); } + + [Fact] + public void findEndpointByUri_should_correctly_find_by_queuename() { + string queueNameInPascalCase = "TestQueue"; + string queueNameLowerCase = "testqueue"; + + var transport = new PubsubTransport("wolverine"); + var abc = transport.Topics[queueNameInPascalCase]; + var xzy = transport.Topics[queueNameLowerCase]; + + var result = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://{queueNameInPascalCase}")); + + transport.Topics.Count.ShouldBe(2); + + result.EndpointName.ShouldBe(queueNameInPascalCase); + } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index 5b8446d78..9ffd5d3db 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -91,24 +91,20 @@ protected override IEnumerable endpoints() { protected override PubsubEndpoint findEndpointByUri(Uri uri) { if (uri.Scheme != Protocol) throw new ArgumentOutOfRangeException(nameof(uri)); - var topicName = uri.Host; - if (uri.Segments.Length == 2) { - var subscription = Subscriptions.FirstOrDefault(x => x.Uri == uri); - - if (subscription != null) return subscription; + var existing = Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString); - var subscriptionName = uri.Segments.Last().TrimEnd('/'); - var topic = Topics[topicName]; + if (existing is not null) return existing; - subscription = new PubsubSubscription(subscriptionName, topic, this); + var topic = Topics[uri.OriginalString.Split("//")[1].TrimEnd('/')]; + var subscription = new PubsubSubscription(uri.OriginalString.Split("/").Last(), topic, this); Subscriptions.Add(subscription); return subscription; } - return Topics[topicName]; + return Topics.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString) ?? Topics[uri.OriginalString.Split("//")[1].TrimEnd('/')]; } protected override void tryBuildSystemEndpoints(IWolverineRuntime runtime) { From bdc24290442b93e8c2f027750d7c1f04c4f3b683 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:33:35 +0200 Subject: [PATCH 03/16] Fixed system endpoints --- .../BufferedSendingAndReceivingCompliance.cs | 6 +- .../InlineSendingAndReceivingCompliance.cs | 45 +++++++++++++++ .../Internal/PubsubSubscriptionTests.cs | 2 +- .../Internal/PubsubTopicTests.cs | 2 +- .../PubsubTransportTests.cs | 20 +++---- .../TestingExtensions.cs | 2 +- ...d_receive_with_topics_and_subscriptions.cs | 40 ++++++++++++++ .../sending_compliance_with_prefixes.cs | 55 +++++++++++++++++++ .../Internal/PubsubEnvelopeMapper.cs | 50 +++++++++++------ .../Internal/PubsubSubscription.cs | 10 ++-- .../Wolverine.Pubsub/Internal/PubsubTopic.cs | 29 ++++++++-- .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 38 +++++++------ ...erinePubsubInvalidEndpointNameException.cs | 5 ++ .../Serialization/NewtonsoftSerializer.cs | 2 +- 14 files changed, 244 insertions(+), 62 deletions(-) create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs index 6191d7f7b..6504652b4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -8,15 +8,15 @@ namespace Wolverine.Pubsub.Tests; public class BufferedComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://buffered-receiver"), 120) { } + public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/buffered-receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var topicName = Guid.NewGuid().ToString(); + var topicName = $"test.{Guid.NewGuid()}"; - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://" + topicName); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/{topicName}"); await SenderIs(opts => { opts diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs new file mode 100644 index 000000000..abeeb593c --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs @@ -0,0 +1,45 @@ +using Wolverine.ComplianceTests.Compliance; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class InlineComplianceFixture : TransportComplianceFixture, IAsyncLifetime { + public InlineComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/inline-receiver"), 120) { } + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + var topicName = $"test.{Guid.NewGuid()}"; + + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/{topicName}"); + + await SenderIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); + opts + .PublishAllMessages() + .ToPubsubTopic(topicName); + }); + + await ReceiverIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); + opts + .ListenToPubsubTopic(topicName) + .ProcessInline(); + }); + } + + public new async Task DisposeAsync() { + await DisposeAsync(); + } +} + +public class InlineSendingAndReceivingCompliance : TransportCompliance; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs index 081f40aaa..aa33c4113 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs @@ -33,7 +33,7 @@ public void create_uri() { var topic = new PubsubTopic("top1", createTransport()); var subscription = topic.FindOrCreateSubscription("sub1"); - subscription.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://top1/sub1")); + subscription.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://wolverine/top1/sub1")); } [Fact] diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs index 38cbb776c..524ad15a0 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs @@ -20,7 +20,7 @@ public class PubsubTopicTests { public void create_uri() { var topic = new PubsubTopic("top1", createTransport()); - topic.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://top1")); + topic.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://wolverine/top1")); } [Fact] diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs index 9a3f87fe9..831e4e76c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs @@ -8,19 +8,19 @@ public class PubsubTransportTests { [Fact] public void find_topic_by_uri() { var transport = new PubsubTransport("wolverine"); - var topic = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://one")).ShouldBeOfType(); + var topic = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://wolverine/one")).ShouldBeOfType(); - topic.Name.TopicId.ShouldBe(PubsubTransport.SanitizePubsubName("one")); + topic.Name.TopicId.ShouldBe("one"); } [Fact] public void find_subscription_by_uri() { var transport = new PubsubTransport("wolverine"); var subscription = transport - .GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://one/red")) + .GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://wolverine/one/red")) .ShouldBeOfType(); - subscription.Name.SubscriptionId.ShouldBe(PubsubTransport.SanitizePubsubName("red")); + subscription.Name.SubscriptionId.ShouldBe("red"); } [Fact] @@ -44,12 +44,12 @@ public void return_all_endpoints_gets_dead_letter_subscription_too() { var endpoints = transport.Endpoints().OfType().ToArray(); - endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("one")); - endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("two")); - endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("three")); + endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.one"); + endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.two"); + endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.three"); - endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName(PubsubTransport.DeadLetterName)); - endpoints.ShouldContain(x => x.Name.SubscriptionId == PubsubTransport.SanitizePubsubName("two-dead-letter")); + endpoints.ShouldContain(x => x.Name.SubscriptionId == $"sub.{PubsubTransport.DeadLetterName}"); + endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.two-dead-letter"); } [Fact] @@ -61,7 +61,7 @@ public void findEndpointByUri_should_correctly_find_by_queuename() { var abc = transport.Topics[queueNameInPascalCase]; var xzy = transport.Topics[queueNameLowerCase]; - var result = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://{queueNameInPascalCase}")); + var result = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://{transport.ProjectId}/{queueNameInPascalCase}")); transport.Topics.Count.ShouldBe(2); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs index 890563969..d324e9c69 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs @@ -3,7 +3,7 @@ namespace Wolverine.Pubsub.Tests; public static class TestingExtensions { - public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) => options.UsePubsub("wolverine", opts => { + public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) => options.UsePubsub(Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? throw new NullReferenceException(), opts => { opts.EmulatorDetection = EmulatorDetection.EmulatorOnly; }); } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs new file mode 100644 index 000000000..3461e8e63 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs @@ -0,0 +1,40 @@ +using Wolverine.ComplianceTests.Compliance; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class TopicsComplianceFixture : TransportComplianceFixture, IAsyncLifetime { + public TopicsComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/topic1"), 120) { } + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + await SenderIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); + opts + .PublishAllMessages() + .ToPubsubTopic("topic1"); + }); + + await ReceiverIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); + opts + .ListenToPubsubTopic("topic1"); + }); + } + + public new async Task DisposeAsync() { + await DisposeAsync(); + } +} + +public class TopicAndSubscriptionSendingAndReceivingCompliance : TransportCompliance; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs new file mode 100644 index 000000000..0fabba2cf --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Wolverine.ComplianceTests.Compliance; +using Wolverine.Pubsub.Internal; +using Wolverine.Runtime; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class PrefixedComplianceFixture : TransportComplianceFixture, IAsyncLifetime { + public PrefixedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.buffered-receiver"), 120) { } + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + var topicName = $"test.{Guid.NewGuid()}"; + + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.{topicName}"); + + await SenderIs(opts => { + opts.UsePubsubTesting() + .PrefixIdentifiers("foo") + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .AutoProvision(); + + }); + + await ReceiverIs(opts => { + opts.UsePubsubTesting() + .PrefixIdentifiers("foo") + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .AutoProvision(); + + opts.ListenToPubsubTopic(topicName).Named("receiver"); + }); + } + + public new Task DisposeAsync() { + return Task.CompletedTask; + } +} + +public class PrefixedSendingAndReceivingCompliance : TransportCompliance { + [Fact] + public void prefix_was_applied_to_queues_for_the_receiver() { + var runtime = theReceiver.Services.GetRequiredService(); + + runtime.Endpoints.EndpointByName("receiver") + .ShouldBeOfType() + .Name.SubscriptionId.ShouldStartWith("foo."); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs index de76dbc1e..d116a5327 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs @@ -1,5 +1,6 @@ using Google.Cloud.PubSub.V1; using Google.Protobuf; +using JasperFx.Core; using Wolverine.Configuration; using Wolverine.Transports; @@ -8,35 +9,49 @@ namespace Wolverine.Pubsub.Internal; internal class PubsubEnvelopeMapper : EnvelopeMapper, IPubsubEnvelopeMapper { public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( - x => x.ContentType!, - (e, m) => e.ContentType = m.Attributes["content-type"], + x => x.Data!, + (e, m) => e.Data = m.Data.ToByteArray(), (e, m) => { - if (e.ContentType is null) return; + if (e.Data is null) return; - m.Attributes["content-type"] = e.ContentType; + m.Data = ByteString.CopyFrom(e.Data); } ); MapProperty( - x => x.Data!, - (e, m) => e.Data = m.Data.ToByteArray(), + x => x.ContentType!, (e, m) => { - if (e.Data is null) return; + if (!m.Attributes.TryGetValue("content-type", out var contentType)) return; - m.Data = ByteString.CopyFrom(e.Data); + e.ContentType = contentType; + }, + (e, m) => { + if (e.ContentType is null) return; + + m.Attributes["content-type"] = e.ContentType; } ); + MapProperty( + x => x.GroupId!, + (e, m) => e.GroupId = m.OrderingKey.IsNotEmpty() ? m.OrderingKey : null, + (e, m) => m.OrderingKey = e.GroupId ?? string.Empty + ); MapProperty( x => x.Id, (e, m) => { - if (Guid.TryParse(m.Attributes["wolverine-id"], out var id)) { - e.Id = id; - } + if (!m.Attributes.TryGetValue("wolverine-id", out var wolverineId)) return; + if (!Guid.TryParse(wolverineId, out var id)) return; + + e.Id = id; }, (e, m) => m.Attributes["wolverine-id"] = e.Id.ToString() ); MapProperty( x => x.CorrelationId!, - (e, m) => e.CorrelationId = m.Attributes["wolverine-correlation-id"], + (e, m) => { + if (!m.Attributes.TryGetValue("wolverine-correlation-id", out var correlationId)) return; + + e.CorrelationId = correlationId; + }, (e, m) => { if (e.CorrelationId is null) return; @@ -45,18 +60,17 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { ); MapProperty( x => x.MessageType!, - (e, m) => e.MessageType = m.Attributes["wolverine-message-type"], + (e, m) => { + if (!m.Attributes.TryGetValue("wolverine-message-type", out var messageType)) return; + + e.MessageType = messageType; + }, (e, m) => { if (e.MessageType is null) return; m.Attributes["wolverine-message-type"] = e.MessageType; } ); - MapProperty( - x => x.GroupId!, - (e, m) => e.GroupId = m.OrderingKey, - (e, m) => m.OrderingKey = e.GroupId ?? string.Empty - ); } protected override void writeOutgoingHeader(PubsubMessage outgoing, string key, string value) { diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs index 91da102d3..01350ccc5 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs @@ -17,7 +17,7 @@ public class PubsubSubscription : PubsubEndpoint, IBrokerQueue { public int RetryDelay = 1000; /// - /// Name of the dead letter queue for this SQS queue where failed messages will be moved + /// Name of the dead letter for this Google Cloud Pub/Sub subcription where failed messages will be moved /// public string? DeadLetterName = PubsubTransport.DeadLetterName; @@ -28,8 +28,10 @@ public PubsubSubscription( PubsubTopic topic, PubsubTransport transport, EndpointRole role = EndpointRole.Application - ) : base(new($"{transport.Protocol}://{topic.EndpointName}/{subscriptionName}"), transport, role) { - Name = new(transport.ProjectId, $"{PubsubTransport.SanitizePubsubName(subscriptionName)}"); + ) : base(new($"{transport.Protocol}://{transport.ProjectId}/{topic.Name.TopicId}/{subscriptionName}"), transport, role) { + if (!PubsubTransport.NameRegex.IsMatch(subscriptionName)) throw new WolverinePubsubInvalidEndpointNameException(subscriptionName); + + Name = new(transport.ProjectId, subscriptionName); Topic = topic; EndpointName = subscriptionName; IsListener = true; @@ -130,6 +132,6 @@ public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISe internal void ConfigureDeadLetter(Action configure) { if (DeadLetterName.IsEmpty()) return; - configure(_transport.Topics[DeadLetterName].FindOrCreateSubscription()); + configure(_transport.Topics[DeadLetterName].FindOrCreateSubscription($"sub.{DeadLetterName}")); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs index 6a6bfdddd..24db43cbf 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs @@ -1,5 +1,7 @@ +using System.Text.RegularExpressions; using Google.Cloud.PubSub.V1; using Grpc.Core; +using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; @@ -15,18 +17,21 @@ public PubsubTopic( string topicName, PubsubTransport transport, EndpointRole role = EndpointRole.Application - ) : base(new($"{transport.Protocol}://{topicName}"), transport, role) { - Name = new(transport.ProjectId, $"{PubsubTransport.SanitizePubsubName(topicName)}"); + ) : base(new($"{transport.Protocol}://{transport.ProjectId}/{topicName}"), transport, role) { + if (!PubsubTransport.NameRegex.IsMatch(topicName)) throw new WolverinePubsubInvalidEndpointNameException(topicName); + + Name = new(transport.ProjectId, topicName); EndpointName = topicName; IsListener = false; } public PubsubSubscription FindOrCreateSubscription(string? subscriptionName = null) { - var existing = _transport.Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == $"{Uri.OriginalString}/{subscriptionName ?? EndpointName}"); + var fallbackName = _transport.MaybeCorrectName($"sub.{(_transport.IdentifierPrefix.IsNotEmpty() && Name.TopicId.StartsWith($"{_transport.IdentifierPrefix}.") ? Name.TopicId.Substring(_transport.IdentifierPrefix.Length + 1) : Name.TopicId)}"); + var existing = _transport.Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == $"{Uri.OriginalString}/{subscriptionName ?? fallbackName}"); if (existing != null) return existing; - var subscription = new PubsubSubscription(subscriptionName ?? EndpointName, this, _transport); + var subscription = new PubsubSubscription(subscriptionName ?? fallbackName, this, _transport); _transport.Subscriptions.Add(subscription); @@ -74,7 +79,21 @@ public override async ValueTask TeardownAsync(ILogger logger) { await _transport.PublisherApiClient.DeleteTopicAsync(Name); } - public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) => throw new NotSupportedException(); + public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { + if (Mode == EndpointMode.Inline) return ValueTask.FromResult(new InlinePubsubListener( + FindOrCreateSubscription(), + _transport, + receiver, + runtime + )); + + return ValueTask.FromResult(new BatchedPubsubListener( + FindOrCreateSubscription(), + _transport, + receiver, + runtime + )); + } protected override ISender CreateSender(IWolverineRuntime runtime) { if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index 9ffd5d3db..c81ef098a 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -6,16 +6,16 @@ using Wolverine.Configuration; using Spectre.Console; using Google.Api.Gax; +using System.Text.RegularExpressions; namespace Wolverine.Pubsub; public class PubsubTransport : BrokerTransport, IAsyncDisposable { - public const char Separator = '-'; public const string ProtocolName = "pubsub"; - public const string ResponseName = "wlvrn-responses"; - public const string DeadLetterName = "wlvrn-dead-letter"; + public const string ResponseName = "wlvrn.responses"; + public const string DeadLetterName = "wlvrn.dead-letter"; - public static string SanitizePubsubName(string identifier) => (!identifier.StartsWith("wlvrn-") ? $"wlvrn-{identifier}" : identifier).ToLowerInvariant().Replace('.', Separator); + public static Regex NameRegex = new("^(?!goog)[A-Za-z][A-Za-z0-9\\-_.~+%]{2,254}$"); internal PublisherServiceApiClient? PublisherApiClient = null; internal SubscriberServiceApiClient? SubscriberApiClient = null; @@ -35,7 +35,7 @@ public class PubsubTransport : BrokerTransport, IAsyncDisposable public bool SystemEndpointsEnabled = false; public PubsubTransport() : base(ProtocolName, "Google Cloud Pub/Sub") { - IdentifierDelimiter = "-"; + IdentifierDelimiter = "."; Topics = new(name => new(name, this)); } @@ -58,16 +58,15 @@ public override async ValueTask ConnectAsync(IWolverineRuntime runtime) { } public override Endpoint? ReplyEndpoint() { - var replies = base.ReplyEndpoint(); + var endpoint = base.ReplyEndpoint(); - if (replies is PubsubTopic) return replies; + if (endpoint is PubsubSubscription e) return e.Topic; return null; } public override IEnumerable DiagnosticColumns() { - yield return new PropertyColumn("Subscription name", "name"); - yield return new PropertyColumn("Messages", "count", Justify.Right); + yield break; } public ValueTask DisposeAsync() => ValueTask.CompletedTask; @@ -81,7 +80,12 @@ protected override IEnumerable endpoints() { if (EnableDeadLettering) { var dlNames = Subscriptions.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); - foreach (var dlName in dlNames) Topics[dlName!].FindOrCreateSubscription(); + foreach (var dlName in dlNames) { + var dlSubscription = Topics[dlName!].FindOrCreateSubscription($"sub.{dlName}"); + + dlSubscription.DeadLetterName = null; + dlSubscription.PubsubOptions.DeadLetterPolicy = null; + } } foreach (var topic in Topics) yield return topic; @@ -91,31 +95,29 @@ protected override IEnumerable endpoints() { protected override PubsubEndpoint findEndpointByUri(Uri uri) { if (uri.Scheme != Protocol) throw new ArgumentOutOfRangeException(nameof(uri)); - if (uri.Segments.Length == 2) { + if (uri.Segments.Length == 3) { var existing = Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString); if (existing is not null) return existing; - var topic = Topics[uri.OriginalString.Split("//")[1].TrimEnd('/')]; - var subscription = new PubsubSubscription(uri.OriginalString.Split("/").Last(), topic, this); + var topic = Topics[uri.Segments[1].TrimEnd('/')]; + var subscription = new PubsubSubscription(uri.Segments[2].TrimEnd('/'), topic, this); Subscriptions.Add(subscription); return subscription; } - return Topics.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString) ?? Topics[uri.OriginalString.Split("//")[1].TrimEnd('/')]; + return Topics.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString) ?? Topics[uri.Segments[1].TrimEnd('/')]; } protected override void tryBuildSystemEndpoints(IWolverineRuntime runtime) { if (!SystemEndpointsEnabled) return; - var responseName = SanitizeIdentifier($"{ResponseName}-{Math.Abs(runtime.DurabilitySettings.AssignedNodeNumber)}"); + var responseName = $"{ResponseName}.{Math.Abs(runtime.DurabilitySettings.AssignedNodeNumber)}"; var responseTopic = new PubsubTopic(responseName, this, EndpointRole.System); - var responseSubscription = new PubsubSubscription(responseName, responseTopic, this, EndpointRole.System); + var responseSubscription = new PubsubSubscription($"sub.{responseName}", responseTopic, this, EndpointRole.System); - responseSubscription.Mode = EndpointMode.BufferedInMemory; - responseSubscription.EndpointName = ResponseName; responseSubscription.IsUsedForReplies = true; Topics[responseName] = responseTopic; diff --git a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs new file mode 100644 index 000000000..308ff3b4c --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs @@ -0,0 +1,5 @@ +namespace Wolverine.Pubsub; + +public class WolverinePubsubInvalidEndpointNameException : Exception { + public WolverinePubsubInvalidEndpointNameException(string topicName, string? message = null, Exception? innerException = null) : base(message ?? $"Google Cloud Pub/Sub endpoint name \"{topicName}\" is invalid.", innerException) { } +} diff --git a/src/Wolverine/Runtime/Serialization/NewtonsoftSerializer.cs b/src/Wolverine/Runtime/Serialization/NewtonsoftSerializer.cs index a9e3ef566..760600f54 100644 --- a/src/Wolverine/Runtime/Serialization/NewtonsoftSerializer.cs +++ b/src/Wolverine/Runtime/Serialization/NewtonsoftSerializer.cs @@ -11,7 +11,7 @@ internal class NewtonsoftSerializer : IMessageSerializer private readonly JsonArrayPool _jsonCharPool; private readonly JsonSerializer _serializer; - private int _bufferSize = 1024; + private int _bufferSize = 2048; public NewtonsoftSerializer(JsonSerializerSettings settings) { From 542d1a57f07d677adc4c91cfafebcd380c151a24 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Sat, 19 Oct 2024 14:50:06 +0200 Subject: [PATCH 04/16] Moved to single endpoint and refactored configurations --- .../BufferedSendingAndReceivingCompliance.cs | 24 +-- .../DurableSendingAndReceivingCompliance.cs | 98 +++++++++ .../InlineSendingAndReceivingCompliance.cs | 47 +++- ...ubTopicTests.cs => PubsubEndpointTests.cs} | 35 +-- .../Internal/PubsubSubscriptionTests.cs | 74 ------- .../PubsubTransportTests.cs | 35 ++- .../{end_to_end.cs => send_and_receive.cs} | 37 ++-- ...d_receive_with_topics_and_subscriptions.cs | 40 ---- .../sending_compliance_with_prefixes.cs | 30 ++- .../Internal/BatchedPubsubListener.cs | 8 +- .../Internal/InlinePubsubListener.cs | 22 +- .../Internal/InlinePubsubSender.cs | 11 +- .../Internal/PubsubEndpoint.cs | 201 +++++++++++++++++- .../Internal/PubsubListener.cs | 24 +-- .../Internal/PubsubSenderProtocol.cs | 14 +- .../Internal/PubsubSubscription.cs | 137 ------------ .../Wolverine.Pubsub/Internal/PubsubTopic.cs | 125 ----------- .../Wolverine.Pubsub/PubsubConfiguration.cs | 30 ++- .../PubsubMessageRoutingConvention.cs | 22 +- ...ubscriptionOptions.cs => PubsubOptions.cs} | 22 ++ ...ubsubTopicBroadcastingRoutingConvention.cs | 61 ------ ...cs => PubsubTopicListenerConfiguration.cs} | 43 ++-- ... => PubsubTopicSubscriberConfiguration.cs} | 6 +- .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 43 ++-- .../PubsubTransportExtensions.cs | 33 +-- 25 files changed, 567 insertions(+), 655 deletions(-) create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs rename src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/{PubsubTopicTests.cs => PubsubEndpointTests.cs} (53%) delete mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs rename src/Transports/GCP/Wolverine.Pubsub.Tests/{end_to_end.cs => send_and_receive.cs} (64%) delete mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs delete mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs delete mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs rename src/Transports/GCP/Wolverine.Pubsub/{PubsubSubscriptionOptions.cs => PubsubOptions.cs} (58%) delete mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs rename src/Transports/GCP/Wolverine.Pubsub/{PubsubSubscriptionConfiguration.cs => PubsubTopicListenerConfiguration.cs} (51%) rename src/Transports/GCP/Wolverine.Pubsub/{PubsubTopicConfiguration.cs => PubsubTopicSubscriberConfiguration.cs} (55%) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs index 6504652b4..67f141b41 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -8,36 +8,36 @@ namespace Wolverine.Pubsub.Tests; public class BufferedComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/buffered-receiver"), 120) { } + public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var topicName = $"test.{Guid.NewGuid()}"; + var id = Guid.NewGuid().ToString(); - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/{topicName}"); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver.{id}"); await SenderIs(opts => { opts .UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); - opts - .PublishAllMessages() - .ToPubsubTopic(topicName); + + opts.ListenToPubsubTopic($"sender.{id}"); }); await ReceiverIs(opts => { opts .UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); - opts - .ListenToPubsubTopic(topicName) - .BufferedInMemory(); + + opts.ListenToPubsubTopic($"receiver.{id}").BufferedInMemory(); }); } @@ -59,12 +59,12 @@ public virtual async Task dl_mechanics() { var runtime = theReceiver.Services.GetRequiredService(); var transport = runtime.Options.Transports.GetOrCreate(); - var dlSubscription = transport.Topics[PubsubTransport.DeadLetterName].FindOrCreateSubscription(); + var topic = transport.Topics[PubsubTransport.DeadLetterName]; - await dlSubscription.InitializeAsync(NullLogger.Instance); + await topic.InitializeAsync(NullLogger.Instance); var pullResponse = await transport.SubscriberApiClient!.PullAsync( - dlSubscription.Name, + topic.Server.Subscription.Name, maxMessages: 5 ); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs new file mode 100644 index 000000000..9ec95a072 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs @@ -0,0 +1,98 @@ +using IntegrationTests; +using Marten; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Oakton.Resources; +using Shouldly; +using Wolverine.ComplianceTests.Compliance; +using Wolverine.Marten; +using Wolverine.Runtime; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class DurableComplianceFixture : TransportComplianceFixture, IAsyncLifetime { + public DurableComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver"), 120) { } + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + var id = Guid.NewGuid().ToString(); + + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver.{id}"); + + await SenderIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .ConfigureListeners(x => x.UseDurableInbox()) + .ConfigureListeners(x => x.UseDurableInbox()); + + opts.Services + .AddMarten(store => { + store.Connection(Servers.PostgresConnectionString); + store.DatabaseSchemaName = "sender"; + }) + .IntegrateWithWolverine(x => x.MessageStorageSchemaName = "sender"); + + opts.Services.AddResourceSetupOnStartup(); + + opts + .ListenToPubsubTopic($"sender.{id}") + .Named("sender"); + }); + + await ReceiverIs(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .ConfigureListeners(x => x.UseDurableInbox()) + .ConfigureListeners(x => x.UseDurableInbox()); + + opts.Services.AddMarten(store => { + store.Connection(Servers.PostgresConnectionString); + store.DatabaseSchemaName = "receiver"; + }).IntegrateWithWolverine(x => x.MessageStorageSchemaName = "receiver"); + + opts.Services.AddResourceSetupOnStartup(); + + opts + .ListenToPubsubTopic($"receiver.{id}") + .Named("receiver"); + }); + } + + public new async Task DisposeAsync() { + await DisposeAsync(); + } +} + +public class DurableSendingAndReceivingCompliance : TransportCompliance { + [Fact] + public virtual async Task dl_mechanics() { + throwOnAttempt(1); + throwOnAttempt(2); + throwOnAttempt(3); + + await shouldMoveToErrorQueueOnAttempt(1); + + var runtime = theReceiver.Services.GetRequiredService(); + + var transport = runtime.Options.Transports.GetOrCreate(); + var topic = transport.Topics[PubsubTransport.DeadLetterName]; + + await topic.InitializeAsync(NullLogger.Instance); + + var pullResponse = await transport.SubscriberApiClient!.PullAsync( + topic.Server.Subscription.Name, + maxMessages: 5 + ); + + pullResponse.ReceivedMessages.ShouldNotBeEmpty(); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs index abeeb593c..cabfa40fa 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs @@ -1,18 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; using Wolverine.ComplianceTests.Compliance; +using Wolverine.Runtime; using Xunit; namespace Wolverine.Pubsub.Tests; public class InlineComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public InlineComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/inline-receiver"), 120) { } + public InlineComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var topicName = $"test.{Guid.NewGuid()}"; + var id = Guid.NewGuid().ToString(); - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/{topicName}"); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver.{id}"); await SenderIs(opts => { opts @@ -20,9 +24,15 @@ await SenderIs(opts => { .AutoProvision() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); + + opts + .ListenToPubsubTopic($"sender.{id}") + .Named("sender"); + opts .PublishAllMessages() - .ToPubsubTopic(topicName); + .To(OutboundAddress) + .SendInline(); }); await ReceiverIs(opts => { @@ -31,8 +41,10 @@ await ReceiverIs(opts => { .AutoProvision() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); + opts - .ListenToPubsubTopic(topicName) + .ListenToPubsubTopic($"receiver.{id}") + .Named("receiver") .ProcessInline(); }); } @@ -42,4 +54,27 @@ await ReceiverIs(opts => { } } -public class InlineSendingAndReceivingCompliance : TransportCompliance; +public class InlineSendingAndReceivingCompliance : TransportCompliance { + [Fact] + public virtual async Task dl_mechanics() { + throwOnAttempt(1); + throwOnAttempt(2); + throwOnAttempt(3); + + await shouldMoveToErrorQueueOnAttempt(1); + + var runtime = theReceiver.Services.GetRequiredService(); + + var transport = runtime.Options.Transports.GetOrCreate(); + var topic = transport.Topics[PubsubTransport.DeadLetterName]; + + await topic.InitializeAsync(NullLogger.Instance); + + var pullResponse = await transport.SubscriberApiClient!.PullAsync( + topic.Server.Subscription.Name, + maxMessages: 5 + ); + + pullResponse.ReceivedMessages.ShouldNotBeEmpty(); + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs similarity index 53% rename from src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs rename to src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs index 524ad15a0..80f26d4df 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubTopicTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs @@ -4,36 +4,47 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; using Shouldly; +using Wolverine.Configuration; using Wolverine.Pubsub.Internal; using Xunit; namespace Wolverine.Pubsub.Tests.Internal; -public class PubsubTopicTests { +public class PubsubEndpointTests { private PubsubTransport createTransport() => new("wolverine") { PublisherApiClient = Substitute.For(), SubscriberApiClient = Substitute.For(), - EmulatorDetection = EmulatorDetection.EmulatorOnly + EmulatorDetection = EmulatorDetection.EmulatorOnly, }; [Fact] - public void create_uri() { - var topic = new PubsubTopic("top1", createTransport()); + public void default_dead_letter_name_is_transport_default() { + new PubsubEndpoint("foo", createTransport()) + .DeadLetterName.ShouldBe(PubsubTransport.DeadLetterName); + } - topic.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://wolverine/top1")); + [Fact] + public void default_mode_is_buffered() { + new PubsubEndpoint("foo", createTransport()) + .Mode.ShouldBe(EndpointMode.BufferedInMemory); } [Fact] - public void endpoint_name_is_topic_name_without_prefix() { - var topic = new PubsubTopic("top1", createTransport()); + public void default_endpoint_name_is_queue_name() { + new PubsubEndpoint("top1", createTransport()) + .EndpointName.ShouldBe("top1"); + } - topic.EndpointName.ShouldBe("top1"); + [Fact] + public void uri() { + new PubsubEndpoint("top1", createTransport()) + .Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://wolverine/top1")); } [Fact] public async Task initialize_with_no_auto_provision() { var transport = createTransport(); - var topic = new PubsubTopic("foo", transport); + var topic = new PubsubEndpoint("foo", transport); await topic.InitializeAsync(NullLogger.Instance); @@ -46,12 +57,12 @@ public async Task initialize_with_auto_provision() { transport.AutoProvision = true; - var topic = new PubsubTopic("foo", transport); + var topic = new PubsubEndpoint("foo", transport); - transport.PublisherApiClient!.GetTopicAsync(Arg.Is(topic.Name)).Throws(); + transport.PublisherApiClient!.GetTopicAsync(Arg.Is(topic.Server.Topic.Name)).Throws(); await topic.InitializeAsync(NullLogger.Instance); - await transport.PublisherApiClient!.Received().CreateTopicAsync(Arg.Is(topic.Name)); + await transport.PublisherApiClient!.Received().CreateTopicAsync(Arg.Is(topic.Server.Topic.Name)); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs deleted file mode 100644 index aa33c4113..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubSubscriptionTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Google.Api.Gax; -using Google.Cloud.PubSub.V1; -using Microsoft.Extensions.Logging.Abstractions; -using NSubstitute; -using Shouldly; -using Wolverine.Configuration; -using Wolverine.Pubsub.Internal; -using Xunit; - -namespace Wolverine.Pubsub.Tests.Internal; - -public class PubsubSubscriptionTests { - private PubsubTransport createTransport() => new("wolverine") { - PublisherApiClient = Substitute.For(), - SubscriberApiClient = Substitute.For(), - EmulatorDetection = EmulatorDetection.EmulatorOnly, - }; - - [Fact] - public void default_dead_letter_name_is_transport_default() { - new PubsubTopic("foo", createTransport()).FindOrCreateSubscription("bar") - .DeadLetterName.ShouldBe(PubsubTransport.DeadLetterName); - } - - [Fact] - public void default_mode_is_buffered() { - new PubsubTopic("foo", createTransport()).FindOrCreateSubscription("bar") - .Mode.ShouldBe(EndpointMode.BufferedInMemory); - } - - [Fact] - public void create_uri() { - var topic = new PubsubTopic("top1", createTransport()); - var subscription = topic.FindOrCreateSubscription("sub1"); - - subscription.Uri.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://wolverine/top1/sub1")); - } - - [Fact] - public void endpoint_name_is_subscription_name_without_prefix() { - var topic = new PubsubTopic("top1", createTransport()); - var subscription = topic.FindOrCreateSubscription("sub1"); - - subscription.EndpointName.ShouldBe("sub1"); - } - - [Fact] - public async Task initialize_with_no_auto_provision() { - var transport = createTransport(); - var topic = new PubsubTopic("foo", transport); - var subscription = topic.FindOrCreateSubscription("bar"); - - await subscription.InitializeAsync(NullLogger.Instance); - - await transport.SubscriberApiClient!.DidNotReceive().CreateSubscriptionAsync(Arg.Any()); - } - - [Fact] - public async Task initialize_with_auto_provision() { - var transport = createTransport(); - - transport.AutoProvision = true; - - var topic = new PubsubTopic("foo", transport); - var subscription = topic.FindOrCreateSubscription("bar"); - - await subscription.InitializeAsync(NullLogger.Instance); - - await transport.SubscriberApiClient!.Received().CreateSubscriptionAsync(Arg.Is(x => - x.SubscriptionName == subscription.Name && - x.TopicAsTopicName == topic.Name - )); - } -} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs index 831e4e76c..dab75efe4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs @@ -8,19 +8,10 @@ public class PubsubTransportTests { [Fact] public void find_topic_by_uri() { var transport = new PubsubTransport("wolverine"); - var topic = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://wolverine/one")).ShouldBeOfType(); + var topic = transport.GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://wolverine/one")).ShouldBeOfType(); - topic.Name.TopicId.ShouldBe("one"); - } - - [Fact] - public void find_subscription_by_uri() { - var transport = new PubsubTransport("wolverine"); - var subscription = transport - .GetOrCreateEndpoint(new Uri($"{PubsubTransport.ProtocolName}://wolverine/one/red")) - .ShouldBeOfType(); - - subscription.Name.SubscriptionId.ShouldBe("red"); + topic.Server.Topic.Name.TopicId.ShouldBe("one"); + topic.Server.Subscription.Name.SubscriptionId.ShouldBe("one"); } [Fact] @@ -35,21 +26,23 @@ public void return_all_endpoints_gets_dead_letter_subscription_too() { var transport = new PubsubTransport("wolverine") { EnableDeadLettering = true }; - var one = transport.Topics["one"].FindOrCreateSubscription(); - var two = transport.Topics["two"].FindOrCreateSubscription(); - var three = transport.Topics["three"].FindOrCreateSubscription(); + var one = transport.Topics["one"]; + var two = transport.Topics["two"]; + var three = transport.Topics["three"]; one.DeadLetterName = null; two.DeadLetterName = "two-dead-letter"; + two.IsListener = true; + three.IsListener = true; - var endpoints = transport.Endpoints().OfType().ToArray(); + var endpoints = transport.Endpoints().OfType().ToArray(); - endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.one"); - endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.two"); - endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.three"); + endpoints.ShouldContain(x => x.EndpointName == "one"); + endpoints.ShouldContain(x => x.EndpointName == "two"); + endpoints.ShouldContain(x => x.EndpointName == "three"); - endpoints.ShouldContain(x => x.Name.SubscriptionId == $"sub.{PubsubTransport.DeadLetterName}"); - endpoints.ShouldContain(x => x.Name.SubscriptionId == "sub.two-dead-letter"); + endpoints.ShouldContain(x => x.EndpointName == PubsubTransport.DeadLetterName); + endpoints.ShouldContain(x => x.EndpointName == "two-dead-letter"); } [Fact] diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs similarity index 64% rename from src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs rename to src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs index 7cf4f2833..a1df87a69 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/end_to_end.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs @@ -8,7 +8,7 @@ namespace Wolverine.Pubsub.Tests; -public class end_to_end : IAsyncLifetime { +public class send_and_receive : IAsyncLifetime { private IHost _host = default!; public async Task InitializeAsync() { @@ -18,7 +18,7 @@ public async Task InitializeAsync() { _host = await Host.CreateDefaultBuilder() .UseWolverine(opts => { opts.UsePubsubTesting().AutoProvision(); - opts.ListenToPubsubTopic("send_and_receive").ConfigureSubscription(x => x.Mapper = new TestPubsubEnvelopeMapper(x)); + opts.ListenToPubsubTopic("send_and_receive", x => x.Mapper = new TestPubsubEnvelopeMapper(x)); opts.PublishMessage().ToPubsubTopic("send_and_receive"); }).StartAsync(); } @@ -51,12 +51,13 @@ public async Task builds_system_endpoints() { var transport = host.GetRuntime().Options.Transports.GetOrCreate(); var endpoints = transport .Endpoints() - .Where(x => x.Role == EndpointRole.System); - var topics = endpoints.OfType().ToArray(); - var subscriptions = endpoints.OfType().ToArray(); + .Where(x => x.Role == EndpointRole.System) + .OfType().ToArray(); - topics.ShouldContain(x => x.Name.TopicId.StartsWith(PubsubTransport.ResponseName)); - subscriptions.ShouldContain(x => x.Name.SubscriptionId.StartsWith(PubsubTransport.ResponseName)); + endpoints.ShouldContain(x => + x.Server.Topic.Name.TopicId.StartsWith(PubsubTransport.ResponseName) && + x.Server.Subscription.Name.SubscriptionId.StartsWith(PubsubTransport.ResponseName) + ); } [Fact] @@ -71,20 +72,16 @@ public async Task send_and_receive_a_single_message() { } [Fact] - public async Task send_and_receive_multiple_messages_concurreently() { - var session = await _host.TrackActivity() - .IncludeExternalTransports() - .Timeout(1.Minutes()) - .ExecuteAndWaitAsync(ctx => Task.Run(async () => { - await ctx.SendAsync(new PubsubMessage1("Red")); - await ctx.SendAsync(new PubsubMessage1("Green")); - await ctx.SendAsync(new PubsubMessage1("Refactor")); - })); - var received = session.Received.MessagesOf().Select(x => x.Name).ToArray(); + public async Task send_and_receive_many_messages() { + Func sending = async bus => { + for (int i = 0; i < 100; i++) + await bus.PublishAsync(new PubsubMessage1(Guid.NewGuid().ToString())); + }; - received.ShouldContain("Red"); - received.ShouldContain("Green"); - received.ShouldContain("Refactor"); + await _host.TrackActivity() + .IncludeExternalTransports() + .Timeout(5.Minutes()) + .ExecuteAndWaitAsync(sending); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs deleted file mode 100644 index 3461e8e63..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive_with_topics_and_subscriptions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Wolverine.ComplianceTests.Compliance; -using Xunit; - -namespace Wolverine.Pubsub.Tests; - -public class TopicsComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public TopicsComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/topic1"), 120) { } - - public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - - await SenderIs(opts => { - opts - .UsePubsubTesting() - .AutoProvision() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); - opts - .PublishAllMessages() - .ToPubsubTopic("topic1"); - }); - - await ReceiverIs(opts => { - opts - .UsePubsubTesting() - .AutoProvision() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); - opts - .ListenToPubsubTopic("topic1"); - }); - } - - public new async Task DisposeAsync() { - await DisposeAsync(); - } -} - -public class TopicAndSubscriptionSendingAndReceivingCompliance : TransportCompliance; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs index 0fabba2cf..e80d6aa91 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs @@ -8,15 +8,15 @@ namespace Wolverine.Pubsub.Tests; public class PrefixedComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public PrefixedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.buffered-receiver"), 120) { } + public PrefixedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var topicName = $"test.{Guid.NewGuid()}"; + var id = Guid.NewGuid().ToString(); - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.{topicName}"); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.receiver.{id}"); await SenderIs(opts => { opts.UsePubsubTesting() @@ -25,6 +25,9 @@ await SenderIs(opts => { .SystemEndpointsAreEnabled(true) .AutoProvision(); + opts + .ListenToPubsubTopic($"foo.sender.{id}") + .Named("sender"); }); await ReceiverIs(opts => { @@ -34,7 +37,9 @@ await ReceiverIs(opts => { .SystemEndpointsAreEnabled(true) .AutoProvision(); - opts.ListenToPubsubTopic(topicName).Named("receiver"); + opts + .ListenToPubsubTopic($"foo.receiver.{id}") + .Named("receiver"); }); } @@ -45,11 +50,20 @@ await ReceiverIs(opts => { public class PrefixedSendingAndReceivingCompliance : TransportCompliance { [Fact] - public void prefix_was_applied_to_queues_for_the_receiver() { + public void prefix_was_applied_to_endpoint_for_the_receiver() { var runtime = theReceiver.Services.GetRequiredService(); + var endpoint = runtime.Endpoints.EndpointByName("receiver"); - runtime.Endpoints.EndpointByName("receiver") - .ShouldBeOfType() - .Name.SubscriptionId.ShouldStartWith("foo."); + endpoint + .ShouldBeOfType() + .Server.Topic.Name.TopicId.ShouldStartWith("foo."); + + endpoint + .ShouldBeOfType() + .Server.Subscription.Name.SubscriptionId.ShouldStartWith("foo."); + + endpoint + .ShouldBeOfType() + .Uri.Segments.Last().ShouldStartWith("foo."); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs index 51fc85212..02cc6ad4b 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs @@ -9,7 +9,7 @@ namespace Wolverine.Pubsub.Internal; public class BatchedPubsubListener : PubsubListener { public BatchedPubsubListener( - PubsubSubscription endpoint, + PubsubEndpoint endpoint, PubsubTransport transport, IReceiver receiver, IWolverineRuntime runtime @@ -21,10 +21,10 @@ public override async Task StartAsync() { using var streamingPull = _transport.SubscriberApiClient.StreamingPull(CallSettings.FromCancellationToken(_cancellation.Token)); await streamingPull.WriteAsync(new() { - SubscriptionAsSubscriptionName = _endpoint.Name, + SubscriptionAsSubscriptionName = _endpoint.Server.Subscription.Name, StreamAckDeadlineSeconds = 20, - MaxOutstandingMessages = _endpoint.PubsubOptions.MaxOutstandingMessages, - MaxOutstandingBytes = _endpoint.PubsubOptions.MaxOutstandingByteCount, + MaxOutstandingMessages = _endpoint.Client.MaxOutstandingMessages, + MaxOutstandingBytes = _endpoint.Client.MaxOutstandingByteCount, }); await using var stream = streamingPull.GetResponseStream(); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs index 825397f9a..0208e1548 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs @@ -5,21 +5,25 @@ namespace Wolverine.Pubsub.Internal; public class InlinePubsubListener : PubsubListener { public InlinePubsubListener( - PubsubSubscription endpoint, + PubsubEndpoint endpoint, PubsubTransport transport, IReceiver receiver, IWolverineRuntime runtime ) : base(endpoint, transport, receiver, runtime) { } - public override Task StartAsync() => listenForMessagesAsync(async () => { + public override async Task StartAsync() { if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - var response = await _transport.SubscriberApiClient.PullAsync( - _endpoint.Name, - maxMessages: 1, - _cancellation.Token - ); + await listenForMessagesAsync(async () => { + if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - await handleMessagesAsync(response.ReceivedMessages); - }); + var response = await _transport.SubscriberApiClient.PullAsync( + _endpoint.Server.Subscription.Name, + maxMessages: 1, + _cancellation.Token + ); + + await handleMessagesAsync(response.ReceivedMessages); + }); + } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs index c93836155..cd0c103a8 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubSender.cs @@ -1,4 +1,3 @@ -using Google.Cloud.PubSub.V1; using Microsoft.Extensions.Logging; using Wolverine.Runtime; using Wolverine.Transports.Sending; @@ -6,17 +5,17 @@ namespace Wolverine.Pubsub.Internal; public class InlinePubsubSender : ISender { - private readonly PubsubTopic _topic; + private readonly PubsubEndpoint _endpoint; private readonly ILogger _logger; public bool SupportsNativeScheduledSend => false; - public Uri Destination => _topic.Uri; + public Uri Destination => _endpoint.Uri; public InlinePubsubSender( - PubsubTopic topic, + PubsubEndpoint endpoint, IWolverineRuntime runtime ) { - _topic = topic; + _endpoint = endpoint; _logger = runtime.LoggerFactory.CreateLogger(); } @@ -33,5 +32,5 @@ public async Task PingAsync() { } } - public async ValueTask SendAsync(Envelope envelope) => await _topic.SendMessageAsync(envelope, _logger); + public async ValueTask SendAsync(Envelope envelope) => await _endpoint.SendMessageAsync(envelope, _logger); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs index 217738c36..c8f36b9ba 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs @@ -1,15 +1,29 @@ +using System.Diagnostics; +using Google.Cloud.PubSub.V1; +using Grpc.Core; +using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Configuration; +using Wolverine.Runtime; using Wolverine.Transports; +using Wolverine.Transports.Sending; namespace Wolverine.Pubsub.Internal; -public abstract class PubsubEndpoint : Endpoint, IBrokerEndpoint { +public class PubsubEndpoint : Endpoint, IBrokerEndpoint, IBrokerQueue { private IPubsubEnvelopeMapper? _mapper; protected readonly PubsubTransport _transport; protected bool _hasInitialized = false; + public PubsubServerOptions Server = new(); + public PubsubClientOptions Client = new(); + + /// + /// Name of the dead letter for this Google Cloud Pub/Sub subcription where failed messages will be moved + /// + public string? DeadLetterName = PubsubTransport.DeadLetterName; + /// /// Pluggable strategy for interoperability with non-Wolverine systems. Customizes how the incoming Google Cloud Pub/Sub messages /// are read and how outgoing messages are written to Google Cloud Pub/Sub. @@ -31,11 +45,17 @@ public IPubsubEnvelopeMapper Mapper { } public PubsubEndpoint( - Uri uri, + string topicName, PubsubTransport transport, EndpointRole role = EndpointRole.Application - ) : base(uri, role) { + ) : base(new($"{transport.Protocol}://{transport.ProjectId}/{topicName}"), role) { + if (!PubsubTransport.NameRegex.IsMatch(topicName)) throw new WolverinePubsubInvalidEndpointNameException(topicName); + _transport = transport; + + Server.Topic.Name = new(transport.ProjectId, topicName); + Server.Subscription.Name = new(transport.ProjectId, _transport.IdentifierPrefix.IsNotEmpty() && topicName.StartsWith($"{_transport.IdentifierPrefix}.") ? _transport.MaybeCorrectName(topicName.Substring(_transport.IdentifierPrefix.Length + 1)) : topicName); + EndpointName = topicName; } public override async ValueTask InitializeAsync(ILogger logger) { @@ -43,6 +63,7 @@ public override async ValueTask InitializeAsync(ILogger logger) { try { if (_transport.AutoProvision) await SetupAsync(logger); + if (_transport.AutoPurgeAllQueues) await PurgeAsync(logger); } catch (Exception ex) { throw new WolverinePubsubTransportException($"{Uri}: Error trying to initialize Google Cloud Pub/Sub endpoint", ex); @@ -51,9 +72,177 @@ public override async ValueTask InitializeAsync(ILogger logger) { _hasInitialized = true; } - public abstract ValueTask SetupAsync(ILogger logger); - public abstract ValueTask CheckAsync(); - public abstract ValueTask TeardownAsync(ILogger logger); + public async ValueTask SetupAsync(ILogger logger) { + if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + try { + await _transport.PublisherApiClient.CreateTopicAsync(Server.Topic.Name); + } + catch (RpcException ex) { + if (ex.StatusCode != StatusCode.AlreadyExists) { + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Uri, Server.Topic.Name); + + throw; + } + + logger.LogInformation("{Uri}: Google Cloud Pub/Sub topic \"{Topic}\" already exists", Uri, Server.Topic.Name); + } + catch (Exception ex) { + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Uri, Server.Topic.Name); + + throw; + } + + if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + try { + var request = new Subscription { + SubscriptionName = Server.Subscription.Name, + TopicAsTopicName = Server.Topic.Name, + AckDeadlineSeconds = Server.Subscription.Options.AckDeadlineSeconds, + EnableExactlyOnceDelivery = Server.Subscription.Options.EnableExactlyOnceDelivery, + EnableMessageOrdering = Server.Subscription.Options.EnableMessageOrdering, + MessageRetentionDuration = Server.Subscription.Options.MessageRetentionDuration, + RetainAckedMessages = Server.Subscription.Options.RetainAckedMessages, + RetryPolicy = Server.Subscription.Options.RetryPolicy + }; + + if (Server.Subscription.Options.DeadLetterPolicy is not null) request.DeadLetterPolicy = Server.Subscription.Options.DeadLetterPolicy; + if (Server.Subscription.Options.ExpirationPolicy is not null) request.ExpirationPolicy = Server.Subscription.Options.ExpirationPolicy; + if (Server.Subscription.Options.Filter is not null) request.Filter = Server.Subscription.Options.Filter; + + await _transport.SubscriberApiClient.CreateSubscriptionAsync(request); + } + catch (RpcException ex) { + if (ex.StatusCode != StatusCode.AlreadyExists) { + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); + + throw; + } + + logger.LogInformation("{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", Uri, Server.Subscription.Name); + } + catch (Exception ex) { + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); + + throw; + } + } + + public async ValueTask CheckAsync() { + if (_transport.PublisherApiClient is null) return false; + + try { + await _transport.PublisherApiClient.GetTopicAsync(Server.Topic.Name); + + return true; + } + catch { + return false; + } + } + + public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { + if (Mode == EndpointMode.Inline) return ValueTask.FromResult(new InlinePubsubListener( + this, + _transport, + receiver, + runtime + )); + + return ValueTask.FromResult(new BatchedPubsubListener( + this, + _transport, + receiver, + runtime + )); + } + + public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISender? deadLetterSender) { + if (DeadLetterName.IsNotEmpty() && _transport.EnableDeadLettering) { + var dl = _transport.Topics[DeadLetterName]; + + deadLetterSender = new InlinePubsubSender(dl, runtime); + + return true; + } + + deadLetterSender = default; + + return false; + } + + public ValueTask> GetAttributesAsync() => ValueTask.FromResult(new Dictionary()); + + public ValueTask PurgeAsync(ILogger logger) => ValueTask.CompletedTask; + + // public async ValueTask PurgeAsync(ILogger logger) { + // if (_transport.SubscriberApiClient is null) return; + + // try { + // var stopwatch = new Stopwatch(); + + // stopwatch.Start(); + + // while (stopwatch.ElapsedMilliseconds < 2000) { + // var response = await _transport.SubscriberApiClient.PullAsync( + // Server.Subscription.Name, + // maxMessages: 50 + // ); + + // if (!response.ReceivedMessages.Any()) return; + + // await _transport.SubscriberApiClient.AcknowledgeAsync( + // Server.Subscription.Name, + // response.ReceivedMessages.Select(x => x.AckId) + // ); + // }; + // } + // catch (Exception e) { + // logger.LogDebug(e, "{Uri}: Error trying to purge Google Cloud Pub/Sub subscription {Subscription}", Uri, Server.Subscription.Name); + // } + // } + + public async ValueTask TeardownAsync(ILogger logger) { + if (_transport.PublisherApiClient is null || _transport.SubscriberApiClient is null) return; + + await _transport.SubscriberApiClient.DeleteSubscriptionAsync(Server.Subscription.Name); + await _transport.PublisherApiClient.DeleteTopicAsync(Server.Topic.Name); + } + + internal async Task SendMessageAsync(Envelope envelope, ILogger logger) { + if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + if (!_hasInitialized) await InitializeAsync(logger); + + var message = new PubsubMessage(); + + Mapper.MapEnvelopeToOutgoing(envelope, message); + + await _transport.PublisherApiClient.PublishAsync(new() { + TopicAsTopicName = Server.Topic.Name, + Messages = { message } + }); + } + + internal void ConfigureDeadLetter(Action configure) { + if (DeadLetterName.IsEmpty()) return; + + configure(_transport.Topics[DeadLetterName]); + } + + protected override ISender CreateSender(IWolverineRuntime runtime) { + if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + if (Mode == EndpointMode.Inline) return new InlinePubsubSender(this, runtime); + + return new BatchedSender( + this, + new PubsubSenderProtocol(this, _transport.PublisherApiClient, runtime), + runtime.DurabilitySettings.Cancellation, + runtime.LoggerFactory.CreateLogger() + ); + } protected override bool supportsMode(EndpointMode mode) => true; } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs index fac2ee21c..31d833888 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -10,13 +10,13 @@ namespace Wolverine.Pubsub.Internal; public abstract class PubsubListener : IListener, ISupportDeadLetterQueue { - protected readonly PubsubSubscription _endpoint; + protected readonly PubsubEndpoint _endpoint; protected readonly PubsubTransport _transport; protected readonly IReceiver _receiver; protected readonly ILogger _logger; - protected readonly PubsubTopic? _deadLetterTopic; + protected readonly PubsubEndpoint? _deadLetterTopic; protected readonly RetryBlock _resend; - protected readonly RetryBlock? _deadLetter; + protected readonly RetryBlock _deadLetter; protected readonly CancellationTokenSource _cancellation = new(); protected RetryBlock _complete; @@ -26,7 +26,7 @@ public abstract class PubsubListener : IListener, ISupportDeadLetterQueue { public Uri Address => _endpoint.Uri; public PubsubListener( - PubsubSubscription endpoint, + PubsubEndpoint endpoint, PubsubTransport transport, IReceiver receiver, IWolverineRuntime runtime @@ -39,10 +39,10 @@ IWolverineRuntime runtime _logger = runtime.LoggerFactory.CreateLogger(); _resend = new RetryBlock(async (envelope, _) => { - await _endpoint.Topic.SendMessageAsync(envelope, _logger); + await _endpoint.SendMessageAsync(envelope, _logger); }, _logger, runtime.Cancellation); - if (_endpoint.DeadLetterName.IsNotEmpty() && !transport.EnableDeadLettering) { + if (_endpoint.DeadLetterName.IsNotEmpty() && transport.EnableDeadLettering) { NativeDeadLetterQueueEnabled = true; _deadLetterTopic = _transport.Topics[_endpoint.DeadLetterName]; } @@ -57,7 +57,7 @@ IWolverineRuntime runtime async (e, _) => { if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - await transport.SubscriberApiClient.AcknowledgeAsync(_endpoint.Name, e.Select(x => x.AckId)); + await transport.SubscriberApiClient.AcknowledgeAsync(_endpoint.Server.Subscription.Name, e.Select(x => x.AckId)); }, _logger, _cancellation.Token @@ -74,7 +74,7 @@ public async ValueTask DeferAsync(Envelope envelope) { if (envelope is PubsubEnvelope e) await _resend.PostAsync(e); } - public Task MoveToErrorsAsync(Envelope envelope, Exception exception) => _deadLetter?.PostAsync(envelope) ?? Task.CompletedTask; + public Task MoveToErrorsAsync(Envelope envelope, Exception exception) => _deadLetter.PostAsync(envelope) ?? Task.CompletedTask; public async Task TryRequeueAsync(Envelope envelope) { if (envelope is PubsubEnvelope) { @@ -97,7 +97,7 @@ public ValueTask DisposeAsync() { _task.SafeDispose(); _complete.SafeDispose(); _resend.SafeDispose(); - _deadLetter?.SafeDispose(); + _deadLetter.SafeDispose(); return ValueTask.CompletedTask; } @@ -122,7 +122,7 @@ protected async Task listenForMessagesAsync(Func listenAsync) { catch (Exception ex) { retryCount++; - if (retryCount > _endpoint.MaxRetryCount) { + if (retryCount > _endpoint.Client.RetryPolicy.MaxRetryCount) { _logger.LogError(ex, "{Uri}: Max retry attempts reached, unable to restart listener.", _endpoint.Uri); throw; @@ -133,10 +133,10 @@ protected async Task listenForMessagesAsync(Func listenAsync) { "{Uri}: Error while trying to retrieve messages from Google Cloud Pub/Sub, attempting to restart stream ({RetryCount}/{MaxRetryCount})...", _endpoint.Uri, retryCount, - _endpoint.MaxRetryCount + _endpoint.Client.RetryPolicy.MaxRetryCount ); - int retryDelay = (int) Math.Pow(2, retryCount) * _endpoint.RetryDelay; + int retryDelay = (int) Math.Pow(2, retryCount) * _endpoint.Client.RetryPolicy.RetryDelay; await Task.Delay(retryDelay, _cancellation.Token); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs index 41bf2e6a5..307494454 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs @@ -7,22 +7,22 @@ namespace Wolverine.Pubsub.Internal; internal class PubsubSenderProtocol : ISenderProtocol { - private readonly PubsubTopic _topic; + private readonly PubsubEndpoint _endpoint; private readonly PublisherServiceApiClient _client; private readonly ILogger _logger; public PubsubSenderProtocol( - PubsubTopic topic, + PubsubEndpoint endpoint, PublisherServiceApiClient client, IWolverineRuntime runtime ) { - _topic = topic; + _endpoint = endpoint; _client = client; _logger = runtime.LoggerFactory.CreateLogger(); } public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch batch) { - await _topic.InitializeAsync(_logger); + await _endpoint.InitializeAsync(_logger); var messages = new List(); var successes = new List(); @@ -32,13 +32,13 @@ public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch try { var message = new PubsubMessage(); - _topic.Mapper.MapEnvelopeToOutgoing(envelope, message); + _endpoint.Mapper.MapEnvelopeToOutgoing(envelope, message); messages.Add(message); successes.Add(envelope); } catch (Exception ex) { - _logger.LogError(ex, "{Uril}: Error while mapping envelope \"{Envelope}\" to a PubsubMessage object.", _topic.Uri, envelope); + _logger.LogError(ex, "{Uril}: Error while mapping envelope \"{Envelope}\" to a PubsubMessage object.", _endpoint.Uri, envelope); fails.Add(envelope); } @@ -46,7 +46,7 @@ public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch try { await _client.PublishAsync(new() { - TopicAsTopicName = _topic.Name, + TopicAsTopicName = _endpoint.Server.Topic.Name, Messages = { messages } }); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs deleted file mode 100644 index 01350ccc5..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSubscription.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Google.Cloud.PubSub.V1; -using Grpc.Core; -using JasperFx.Core; -using Microsoft.Extensions.Logging; -using Wolverine.Configuration; -using Wolverine.Runtime; -using Wolverine.Transports; -using Wolverine.Transports.Sending; - -namespace Wolverine.Pubsub.Internal; - -public class PubsubSubscription : PubsubEndpoint, IBrokerQueue { - public readonly SubscriptionName Name; - public readonly PubsubTopic Topic; - - public int MaxRetryCount = 5; - public int RetryDelay = 1000; - - /// - /// Name of the dead letter for this Google Cloud Pub/Sub subcription where failed messages will be moved - /// - public string? DeadLetterName = PubsubTransport.DeadLetterName; - - public PubsubSubscriptionOptions PubsubOptions = new PubsubSubscriptionOptions(); - - public PubsubSubscription( - string subscriptionName, - PubsubTopic topic, - PubsubTransport transport, - EndpointRole role = EndpointRole.Application - ) : base(new($"{transport.Protocol}://{transport.ProjectId}/{topic.Name.TopicId}/{subscriptionName}"), transport, role) { - if (!PubsubTransport.NameRegex.IsMatch(subscriptionName)) throw new WolverinePubsubInvalidEndpointNameException(subscriptionName); - - Name = new(transport.ProjectId, subscriptionName); - Topic = topic; - EndpointName = subscriptionName; - IsListener = true; - } - - public override async ValueTask SetupAsync(ILogger logger) { - if (_transport.SubscriberApiClient is null || _transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - - try { - var request = new Subscription { - SubscriptionName = Name, - TopicAsTopicName = Topic.Name, - AckDeadlineSeconds = PubsubOptions.AckDeadlineSeconds, - EnableExactlyOnceDelivery = PubsubOptions.EnableExactlyOnceDelivery, - EnableMessageOrdering = PubsubOptions.EnableMessageOrdering, - MessageRetentionDuration = PubsubOptions.MessageRetentionDuration, - RetainAckedMessages = PubsubOptions.RetainAckedMessages, - RetryPolicy = PubsubOptions.RetryPolicy - }; - - if (PubsubOptions.DeadLetterPolicy is not null) request.DeadLetterPolicy = PubsubOptions.DeadLetterPolicy; - if (PubsubOptions.ExpirationPolicy is not null) request.ExpirationPolicy = PubsubOptions.ExpirationPolicy; - if (PubsubOptions.Filter is not null) request.Filter = PubsubOptions.Filter; - - await _transport.SubscriberApiClient.CreateSubscriptionAsync(request); - } - catch (RpcException ex) { - if (ex.StatusCode != StatusCode.AlreadyExists) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Name, Topic.Name); - - throw; - } - - logger.LogInformation("{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", Uri, Name); - } - catch (Exception ex) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Name, Topic.Name); - - throw; - } - } - - public ValueTask PurgeAsync(ILogger logger) => ValueTask.CompletedTask; - - public override async ValueTask CheckAsync() { - if (_transport.SubscriberApiClient is null) return false; - - try { - await _transport.SubscriberApiClient.GetSubscriptionAsync(Name); - - return true; - } - catch { - return false; - } - } - - public ValueTask> GetAttributesAsync() => ValueTask.FromResult(new Dictionary()); - - public override async ValueTask TeardownAsync(ILogger logger) { - if (_transport.SubscriberApiClient is null) return; - - await _transport.SubscriberApiClient.DeleteSubscriptionAsync(Name); - } - - public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { - if (Mode == EndpointMode.Inline) return ValueTask.FromResult(new InlinePubsubListener( - this, - _transport, - receiver, - runtime - )); - - return ValueTask.FromResult(new BatchedPubsubListener( - this, - _transport, - receiver, - runtime - )); - } - - public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISender? deadLetterSender) { - if (DeadLetterName.IsNotEmpty() && _transport.EnableDeadLettering) { - var dl = _transport.Topics[DeadLetterName]; - - deadLetterSender = new InlinePubsubSender(dl, runtime); - - return true; - } - - deadLetterSender = default; - - return false; - } - - protected override ISender CreateSender(IWolverineRuntime runtime) => throw new NotSupportedException(); - - internal void ConfigureDeadLetter(Action configure) { - if (DeadLetterName.IsEmpty()) return; - - configure(_transport.Topics[DeadLetterName].FindOrCreateSubscription($"sub.{DeadLetterName}")); - } -} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs deleted file mode 100644 index 24db43cbf..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubTopic.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Text.RegularExpressions; -using Google.Cloud.PubSub.V1; -using Grpc.Core; -using JasperFx.Core; -using Microsoft.Extensions.Logging; -using Wolverine.Configuration; -using Wolverine.Runtime; -using Wolverine.Transports; -using Wolverine.Transports.Sending; - -namespace Wolverine.Pubsub.Internal; - -public class PubsubTopic : PubsubEndpoint { - public TopicName Name { get; } - - public PubsubTopic( - string topicName, - PubsubTransport transport, - EndpointRole role = EndpointRole.Application - ) : base(new($"{transport.Protocol}://{transport.ProjectId}/{topicName}"), transport, role) { - if (!PubsubTransport.NameRegex.IsMatch(topicName)) throw new WolverinePubsubInvalidEndpointNameException(topicName); - - Name = new(transport.ProjectId, topicName); - EndpointName = topicName; - IsListener = false; - } - - public PubsubSubscription FindOrCreateSubscription(string? subscriptionName = null) { - var fallbackName = _transport.MaybeCorrectName($"sub.{(_transport.IdentifierPrefix.IsNotEmpty() && Name.TopicId.StartsWith($"{_transport.IdentifierPrefix}.") ? Name.TopicId.Substring(_transport.IdentifierPrefix.Length + 1) : Name.TopicId)}"); - var existing = _transport.Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == $"{Uri.OriginalString}/{subscriptionName ?? fallbackName}"); - - if (existing != null) return existing; - - var subscription = new PubsubSubscription(subscriptionName ?? fallbackName, this, _transport); - - _transport.Subscriptions.Add(subscription); - - return subscription; - } - - public override async ValueTask SetupAsync(ILogger logger) { - if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - - try { - await _transport.PublisherApiClient.CreateTopicAsync(Name); - } - catch (RpcException ex) { - if (ex.StatusCode != StatusCode.AlreadyExists) { - logger.LogError(ex, "Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Name); - - throw; - } - - logger.LogInformation("Google Cloud Pub/Sub topic \"{Topic}\" already exists", Name); - } - catch (Exception ex) { - logger.LogError(ex, "Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Name); - - throw; - } - } - - public override async ValueTask CheckAsync() { - if (_transport.PublisherApiClient is null) return false; - - try { - await _transport.PublisherApiClient.GetTopicAsync(Name); - - return true; - } - catch { - return false; - } - } - - public override async ValueTask TeardownAsync(ILogger logger) { - if (_transport.PublisherApiClient is null) return; - - await _transport.PublisherApiClient.DeleteTopicAsync(Name); - } - - public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { - if (Mode == EndpointMode.Inline) return ValueTask.FromResult(new InlinePubsubListener( - FindOrCreateSubscription(), - _transport, - receiver, - runtime - )); - - return ValueTask.FromResult(new BatchedPubsubListener( - FindOrCreateSubscription(), - _transport, - receiver, - runtime - )); - } - - protected override ISender CreateSender(IWolverineRuntime runtime) { - if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - - if (Mode == EndpointMode.Inline) return new InlinePubsubSender(this, runtime); - - return new BatchedSender( - this, - new PubsubSenderProtocol(this, _transport.PublisherApiClient, runtime), - runtime.DurabilitySettings.Cancellation, - runtime.LoggerFactory.CreateLogger() - ); - } - - internal async Task SendMessageAsync(Envelope envelope, ILogger logger) { - if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - - if (!_hasInitialized) await InitializeAsync(logger); - - var message = new PubsubMessage(); - - Mapper.MapEnvelopeToOutgoing(envelope, message); - - await _transport.PublisherApiClient.PublishAsync(new() { - TopicAsTopicName = Name, - Messages = { message } - }); - } -} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs index acfd54eda..64500ea77 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs @@ -1,3 +1,4 @@ +using Google.Api.Gax; using Wolverine.Pubsub.Internal; using Wolverine.Transports; @@ -5,28 +6,25 @@ namespace Wolverine.Pubsub; public class PubsubConfiguration : BrokerExpression< PubsubTransport, - PubsubSubscription, - PubsubTopic, - PubsubSubscriptionConfiguration, - PubsubTopicConfiguration, + PubsubEndpoint, + PubsubEndpoint, + PubsubTopicListenerConfiguration, + PubsubTopicSubscriberConfiguration, PubsubConfiguration > { public PubsubConfiguration(PubsubTransport transport, WolverineOptions options) : base(transport, options) { } /// - /// Opt into using conventional message routing using topics and - /// subscriptions based on message type names + /// Set emulator detection for the Google Cloud Pub/Sub transport /// + /// + /// Remember to set the environment variable `PUBSUB_EMULATOR_HOST` to the emulator's host and port + /// and the eniviroment variable `PUBSUB_PROJECT_ID` to a project id + /// /// /// - public PubsubConfiguration UseTopicAndSubscriptionConventionalRouting( - Action? configure = null - ) { - var routing = new PubsubTopicBroadcastingRoutingConvention(); - - configure?.Invoke(routing); - - Options.RouteWith(routing); + public PubsubConfiguration UseEmulatorDetection(EmulatorDetection emulatorDetection = EmulatorDetection.EmulatorOrProduction) { + Transport.EmulatorDetection = emulatorDetection; return this; } @@ -72,6 +70,6 @@ public PubsubConfiguration EnableAllNativeDeadLettering() { return this; } - protected override PubsubSubscriptionConfiguration createListenerExpression(PubsubSubscription subscriberEndpoint) => new(subscriberEndpoint); - protected override PubsubTopicConfiguration createSubscriberExpression(PubsubTopic topicEndpoint) => new(topicEndpoint); + protected override PubsubTopicListenerConfiguration createListenerExpression(PubsubEndpoint listenerEndpoint) => new(listenerEndpoint); + protected override PubsubTopicSubscriberConfiguration createSubscriberExpression(PubsubEndpoint subscriberEndpoint) => new(subscriberEndpoint); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs index 2c335983f..777894c49 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubMessageRoutingConvention.cs @@ -5,32 +5,24 @@ namespace Wolverine.Pubsub; public class PubsubMessageRoutingConvention : MessageRoutingConvention< PubsubTransport, - PubsubSubscriptionConfiguration, - PubsubTopicConfiguration, + PubsubTopicListenerConfiguration, + PubsubTopicSubscriberConfiguration, PubsubMessageRoutingConvention > { - protected override (PubsubSubscriptionConfiguration, Endpoint) FindOrCreateListenerForIdentifier( - string identifier, - PubsubTransport transport, - Type messageType - ) { + protected override (PubsubTopicListenerConfiguration, Endpoint) FindOrCreateListenerForIdentifier(string identifier, PubsubTransport transport, Type messageType) { var topic = transport.Topics[identifier]; - var subscription = topic.FindOrCreateSubscription(); - return (new PubsubSubscriptionConfiguration(subscription), subscription); + return (new PubsubTopicListenerConfiguration(topic), topic); } - protected override (PubsubTopicConfiguration, Endpoint) FindOrCreateSubscriber( - string identifier, - PubsubTransport transport - ) { + protected override (PubsubTopicSubscriberConfiguration, Endpoint) FindOrCreateSubscriber(string identifier, PubsubTransport transport) { var topic = transport.Topics[identifier]; - return (new PubsubTopicConfiguration(topic), topic); + return (new PubsubTopicSubscriberConfiguration(topic), topic); } /// - /// Specify naming rules for the subscribing topic for message types + /// Alternative syntax to specify the name for the queue that each message type will be sent /// /// /// diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs similarity index 58% rename from src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs rename to src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs index 2b9d3a351..6565bbfe3 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionOptions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs @@ -3,10 +3,27 @@ namespace Wolverine.Pubsub; +public class PubsubServerOptions { + public PubsubTopicOptions Topic { get; set; } = new(); + public PubsubSubscriptionOptions Subscription { get; set; } = new(); +} + +public class PubsubTopicOptions { + public TopicName Name { get; set; } = default!; +} + public class PubsubSubscriptionOptions { + public SubscriptionName Name { get; set; } = default!; + public CreateSubscriptionOptions Options = new(); +} + +public class PubsubClientOptions { public long MaxOutstandingMessages = 1000; public long MaxOutstandingByteCount = 100 * 1024 * 1024; + public PubsubRetryPolicy RetryPolicy = new(); +} +public class CreateSubscriptionOptions { public int AckDeadlineSeconds = 10; public DeadLetterPolicy? DeadLetterPolicy = null; public bool EnableExactlyOnceDelivery = false; @@ -20,3 +37,8 @@ public class PubsubSubscriptionOptions { MaximumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(600)) }; } + +public class PubsubRetryPolicy { + public int MaxRetryCount = 5; + public int RetryDelay = 1000; +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs deleted file mode 100644 index 3b8db6a76..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicBroadcastingRoutingConvention.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Wolverine.Pubsub.Internal; -using Wolverine.Configuration; -using Wolverine.Transports; - -namespace Wolverine.Pubsub; - -public class PubsubTopicBroadcastingRoutingConvention : MessageRoutingConvention< - PubsubTransport, - PubsubSubscriptionConfiguration, - PubsubTopicConfiguration, - PubsubTopicBroadcastingRoutingConvention -> { - private Func? _subscriptionNameSource; - - protected override (PubsubSubscriptionConfiguration, Endpoint) FindOrCreateListenerForIdentifier( - string identifier, - PubsubTransport transport, - Type messageType - ) { - var topic = transport.Topics[identifier]; - var subscriptionName = _subscriptionNameSource == null ? identifier : _subscriptionNameSource(messageType); - var subscription = topic.FindOrCreateSubscription(subscriptionName); - - return (new PubsubSubscriptionConfiguration(subscription), subscription); - } - - protected override (PubsubTopicConfiguration, Endpoint) FindOrCreateSubscriber( - string identifier, - PubsubTransport transport - ) { - var topic = transport.Topics[identifier]; - - return (new PubsubTopicConfiguration(topic), topic); - } - - /// - /// Override the naming convention for topics. Identical in functionality to IdentifierForSender() - /// - /// - /// - public PubsubTopicBroadcastingRoutingConvention TopicNameForSender(Func nameSource) => IdentifierForSender(nameSource); - - /// - /// Override the subscription name for a message type. By default this would be the same as the topic - /// - /// - /// - /// - public PubsubTopicBroadcastingRoutingConvention SubscriptionNameForListener(Func nameSource) { - _subscriptionNameSource = nameSource; - - return this; - } - - /// - /// Override the topic name by message type for listeners. This has the same functionality as IdentifierForListener() - /// - /// - /// - public PubsubTopicBroadcastingRoutingConvention TopicNameForListener(Func nameSource) => IdentifierForListener(nameSource); -} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs similarity index 51% rename from src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs rename to src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs index b544cb96a..16c29afb8 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubSubscriptionConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs @@ -4,15 +4,15 @@ namespace Wolverine.Pubsub; -public class PubsubSubscriptionConfiguration : ListenerConfiguration { - public PubsubSubscriptionConfiguration(PubsubSubscription endpoint) : base(endpoint) { } +public class PubsubTopicListenerConfiguration : ListenerConfiguration { + public PubsubTopicListenerConfiguration(PubsubEndpoint endpoint) : base(endpoint) { } /// /// Add circuit breaker exception handling to this listener /// /// /// - public PubsubSubscriptionConfiguration CircuitBreaker(Action? configure = null) { + public PubsubTopicListenerConfiguration CircuitBreaker(Action? configure = null) { add(e => { e.CircuitBreakerOptions = new CircuitBreakerOptions(); @@ -23,37 +23,52 @@ public PubsubSubscriptionConfiguration CircuitBreaker(Action - /// Configure the underlying Google Cloud Pub/Sub subscription. This is only applicable when - /// Wolverine is creating the subscriptions + /// Configure the underlying Google Cloud Pub/Sub topic and subscription. This is only applicable when + /// Wolverine is creating the topic and subscription /// /// /// - public PubsubSubscriptionConfiguration ConfigureSubscription(Action configure) { - add(s => configure(s)); + public PubsubTopicListenerConfiguration ConfigureServer(Action configure) { + add(e => configure(e.Server)); return this; } /// - /// Completely disable all Google Cloud Pub/Sub dead lettering for just this subscription + /// Configure the underlying Google Cloud Pub/Sub subscriber client. This is only applicable when + /// Wolverine is creating the subscriber client + /// + /// + /// + public PubsubTopicListenerConfiguration ConfigureClient(Action configure) { + add(e => configure(e.Client)); + + return this; + } + + /// + /// Completely disable all Google Cloud Pub/Sub dead lettering for just this endpoint /// /// - public PubsubSubscriptionConfiguration DisableDeadLettering() { - add(e => e.DeadLetterName = null); + public PubsubTopicListenerConfiguration DisableDeadLettering() { + add(e => { + e.DeadLetterName = null; + e.Server.Subscription.Options.DeadLetterPolicy = null; + }); return this; } /// - /// Customize the dead lettering for just this subscription + /// Customize the dead lettering for just this endpoint /// /// /// Optionally configure properties of the dead lettering itself /// /// - public PubsubSubscriptionConfiguration ConfigureDeadLettering( + public PubsubTopicListenerConfiguration ConfigureDeadLettering( string deadLetterName, - Action? configure = null + Action? configure = null ) { add(e => { e.DeadLetterName = deadLetterName; @@ -69,7 +84,7 @@ public PubsubSubscriptionConfiguration ConfigureDeadLettering( /// /// /// - public PubsubSubscriptionConfiguration InteropWith(IPubsubEnvelopeMapper mapper) { + public PubsubTopicListenerConfiguration InteropWith(IPubsubEnvelopeMapper mapper) { add(e => e.Mapper = mapper); return this; diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs similarity index 55% rename from src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs rename to src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs index 46d629172..47fc64ddd 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs @@ -3,15 +3,15 @@ namespace Wolverine.Pubsub; -public class PubsubTopicConfiguration : SubscriberConfiguration { - public PubsubTopicConfiguration(PubsubTopic endpoint) : base(endpoint) { } +public class PubsubTopicSubscriberConfiguration : SubscriberConfiguration { + public PubsubTopicSubscriberConfiguration(PubsubEndpoint endpoint) : base(endpoint) { } /// /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems /// /// /// - public PubsubTopicConfiguration InteropWith(IPubsubEnvelopeMapper mapper) { + public PubsubTopicSubscriberConfiguration InteropWith(IPubsubEnvelopeMapper mapper) { add(e => e.Mapper = mapper); return this; diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index c81ef098a..f6bbd10fd 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -21,15 +21,14 @@ public class PubsubTransport : BrokerTransport, IAsyncDisposable internal SubscriberServiceApiClient? SubscriberApiClient = null; - public readonly LightweightCache Topics; - public readonly List Subscriptions = new(); + public readonly LightweightCache Topics; public string ProjectId = string.Empty; public EmulatorDetection EmulatorDetection = EmulatorDetection.None; public bool EnableDeadLettering = false; /// - /// Is this transport connection allowed to build and use response and retry queues + /// Is this transport connection allowed to build and use response topic and subscription /// for just this node? /// public bool SystemEndpointsEnabled = false; @@ -60,7 +59,7 @@ public override async ValueTask ConnectAsync(IWolverineRuntime runtime) { public override Endpoint? ReplyEndpoint() { var endpoint = base.ReplyEndpoint(); - if (endpoint is PubsubSubscription e) return e.Topic; + if (endpoint is PubsubEndpoint) return endpoint; return null; } @@ -71,43 +70,27 @@ public override IEnumerable DiagnosticColumns() { public ValueTask DisposeAsync() => ValueTask.CompletedTask; - protected override IEnumerable explicitEndpoints() { - foreach (var topic in Topics) yield return topic; - foreach (var subscription in Subscriptions) yield return subscription; - } + protected override IEnumerable explicitEndpoints() => Topics; protected override IEnumerable endpoints() { if (EnableDeadLettering) { - var dlNames = Subscriptions.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); + var dlNames = Topics.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); foreach (var dlName in dlNames) { - var dlSubscription = Topics[dlName!].FindOrCreateSubscription($"sub.{dlName}"); + var dlTopic = Topics[dlName!]; - dlSubscription.DeadLetterName = null; - dlSubscription.PubsubOptions.DeadLetterPolicy = null; + dlTopic.DeadLetterName = null; + dlTopic.Server.Subscription.Options.DeadLetterPolicy = null; + dlTopic.IsListener = true; } } - foreach (var topic in Topics) yield return topic; - foreach (var subscription in Subscriptions) yield return subscription; + return Topics; } protected override PubsubEndpoint findEndpointByUri(Uri uri) { if (uri.Scheme != Protocol) throw new ArgumentOutOfRangeException(nameof(uri)); - if (uri.Segments.Length == 3) { - var existing = Subscriptions.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString); - - if (existing is not null) return existing; - - var topic = Topics[uri.Segments[1].TrimEnd('/')]; - var subscription = new PubsubSubscription(uri.Segments[2].TrimEnd('/'), topic, this); - - Subscriptions.Add(subscription); - - return subscription; - } - return Topics.FirstOrDefault(x => x.Uri.OriginalString == uri.OriginalString) ?? Topics[uri.Segments[1].TrimEnd('/')]; } @@ -115,12 +98,10 @@ protected override void tryBuildSystemEndpoints(IWolverineRuntime runtime) { if (!SystemEndpointsEnabled) return; var responseName = $"{ResponseName}.{Math.Abs(runtime.DurabilitySettings.AssignedNodeNumber)}"; - var responseTopic = new PubsubTopic(responseName, this, EndpointRole.System); - var responseSubscription = new PubsubSubscription($"sub.{responseName}", responseTopic, this, EndpointRole.System); + var responseTopic = new PubsubEndpoint(responseName, this, EndpointRole.System); - responseSubscription.IsUsedForReplies = true; + responseTopic.IsListener = responseTopic.IsUsedForReplies = true; Topics[responseName] = responseTopic; - Subscriptions.Add(responseSubscription); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs index 2a9a69a9d..d6611be02 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs @@ -1,3 +1,4 @@ +using Google.Api.Gax; using JasperFx.Core.Reflection; using Wolverine.Configuration; using Wolverine.Pubsub.Internal; @@ -7,8 +8,8 @@ namespace Wolverine.Pubsub; public static class PubsubTransportExtensions { /// - /// Quick access to the Google Cloud Pub/Sub Transport within this application. - /// This is for advanced usage + /// Quick access to the Google Cloud Pub/Sub Transport within this application. + /// This is for advanced usage. /// /// /// @@ -19,14 +20,14 @@ internal static PubsubTransport PubsubTransport(this WolverineOptions endpoints) } /// - /// Additive configuration to the Google Cloud Pub/Sub integration for this Wolverine application + /// Additive configuration to the Google Cloud Pub/Sub integration for this Wolverine application. /// /// /// public static PubsubConfiguration ConfigurePubsub(this WolverineOptions endpoints) => new PubsubConfiguration(endpoints.PubsubTransport(), endpoints); /// - /// Connect to Google Cloud Pub/Sub with a prject id + /// Connect to Google Cloud Pub/Sub with a project id. /// /// /// @@ -44,37 +45,37 @@ public static PubsubConfiguration UsePubsub(this WolverineOptions endpoints, str } /// - /// Listen for incoming messages at the designated Google Cloud Pub/Sub topic by name + /// Listen for incoming messages at the designated Google Cloud Pub/Sub topic by name. /// /// /// The name of the Google Cloud Pub/Sub topic /// - /// Optional configuration for this Google Cloud Pub/Sub subscription if being initialized by Wolverine - /// - public static PubsubSubscriptionConfiguration ListenToPubsubTopic( + /// Optional configuration for this Google Cloud Pub/Sub endpoint if being initialized by Wolverine. + /// + /// + public static PubsubTopicListenerConfiguration ListenToPubsubTopic( this WolverineOptions endpoints, string topicName, - Action? configure = null + Action? configure = null ) { var transport = endpoints.PubsubTransport(); var topic = transport.Topics[transport.MaybeCorrectName(topicName)]; topic.EndpointName = topicName; + topic.IsListener = true; - var subscription = topic.FindOrCreateSubscription(); - - configure?.Invoke(subscription); + configure?.Invoke(topic); - return new PubsubSubscriptionConfiguration(subscription); + return new PubsubTopicListenerConfiguration(topic); } /// - /// Publish the designated messages to a Google Cloud Pub/Sub topic + /// Publish the designated messages to a Google Cloud Pub/Sub topic. /// /// /// /// - public static PubsubTopicConfiguration ToPubsubTopic( + public static PubsubTopicSubscriberConfiguration ToPubsubTopic( this IPublishToExpression publishing, string topicName ) { @@ -87,6 +88,6 @@ string topicName // This is necessary unfortunately to hook up the subscription rules publishing.To(topic.Uri); - return new PubsubTopicConfiguration(topic); + return new PubsubTopicSubscriberConfiguration(topic); } } From 4395976846dd77fa47703070f4c06cb1af4416bb Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:02:13 +0200 Subject: [PATCH 05/16] Refactored envelope mapping --- .../BufferedSendingAndReceivingCompliance.cs | 19 ++- .../DurableSendingAndReceivingCompliance.cs | 25 ++-- .../InlineSendingAndReceivingCompliance.cs | 25 ++-- .../Internal/PubsubEndpointTests.cs | 7 +- .../TestPubsubEnvelopeMapper.cs | 11 ++ .../TestingExtensions.cs | 12 +- .../send_and_receive.cs | 37 +++-- .../sending_compliance_with_prefixes.cs | 24 ++- .../Internal/BatchedPubsubListener.cs | 15 +- .../Internal/PubsubEndpoint.cs | 138 +++++++++++------- .../Internal/PubsubEnvelope.cs | 16 +- .../Internal/PubsubEnvelopeMapper.cs | 55 ++----- .../Internal/PubsubListener.cs | 34 +++-- .../Internal/PubsubSenderProtocol.cs | 6 +- .../GCP/Wolverine.Pubsub/PubsubOptions.cs | 17 ++- .../PubsubTopicListenerConfiguration.cs | 11 ++ .../PubsubTopicSubscriberConfiguration.cs | 68 +++++++++ .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 3 + .../PubsubTransportExtensions.cs | 6 +- .../SubscriptionNameExtensions.cs | 10 ++ 20 files changed, 342 insertions(+), 197 deletions(-) create mode 100644 src/Transports/GCP/Wolverine.Pubsub/SubscriptionNameExtensions.cs diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs index 67f141b41..c9e480899 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -8,7 +8,7 @@ namespace Wolverine.Pubsub.Tests; public class BufferedComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver"), 120) { } + public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/buffered-receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); @@ -16,7 +16,7 @@ public async Task InitializeAsync() { var id = Guid.NewGuid().ToString(); - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver.{id}"); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/buffered-receiver.{id}"); await SenderIs(opts => { opts @@ -25,8 +25,6 @@ await SenderIs(opts => { .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); - - opts.ListenToPubsubTopic($"sender.{id}"); }); await ReceiverIs(opts => { @@ -37,7 +35,9 @@ await ReceiverIs(opts => { .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); - opts.ListenToPubsubTopic($"receiver.{id}").BufferedInMemory(); + opts + .ListenToPubsubTopic($"buffered-receiver.{id}") + .BufferedInMemory(); }); } @@ -57,15 +57,14 @@ public virtual async Task dl_mechanics() { await shouldMoveToErrorQueueOnAttempt(1); var runtime = theReceiver.Services.GetRequiredService(); - var transport = runtime.Options.Transports.GetOrCreate(); - var topic = transport.Topics[PubsubTransport.DeadLetterName]; + var dl = transport.Topics[PubsubTransport.DeadLetterName]; - await topic.InitializeAsync(NullLogger.Instance); + await dl.InitializeAsync(NullLogger.Instance); var pullResponse = await transport.SubscriberApiClient!.PullAsync( - topic.Server.Subscription.Name, - maxMessages: 5 + dl.Server.Subscription.Name, + maxMessages: 1 ); pullResponse.ReceivedMessages.ShouldNotBeEmpty(); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs index 9ec95a072..29cbd9767 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs @@ -1,3 +1,4 @@ +using Google.Cloud.PubSub.V1; using IntegrationTests; using Marten; using Microsoft.Extensions.DependencyInjection; @@ -12,7 +13,7 @@ namespace Wolverine.Pubsub.Tests; public class DurableComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public DurableComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver"), 120) { } + public DurableComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/durable-receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); @@ -20,12 +21,13 @@ public async Task InitializeAsync() { var id = Guid.NewGuid().ToString(); - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver.{id}"); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/durable-receiver.{id}"); await SenderIs(opts => { opts .UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true) .ConfigureListeners(x => x.UseDurableInbox()) @@ -39,16 +41,13 @@ await SenderIs(opts => { .IntegrateWithWolverine(x => x.MessageStorageSchemaName = "sender"); opts.Services.AddResourceSetupOnStartup(); - - opts - .ListenToPubsubTopic($"sender.{id}") - .Named("sender"); }); await ReceiverIs(opts => { opts .UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true) .ConfigureListeners(x => x.UseDurableInbox()) @@ -61,9 +60,7 @@ await ReceiverIs(opts => { opts.Services.AddResourceSetupOnStartup(); - opts - .ListenToPubsubTopic($"receiver.{id}") - .Named("receiver"); + opts.ListenToPubsubTopic($"durable-receiver.{id}"); }); } @@ -72,6 +69,7 @@ await ReceiverIs(opts => { } } +[Collection("acceptance")] public class DurableSendingAndReceivingCompliance : TransportCompliance { [Fact] public virtual async Task dl_mechanics() { @@ -82,15 +80,14 @@ public virtual async Task dl_mechanics() { await shouldMoveToErrorQueueOnAttempt(1); var runtime = theReceiver.Services.GetRequiredService(); - var transport = runtime.Options.Transports.GetOrCreate(); - var topic = transport.Topics[PubsubTransport.DeadLetterName]; + var dl = transport.Topics[PubsubTransport.DeadLetterName]; - await topic.InitializeAsync(NullLogger.Instance); + await dl.InitializeAsync(NullLogger.Instance); var pullResponse = await transport.SubscriberApiClient!.PullAsync( - topic.Server.Subscription.Name, - maxMessages: 5 + dl.Server.Subscription.Name, + maxMessages: 1 ); pullResponse.ReceivedMessages.ShouldNotBeEmpty(); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs index cabfa40fa..374a4ce24 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs @@ -1,3 +1,4 @@ +using Google.Cloud.PubSub.V1; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; @@ -8,7 +9,7 @@ namespace Wolverine.Pubsub.Tests; public class InlineComplianceFixture : TransportComplianceFixture, IAsyncLifetime { - public InlineComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver"), 120) { } + public InlineComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/inline-receiver"), 120) { } public async Task InitializeAsync() { Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); @@ -16,19 +17,16 @@ public async Task InitializeAsync() { var id = Guid.NewGuid().ToString(); - OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/receiver.{id}"); + OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/inline-receiver.{id}"); await SenderIs(opts => { opts .UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); - opts - .ListenToPubsubTopic($"sender.{id}") - .Named("sender"); - opts .PublishAllMessages() .To(OutboundAddress) @@ -39,12 +37,12 @@ await ReceiverIs(opts => { opts .UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .EnableAllNativeDeadLettering() .SystemEndpointsAreEnabled(true); opts - .ListenToPubsubTopic($"receiver.{id}") - .Named("receiver") + .ListenToPubsubTopic($"inline-receiver.{id}") .ProcessInline(); }); } @@ -54,6 +52,8 @@ await ReceiverIs(opts => { } } + +[Collection("acceptance")] public class InlineSendingAndReceivingCompliance : TransportCompliance { [Fact] public virtual async Task dl_mechanics() { @@ -64,15 +64,14 @@ public virtual async Task dl_mechanics() { await shouldMoveToErrorQueueOnAttempt(1); var runtime = theReceiver.Services.GetRequiredService(); - var transport = runtime.Options.Transports.GetOrCreate(); - var topic = transport.Topics[PubsubTransport.DeadLetterName]; + var dl = transport.Topics[PubsubTransport.DeadLetterName]; - await topic.InitializeAsync(NullLogger.Instance); + await dl.InitializeAsync(NullLogger.Instance); var pullResponse = await transport.SubscriberApiClient!.PullAsync( - topic.Server.Subscription.Name, - maxMessages: 5 + dl.Server.Subscription.Name, + maxMessages: 1 ); pullResponse.ReceivedMessages.ShouldNotBeEmpty(); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs index 80f26d4df..5691326bf 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs @@ -30,7 +30,7 @@ public void default_mode_is_buffered() { } [Fact] - public void default_endpoint_name_is_queue_name() { + public void default_endpoint_name_is_topic_name() { new PubsubEndpoint("top1", createTransport()) .EndpointName.ShouldBe("top1"); } @@ -63,6 +63,9 @@ public async Task initialize_with_auto_provision() { await topic.InitializeAsync(NullLogger.Instance); - await transport.PublisherApiClient!.Received().CreateTopicAsync(Arg.Is(topic.Server.Topic.Name)); + await transport.PublisherApiClient!.Received().CreateTopicAsync(Arg.Is(new Topic { + TopicName = topic.Server.Topic.Name, + MessageRetentionDuration = topic.Server.Topic.Options.MessageRetentionDuration, + })); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs index 20b97b36d..95941c2ae 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs @@ -1,4 +1,7 @@ +using System.Text; using JasperFx.Core; +using Newtonsoft.Json; +using Wolverine.ComplianceTests.ErrorHandling; using Wolverine.Configuration; using Wolverine.Pubsub.Internal; using Wolverine.Runtime.Serialization; @@ -14,6 +17,14 @@ public TestPubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { (e, m) => { if (e.Data is null || e.MessageType.IsEmpty()) return; + if (e.MessageType.EndsWith(".ErrorCausingMessage")) { + string jsonString = Encoding.UTF8.GetString(e.Data); + + e.Message = JsonConvert.DeserializeObject(jsonString); + + return; + } + var type = Type.GetType(e.MessageType); if (type is null) return; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs index d324e9c69..fe906a5a4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs @@ -3,7 +3,13 @@ namespace Wolverine.Pubsub.Tests; public static class TestingExtensions { - public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) => options.UsePubsub(Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? throw new NullReferenceException(), opts => { - opts.EmulatorDetection = EmulatorDetection.EmulatorOnly; - }); + public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) { + // options.Policies.Add(new LambdaEndpointPolicy((e, _) => { + // e.Mapper = new TestPubsubEnvelopeMapper(e); + // })); + + return options.UsePubsub(Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? throw new NullReferenceException(), opts => { + opts.EmulatorDetection = EmulatorDetection.EmulatorOnly; + }); + } } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs index a1df87a69..c1f1bcbf6 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs @@ -17,9 +17,16 @@ public async Task InitializeAsync() { _host = await Host.CreateDefaultBuilder() .UseWolverine(opts => { - opts.UsePubsubTesting().AutoProvision(); - opts.ListenToPubsubTopic("send_and_receive", x => x.Mapper = new TestPubsubEnvelopeMapper(x)); - opts.PublishMessage().ToPubsubTopic("send_and_receive"); + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup(); + + opts + .PublishMessage() + .ToPubsubTopic("send_and_receive"); + + opts.ListenToPubsubTopic("send_and_receive"); }).StartAsync(); } @@ -30,8 +37,7 @@ public async Task DisposeAsync() { [Fact] public void system_endpoints_disabled_by_default() { var transport = _host.GetRuntime().Options.Transports.GetOrCreate(); - var endpoints = transport - .Endpoints() + var endpoints = transport.Endpoints() .Where(x => x.Role == EndpointRole.System) .OfType().ToArray(); @@ -44,9 +50,14 @@ public async Task builds_system_endpoints() { .UseWolverine(opts => { opts.UsePubsubTesting() .AutoProvision() + .AutoPurgeOnStartup() .SystemEndpointsAreEnabled(true); + opts.ListenToPubsubTopic("send_and_receive"); - opts.PublishAllMessages().ToPubsubTopic("send_and_receive"); + + opts + .PublishAllMessages() + .ToPubsubTopic("send_and_receive"); }).StartAsync(); var transport = host.GetRuntime().Options.Transports.GetOrCreate(); var endpoints = transport @@ -62,20 +73,20 @@ public async Task builds_system_endpoints() { [Fact] public async Task send_and_receive_a_single_message() { - var message = new PubsubMessage1("Josh Allen"); + var message = new TestPubsubMessage("Josh Allen"); var session = await _host.TrackActivity() .IncludeExternalTransports() .Timeout(1.Minutes()) .SendMessageAndWaitAsync(message); - session.Received.SingleMessage().Name.ShouldBe(message.Name); + session.Received.SingleMessage().Name.ShouldBe(message.Name); } [Fact] public async Task send_and_receive_many_messages() { Func sending = async bus => { - for (int i = 0; i < 100; i++) - await bus.PublishAsync(new PubsubMessage1(Guid.NewGuid().ToString())); + for (int i = 0; i < 10; i++) + await bus.PublishAsync(new TestPubsubMessage(Guid.NewGuid().ToString())); }; await _host.TrackActivity() @@ -85,4 +96,8 @@ await _host.TrackActivity() } } -public record PubsubMessage1(string Name); +public record TestPubsubMessage(string Name); + +public static class TestPubsubMessageHandler { + public static void Handle(TestPubsubMessage message) { } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs index e80d6aa91..ff5dce85f 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs @@ -20,34 +20,30 @@ public async Task InitializeAsync() { await SenderIs(opts => { opts.UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() .PrefixIdentifiers("foo") - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) - .AutoProvision(); - - opts - .ListenToPubsubTopic($"foo.sender.{id}") - .Named("sender"); + .EnableAllNativeDeadLettering(); }); await ReceiverIs(opts => { opts.UsePubsubTesting() - .PrefixIdentifiers("foo") - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) - .AutoProvision(); + .AutoProvision() + .AutoPurgeOnStartup() + .PrefixIdentifiers("foo"); opts - .ListenToPubsubTopic($"foo.receiver.{id}") + .ListenToPubsubTopic($"receiver.{id}") .Named("receiver"); }); } - public new Task DisposeAsync() { - return Task.CompletedTask; + public new async Task DisposeAsync() { + await DisposeAsync(); } } +[Collection("acceptance")] public class PrefixedSendingAndReceivingCompliance : TransportCompliance { [Fact] public void prefix_was_applied_to_endpoint_for_the_receiver() { diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs index 02cc6ad4b..98539c794 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Wolverine.Runtime; using Wolverine.Transports; -using Wolverine.Util.Dataflow; namespace Wolverine.Pubsub.Internal; @@ -29,10 +28,16 @@ await streamingPull.WriteAsync(new() { await using var stream = streamingPull.GetResponseStream(); - _complete = new RetryBlock( - async (envelopes, _) => { - await streamingPull.WriteAsync(new() { AckIds = { envelopes.Select(x => x.AckId).ToArray() } }); - }, + _complete = new( + (envelopes, _) => streamingPull.WriteAsync(new() { + AckIds = { + envelopes + .Select(x => x.Headers[PubsubTransport.AckIdHeader]) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct() + .ToArray() + } + }), _logger, _cancellation.Token ); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs index c8f36b9ba..50d534842 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs @@ -1,20 +1,23 @@ -using System.Diagnostics; +using Google.Api.Gax; +using Google.Api.Gax.Grpc; using Google.Cloud.PubSub.V1; +using Google.Protobuf; using Grpc.Core; using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; +using Wolverine.Runtime.Serialization; using Wolverine.Transports; using Wolverine.Transports.Sending; namespace Wolverine.Pubsub.Internal; -public class PubsubEndpoint : Endpoint, IBrokerEndpoint, IBrokerQueue { +public class PubsubEndpoint : Endpoint, IBrokerQueue { private IPubsubEnvelopeMapper? _mapper; - protected readonly PubsubTransport _transport; + private readonly PubsubTransport _transport; - protected bool _hasInitialized = false; + private bool _hasInitialized = false; public PubsubServerOptions Server = new(); public PubsubClientOptions Client = new(); @@ -54,7 +57,13 @@ public PubsubEndpoint( _transport = transport; Server.Topic.Name = new(transport.ProjectId, topicName); - Server.Subscription.Name = new(transport.ProjectId, _transport.IdentifierPrefix.IsNotEmpty() && topicName.StartsWith($"{_transport.IdentifierPrefix}.") ? _transport.MaybeCorrectName(topicName.Substring(_transport.IdentifierPrefix.Length + 1)) : topicName); + Server.Subscription.Name = new( + transport.ProjectId, + _transport.IdentifierPrefix.IsNotEmpty() && + topicName.StartsWith($"{_transport.IdentifierPrefix}.") + ? _transport.MaybeCorrectName(topicName.Substring(_transport.IdentifierPrefix.Length + 1)) + : topicName + ); EndpointName = topicName; } @@ -76,7 +85,12 @@ public async ValueTask SetupAsync(ILogger logger) { if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); try { - await _transport.PublisherApiClient.CreateTopicAsync(Server.Topic.Name); + var request = new Topic { + TopicName = Server.Topic.Name, + MessageRetentionDuration = Server.Topic.Options.MessageRetentionDuration + }; + + await _transport.PublisherApiClient.CreateTopicAsync(request); } catch (RpcException ex) { if (ex.StatusCode != StatusCode.AlreadyExists) { @@ -93,8 +107,12 @@ public async ValueTask SetupAsync(ILogger logger) { throw; } + if (!IsListener) return; + if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + Server.Subscription.Name = Server.Subscription.Name.WithAssignedNodeNumber(_transport.AssignedNodeNumber); + try { var request = new Subscription { SubscriptionName = Server.Subscription.Name, @@ -115,31 +133,43 @@ public async ValueTask SetupAsync(ILogger logger) { } catch (RpcException ex) { if (ex.StatusCode != StatusCode.AlreadyExists) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); + logger.LogError( + ex, + "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", + Uri, + Server.Subscription.Name, + Server.Topic.Name + ); throw; } - logger.LogInformation("{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", Uri, Server.Subscription.Name); + logger.LogInformation( + "{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", + Uri, + Server.Subscription.Name + ); } catch (Exception ex) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); + logger.LogError( + ex, + "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", + Uri, + Server.Subscription.Name, + Server.Topic.Name + ); throw; } } - public async ValueTask CheckAsync() { - if (_transport.PublisherApiClient is null) return false; + public ValueTask CheckAsync() { + if ( + _transport.PublisherApiClient is null || + _transport.SubscriberApiClient is null + ) return ValueTask.FromResult(false); - try { - await _transport.PublisherApiClient.GetTopicAsync(Server.Topic.Name); - - return true; - } - catch { - return false; - } + return ValueTask.FromResult(_hasInitialized); } public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { @@ -174,40 +204,40 @@ public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISe public ValueTask> GetAttributesAsync() => ValueTask.FromResult(new Dictionary()); - public ValueTask PurgeAsync(ILogger logger) => ValueTask.CompletedTask; - - // public async ValueTask PurgeAsync(ILogger logger) { - // if (_transport.SubscriberApiClient is null) return; + // public ValueTask PurgeAsync(ILogger logger) => ValueTask.CompletedTask; - // try { - // var stopwatch = new Stopwatch(); + public async ValueTask PurgeAsync(ILogger logger) { + if (_transport.SubscriberApiClient is null || !IsListener) return; - // stopwatch.Start(); - - // while (stopwatch.ElapsedMilliseconds < 2000) { - // var response = await _transport.SubscriberApiClient.PullAsync( - // Server.Subscription.Name, - // maxMessages: 50 - // ); - - // if (!response.ReceivedMessages.Any()) return; - - // await _transport.SubscriberApiClient.AcknowledgeAsync( - // Server.Subscription.Name, - // response.ReceivedMessages.Select(x => x.AckId) - // ); - // }; - // } - // catch (Exception e) { - // logger.LogDebug(e, "{Uri}: Error trying to purge Google Cloud Pub/Sub subscription {Subscription}", Uri, Server.Subscription.Name); - // } - // } + try { + var response = await _transport.SubscriberApiClient.PullAsync( + Server.Subscription.Name, + maxMessages: 50, + CallSettings.FromExpiration(Expiration.FromTimeout(TimeSpan.FromSeconds(2))) + ); + + if (!response.ReceivedMessages.Any()) return; + + await _transport.SubscriberApiClient.AcknowledgeAsync( + Server.Subscription.Name, + response.ReceivedMessages.Select(x => x.AckId) + ); + } + catch (Exception ex) { + logger.LogDebug( + ex, + "{Uri}: Error trying to purge Google Cloud Pub/Sub subscription {Subscription}", + Uri, + Server.Subscription.Name + ); + } + } public async ValueTask TeardownAsync(ILogger logger) { - if (_transport.PublisherApiClient is null || _transport.SubscriberApiClient is null) return; + if (_transport.SubscriberApiClient is not null && IsListener) + await _transport.SubscriberApiClient.DeleteSubscriptionAsync(Server.Subscription.Name); - await _transport.SubscriberApiClient.DeleteSubscriptionAsync(Server.Subscription.Name); - await _transport.PublisherApiClient.DeleteTopicAsync(Server.Topic.Name); + if (_transport.PublisherApiClient is not null) await _transport.PublisherApiClient.DeleteTopicAsync(Server.Topic.Name); } internal async Task SendMessageAsync(Envelope envelope, ILogger logger) { @@ -215,7 +245,9 @@ internal async Task SendMessageAsync(Envelope envelope, ILogger logger) { if (!_hasInitialized) await InitializeAsync(logger); - var message = new PubsubMessage(); + var message = new PubsubMessage { + Data = ByteString.CopyFrom(EnvelopeSerializer.Serialize(envelope)) + }; Mapper.MapEnvelopeToOutgoing(envelope, message); @@ -228,7 +260,13 @@ await _transport.PublisherApiClient.PublishAsync(new() { internal void ConfigureDeadLetter(Action configure) { if (DeadLetterName.IsEmpty()) return; - configure(_transport.Topics[DeadLetterName]); + var dl = _transport.Topics[DeadLetterName]; + + dl.DeadLetterName = null; + dl.Server.Subscription.Options.DeadLetterPolicy = null; + dl.IsListener = true; + + configure(dl); } protected override ISender CreateSender(IWolverineRuntime runtime) { diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs index 397b5cc9d..c9bdc9c3e 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs @@ -1,9 +1,11 @@ -namespace Wolverine.Pubsub.Internal; +// namespace Wolverine.Pubsub.Internal; -public class PubsubEnvelope : Envelope { - public string AckId { get; set; } = string.Empty; +// public class PubsubEnvelope : Envelope { +// public readonly Envelope Envelope; +// public readonly string AckId; - public PubsubEnvelope(string ackId) { - AckId = ackId; - } -} +// public PubsubEnvelope(Envelope envelope, string ackId) { +// Envelope = envelope; +// AckId = ackId; +// } +// } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs index d116a5327..6da0657c2 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs @@ -1,74 +1,41 @@ +using System.Text.RegularExpressions; using Google.Cloud.PubSub.V1; -using Google.Protobuf; -using JasperFx.Core; using Wolverine.Configuration; using Wolverine.Transports; namespace Wolverine.Pubsub.Internal; internal class PubsubEnvelopeMapper : EnvelopeMapper, IPubsubEnvelopeMapper { - public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { - MapProperty( - x => x.Data!, - (e, m) => e.Data = m.Data.ToByteArray(), - (e, m) => { - if (e.Data is null) return; - - m.Data = ByteString.CopyFrom(e.Data); - } - ); - MapProperty( - x => x.ContentType!, - (e, m) => { - if (!m.Attributes.TryGetValue("content-type", out var contentType)) return; - - e.ContentType = contentType; - }, - (e, m) => { - if (e.ContentType is null) return; + // private const string _wlvrnPrefix = "wlvrn"; + // private static Regex _wlvrnRegex = new Regex($"^{_wlvrnPrefix}\\."); - m.Attributes["content-type"] = e.ContentType; - } - ); + public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( - x => x.GroupId!, - (e, m) => e.GroupId = m.OrderingKey.IsNotEmpty() ? m.OrderingKey : null, + e => e.CorrelationId!, + (e, m) => { }, (e, m) => m.OrderingKey = e.GroupId ?? string.Empty ); MapProperty( x => x.Id, (e, m) => { - if (!m.Attributes.TryGetValue("wolverine-id", out var wolverineId)) return; + if (!m.Attributes.TryGetValue("id", out var wolverineId)) return; if (!Guid.TryParse(wolverineId, out var id)) return; e.Id = id; }, - (e, m) => m.Attributes["wolverine-id"] = e.Id.ToString() - ); - MapProperty( - x => x.CorrelationId!, - (e, m) => { - if (!m.Attributes.TryGetValue("wolverine-correlation-id", out var correlationId)) return; - - e.CorrelationId = correlationId; - }, - (e, m) => { - if (e.CorrelationId is null) return; - - m.Attributes["wolverine-correlation-id"] = e.CorrelationId; - } + (e, m) => m.Attributes["id"] = e.Id.ToString() ); MapProperty( - x => x.MessageType!, + e => e.MessageType!, (e, m) => { - if (!m.Attributes.TryGetValue("wolverine-message-type", out var messageType)) return; + if (!m.Attributes.TryGetValue("message-type", out var messageType)) return; e.MessageType = messageType; }, (e, m) => { if (e.MessageType is null) return; - m.Attributes["wolverine-message-type"] = e.MessageType; + m.Attributes["message-type"] = e.MessageType; } ); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs index 31d833888..836c66efa 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -4,6 +4,7 @@ using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Runtime; +using Wolverine.Runtime.Serialization; using Wolverine.Transports; using Wolverine.Util.Dataflow; @@ -19,7 +20,7 @@ public abstract class PubsubListener : IListener, ISupportDeadLetterQueue { protected readonly RetryBlock _deadLetter; protected readonly CancellationTokenSource _cancellation = new(); - protected RetryBlock _complete; + protected RetryBlock _complete; protected Task _task; public bool NativeDeadLetterQueueEnabled { get; } = false; @@ -38,7 +39,7 @@ IWolverineRuntime runtime _receiver = receiver; _logger = runtime.LoggerFactory.CreateLogger(); - _resend = new RetryBlock(async (envelope, _) => { + _resend = new(async (envelope, _) => { await _endpoint.SendMessageAsync(envelope, _logger); }, _logger, runtime.Cancellation); @@ -47,21 +48,22 @@ IWolverineRuntime runtime _deadLetterTopic = _transport.Topics[_endpoint.DeadLetterName]; } - _deadLetter = new RetryBlock(async (e, _) => { + _deadLetter = new(async (e, _) => { if (_deadLetterTopic is null) return; await _deadLetterTopic.SendMessageAsync(e, _logger); }, _logger, runtime.Cancellation); - _complete = new RetryBlock( - async (e, _) => { - if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + _complete = new(async (e, _) => { + if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - await transport.SubscriberApiClient.AcknowledgeAsync(_endpoint.Server.Subscription.Name, e.Select(x => x.AckId)); - }, - _logger, - _cancellation.Token - ); + await transport.SubscriberApiClient.AcknowledgeAsync( + _endpoint.Server.Subscription.Name, + e.Select(x => x.Headers[PubsubTransport.AckIdHeader]) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct() + ); + }, _logger, _cancellation.Token); _task = StartAsync(); } @@ -71,13 +73,13 @@ IWolverineRuntime runtime public virtual ValueTask CompleteAsync(Envelope envelope) => ValueTask.CompletedTask; public async ValueTask DeferAsync(Envelope envelope) { - if (envelope is PubsubEnvelope e) await _resend.PostAsync(e); + if (envelope.Headers.ContainsKey(PubsubTransport.AckIdHeader)) await _resend.PostAsync(envelope); } public Task MoveToErrorsAsync(Envelope envelope, Exception exception) => _deadLetter.PostAsync(envelope) ?? Task.CompletedTask; public async Task TryRequeueAsync(Envelope envelope) { - if (envelope is PubsubEnvelope) { + if (envelope.Headers.ContainsKey(PubsubTransport.AckIdHeader)) { await _resend.PostAsync(envelope); return true; @@ -144,14 +146,16 @@ protected async Task listenForMessagesAsync(Func listenAsync) { } protected async Task handleMessagesAsync(RepeatedField messages) { - var envelopes = new List(messages.Count); + var envelopes = new List(messages.Count); foreach (var message in messages) { try { - var envelope = new PubsubEnvelope(message.AckId); + var envelope = EnvelopeSerializer.Deserialize(message.Message.Data.ToByteArray()); _endpoint.Mapper.MapIncomingToEnvelope(envelope, message.Message); + envelope.Headers.Add(PubsubTransport.AckIdHeader, message.AckId); + if (envelope.IsPing()) { try { await _complete.PostAsync([envelope]); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs index 307494454..cf44021bc 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs @@ -1,6 +1,8 @@ using Google.Cloud.PubSub.V1; +using Google.Protobuf; using Microsoft.Extensions.Logging; using Wolverine.Runtime; +using Wolverine.Runtime.Serialization; using Wolverine.Transports; using Wolverine.Transports.Sending; @@ -30,7 +32,9 @@ public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch foreach (var envelope in batch.Messages) { try { - var message = new PubsubMessage(); + var message = new PubsubMessage { + Data = ByteString.CopyFrom(EnvelopeSerializer.Serialize(envelope)) + }; _endpoint.Mapper.MapEnvelopeToOutgoing(envelope, message); diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs index 6565bbfe3..d578bfac2 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs @@ -10,6 +10,11 @@ public class PubsubServerOptions { public class PubsubTopicOptions { public TopicName Name { get; set; } = default!; + public CreateTopicOptions Options = new(); +} + +public class CreateTopicOptions { + public Duration MessageRetentionDuration = Duration.FromTimeSpan(TimeSpan.FromMinutes(10)); } public class PubsubSubscriptionOptions { @@ -17,12 +22,6 @@ public class PubsubSubscriptionOptions { public CreateSubscriptionOptions Options = new(); } -public class PubsubClientOptions { - public long MaxOutstandingMessages = 1000; - public long MaxOutstandingByteCount = 100 * 1024 * 1024; - public PubsubRetryPolicy RetryPolicy = new(); -} - public class CreateSubscriptionOptions { public int AckDeadlineSeconds = 10; public DeadLetterPolicy? DeadLetterPolicy = null; @@ -38,6 +37,12 @@ public class CreateSubscriptionOptions { }; } +public class PubsubClientOptions { + public long MaxOutstandingMessages = 1000; + public long MaxOutstandingByteCount = 100 * 1024 * 1024; + public PubsubRetryPolicy RetryPolicy = new(); +} + public class PubsubRetryPolicy { public int MaxRetryCount = 5; public int RetryDelay = 1000; diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs index 16c29afb8..df3baee18 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs @@ -89,4 +89,15 @@ public PubsubTopicListenerConfiguration InteropWith(IPubsubEnvelopeMapper mapper return this; } + + /// + /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// + /// + /// + public PubsubTopicListenerConfiguration InteropWith(Func mapper) { + add(e => e.Mapper = mapper(e)); + + return this; + } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs index 47fc64ddd..50748b884 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs @@ -6,6 +6,63 @@ namespace Wolverine.Pubsub; public class PubsubTopicSubscriberConfiguration : SubscriberConfiguration { public PubsubTopicSubscriberConfiguration(PubsubEndpoint endpoint) : base(endpoint) { } + /// + /// Configure the underlying Google Cloud Pub/Sub topic and subscription. This is only applicable when + /// Wolverine is creating the topic and subscription + /// + /// + /// + public PubsubTopicSubscriberConfiguration ConfigureServer(Action configure) { + add(e => configure(e.Server)); + + return this; + } + + /// + /// Configure the underlying Google Cloud Pub/Sub subscriber client. This is only applicable when + /// Wolverine is creating the subscriber client + /// + /// + /// + public PubsubTopicSubscriberConfiguration ConfigureClient(Action configure) { + add(e => configure(e.Client)); + + return this; + } + + /// + /// Completely disable all Google Cloud Pub/Sub dead lettering for just this endpoint + /// + /// + public PubsubTopicSubscriberConfiguration DisableDeadLettering() { + add(e => { + e.DeadLetterName = null; + e.Server.Subscription.Options.DeadLetterPolicy = null; + }); + + return this; + } + + /// + /// Customize the dead lettering for just this endpoint + /// + /// + /// Optionally configure properties of the dead lettering itself + /// + /// + public PubsubTopicSubscriberConfiguration ConfigureDeadLettering( + string deadLetterName, + Action? configure = null + ) { + add(e => { + e.DeadLetterName = deadLetterName; + + if (configure is not null) e.ConfigureDeadLetter(configure); + }); + + return this; + } + /// /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems /// @@ -16,4 +73,15 @@ public PubsubTopicSubscriberConfiguration InteropWith(IPubsubEnvelopeMapper mapp return this; } + + /// + /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// + /// + /// + public PubsubTopicSubscriberConfiguration InteropWith(Func mapper) { + add(e => e.Mapper = mapper(e)); + + return this; + } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index f6bbd10fd..963890434 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -12,11 +12,13 @@ namespace Wolverine.Pubsub; public class PubsubTransport : BrokerTransport, IAsyncDisposable { public const string ProtocolName = "pubsub"; + public const string AckIdHeader = "pubsub.ack-id"; public const string ResponseName = "wlvrn.responses"; public const string DeadLetterName = "wlvrn.dead-letter"; public static Regex NameRegex = new("^(?!goog)[A-Za-z][A-Za-z0-9\\-_.~+%]{2,254}$"); + internal int AssignedNodeNumber = 0; internal PublisherServiceApiClient? PublisherApiClient = null; internal SubscriberServiceApiClient? SubscriberApiClient = null; @@ -52,6 +54,7 @@ public override async ValueTask ConnectAsync(IWolverineRuntime runtime) { if (string.IsNullOrWhiteSpace(ProjectId)) throw new InvalidOperationException("Google Cloud Pub/Sub project id must be set before connecting"); + AssignedNodeNumber = runtime.DurabilitySettings.AssignedNodeNumber; PublisherApiClient = await pubBuilder.BuildAsync(); SubscriberApiClient = await subBuilder.BuildAsync(); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs index d6611be02..d1c59df94 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs @@ -1,4 +1,3 @@ -using Google.Api.Gax; using JasperFx.Core.Reflection; using Wolverine.Configuration; using Wolverine.Pubsub.Internal; @@ -77,7 +76,8 @@ public static PubsubTopicListenerConfiguration ListenToPubsubTopic( /// public static PubsubTopicSubscriberConfiguration ToPubsubTopic( this IPublishToExpression publishing, - string topicName + string topicName, + Action? configure = null ) { var transports = publishing.As().Parent.Transports; var transport = transports.GetOrCreate(); @@ -85,6 +85,8 @@ string topicName topic.EndpointName = topicName; + configure?.Invoke(topic); + // This is necessary unfortunately to hook up the subscription rules publishing.To(topic.Uri); diff --git a/src/Transports/GCP/Wolverine.Pubsub/SubscriptionNameExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub/SubscriptionNameExtensions.cs new file mode 100644 index 000000000..727544a8e --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/SubscriptionNameExtensions.cs @@ -0,0 +1,10 @@ +namespace Google.Cloud.PubSub.V1; + +public static class SubscriptionNameExtensions { + public static SubscriptionName WithAssignedNodeNumber(this SubscriptionName subscriptionName, int assignedNodeNumber) => new( + subscriptionName.ProjectId, + !subscriptionName.SubscriptionId.EndsWith($".{Math.Abs(assignedNodeNumber)}") + ? $"{subscriptionName.SubscriptionId}.{Math.Abs(assignedNodeNumber)}" + : subscriptionName.SubscriptionId + ); +} From 6594b23de10c17c4f1d474eebfd4ff6564ac54ce Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:46:56 +0200 Subject: [PATCH 06/16] Refactored batching --- .../BufferedSendingAndReceivingCompliance.cs | 4 +- .../DurableSendingAndReceivingCompliance.cs | 1 - .../Internal/BatchedPubsubListener.cs | 17 +- .../Internal/PubsubEndpoint.cs | 4 +- .../Internal/PubsubEnvelope.cs | 16 +- .../Internal/PubsubEnvelopeMapper.cs | 159 +++++++++++++++++- .../Internal/PubsubListener.cs | 84 ++++++--- .../Internal/PubsubSenderProtocol.cs | 35 +--- .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 1 - 9 files changed, 229 insertions(+), 92 deletions(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs index c9e480899..9aebc66c5 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -22,9 +22,7 @@ await SenderIs(opts => { opts .UsePubsubTesting() .AutoProvision() - .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .AutoPurgeOnStartup(); }); await ReceiverIs(opts => { diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs index 29cbd9767..16f4f5236 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs @@ -1,4 +1,3 @@ -using Google.Cloud.PubSub.V1; using IntegrationTests; using Marten; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs index 98539c794..f2e180f9c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs @@ -1,5 +1,4 @@ using Google.Api.Gax.Grpc; -using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Runtime; using Wolverine.Transports; @@ -28,19 +27,9 @@ await streamingPull.WriteAsync(new() { await using var stream = streamingPull.GetResponseStream(); - _complete = new( - (envelopes, _) => streamingPull.WriteAsync(new() { - AckIds = { - envelopes - .Select(x => x.Headers[PubsubTransport.AckIdHeader]) - .Where(x => !string.IsNullOrEmpty(x)) - .Distinct() - .ToArray() - } - }), - _logger, - _cancellation.Token - ); + _acknowledge = ackIds => streamingPull.WriteAsync(new() { + AckIds = { ackIds } + }); try { await listenForMessagesAsync(async () => { diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs index 50d534842..061754248 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs @@ -245,9 +245,7 @@ internal async Task SendMessageAsync(Envelope envelope, ILogger logger) { if (!_hasInitialized) await InitializeAsync(logger); - var message = new PubsubMessage { - Data = ByteString.CopyFrom(EnvelopeSerializer.Serialize(envelope)) - }; + var message = new PubsubMessage(); Mapper.MapEnvelopeToOutgoing(envelope, message); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs index c9bdc9c3e..612dc8bec 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs @@ -1,11 +1,9 @@ -// namespace Wolverine.Pubsub.Internal; +namespace Wolverine.Pubsub.Internal; -// public class PubsubEnvelope : Envelope { -// public readonly Envelope Envelope; -// public readonly string AckId; +public class PubsubEnvelope : Envelope { + public readonly string AckId; -// public PubsubEnvelope(Envelope envelope, string ackId) { -// Envelope = envelope; -// AckId = ackId; -// } -// } + public PubsubEnvelope(string ackId) { + AckId = ackId; + } +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs index 6da0657c2..458c0d176 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs @@ -1,5 +1,5 @@ -using System.Text.RegularExpressions; using Google.Cloud.PubSub.V1; +using Google.Protobuf; using Wolverine.Configuration; using Wolverine.Transports; @@ -10,11 +10,6 @@ internal class PubsubEnvelopeMapper : EnvelopeMapper e.CorrelationId!, - (e, m) => { }, - (e, m) => m.OrderingKey = e.GroupId ?? string.Empty - ); MapProperty( x => x.Id, (e, m) => { @@ -25,6 +20,11 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { }, (e, m) => m.Attributes["id"] = e.Id.ToString() ); + MapProperty( + e => e.CorrelationId!, + (e, m) => { }, + (e, m) => m.OrderingKey = e.GroupId ?? string.Empty + ); MapProperty( e => e.MessageType!, (e, m) => { @@ -38,6 +38,153 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { m.Attributes["message-type"] = e.MessageType; } ); + MapProperty( + e => e.Data!, + (e, m) => { + if (m.Data is null) return; + + e.Data = m.Data.ToByteArray(); + }, + (e, m) => m.Data = ByteString.CopyFrom(e.Data) + ); + MapProperty( + e => e.Attempts, + (e, m) => { + if (!m.Attributes.TryGetValue("attempts", out var attempts)) return; + if (!int.TryParse(attempts, out var count)) return; + + e.Attempts = count; + }, + (e, m) => m.Attributes["attempts"] = e.Attempts.ToString() + ); + MapProperty( + e => e.ContentType!, + (e, m) => { + if (!m.Attributes.TryGetValue("content-type", out var contentType)) return; + + e.ContentType = contentType; + }, + (e, m) => { + if (e.ContentType is null) return; + + m.Attributes["content-type"] = e.ContentType; + } + ); + MapProperty( + e => e.Destination!, + (e, m) => { + if (!m.Attributes.TryGetValue("destination", out var destination)) return; + if (!Uri.TryCreate(destination, UriKind.Absolute, out var uri)) return; + + e.Destination = uri; + }, + (e, m) => { + if (e.Destination is null) return; + + m.Attributes["destination"] = e.Destination.ToString(); + } + ); + MapProperty( + e => e.TenantId!, + (e, m) => { + if (!m.Attributes.TryGetValue("tenant-id", out var tenantId)) return; + + e.TenantId = tenantId; + }, + (e, m) => { + if (e.TenantId is null) return; + + m.Attributes["tenant-id"] = e.TenantId; + } + ); + MapProperty( + e => e.AcceptedContentTypes, + (e, m) => { + if (!m.Attributes.TryGetValue("accepted-content-types", out var acceptedContentTypes)) return; + + e.AcceptedContentTypes = acceptedContentTypes.Split(','); + }, + (e, m) => { + if (e.AcceptedContentTypes is null) return; + + m.Attributes["accepted-content-types"] = string.Join(",", e.AcceptedContentTypes.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()); + } + ); + MapProperty( + e => e.TopicName!, + (e, m) => { + if (!m.Attributes.TryGetValue("topic-name", out var topicName)) return; + + e.TopicName = topicName; + }, + (e, m) => { + if (e.TopicName is null) return; + + m.Attributes["topic-name"] = e.TopicName; + } + ); + MapProperty( + e => e.EndpointName!, + (e, m) => { + if (!m.Attributes.TryGetValue("endpoint-name", out var endpointName)) return; + + e.EndpointName = endpointName; + }, + (e, m) => { + if (e.EndpointName is null) return; + + m.Attributes["endpoint-name"] = e.EndpointName; + } + ); + MapProperty( + e => e.WasPersistedInOutbox, + (e, m) => { + if (!m.Attributes.TryGetValue("was-persisted-in-outbox", out var wasPersistedInOutbox)) return; + if (!bool.TryParse(wasPersistedInOutbox, out var wasPersisted)) return; + + e.WasPersistedInOutbox = wasPersisted; + }, + (e, m) => m.Attributes["was-persisted-in-outbox"] = e.WasPersistedInOutbox.ToString() + ); + MapProperty( + e => e.GroupId!, + (e, m) => { + if (!m.Attributes.TryGetValue("group-id", out var groupId)) return; + + e.GroupId = groupId; + }, + (e, m) => { + if (e.GroupId is null) return; + + m.Attributes["group-id"] = e.GroupId; + } + ); + MapProperty( + e => e.DeduplicationId!, + (e, m) => { + if (!m.Attributes.TryGetValue("deduplication-id", out var deduplicationId)) return; + + e.DeduplicationId = deduplicationId; + }, + (e, m) => { + if (e.DeduplicationId is null) return; + + m.Attributes["deduplication-id"] = e.DeduplicationId; + } + ); + MapProperty( + e => e.PartitionKey!, + (e, m) => { + if (!m.Attributes.TryGetValue("partition-key", out var partitionKey)) return; + + e.PartitionKey = partitionKey; + }, + (e, m) => { + if (e.PartitionKey is null) return; + + m.Attributes["partition-key"] = e.PartitionKey; + } + ); } protected override void writeOutgoingHeader(PubsubMessage outgoing, string key, string value) { diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs index 836c66efa..4678de48a 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -14,13 +14,15 @@ public abstract class PubsubListener : IListener, ISupportDeadLetterQueue { protected readonly PubsubEndpoint _endpoint; protected readonly PubsubTransport _transport; protected readonly IReceiver _receiver; + protected readonly IWolverineRuntime _runtime; protected readonly ILogger _logger; protected readonly PubsubEndpoint? _deadLetterTopic; - protected readonly RetryBlock _resend; protected readonly RetryBlock _deadLetter; + protected readonly RetryBlock _requeue; + protected readonly RetryBlock _complete; protected readonly CancellationTokenSource _cancellation = new(); - protected RetryBlock _complete; + protected Func _acknowledge; protected Task _task; public bool NativeDeadLetterQueueEnabled { get; } = false; @@ -37,32 +39,52 @@ IWolverineRuntime runtime _endpoint = endpoint; _transport = transport; _receiver = receiver; + _runtime = runtime; _logger = runtime.LoggerFactory.CreateLogger(); - _resend = new(async (envelope, _) => { - await _endpoint.SendMessageAsync(envelope, _logger); - }, _logger, runtime.Cancellation); - if (_endpoint.DeadLetterName.IsNotEmpty() && transport.EnableDeadLettering) { - NativeDeadLetterQueueEnabled = true; _deadLetterTopic = _transport.Topics[_endpoint.DeadLetterName]; + + NativeDeadLetterQueueEnabled = true; } + _acknowledge = async ackIds => { + if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + + if (ackIds.Any()) await transport.SubscriberApiClient.AcknowledgeAsync( + _endpoint.Server.Subscription.Name, + ackIds + ); + }; + _deadLetter = new(async (e, _) => { if (_deadLetterTopic is null) return; + if (e is PubsubEnvelope pubsubEnvelope) await _acknowledge([pubsubEnvelope.AckId]); + await _deadLetterTopic.SendMessageAsync(e, _logger); }, _logger, runtime.Cancellation); - _complete = new(async (e, _) => { + _requeue = new(async (e, _) => { + if (e is PubsubEnvelope pubsubEnvelope) await _acknowledge([pubsubEnvelope.AckId]); + + await _endpoint.SendMessageAsync(e, _logger); + }, _logger, runtime.Cancellation); + + _complete = new(async (envelopes, _) => { + var pubsubEnvelopes = envelopes.OfType().ToArray(); + + if (!pubsubEnvelopes.Any()) return; + if (transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - await transport.SubscriberApiClient.AcknowledgeAsync( - _endpoint.Server.Subscription.Name, - e.Select(x => x.Headers[PubsubTransport.AckIdHeader]) - .Where(x => !string.IsNullOrEmpty(x)) - .Distinct() - ); + var ackIds = pubsubEnvelopes + .Select(e => e.AckId) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct() + .ToArray(); + + if (ackIds.Any()) await _acknowledge(ackIds); }, _logger, _cancellation.Token); _task = StartAsync(); @@ -70,22 +92,20 @@ await transport.SubscriberApiClient.AcknowledgeAsync( public abstract Task StartAsync(); - public virtual ValueTask CompleteAsync(Envelope envelope) => ValueTask.CompletedTask; + public async ValueTask CompleteAsync(Envelope envelope) { + await _complete.PostAsync([envelope]); + } public async ValueTask DeferAsync(Envelope envelope) { - if (envelope.Headers.ContainsKey(PubsubTransport.AckIdHeader)) await _resend.PostAsync(envelope); + await _requeue.PostAsync(envelope); } - public Task MoveToErrorsAsync(Envelope envelope, Exception exception) => _deadLetter.PostAsync(envelope) ?? Task.CompletedTask; + public Task MoveToErrorsAsync(Envelope envelope, Exception exception) => _deadLetter.PostAsync(envelope); public async Task TryRequeueAsync(Envelope envelope) { - if (envelope.Headers.ContainsKey(PubsubTransport.AckIdHeader)) { - await _resend.PostAsync(envelope); - - return true; - } + await _requeue.PostAsync(envelope); - return false; + return true; } public ValueTask StopAsync() { @@ -98,7 +118,7 @@ public ValueTask DisposeAsync() { _cancellation.Cancel(); _task.SafeDispose(); _complete.SafeDispose(); - _resend.SafeDispose(); + _requeue.SafeDispose(); _deadLetter.SafeDispose(); return ValueTask.CompletedTask; @@ -146,16 +166,24 @@ protected async Task listenForMessagesAsync(Func listenAsync) { } protected async Task handleMessagesAsync(RepeatedField messages) { - var envelopes = new List(messages.Count); + var envelopes = new List(messages.Count); foreach (var message in messages) { + if (message.Message.Attributes.Keys.Contains("batched")) { + var batched = EnvelopeSerializer.ReadMany(message.Message.Data.ToByteArray()); + + if (batched.Any()) await _receiver.ReceivedAsync(this, batched); + + await _complete.PostAsync([new(message.AckId)]); + + continue; + } + try { - var envelope = EnvelopeSerializer.Deserialize(message.Message.Data.ToByteArray()); + var envelope = new PubsubEnvelope(message.AckId); _endpoint.Mapper.MapIncomingToEnvelope(envelope, message.Message); - envelope.Headers.Add(PubsubTransport.AckIdHeader, message.AckId); - if (envelope.IsPing()) { try { await _complete.PostAsync([envelope]); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs index cf44021bc..9b316852c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs @@ -2,7 +2,6 @@ using Google.Protobuf; using Microsoft.Extensions.Logging; using Wolverine.Runtime; -using Wolverine.Runtime.Serialization; using Wolverine.Transports; using Wolverine.Transports.Sending; @@ -26,38 +25,20 @@ IWolverineRuntime runtime public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch batch) { await _endpoint.InitializeAsync(_logger); - var messages = new List(); - var successes = new List(); - var fails = new List(); - - foreach (var envelope in batch.Messages) { - try { - var message = new PubsubMessage { - Data = ByteString.CopyFrom(EnvelopeSerializer.Serialize(envelope)) - }; - - _endpoint.Mapper.MapEnvelopeToOutgoing(envelope, message); + try { + var message = new PubsubMessage { + Data = ByteString.CopyFrom(batch.Data) + }; - messages.Add(message); - successes.Add(envelope); - } - catch (Exception ex) { - _logger.LogError(ex, "{Uril}: Error while mapping envelope \"{Envelope}\" to a PubsubMessage object.", _endpoint.Uri, envelope); + message.Attributes["destination"] = batch.Destination.ToString(); + message.Attributes["batched"] = string.Empty; - fails.Add(envelope); - } - } - - try { await _client.PublishAsync(new() { TopicAsTopicName = _endpoint.Server.Topic.Name, - Messages = { messages } + Messages = { message } }); - await callback.MarkSuccessfulAsync(new OutgoingMessageBatch(batch.Destination, successes)); - - if (fails.Any()) - await callback.MarkProcessingFailureAsync(new OutgoingMessageBatch(batch.Destination, fails)); + await callback.MarkSuccessfulAsync(batch); } catch (Exception ex) { await callback.MarkProcessingFailureAsync(batch, ex); diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index 963890434..999107f90 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -12,7 +12,6 @@ namespace Wolverine.Pubsub; public class PubsubTransport : BrokerTransport, IAsyncDisposable { public const string ProtocolName = "pubsub"; - public const string AckIdHeader = "pubsub.ack-id"; public const string ResponseName = "wlvrn.responses"; public const string DeadLetterName = "wlvrn.dead-letter"; From 66af370c6c0f466955dc98c34c6a9ea96ea0b589 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:16:05 +0200 Subject: [PATCH 07/16] Disable dead letter subscriber --- .../BufferedSendingAndReceivingCompliance.cs | 4 +++- .../sending_compliance_with_prefixes.cs | 7 +++++-- .../Wolverine.Pubsub/Internal/PubsubEndpoint.cs | 16 +++++++--------- .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 8 ++++---- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs index 9aebc66c5..c9e480899 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -22,7 +22,9 @@ await SenderIs(opts => { opts .UsePubsubTesting() .AutoProvision() - .AutoPurgeOnStartup(); + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); }); await ReceiverIs(opts => { diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs index ff5dce85f..6fed58c94 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs @@ -23,14 +23,17 @@ await SenderIs(opts => { .AutoProvision() .AutoPurgeOnStartup() .PrefixIdentifiers("foo") - .EnableAllNativeDeadLettering(); + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); }); await ReceiverIs(opts => { opts.UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .PrefixIdentifiers("foo"); + .PrefixIdentifiers("foo") + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true); opts .ListenToPubsubTopic($"receiver.{id}") diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs index 061754248..572e97acd 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs @@ -1,13 +1,11 @@ using Google.Api.Gax; using Google.Api.Gax.Grpc; using Google.Cloud.PubSub.V1; -using Google.Protobuf; using Grpc.Core; using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; -using Wolverine.Runtime.Serialization; using Wolverine.Transports; using Wolverine.Transports.Sending; @@ -21,6 +19,7 @@ public class PubsubEndpoint : Endpoint, IBrokerQueue { public PubsubServerOptions Server = new(); public PubsubClientOptions Client = new(); + public bool IsDeadLetter = false; /// /// Name of the dead letter for this Google Cloud Pub/Sub subcription where failed messages will be moved @@ -85,12 +84,10 @@ public async ValueTask SetupAsync(ILogger logger) { if (_transport.PublisherApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); try { - var request = new Topic { + await _transport.PublisherApiClient.CreateTopicAsync(new Topic { TopicName = Server.Topic.Name, MessageRetentionDuration = Server.Topic.Options.MessageRetentionDuration - }; - - await _transport.PublisherApiClient.CreateTopicAsync(request); + }); } catch (RpcException ex) { if (ex.StatusCode != StatusCode.AlreadyExists) { @@ -107,13 +104,14 @@ public async ValueTask SetupAsync(ILogger logger) { throw; } - if (!IsListener) return; + if (!IsListener && !IsDeadLetter) return; if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - Server.Subscription.Name = Server.Subscription.Name.WithAssignedNodeNumber(_transport.AssignedNodeNumber); try { + if (!IsDeadLetter) Server.Subscription.Name = Server.Subscription.Name.WithAssignedNodeNumber(_transport.AssignedNodeNumber); + var request = new Subscription { SubscriptionName = Server.Subscription.Name, TopicAsTopicName = Server.Topic.Name, @@ -262,7 +260,7 @@ internal void ConfigureDeadLetter(Action configure) { dl.DeadLetterName = null; dl.Server.Subscription.Options.DeadLetterPolicy = null; - dl.IsListener = true; + dl.IsDeadLetter = true; configure(dl); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index 999107f90..2606206c5 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -79,11 +79,11 @@ protected override IEnumerable endpoints() { var dlNames = Topics.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); foreach (var dlName in dlNames) { - var dlTopic = Topics[dlName!]; + var dl = Topics[dlName!]; - dlTopic.DeadLetterName = null; - dlTopic.Server.Subscription.Options.DeadLetterPolicy = null; - dlTopic.IsListener = true; + dl.DeadLetterName = null; + dl.Server.Subscription.Options.DeadLetterPolicy = null; + dl.IsDeadLetter = true; } } From d5964920ab4d48b696c59c9b9e2924e770bfacb6 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:15:41 +0200 Subject: [PATCH 08/16] Clean-ups, added StatefulResourceSmokeTests and refactored CheckAsync --- .../StatefulResourceSmokeTests.cs | 98 +++++++++++++++++++ .../TestPubsubEnvelopeMapper.cs | 37 ------- .../TestingExtensions.cs | 9 +- .../Wolverine.Pubsub.Tests.csproj | 7 -- .../Internal/PubsubEndpoint.cs | 38 +++---- 5 files changed, 115 insertions(+), 74 deletions(-) create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs delete mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs new file mode 100644 index 000000000..cc48229bc --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs @@ -0,0 +1,98 @@ +using JasperFx.Core; +using Microsoft.Extensions.Hosting; +using Oakton; +using Shouldly; +using Xunit; + +namespace Wolverine.Pubsub.Tests; + +public class StatefulResourceSmokeTests { + private IHostBuilder ConfigureBuilder(bool autoProvision, int starting = 1) { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + return Host + .CreateDefaultBuilder() + .UseWolverine(opts => { + if (autoProvision) { + opts + .UsePubsubTesting() + .AutoProvision(); + } + else { + opts.UsePubsubTesting(); + } + + opts.PublishMessage() + .ToPubsubTopic("sr" + starting++); + + opts.PublishMessage() + .ToPubsubTopic("sr" + starting++); + + opts.PublishMessage() + .ToPubsubTopic("sr" + starting++); + + opts.PublishMessage() + .ToPubsubTopic("sr" + starting++); + }); + } + + [Fact] + public async Task run_setup() { + var result = await ConfigureBuilder(false).RunOaktonCommands(["resources", "setup"]); + + result.ShouldBe(0); + } + + [Fact] + public async Task statistics() { + (await ConfigureBuilder(false).RunOaktonCommands(["resources", "setup"])).ShouldBe(0); + + var result = await ConfigureBuilder(false).RunOaktonCommands(["resources", "statistics"]); + + result.ShouldBe(0); + } + + [Fact] + public async Task check_positive() { + (await ConfigureBuilder(false).RunOaktonCommands(["resources", "setup"])).ShouldBe(0); + + var result = await ConfigureBuilder(false).RunOaktonCommands(["resources", "check"]); + + result.ShouldBe(0); + } + + [Fact] + public async Task check_negative() { + var result = await ConfigureBuilder(false, 10).RunOaktonCommands(["resources", "check"]); + + result.ShouldBe(1); + } + + [Fact] + public async Task clear_state() { + (await ConfigureBuilder(false, 20).RunOaktonCommands(["resources", "setup"])).ShouldBe(0); + (await ConfigureBuilder(false, 20).RunOaktonCommands(["resources", "clear"])).ShouldBe(0); + } + + [Fact] + public async Task teardown() { + (await ConfigureBuilder(false, 30).RunOaktonCommands(["resources", "setup"])).ShouldBe(0); + (await ConfigureBuilder(false, 30).RunOaktonCommands(["resources", "teardown"])).ShouldBe(0); + } +} + +public class SRMessage1; + +public class SRMessage2; + +public class SRMessage3; + +public class SRMessage4; + +public class SRMessageHandlers { + public Task Handle(SRMessage1 message) => Task.Delay(100.Milliseconds()); + public Task Handle(SRMessage2 message) => Task.Delay(100.Milliseconds()); + public Task Handle(SRMessage3 message) => Task.Delay(100.Milliseconds()); + public Task Handle(SRMessage4 message) => Task.Delay(100.Milliseconds()); +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs deleted file mode 100644 index 95941c2ae..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestPubsubEnvelopeMapper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text; -using JasperFx.Core; -using Newtonsoft.Json; -using Wolverine.ComplianceTests.ErrorHandling; -using Wolverine.Configuration; -using Wolverine.Pubsub.Internal; -using Wolverine.Runtime.Serialization; - -namespace Wolverine.Pubsub.Tests; - -internal class TestPubsubEnvelopeMapper : PubsubEnvelopeMapper { - private SystemTextJsonSerializer _serializer = new(SystemTextJsonSerializer.DefaultOptions()); - - public TestPubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { - MapProperty( - x => x.Message!, - (e, m) => { - if (e.Data is null || e.MessageType.IsEmpty()) return; - - if (e.MessageType.EndsWith(".ErrorCausingMessage")) { - string jsonString = Encoding.UTF8.GetString(e.Data); - - e.Message = JsonConvert.DeserializeObject(jsonString); - - return; - } - - var type = Type.GetType(e.MessageType); - - if (type is null) return; - - e.Message = _serializer.ReadFromData(type, e); - }, - (e, m) => { } - ); - } -} diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs index fe906a5a4..ec8d9c12d 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs @@ -3,13 +3,8 @@ namespace Wolverine.Pubsub.Tests; public static class TestingExtensions { - public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) { - // options.Policies.Add(new LambdaEndpointPolicy((e, _) => { - // e.Mapper = new TestPubsubEnvelopeMapper(e); - // })); - - return options.UsePubsub(Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? throw new NullReferenceException(), opts => { + public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) => options + .UsePubsub(Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? throw new NullReferenceException(), opts => { opts.EmulatorDetection = EmulatorDetection.EmulatorOnly; }); - } } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj b/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj index 36518cbff..e15afc54d 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Wolverine.Pubsub.Tests.csproj @@ -28,11 +28,4 @@ - - - - Always - Always - - diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs index 572e97acd..57c7af3bf 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs @@ -108,7 +108,6 @@ await _transport.PublisherApiClient.CreateTopicAsync(new Topic { if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - try { if (!IsDeadLetter) Server.Subscription.Name = Server.Subscription.Name.WithAssignedNodeNumber(_transport.AssignedNodeNumber); @@ -131,43 +130,36 @@ await _transport.PublisherApiClient.CreateTopicAsync(new Topic { } catch (RpcException ex) { if (ex.StatusCode != StatusCode.AlreadyExists) { - logger.LogError( - ex, - "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", - Uri, - Server.Subscription.Name, - Server.Topic.Name - ); + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); throw; } - logger.LogInformation( - "{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", - Uri, - Server.Subscription.Name - ); + logger.LogInformation("{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", Uri, Server.Subscription.Name); } catch (Exception ex) { - logger.LogError( - ex, - "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", - Uri, - Server.Subscription.Name, - Server.Topic.Name - ); + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); throw; } } - public ValueTask CheckAsync() { + public async ValueTask CheckAsync() { if ( _transport.PublisherApiClient is null || _transport.SubscriberApiClient is null - ) return ValueTask.FromResult(false); + ) return false; + + try { + await _transport.PublisherApiClient.GetTopicAsync(Server.Topic.Name); + + if (IsListener || IsDeadLetter) await _transport.SubscriberApiClient.GetSubscriptionAsync(Server.Subscription.Name); - return ValueTask.FromResult(_hasInitialized); + return true; + } + catch { + return false; + } } public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { From 9ef696e883a64adee26dafc1390bfba183416841 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:14:43 +0200 Subject: [PATCH 09/16] Added conventional routing tests --- .../ConventionalRoutingContext.cs | 63 +++++++++++ .../ConventionalRouting/PublishedMessage.cs | 6 + .../ConventionalRouting/RoutedMessage.cs | 6 + .../RoutedMessageHandler.cs | 5 + .../conventional_listener_discovery.cs | 104 ++++++++++++++++++ .../discover_with_naming_prefix.cs | 45 ++++++++ .../end_to_end_with_conventional_routing.cs | 75 +++++++++++++ ...d_with_conventional_routing_with_prefix.cs | 69 ++++++++++++ ..._a_listening_endpoint_with_all_defaults.cs | 39 +++++++ ...g_endpoint_with_overridden_queue_naming.cs | 37 +++++++ ..._discovering_a_sender_with_all_defaults.cs | 35 ++++++ 11 files changed, 484 insertions(+) create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/PublishedMessage.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessage.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessageHandler.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs new file mode 100644 index 000000000..304c7e26d --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Wolverine.ComplianceTests; +using Wolverine.Runtime; +using Wolverine.Runtime.Routing; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public abstract class ConventionalRoutingContext : IDisposable { + private IHost _host = default!; + + internal IWolverineRuntime theRuntime { + get { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + _host ??= WolverineHost.For(opts => opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .UseConventionalRouting() + ); + + return _host.Services.GetRequiredService(); + } + } + + public void Dispose() { + _host?.Dispose(); + } + + internal void ConfigureConventions(Action configure) { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + _host = Host + .CreateDefaultBuilder() + .UseWolverine(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .UseConventionalRouting(configure); + }).Start(); + } + + internal IMessageRouter RoutingFor() { + return theRuntime.RoutingFor(typeof(T)); + } + + internal void AssertNoRoutes() { + RoutingFor().ShouldBeOfType>(); + } + + internal IMessageRoute[] PublishingRoutesFor() { + return RoutingFor().ShouldBeOfType>().Routes; + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/PublishedMessage.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/PublishedMessage.cs new file mode 100644 index 000000000..1f801c882 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/PublishedMessage.cs @@ -0,0 +1,6 @@ +using Wolverine.Attributes; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +[MessageIdentity("published-message")] +public class PublishedMessage; \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessage.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessage.cs new file mode 100644 index 000000000..2b27550f0 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessage.cs @@ -0,0 +1,6 @@ +using Wolverine.Attributes; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +[MessageIdentity("routed")] +public class RoutedMessage; \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessageHandler.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessageHandler.cs new file mode 100644 index 000000000..e9a2b91ec --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/RoutedMessageHandler.cs @@ -0,0 +1,5 @@ +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class RoutedMessageHandler { + public void Handle(RoutedMessage message) { } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs new file mode 100644 index 000000000..b74b91ed7 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs @@ -0,0 +1,104 @@ +using JasperFx.Core; +using JasperFx.Core.Reflection; +using Shouldly; +using Wolverine.Pubsub.Internal; +using Wolverine.ComplianceTests.Compliance; +using Wolverine.Configuration; +using Wolverine.Runtime.Routing; +using Wolverine.Util; +using Xunit; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class conventional_listener_discovery : ConventionalRoutingContext { + [Fact] + public void disable_sender_with_lambda() { + ConfigureConventions(c => c.TopicNameForSender(t => { + if (t == typeof(PublishedMessage)) return null; // should not be routed + + return t.ToMessageTypeName(); + })); + + AssertNoRoutes(); + } + + [Fact] + public void exclude_types() { + ConfigureConventions(c => { + c.ExcludeTypes(t => t == typeof(PublishedMessage)); + }); + + AssertNoRoutes(); + + var uri = $"{PubsubTransport.ProtocolName}://wolverine/published-message".ToUri(); + var endpoint = theRuntime.Endpoints.EndpointFor(uri); + + endpoint.ShouldBeNull(); + + theRuntime.Endpoints.ActiveListeners() + .Any(x => x.Uri == uri) + .ShouldBeFalse(); + } + + [Fact] + public void include_types() { + ConfigureConventions(c => { + c.IncludeTypes(t => t == typeof(PublishedMessage)); + }); + + AssertNoRoutes(); + + PublishingRoutesFor().Any().ShouldBeTrue(); + + var uri = $"{PubsubTransport.ProtocolName}://wolverine/Message1".ToUri(); + var endpoint = theRuntime.Endpoints.EndpointFor(uri); + + endpoint.ShouldBeNull(); + + theRuntime.Endpoints.ActiveListeners() + .Any(x => x.Uri == uri) + .ShouldBeFalse(); + } + + [Fact] + public void configure_sender_overrides() { + ConfigureConventions(c => c.ConfigureSending((c, _) => c.AddOutgoingRule(new FakeEnvelopeRule()))); + + var route = PublishingRoutesFor().Single().As().Sender.Endpoint + .ShouldBeOfType(); + + route.OutgoingRules.Single().ShouldBeOfType(); + } + + [Fact] + public void disable_listener_by_lambda() { + ConfigureConventions(c => c.QueueNameForListener(t => { + if (t == typeof(RoutedMessage)) return null; // should not be routed + + return t.ToMessageTypeName(); + })); + + var uri = $"{PubsubTransport.ProtocolName}://wolverine/routed".ToUri(); + var endpoint = theRuntime.Endpoints.EndpointFor(uri); + + endpoint.ShouldBeNull(); + + theRuntime.Endpoints.ActiveListeners() + .Any(x => x.Uri == uri) + .ShouldBeFalse(); + } + + [Fact] + public void configure_listener() { + ConfigureConventions(c => c.ConfigureListeners((x, _) => { x.UseDurableInbox(); })); + + var endpoint = theRuntime.Endpoints.EndpointFor($"{PubsubTransport.ProtocolName}://wolverine/routed".ToUri()) + .ShouldBeOfType(); + + endpoint.Mode.ShouldBe(EndpointMode.Durable); + } + + public class FakeEnvelopeRule : IEnvelopeRule { + public void Modify(Envelope envelope) => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs new file mode 100644 index 000000000..d496e0b81 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Wolverine.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class discover_with_naming_prefix : IDisposable { + private readonly IHost _host; + private readonly ITestOutputHelper _output; + + public discover_with_naming_prefix(ITestOutputHelper output) { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + _output = output; + _host = Host + .CreateDefaultBuilder() + .UseWolverine(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .PrefixIdentifiers("zztop") + .UseConventionalRouting(); + }).Start(); + } + + public void Dispose() { + _host.Dispose(); + } + + [Fact] + public void discover_listener_with_prefix() { + var runtime = _host.Services.GetRequiredService(); + var uris = runtime.Endpoints.ActiveListeners().Select(x => x.Uri).ToArray(); + + uris.ShouldContain(new Uri($"{PubsubTransport.ProtocolName}://wolverine/zztop.routed")); + uris.ShouldContain(new Uri($"{PubsubTransport.ProtocolName}://wolverine/zztop.Wolverine.Pubsub.Tests.TestPubsubMessage")); + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs new file mode 100644 index 000000000..28c69f856 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs @@ -0,0 +1,75 @@ +using JasperFx.Core; +using Microsoft.Extensions.Hosting; +using Oakton.Resources; +using Shouldly; +using Wolverine.ComplianceTests; +using Wolverine.Tracking; +using Xunit; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class end_to_end_with_conventional_routing : IAsyncLifetime { + private IHost _receiver = default!; + private IHost _sender = default!; + + public async Task InitializeAsync() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + _sender = await Host + .CreateDefaultBuilder() + .UseWolverine(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .UseConventionalRouting(); + + opts.DisableConventionalDiscovery(); + + opts.ServiceName = "Sender"; + + opts.Services.AddResourceSetupOnStartup(); + }).StartAsync(); + + _receiver = await Host + .CreateDefaultBuilder() + .UseWolverine(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .UseConventionalRouting(); + + opts.ServiceName = "Receiver"; + + opts.Services.AddResourceSetupOnStartup(); + }).StartAsync(); + } + + public async Task DisposeAsync() { + await _sender.StopAsync(); + await _receiver.StopAsync(); + } + + [Fact] + public async Task send_from_one_node_to_another_all_with_conventional_routing() { + var session = await _sender + .TrackActivity() + .AlsoTrack(_receiver) + .IncludeExternalTransports() + .Timeout(30.Seconds()) + .SendMessageAndWaitAsync(new RoutedMessage()); + + var received = session + .AllRecordsInOrder() + .Where(x => x.Envelope.Message?.GetType() == typeof(RoutedMessage)) + .Single(x => x.MessageEventType == MessageEventType.Received); + + received.ServiceName.ShouldBe("Receiver"); + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs new file mode 100644 index 000000000..e9727ba78 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs @@ -0,0 +1,69 @@ +using JasperFx.Core; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Wolverine.ComplianceTests; +using Wolverine.Tracking; +using Xunit; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class end_to_end_with_conventional_routing_with_prefix : IDisposable { + private readonly IHost _receiver; + private readonly IHost _sender; + + public end_to_end_with_conventional_routing_with_prefix() { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + _sender = WolverineHost.For(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .PrefixIdentifiers("shazaam") + .UseConventionalRouting(); + + opts.DisableConventionalDiscovery(); + + opts.ServiceName = "Sender"; + }); + + _receiver = WolverineHost.For(opts => { + opts + .UsePubsubTesting() + .AutoProvision() + .AutoPurgeOnStartup() + .EnableAllNativeDeadLettering() + .SystemEndpointsAreEnabled(true) + .PrefixIdentifiers("shazaam") + .UseConventionalRouting(); + + opts.ServiceName = "Receiver"; + }); + } + + public void Dispose() { + _sender?.Dispose(); + _receiver?.Dispose(); + } + + [Fact] + public async Task send_from_one_node_to_another_all_with_conventional_routing() { + var session = await _sender + .TrackActivity() + .AlsoTrack(_receiver) + .IncludeExternalTransports() + .Timeout(30.Seconds()) + .SendMessageAndWaitAsync(new RoutedMessage()); + + var received = session + .AllRecordsInOrder() + .Where(x => x.Envelope.Message?.GetType() == typeof(RoutedMessage)) + .Single(x => x.MessageEventType == MessageEventType.Received); + + received.ServiceName.ShouldBe("Receiver"); + received.Envelope.Destination.ShouldBe(new Uri($"{PubsubTransport.ProtocolName}://wolverine/shazaam.routed")); + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs new file mode 100644 index 000000000..1230bd811 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs @@ -0,0 +1,39 @@ +using JasperFx.Core; +using Shouldly; +using Wolverine.Pubsub.Internal; +using Wolverine.Configuration; +using Xunit; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class when_discovering_a_listening_endpoint_with_all_defaults : ConventionalRoutingContext { + private readonly PubsubEndpoint theEndpoint; + private readonly Uri theExpectedUri = $"{PubsubTransport.ProtocolName}://wolverine/routed".ToUri(); + + public when_discovering_a_listening_endpoint_with_all_defaults() { + theEndpoint = theRuntime.Endpoints.EndpointFor(theExpectedUri).ShouldBeOfType(); + } + + [Fact] + public void endpoint_should_be_a_listener() { + theEndpoint.IsListener.ShouldBeTrue(); + } + + [Fact] + public void endpoint_should_not_be_null() { + theEndpoint.ShouldNotBeNull(); + } + + [Fact] + public void mode_is_buffered_by_default() { + theEndpoint.Mode.ShouldBe(EndpointMode.BufferedInMemory); + } + + [Fact] + public void should_be_an_active_listener() { + theRuntime.Endpoints + .ActiveListeners() + .Any(x => x.Uri == theExpectedUri) + .ShouldBeTrue(); + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs new file mode 100644 index 000000000..da7af66ac --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs @@ -0,0 +1,37 @@ +using JasperFx.Core; +using Shouldly; +using Wolverine.Pubsub.Internal; +using Xunit; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class when_discovering_a_listening_endpoint_with_overridden_queue_naming : ConventionalRoutingContext { + private readonly Uri theExpectedUri = $"{PubsubTransport.ProtocolName}://wolverine/routedmessage2".ToUri(); + private readonly PubsubEndpoint theEndpoint; + + public when_discovering_a_listening_endpoint_with_overridden_queue_naming() { + ConfigureConventions(c => c.QueueNameForListener(t => t.Name.ToLower() + "2")); + + var theRuntimeEndpoints = theRuntime.Endpoints.ActiveListeners().ToArray(); + + theEndpoint = theRuntime.Endpoints.EndpointFor(theExpectedUri).ShouldBeOfType(); + } + + [Fact] + public void endpoint_should_be_a_listener() { + theEndpoint.IsListener.ShouldBeTrue(); + } + + [Fact] + public void endpoint_should_not_be_null() { + theEndpoint.ShouldNotBeNull(); + } + + [Fact] + public void should_be_an_active_listener() { + theRuntime.Endpoints + .ActiveListeners() + .Any(x => x.Uri == theExpectedUri) + .ShouldBeTrue(); + } +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs new file mode 100644 index 000000000..ddefe1455 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs @@ -0,0 +1,35 @@ +using JasperFx.Core.Reflection; +using Shouldly; +using Wolverine.Pubsub.Internal; +using Wolverine.Configuration; +using Wolverine.Runtime.Routing; +using Xunit; + +namespace Wolverine.Pubsub.Tests.ConventionalRouting; + +public class when_discovering_a_sender_with_all_defaults : ConventionalRoutingContext { + private readonly MessageRoute theRoute; + + public when_discovering_a_sender_with_all_defaults() { + theRoute = PublishingRoutesFor().Single().As(); + } + + [Fact] + public void should_have_exactly_one_route() { + theRoute.ShouldNotBeNull(); + } + + [Fact] + public void routed_to_azure_service_bus_queue() { + var endpoint = theRoute.Sender.Endpoint.ShouldBeOfType(); + + endpoint.Server.Topic.Name.TopicId.ShouldBe("published-message"); + } + + [Fact] + public void endpoint_mode_is_buffered_by_default() { + var endpoint = theRoute.Sender.Endpoint.ShouldBeOfType(); + + endpoint.Mode.ShouldBe(EndpointMode.BufferedInMemory); + } +} \ No newline at end of file From 3639150edb03ef9bfb56894871b3c01a4144a14e Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:28:35 +0200 Subject: [PATCH 10/16] Minor clean-ups, refactored mapper, added documentation --- .../gcp-pubsub/conventional-routing.md | 50 +++ .../transports/gcp-pubsub/deadlettering.md | 66 ++++ .../messaging/transports/gcp-pubsub/index.md | 80 +++++ .../transports/gcp-pubsub/interoperability.md | 78 +++++ .../transports/gcp-pubsub/listening.md | 39 +++ .../transports/gcp-pubsub/publishing.md | 28 ++ .../BufferedSendingAndReceivingCompliance.cs | 11 +- .../ConventionalRoutingContext.cs | 14 +- .../conventional_listener_discovery.cs | 1 - .../discover_with_naming_prefix.cs | 7 +- .../end_to_end_with_conventional_routing.cs | 11 +- ...d_with_conventional_routing_with_prefix.cs | 11 +- ..._a_listening_endpoint_with_all_defaults.cs | 1 - ...g_endpoint_with_overridden_queue_naming.cs | 1 - ..._discovering_a_sender_with_all_defaults.cs | 1 - .../DocumentationSamples.cs | 301 ++++++++++++++++++ .../DurableSendingAndReceivingCompliance.cs | 11 +- .../InlineSendingAndReceivingCompliance.cs | 12 +- .../Internal/PubsubEndpointTests.cs | 1 - .../PubsubTransportTests.cs | 8 +- .../StatefulResourceSmokeTests.cs | 3 - .../TestingExtensions.cs | 11 +- .../send_and_receive.cs | 5 +- .../sending_compliance_with_prefixes.cs | 12 +- .../Wolverine.Pubsub/IPubsubEnvelopeMapper.cs | 5 +- .../Internal/PubsubEnvelope.cs | 9 - .../Internal/PubsubEnvelopeMapper.cs | 100 +++--- .../Internal/PubsubListener.cs | 6 +- .../Internal/PubsubSenderProtocol.cs | 10 +- .../Wolverine.Pubsub/PubsubConfiguration.cs | 19 +- .../{Internal => }/PubsubEndpoint.cs | 24 +- .../GCP/Wolverine.Pubsub/PubsubEnvelope.cs | 5 + .../GCP/Wolverine.Pubsub/PubsubOptions.cs | 6 + .../PubsubTopicListenerConfiguration.cs | 28 +- .../PubsubTopicSubscriberConfiguration.cs | 54 +--- .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 26 +- .../PubsubTransportExtensions.cs | 6 +- 37 files changed, 831 insertions(+), 230 deletions(-) create mode 100644 docs/guide/messaging/transports/gcp-pubsub/conventional-routing.md create mode 100644 docs/guide/messaging/transports/gcp-pubsub/deadlettering.md create mode 100644 docs/guide/messaging/transports/gcp-pubsub/index.md create mode 100644 docs/guide/messaging/transports/gcp-pubsub/interoperability.md create mode 100644 docs/guide/messaging/transports/gcp-pubsub/listening.md create mode 100644 docs/guide/messaging/transports/gcp-pubsub/publishing.md create mode 100644 src/Transports/GCP/Wolverine.Pubsub.Tests/DocumentationSamples.cs delete mode 100644 src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs rename src/Transports/GCP/Wolverine.Pubsub/{Internal => }/PubsubEndpoint.cs (94%) create mode 100644 src/Transports/GCP/Wolverine.Pubsub/PubsubEnvelope.cs diff --git a/docs/guide/messaging/transports/gcp-pubsub/conventional-routing.md b/docs/guide/messaging/transports/gcp-pubsub/conventional-routing.md new file mode 100644 index 000000000..07bf10163 --- /dev/null +++ b/docs/guide/messaging/transports/gcp-pubsub/conventional-routing.md @@ -0,0 +1,50 @@ +# Conventional Message Routing + +You can have Wolverine automatically determine message routing to GCP Pub/Sub +based on conventions as shown in the code snippet below. By default, this approach assumes that +each outgoing message type should be sent to topic named with the [message type name](/guide/messages.html#message-type-name-or-alias) for that +message type. + +Likewise, Wolverine sets up a listener for a topic named similarly for each message type known +to be handled by the application. + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .UseConventionalRouting(convention => + { + + // Optionally override the default queue naming scheme + convention.TopicNameForSender(t => t.Namespace) + + // Optionally override the default queue naming scheme + .QueueNameForListener(t => t.Namespace) + + // Fine tune the conventionally discovered listeners + .ConfigureListeners((listener, builder) => + { + var messageType = builder.MessageType; + var runtime = builder.Runtime; // Access to basically everything + + // customize the new queue + listener.CircuitBreaker(queue => { }); + + // other options... + }) + + // Fine tune the conventionally discovered sending endpoints + .ConfigureSending((subscriber, builder) => + { + // Similarly, use the message type and/or wolverine runtime + // to customize the message sending + }); + + }); + }).StartAsync(); +``` +snippet source | anchor + \ No newline at end of file diff --git a/docs/guide/messaging/transports/gcp-pubsub/deadlettering.md b/docs/guide/messaging/transports/gcp-pubsub/deadlettering.md new file mode 100644 index 000000000..c7e140408 --- /dev/null +++ b/docs/guide/messaging/transports/gcp-pubsub/deadlettering.md @@ -0,0 +1,66 @@ +# Dead Lettering + +By default, Wolverine dead lettering is disabled for GCP Pub/Sub transport and Wolverine uses any persistent envelope storage for dead lettering. You can opt in to Wolverine dead lettering through GCP Pub/Sub globally as shown below. + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + + // Enable dead lettering for all Wolverine endpoints + .EnableDeadLettering( + + // Optionally configure how the GCP Pub/Sub dead letter itself + // is created by Wolverine + options => + { + options.Topic.MessageRetentionDuration = + Duration.FromTimeSpan(TimeSpan.FromDays(14)); + + options.Subscription.MessageRetentionDuration = + Duration.FromTimeSpan(TimeSpan.FromDays(14)); + } + + ); + }).StartAsync(); +``` +snippet source | anchor + + +When enabled, Wolverine will try to move dead letter messages in GCP Pub/Sub to a single, global topic named "wlvrn.dead-letter". + +That can be overridden on a single endpoint at a time (or by conventions too of course) like: + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .EnableDeadLettering(); + + // No dead letter queueing + opts.ListenToPubsubTopic("incoming") + .DisableDeadLettering(); + + // Use a different dead letter queue + opts.ListenToPubsubTopic("important") + .ConfigureDeadLettering( + "important_errors", + + // Optionally configure how the dead letter itself + // is built by Wolverine + e => + { + e.TelemetryEnabled = true; + } + + ); + }).StartAsync(); +``` +snippet source | anchor + \ No newline at end of file diff --git a/docs/guide/messaging/transports/gcp-pubsub/index.md b/docs/guide/messaging/transports/gcp-pubsub/index.md new file mode 100644 index 000000000..096f9e6c0 --- /dev/null +++ b/docs/guide/messaging/transports/gcp-pubsub/index.md @@ -0,0 +1,80 @@ +# Using Google Cloud Platform (GCP) Pub/Sub + +::: tip +Wolverine.AzureServiceBus is able to support inline, buffered, or durable endpoints. +::: + +Wolverine supports [GCP Pub/Sub](https://cloud.google.com/pubsub) as a messaging transport through the WolverineFx.Pubsub package. + +## Connecting to the Broker + +After referencing the Nuget package, the next step to using GCP Pub/Sub within your Wolverine application is to connect to the service broker using the `UsePubsub()` extension method. + +If you are running on Google Cloud or with Application Default Credentials (ADC), you just need to supply [your GCP project id](https://support.google.com/googleapi/answer/7014113): + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + + // Let Wolverine create missing topics and subscriptions as necessary + .AutoProvision() + + // Optionally purge all subscriptions on application startup. + // Warning though, this is potentially slow + .AutoPurgeOnStartup(); + + }).StartAsync(); +``` +snippet source | anchor + + +If you'd like to connect to a GCP Pub/Sub emulator running on your development box, +you set emulator detection throught this helper: + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + + // Tries to use GCP Pub/Sub emulator, as it defaults + // to EmulatorDetection.EmulatorOrProduction. But you can + // supply your own, like EmulatorDetection.EmulatorOnly + .UseEmulatorDetection(); + + }).StartAsync(); +``` +snippet source | anchor + + +## Request/Reply + +[Request/reply](https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html) mechanics (`IMessageBus.InvokeAsync()`) are possible with the GCP Pub/Sub transport *if* Wolverine has the ability to auto-provision a specific response topic and subscription for each node. That topic and subscription would be named like `wlvrn.response.[application node id]` if you happen to notice that in your GCP Pub/Sub. + +### Enable System Endpoints + +If your application has permissions to create topics and subscriptions in GCP Pub/Sub, you can enable system endpoints and opt in to the request/reply mechanics mentioned above. + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UseAzureServiceBusTesting() + .AutoProvision().AutoPurgeOnStartup() + .EnableSystemEndpoints(); + + opts.ListenToAzureServiceBusQueue("send_and_receive"); + + opts.PublishAllMessages().ToAzureServiceBusQueue("send_and_receive"); + }).StartAsync(); +``` +snippet source | anchor + \ No newline at end of file diff --git a/docs/guide/messaging/transports/gcp-pubsub/interoperability.md b/docs/guide/messaging/transports/gcp-pubsub/interoperability.md new file mode 100644 index 000000000..b365eccc6 --- /dev/null +++ b/docs/guide/messaging/transports/gcp-pubsub/interoperability.md @@ -0,0 +1,78 @@ +# Interoperability + +Hey, it's a complicated world and Wolverine is a relative newcomer, so it's somewhat likely you'll find yourself needing to make a Wolverine application talk via GCP Pub/Sub to +a non-Wolverine application. Not to worry (too much), Wolverine has you covered with the ability to customize Wolverine to GCP Pub/Sub mapping. + +You can create interoperability with non-Wolverine applications by writing a custom `IPubsubEnvelopeMapper` +as shown in the following sample: + + + +```cs +public class CustomPubsubMapper : EnvelopeMapper, IPubsubEnvelopeMapper +{ + public CustomPubsubMapper(PubsubEndpoint endpoint) : base(endpoint) { } + + public void MapIncomingToEnvelope(PubsubEnvelope envelope, ReceivedMessage incoming) + { + envelope.AckId = incoming.AckId; + + // You will have to help Wolverine out by either telling Wolverine + // what the message type is, or by reading the actual message object, + // or by telling Wolverine separately what the default message type + // is for a listening endpoint + envelope.MessageType = typeof(Message1).ToMessageTypeName(); + + } + + public void MapOutgoingToMessage(OutgoingMessageBatch outgoing, PubsubMessage message) + { + message.Data = ByteString.CopyFrom(outgoing.Data); + } + + protected override void writeOutgoingHeader(PubsubMessage outgoing, string key, string value) + { + outgoing.Attributes[key] = value; + } + + protected override void writeIncomingHeaders(ReceivedMessage incoming, Envelope envelope) + { + if (incoming.Message.Attributes is null) return; + + foreach (var pair in incoming.Message.Attributes) envelope.Headers[pair.Key] = pair.Value?.ToString(); + } + + protected override bool tryReadIncomingHeader(ReceivedMessage incoming, string key, out string? value) + { + if (incoming.Message.Attributes.TryGetValue(key, out var header)) + { + value = header.ToString(); + + return true; + } + + value = null; + + return false; + } +} +``` +snippet source | anchor + + +To apply that mapper to specific endpoints, use this syntax on any type of GCP Pub/Sub endpoint: + + + +```cs +using var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .UseConventionalRouting() + .ConfigureListeners(l => l.InteropWith(e => new CustomPubsubMapper(e))) + .ConfigureSenders(s => s.InteropWith(e => new CustomPubsubMapper(e))); + }).StartAsync(); +``` +snippet source | anchor + diff --git a/docs/guide/messaging/transports/gcp-pubsub/listening.md b/docs/guide/messaging/transports/gcp-pubsub/listening.md new file mode 100644 index 000000000..11d097457 --- /dev/null +++ b/docs/guide/messaging/transports/gcp-pubsub/listening.md @@ -0,0 +1,39 @@ +# Listening + +Setting up Wolverine listeners and GCP Pub/Sub subscriptions for GCP Pub/Sub topics is shown below: + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id"); + + opts.ListenToPubsubTopic("incoming1"); + + opts.ListenToPubsubTopic("incoming2") + + // You can optimize the throughput by running multiple listeners + // in parallel + .ListenerCount(5) + + .ConfigurePubsubSubscription(options => + { + + // Optionally configure the subscription itself + options.DeadLetterPolicy = new() { + DeadLetterTopic = "errors", + MaxDeliveryAttempts = 5 + }; + options.AckDeadlineSeconds = 60; + options.RetryPolicy = new() { + MinimumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(1)), + MaximumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)) + }; + + }); + }).StartAsync(); +``` +snippet source | anchor + diff --git a/docs/guide/messaging/transports/gcp-pubsub/publishing.md b/docs/guide/messaging/transports/gcp-pubsub/publishing.md new file mode 100644 index 000000000..4db8ffe89 --- /dev/null +++ b/docs/guide/messaging/transports/gcp-pubsub/publishing.md @@ -0,0 +1,28 @@ +# Publishing + +Configuring Wolverine subscriptions through GCP Pub/Sub topics is done with the `ToPubsubTopic()` extension method shown in the example below: + + + +```cs +var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id"); + + opts + .PublishMessage() + .ToPubsubTopic("outbound1"); + + opts + .PublishMessage() + .ToPubsubTopic("outbound2") + .ConfigurePubsubTopic(options => + { + options.MessageRetentionDuration = + Duration.FromTimeSpan(TimeSpan.FromMinutes(10)); + }); + }).StartAsync(); +``` +snippet source | anchor + diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs index c9e480899..f5ff26f35 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/BufferedSendingAndReceivingCompliance.cs @@ -11,9 +11,6 @@ public class BufferedComplianceFixture : TransportComplianceFixture, IAsyncLifet public BufferedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/buffered-receiver"), 120) { } public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var id = Guid.NewGuid().ToString(); OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/buffered-receiver.{id}"); @@ -23,8 +20,8 @@ await SenderIs(opts => { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .EnableDeadLettering() + .EnableSystemEndpoints(); }); await ReceiverIs(opts => { @@ -32,8 +29,8 @@ await ReceiverIs(opts => { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .EnableDeadLettering() + .EnableSystemEndpoints(); opts .ListenToPubsubTopic($"buffered-receiver.{id}") diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs index 304c7e26d..6bab2f91e 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/ConventionalRoutingContext.cs @@ -12,15 +12,12 @@ public abstract class ConventionalRoutingContext : IDisposable { internal IWolverineRuntime theRuntime { get { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - _host ??= WolverineHost.For(opts => opts .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .UseConventionalRouting() ); @@ -33,9 +30,6 @@ public void Dispose() { } internal void ConfigureConventions(Action configure) { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - _host = Host .CreateDefaultBuilder() .UseWolverine(opts => { @@ -43,8 +37,8 @@ internal void ConfigureConventions(Action config .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .UseConventionalRouting(configure); }).Start(); } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs index b74b91ed7..42c1a6b8a 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/conventional_listener_discovery.cs @@ -1,7 +1,6 @@ using JasperFx.Core; using JasperFx.Core.Reflection; using Shouldly; -using Wolverine.Pubsub.Internal; using Wolverine.ComplianceTests.Compliance; using Wolverine.Configuration; using Wolverine.Runtime.Routing; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs index d496e0b81..5b07c9294 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/discover_with_naming_prefix.cs @@ -12,9 +12,6 @@ public class discover_with_naming_prefix : IDisposable { private readonly ITestOutputHelper _output; public discover_with_naming_prefix(ITestOutputHelper output) { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - _output = output; _host = Host .CreateDefaultBuilder() @@ -23,8 +20,8 @@ public discover_with_naming_prefix(ITestOutputHelper output) { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .PrefixIdentifiers("zztop") .UseConventionalRouting(); }).Start(); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs index 28c69f856..2d0dff3bc 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs @@ -13,9 +13,6 @@ public class end_to_end_with_conventional_routing : IAsyncLifetime { private IHost _sender = default!; public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - _sender = await Host .CreateDefaultBuilder() .UseWolverine(opts => { @@ -23,8 +20,8 @@ public async Task InitializeAsync() { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .UseConventionalRouting(); opts.DisableConventionalDiscovery(); @@ -41,8 +38,8 @@ public async Task InitializeAsync() { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .UseConventionalRouting(); opts.ServiceName = "Receiver"; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs index e9727ba78..50b0ad270 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs @@ -12,16 +12,13 @@ public class end_to_end_with_conventional_routing_with_prefix : IDisposable { private readonly IHost _sender; public end_to_end_with_conventional_routing_with_prefix() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - _sender = WolverineHost.For(opts => { opts .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .PrefixIdentifiers("shazaam") .UseConventionalRouting(); @@ -35,8 +32,8 @@ public end_to_end_with_conventional_routing_with_prefix() { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .PrefixIdentifiers("shazaam") .UseConventionalRouting(); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs index 1230bd811..eab3da7b2 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs @@ -1,6 +1,5 @@ using JasperFx.Core; using Shouldly; -using Wolverine.Pubsub.Internal; using Wolverine.Configuration; using Xunit; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs index da7af66ac..8e1717ff4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs @@ -1,6 +1,5 @@ using JasperFx.Core; using Shouldly; -using Wolverine.Pubsub.Internal; using Xunit; namespace Wolverine.Pubsub.Tests.ConventionalRouting; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs index ddefe1455..a174b2256 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs @@ -1,6 +1,5 @@ using JasperFx.Core.Reflection; using Shouldly; -using Wolverine.Pubsub.Internal; using Wolverine.Configuration; using Wolverine.Runtime.Routing; using Xunit; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/DocumentationSamples.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/DocumentationSamples.cs new file mode 100644 index 000000000..9be809b74 --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/DocumentationSamples.cs @@ -0,0 +1,301 @@ +using Google.Cloud.PubSub.V1; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.Hosting; +using Wolverine.ComplianceTests.Compliance; +using Wolverine.Transports; +using Wolverine.Util; + +namespace Wolverine.Pubsub.Tests; + +public class DocumentationSamples +{ + public async Task bootstraping() + { + #region sample_basic_setup_to_pubsub + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + + // Let Wolverine create missing topics and subscriptions as necessary + .AutoProvision() + + // Optionally purge all subscriptions on application startup. + // Warning though, this is potentially slow + .AutoPurgeOnStartup(); + + }).StartAsync(); + + #endregion + } + + public async Task for_local_development() + { + #region sample_connect_to_pubsub_emulator + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + + // Tries to use GCP Pub/Sub emulator, as it defaults + // to EmulatorDetection.EmulatorOrProduction. But you can + // supply your own, like EmulatorDetection.EmulatorOnly + .UseEmulatorDetection(); + + }).StartAsync(); + + #endregion + } + + public async Task enable_system_endpoints() + { + #region sample_enable_system_endpoints_in_pubsub + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .EnableSystemEndpoints(); + + }).StartAsync(); + + #endregion + } + + public async Task configuring_listeners() + { + #region sample_listen_to_pubsub_topic + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id"); + + opts.ListenToPubsubTopic("incoming1"); + + opts.ListenToPubsubTopic("incoming2") + + // You can optimize the throughput by running multiple listeners + // in parallel + .ListenerCount(5) + + .ConfigurePubsubSubscription(options => + { + + // Optionally configure the subscription itself + options.DeadLetterPolicy = new() { + DeadLetterTopic = "errors", + MaxDeliveryAttempts = 5 + }; + options.AckDeadlineSeconds = 60; + options.RetryPolicy = new() { + MinimumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(1)), + MaximumBackoff = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)) + }; + + }); + }).StartAsync(); + + #endregion + } + + public async Task publishing() + { + #region sample_subscriber_rules_for_pubsub + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id"); + + opts + .PublishMessage() + .ToPubsubTopic("outbound1"); + + opts + .PublishMessage() + .ToPubsubTopic("outbound2") + .ConfigurePubsubTopic(options => + { + options.MessageRetentionDuration = + Duration.FromTimeSpan(TimeSpan.FromMinutes(10)); + }); + }).StartAsync(); + + #endregion + } + + public async Task conventional_routing() + { + #region sample_conventional_routing_for_pubsub + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .UseConventionalRouting(convention => + { + + // Optionally override the default queue naming scheme + convention.TopicNameForSender(t => t.Namespace) + + // Optionally override the default queue naming scheme + .QueueNameForListener(t => t.Namespace) + + // Fine tune the conventionally discovered listeners + .ConfigureListeners((listener, builder) => + { + var messageType = builder.MessageType; + var runtime = builder.Runtime; // Access to basically everything + + // customize the new queue + listener.CircuitBreaker(queue => { }); + + // other options... + }) + + // Fine tune the conventionally discovered sending endpoints + .ConfigureSending((subscriber, builder) => + { + // Similarly, use the message type and/or wolverine runtime + // to customize the message sending + }); + + }); + }).StartAsync(); + + #endregion + } + + public async Task enable_dead_lettering() + { + #region sample_enable_wolverine_dead_lettering_for_pubsub + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + + // Enable dead lettering for all Wolverine endpoints + .EnableDeadLettering( + + // Optionally configure how the GCP Pub/Sub dead letter itself + // is created by Wolverine + options => + { + options.Topic.MessageRetentionDuration = + Duration.FromTimeSpan(TimeSpan.FromDays(14)); + + options.Subscription.MessageRetentionDuration = + Duration.FromTimeSpan(TimeSpan.FromDays(14)); + } + + ); + }).StartAsync(); + + #endregion + } + + public async Task overriding_wolverine_dead_lettering() + { + #region sample_configuring_wolverine_dead_lettering_for_pubsub + + var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .EnableDeadLettering(); + + // No dead letter queueing + opts.ListenToPubsubTopic("incoming") + .DisableDeadLettering(); + + // Use a different dead letter queue + opts.ListenToPubsubTopic("important") + .ConfigureDeadLettering( + "important_errors", + + // Optionally configure how the dead letter itself + // is built by Wolverine + e => + { + e.TelemetryEnabled = true; + } + + ); + }).StartAsync(); + + #endregion + } + + public async Task customize_mappers() + { + #region sample_configuring_custom_envelope_mapper_for_pubsub + + using var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.UsePubsub("your-project-id") + .UseConventionalRouting() + .ConfigureListeners(l => l.InteropWith(e => new CustomPubsubMapper(e))) + .ConfigureSenders(s => s.InteropWith(e => new CustomPubsubMapper(e))); + }).StartAsync(); + + #endregion + } +} + +#region sample_custom_pubsub_mapper + +public class CustomPubsubMapper : EnvelopeMapper, IPubsubEnvelopeMapper +{ + public CustomPubsubMapper(PubsubEndpoint endpoint) : base(endpoint) { } + + public void MapIncomingToEnvelope(PubsubEnvelope envelope, ReceivedMessage incoming) + { + envelope.AckId = incoming.AckId; + + // You will have to help Wolverine out by either telling Wolverine + // what the message type is, or by reading the actual message object, + // or by telling Wolverine separately what the default message type + // is for a listening endpoint + envelope.MessageType = typeof(Message1).ToMessageTypeName(); + + } + + public void MapOutgoingToMessage(OutgoingMessageBatch outgoing, PubsubMessage message) + { + message.Data = ByteString.CopyFrom(outgoing.Data); + } + + protected override void writeOutgoingHeader(PubsubMessage outgoing, string key, string value) + { + outgoing.Attributes[key] = value; + } + + protected override void writeIncomingHeaders(ReceivedMessage incoming, Envelope envelope) + { + if (incoming.Message.Attributes is null) return; + + foreach (var pair in incoming.Message.Attributes) envelope.Headers[pair.Key] = pair.Value?.ToString(); + } + + protected override bool tryReadIncomingHeader(ReceivedMessage incoming, string key, out string? value) + { + if (incoming.Message.Attributes.TryGetValue(key, out var header)) + { + value = header.ToString(); + + return true; + } + + value = null; + + return false; + } +} + +#endregion \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs index 16f4f5236..3f95c78e8 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs @@ -15,9 +15,6 @@ public class DurableComplianceFixture : TransportComplianceFixture, IAsyncLifeti public DurableComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/durable-receiver"), 120) { } public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var id = Guid.NewGuid().ToString(); OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/durable-receiver.{id}"); @@ -27,8 +24,8 @@ await SenderIs(opts => { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .ConfigureListeners(x => x.UseDurableInbox()) .ConfigureListeners(x => x.UseDurableInbox()); @@ -47,8 +44,8 @@ await ReceiverIs(opts => { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true) + .EnableDeadLettering() + .EnableSystemEndpoints() .ConfigureListeners(x => x.UseDurableInbox()) .ConfigureListeners(x => x.UseDurableInbox()); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs index 374a4ce24..e284dfc5c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/InlineSendingAndReceivingCompliance.cs @@ -1,4 +1,3 @@ -using Google.Cloud.PubSub.V1; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; @@ -12,9 +11,6 @@ public class InlineComplianceFixture : TransportComplianceFixture, IAsyncLifetim public InlineComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/inline-receiver"), 120) { } public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var id = Guid.NewGuid().ToString(); OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/inline-receiver.{id}"); @@ -24,8 +20,8 @@ await SenderIs(opts => { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .EnableDeadLettering() + .EnableSystemEndpoints(); opts .PublishAllMessages() @@ -38,8 +34,8 @@ await ReceiverIs(opts => { .UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .EnableDeadLettering() + .EnableSystemEndpoints(); opts .ListenToPubsubTopic($"inline-receiver.{id}") diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs index 5691326bf..034a2747e 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs @@ -5,7 +5,6 @@ using NSubstitute.ExceptionExtensions; using Shouldly; using Wolverine.Configuration; -using Wolverine.Pubsub.Internal; using Xunit; namespace Wolverine.Pubsub.Tests.Internal; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs index dab75efe4..a35ecffc3 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubTransportTests.cs @@ -1,5 +1,4 @@ using Shouldly; -using Wolverine.Pubsub.Internal; using Xunit; namespace Wolverine.Pubsub.Tests; @@ -23,9 +22,10 @@ public void response_subscriptions_are_disabled_by_default() { [Fact] public void return_all_endpoints_gets_dead_letter_subscription_too() { - var transport = new PubsubTransport("wolverine") { - EnableDeadLettering = true - }; + var transport = new PubsubTransport("wolverine"); + + transport.DeadLetter.Enabled = true; + var one = transport.Topics["one"]; var two = transport.Topics["two"]; var three = transport.Topics["three"]; diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs index cc48229bc..0e6c45e6c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/StatefulResourceSmokeTests.cs @@ -8,9 +8,6 @@ namespace Wolverine.Pubsub.Tests; public class StatefulResourceSmokeTests { private IHostBuilder ConfigureBuilder(bool autoProvision, int starting = 1) { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - return Host .CreateDefaultBuilder() .UseWolverine(opts => { diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs index ec8d9c12d..e25b61a92 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/TestingExtensions.cs @@ -3,8 +3,11 @@ namespace Wolverine.Pubsub.Tests; public static class TestingExtensions { - public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) => options - .UsePubsub(Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? throw new NullReferenceException(), opts => { - opts.EmulatorDetection = EmulatorDetection.EmulatorOnly; - }); + public static PubsubConfiguration UsePubsubTesting(this WolverineOptions options) { + Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); + Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); + + return options + .UsePubsub("wolverine") + .UseEmulatorDetection(EmulatorDetection.EmulatorOnly);} } diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs index c1f1bcbf6..788fc35f1 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/send_and_receive.cs @@ -12,9 +12,6 @@ public class send_and_receive : IAsyncLifetime { private IHost _host = default!; public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - _host = await Host.CreateDefaultBuilder() .UseWolverine(opts => { opts @@ -51,7 +48,7 @@ public async Task builds_system_endpoints() { opts.UsePubsubTesting() .AutoProvision() .AutoPurgeOnStartup() - .SystemEndpointsAreEnabled(true); + .EnableSystemEndpoints(); opts.ListenToPubsubTopic("send_and_receive"); diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs index 6fed58c94..66cc5ae66 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/sending_compliance_with_prefixes.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Shouldly; using Wolverine.ComplianceTests.Compliance; -using Wolverine.Pubsub.Internal; using Wolverine.Runtime; using Xunit; @@ -11,9 +10,6 @@ public class PrefixedComplianceFixture : TransportComplianceFixture, IAsyncLifet public PrefixedComplianceFixture() : base(new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.receiver"), 120) { } public async Task InitializeAsync() { - Environment.SetEnvironmentVariable("PUBSUB_EMULATOR_HOST", "[::1]:8085"); - Environment.SetEnvironmentVariable("PUBSUB_PROJECT_ID", "wolverine"); - var id = Guid.NewGuid().ToString(); OutboundAddress = new Uri($"{PubsubTransport.ProtocolName}://wolverine/foo.receiver.{id}"); @@ -23,8 +19,8 @@ await SenderIs(opts => { .AutoProvision() .AutoPurgeOnStartup() .PrefixIdentifiers("foo") - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .EnableDeadLettering() + .EnableSystemEndpoints(); }); await ReceiverIs(opts => { @@ -32,8 +28,8 @@ await ReceiverIs(opts => { .AutoProvision() .AutoPurgeOnStartup() .PrefixIdentifiers("foo") - .EnableAllNativeDeadLettering() - .SystemEndpointsAreEnabled(true); + .EnableDeadLettering() + .EnableSystemEndpoints(); opts .ListenToPubsubTopic($"receiver.{id}") diff --git a/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs index d155ce26e..a6b2017f7 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs @@ -6,4 +6,7 @@ namespace Wolverine.Pubsub; /// /// Pluggable strategy for reading and writing data to Google Cloud Pub/Sub /// -public interface IPubsubEnvelopeMapper : IEnvelopeMapper; +public interface IPubsubEnvelopeMapper : IEnvelopeMapper { + void MapIncomingToEnvelope(PubsubEnvelope envelope, ReceivedMessage incoming); + void MapOutgoingToMessage(OutgoingMessageBatch outgoing, PubsubMessage message); +}; diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs deleted file mode 100644 index 612dc8bec..000000000 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelope.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Wolverine.Pubsub.Internal; - -public class PubsubEnvelope : Envelope { - public readonly string AckId; - - public PubsubEnvelope(string ackId) { - AckId = ackId; - } -} diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs index 458c0d176..38c269ac4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEnvelopeMapper.cs @@ -1,19 +1,16 @@ using Google.Cloud.PubSub.V1; using Google.Protobuf; -using Wolverine.Configuration; +using JasperFx.Core; using Wolverine.Transports; namespace Wolverine.Pubsub.Internal; -internal class PubsubEnvelopeMapper : EnvelopeMapper, IPubsubEnvelopeMapper { - // private const string _wlvrnPrefix = "wlvrn"; - // private static Regex _wlvrnRegex = new Regex($"^{_wlvrnPrefix}\\."); - - public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { +internal class PubsubEnvelopeMapper : EnvelopeMapper, IPubsubEnvelopeMapper { + public PubsubEnvelopeMapper(PubsubEndpoint endpoint) : base(endpoint) { MapProperty( x => x.Id, (e, m) => { - if (!m.Attributes.TryGetValue("id", out var wolverineId)) return; + if (!m.Message.Attributes.TryGetValue("id", out var wolverineId)) return; if (!Guid.TryParse(wolverineId, out var id)) return; e.Id = id; @@ -23,17 +20,21 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.CorrelationId!, (e, m) => { }, - (e, m) => m.OrderingKey = e.GroupId ?? string.Empty + (e, m) => { + if (e.CorrelationId.IsEmpty()) return; + + m.OrderingKey = e.CorrelationId; + } ); MapProperty( e => e.MessageType!, (e, m) => { - if (!m.Attributes.TryGetValue("message-type", out var messageType)) return; + if (!m.Message.Attributes.TryGetValue("message-type", out var messageType)) return; e.MessageType = messageType; }, (e, m) => { - if (e.MessageType is null) return; + if (e.MessageType.IsEmpty()) return; m.Attributes["message-type"] = e.MessageType; } @@ -41,16 +42,20 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.Data!, (e, m) => { - if (m.Data is null) return; + if (m.Message.Data.IsEmpty) return; - e.Data = m.Data.ToByteArray(); + e.Data = m.Message.Data.ToByteArray(); }, - (e, m) => m.Data = ByteString.CopyFrom(e.Data) + (e, m) => { + if (e.Data.IsNullOrEmpty()) return; + + m.Data = ByteString.CopyFrom(e.Data); + } ); MapProperty( e => e.Attempts, (e, m) => { - if (!m.Attributes.TryGetValue("attempts", out var attempts)) return; + if (!m.Message.Attributes.TryGetValue("attempts", out var attempts)) return; if (!int.TryParse(attempts, out var count)) return; e.Attempts = count; @@ -60,12 +65,12 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.ContentType!, (e, m) => { - if (!m.Attributes.TryGetValue("content-type", out var contentType)) return; + if (!m.Message.Attributes.TryGetValue("content-type", out var contentType)) return; e.ContentType = contentType; }, (e, m) => { - if (e.ContentType is null) return; + if (e.ContentType.IsEmpty()) return; m.Attributes["content-type"] = e.ContentType; } @@ -73,7 +78,7 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.Destination!, (e, m) => { - if (!m.Attributes.TryGetValue("destination", out var destination)) return; + if (!m.Message.Attributes.TryGetValue("destination", out var destination)) return; if (!Uri.TryCreate(destination, UriKind.Absolute, out var uri)) return; e.Destination = uri; @@ -87,12 +92,12 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.TenantId!, (e, m) => { - if (!m.Attributes.TryGetValue("tenant-id", out var tenantId)) return; + if (!m.Message.Attributes.TryGetValue("tenant-id", out var tenantId)) return; e.TenantId = tenantId; }, (e, m) => { - if (e.TenantId is null) return; + if (e.TenantId.IsEmpty()) return; m.Attributes["tenant-id"] = e.TenantId; } @@ -100,12 +105,12 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.AcceptedContentTypes, (e, m) => { - if (!m.Attributes.TryGetValue("accepted-content-types", out var acceptedContentTypes)) return; + if (!m.Message.Attributes.TryGetValue("accepted-content-types", out var acceptedContentTypes)) return; e.AcceptedContentTypes = acceptedContentTypes.Split(','); }, (e, m) => { - if (e.AcceptedContentTypes is null) return; + if (e.AcceptedContentTypes.IsNullOrEmpty()) return; m.Attributes["accepted-content-types"] = string.Join(",", e.AcceptedContentTypes.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()); } @@ -113,12 +118,12 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.TopicName!, (e, m) => { - if (!m.Attributes.TryGetValue("topic-name", out var topicName)) return; + if (!m.Message.Attributes.TryGetValue("topic-name", out var topicName)) return; e.TopicName = topicName; }, (e, m) => { - if (e.TopicName is null) return; + if (e.TopicName.IsEmpty()) return; m.Attributes["topic-name"] = e.TopicName; } @@ -126,12 +131,12 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.EndpointName!, (e, m) => { - if (!m.Attributes.TryGetValue("endpoint-name", out var endpointName)) return; + if (!m.Message.Attributes.TryGetValue("endpoint-name", out var endpointName)) return; e.EndpointName = endpointName; }, (e, m) => { - if (e.EndpointName is null) return; + if (e.EndpointName.IsEmpty()) return; m.Attributes["endpoint-name"] = e.EndpointName; } @@ -139,22 +144,25 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.WasPersistedInOutbox, (e, m) => { - if (!m.Attributes.TryGetValue("was-persisted-in-outbox", out var wasPersistedInOutbox)) return; - if (!bool.TryParse(wasPersistedInOutbox, out var wasPersisted)) return; + if (!m.Message.Attributes.Keys.Contains("was-persisted-in-outbox")) return; - e.WasPersistedInOutbox = wasPersisted; + e.WasPersistedInOutbox = true; }, - (e, m) => m.Attributes["was-persisted-in-outbox"] = e.WasPersistedInOutbox.ToString() + (e, m) => { + if (!e.WasPersistedInOutbox) return; + + m.Attributes["was-persisted-in-outbox"] = string.Empty; + } ); MapProperty( e => e.GroupId!, (e, m) => { - if (!m.Attributes.TryGetValue("group-id", out var groupId)) return; + if (!m.Message.Attributes.TryGetValue("group-id", out var groupId)) return; e.GroupId = groupId; }, (e, m) => { - if (e.GroupId is null) return; + if (e.GroupId.IsEmpty()) return; m.Attributes["group-id"] = e.GroupId; } @@ -162,12 +170,12 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.DeduplicationId!, (e, m) => { - if (!m.Attributes.TryGetValue("deduplication-id", out var deduplicationId)) return; + if (!m.Message.Attributes.TryGetValue("deduplication-id", out var deduplicationId)) return; e.DeduplicationId = deduplicationId; }, (e, m) => { - if (e.DeduplicationId is null) return; + if (e.DeduplicationId.IsEmpty()) return; m.Attributes["deduplication-id"] = e.DeduplicationId; } @@ -175,30 +183,42 @@ public PubsubEnvelopeMapper(Endpoint endpoint) : base(endpoint) { MapProperty( e => e.PartitionKey!, (e, m) => { - if (!m.Attributes.TryGetValue("partition-key", out var partitionKey)) return; + if (!m.Message.Attributes.TryGetValue("partition-key", out var partitionKey)) return; e.PartitionKey = partitionKey; }, (e, m) => { - if (e.PartitionKey is null) return; + if (e.PartitionKey.IsEmpty()) return; m.Attributes["partition-key"] = e.PartitionKey; } ); } + public void MapIncomingToEnvelope(PubsubEnvelope envelope, ReceivedMessage incoming) { + envelope.AckId = incoming.AckId; + + base.MapIncomingToEnvelope(envelope, incoming); + } + + public void MapOutgoingToMessage(OutgoingMessageBatch outgoing, PubsubMessage message) { + message.Data = ByteString.CopyFrom(outgoing.Data); + message.Attributes["destination"] = outgoing.Destination.ToString(); + message.Attributes["batched"] = string.Empty; + } + protected override void writeOutgoingHeader(PubsubMessage outgoing, string key, string value) { outgoing.Attributes[key] = value; } - protected override void writeIncomingHeaders(PubsubMessage incoming, Envelope envelope) { - if (incoming.Attributes is null) return; + protected override void writeIncomingHeaders(ReceivedMessage incoming, Envelope envelope) { + if (incoming.Message.Attributes is null) return; - foreach (var pair in incoming.Attributes) envelope.Headers[pair.Key] = pair.Value?.ToString(); + foreach (var pair in incoming.Message.Attributes) envelope.Headers[pair.Key] = pair.Value?.ToString(); } - protected override bool tryReadIncomingHeader(PubsubMessage incoming, string key, out string? value) { - if (incoming.Attributes.TryGetValue(key, out var header)) { + protected override bool tryReadIncomingHeader(ReceivedMessage incoming, string key, out string? value) { + if (incoming.Message.Attributes.TryGetValue(key, out var header)) { value = header.ToString(); return true; diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs index 4678de48a..e6e7c6fc8 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -42,7 +42,7 @@ IWolverineRuntime runtime _runtime = runtime; _logger = runtime.LoggerFactory.CreateLogger(); - if (_endpoint.DeadLetterName.IsNotEmpty() && transport.EnableDeadLettering) { + if (_endpoint.DeadLetterName.IsNotEmpty()) { _deadLetterTopic = _transport.Topics[_endpoint.DeadLetterName]; NativeDeadLetterQueueEnabled = true; @@ -180,9 +180,9 @@ protected async Task handleMessagesAsync(RepeatedField messages } try { - var envelope = new PubsubEnvelope(message.AckId); + var envelope = new PubsubEnvelope(); - _endpoint.Mapper.MapIncomingToEnvelope(envelope, message.Message); + _endpoint.Mapper.MapIncomingToEnvelope(envelope, message); if (envelope.IsPing()) { try { diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs index 9b316852c..964c8e143 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubSenderProtocol.cs @@ -1,5 +1,4 @@ using Google.Cloud.PubSub.V1; -using Google.Protobuf; using Microsoft.Extensions.Logging; using Wolverine.Runtime; using Wolverine.Transports; @@ -26,12 +25,9 @@ public async Task SendBatchAsync(ISenderCallback callback, OutgoingMessageBatch await _endpoint.InitializeAsync(_logger); try { - var message = new PubsubMessage { - Data = ByteString.CopyFrom(batch.Data) - }; - - message.Attributes["destination"] = batch.Destination.ToString(); - message.Attributes["batched"] = string.Empty; + var message = new PubsubMessage(); + + _endpoint.Mapper.MapOutgoingToMessage(batch, message); await _client.PublishAsync(new() { TopicAsTopicName = _endpoint.Server.Topic.Name, diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs index 64500ea77..64ed9f77a 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs @@ -1,5 +1,4 @@ using Google.Api.Gax; -using Wolverine.Pubsub.Internal; using Wolverine.Transports; namespace Wolverine.Pubsub; @@ -48,24 +47,26 @@ public PubsubConfiguration UseConventionalRouting( } /// - /// Is Wolverine enabled to create system endpoints automatically for responses and retries? This - /// should probably be set to false if the application does not have permissions to create topcis and subscriptions + /// Enable WOlverine to create system endpoints automatically for responses and retries. This + /// should probably be set if the application does have permissions to create topcis and subscriptions /// - /// /// - public PubsubConfiguration SystemEndpointsAreEnabled(bool enabled) { - Transport.SystemEndpointsEnabled = enabled; + public PubsubConfiguration EnableSystemEndpoints() { + Transport.SystemEndpointsEnabled = true; return this; } /// - /// Globally enable all native dead lettering with Google Cloud Pub/Sub within this entire + /// Enable dead lettering with Google Cloud Pub/Sub within this entire /// application /// + /// /// - public PubsubConfiguration EnableAllNativeDeadLettering() { - Transport.EnableDeadLettering = true; + public PubsubConfiguration EnableDeadLettering(Action? configure = null) { + Transport.DeadLetter.Enabled = true; + + configure?.Invoke(Transport.DeadLetter); return this; } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs similarity index 94% rename from src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs rename to src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs index 57c7af3bf..32b6abb53 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs @@ -5,26 +5,28 @@ using JasperFx.Core; using Microsoft.Extensions.Logging; using Wolverine.Configuration; +using Wolverine.Pubsub.Internal; using Wolverine.Runtime; using Wolverine.Transports; using Wolverine.Transports.Sending; -namespace Wolverine.Pubsub.Internal; +namespace Wolverine.Pubsub; public class PubsubEndpoint : Endpoint, IBrokerQueue { private IPubsubEnvelopeMapper? _mapper; private readonly PubsubTransport _transport; private bool _hasInitialized = false; + + internal bool IsDeadLetter = false; public PubsubServerOptions Server = new(); public PubsubClientOptions Client = new(); - public bool IsDeadLetter = false; /// /// Name of the dead letter for this Google Cloud Pub/Sub subcription where failed messages will be moved /// - public string? DeadLetterName = PubsubTransport.DeadLetterName; + public string? DeadLetterName = null; /// /// Pluggable strategy for interoperability with non-Wolverine systems. Customizes how the incoming Google Cloud Pub/Sub messages @@ -64,6 +66,8 @@ public PubsubEndpoint( : topicName ); EndpointName = topicName; + + if (transport.DeadLetter.Enabled) DeadLetterName = PubsubTransport.DeadLetterName; } public override async ValueTask InitializeAsync(ILogger logger) { @@ -179,7 +183,7 @@ public override ValueTask BuildListenerAsync(IWolverineRuntime runtim } public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISender? deadLetterSender) { - if (DeadLetterName.IsNotEmpty() && _transport.EnableDeadLettering) { + if (DeadLetterName.IsNotEmpty()) { var dl = _transport.Topics[DeadLetterName]; deadLetterSender = new InlinePubsubSender(dl, runtime); @@ -194,8 +198,6 @@ public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISe public ValueTask> GetAttributesAsync() => ValueTask.FromResult(new Dictionary()); - // public ValueTask PurgeAsync(ILogger logger) => ValueTask.CompletedTask; - public async ValueTask PurgeAsync(ILogger logger) { if (_transport.SubscriberApiClient is null || !IsListener) return; @@ -248,13 +250,19 @@ await _transport.PublisherApiClient.PublishAsync(new() { internal void ConfigureDeadLetter(Action configure) { if (DeadLetterName.IsEmpty()) return; + var initialized = !_transport.Topics.Contains(DeadLetterName); var dl = _transport.Topics[DeadLetterName]; + if (initialized) { + dl.Server.Topic.Options = _transport.DeadLetter.Topic; + dl.Server.Subscription.Options = _transport.DeadLetter.Subscription; + } + + configure(dl); + dl.DeadLetterName = null; dl.Server.Subscription.Options.DeadLetterPolicy = null; dl.IsDeadLetter = true; - - configure(dl); } protected override ISender CreateSender(IWolverineRuntime runtime) { diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubEnvelope.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubEnvelope.cs new file mode 100644 index 000000000..fdf379b4f --- /dev/null +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubEnvelope.cs @@ -0,0 +1,5 @@ +namespace Wolverine.Pubsub; + +public class PubsubEnvelope : Envelope { + internal string AckId = string.Empty; +} diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs index d578bfac2..bc6aa3dcf 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubOptions.cs @@ -47,3 +47,9 @@ public class PubsubRetryPolicy { public int MaxRetryCount = 5; public int RetryDelay = 1000; } + +public class PubsubDeadLetterOptions { + public bool Enabled = false; + public CreateTopicOptions Topic = new(); + public CreateSubscriptionOptions Subscription = new(); +} \ No newline at end of file diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs index df3baee18..1731fcacb 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs @@ -1,4 +1,3 @@ -using Wolverine.Pubsub.Internal; using Wolverine.Configuration; using Wolverine.ErrorHandling; @@ -23,24 +22,35 @@ public PubsubTopicListenerConfiguration CircuitBreaker(Action - /// Configure the underlying Google Cloud Pub/Sub topic and subscription. This is only applicable when - /// Wolverine is creating the topic and subscription + /// Configure the underlying Google Cloud Pub/Sub topic. This is only applicable when + /// Wolverine is creating the topic. /// /// /// - public PubsubTopicListenerConfiguration ConfigureServer(Action configure) { - add(e => configure(e.Server)); + public PubsubTopicListenerConfiguration ConfigurePubsubTopic(Action configure) { + add(e => configure(e.Server.Topic.Options)); return this; } /// - /// Configure the underlying Google Cloud Pub/Sub subscriber client. This is only applicable when - /// Wolverine is creating the subscriber client + /// Configure the underlying Google Cloud Pub/Sub subscription. This is only applicable when + /// Wolverine is creating the subscription. /// /// /// - public PubsubTopicListenerConfiguration ConfigureClient(Action configure) { + public PubsubTopicListenerConfiguration ConfigurePubsubSubscription(Action configure) { + add(e => configure(e.Server.Subscription.Options)); + + return this; + } + + /// + /// Configure the underlying Google Cloud Pub/Sub subscriber. + /// + /// + /// + public PubsubTopicListenerConfiguration ConfigureListener(Action configure) { add(e => configure(e.Client)); return this; @@ -63,7 +73,7 @@ public PubsubTopicListenerConfiguration DisableDeadLettering() { /// Customize the dead lettering for just this endpoint /// /// - /// Optionally configure properties of the dead lettering itself + /// Optionally configure properties of the dead letter itself /// /// public PubsubTopicListenerConfiguration ConfigureDeadLettering( diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs index 50748b884..9fcb82509 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs @@ -1,4 +1,3 @@ -using Wolverine.Pubsub.Internal; using Wolverine.Configuration; namespace Wolverine.Pubsub; @@ -7,58 +6,13 @@ public class PubsubTopicSubscriberConfiguration : SubscriberConfiguration - /// Configure the underlying Google Cloud Pub/Sub topic and subscription. This is only applicable when - /// Wolverine is creating the topic and subscription + /// Configure the underlying Google Cloud Pub/Sub topic. This is only applicable when + /// Wolverine is creating the topic. /// /// /// - public PubsubTopicSubscriberConfiguration ConfigureServer(Action configure) { - add(e => configure(e.Server)); - - return this; - } - - /// - /// Configure the underlying Google Cloud Pub/Sub subscriber client. This is only applicable when - /// Wolverine is creating the subscriber client - /// - /// - /// - public PubsubTopicSubscriberConfiguration ConfigureClient(Action configure) { - add(e => configure(e.Client)); - - return this; - } - - /// - /// Completely disable all Google Cloud Pub/Sub dead lettering for just this endpoint - /// - /// - public PubsubTopicSubscriberConfiguration DisableDeadLettering() { - add(e => { - e.DeadLetterName = null; - e.Server.Subscription.Options.DeadLetterPolicy = null; - }); - - return this; - } - - /// - /// Customize the dead lettering for just this endpoint - /// - /// - /// Optionally configure properties of the dead lettering itself - /// - /// - public PubsubTopicSubscriberConfiguration ConfigureDeadLettering( - string deadLetterName, - Action? configure = null - ) { - add(e => { - e.DeadLetterName = deadLetterName; - - if (configure is not null) e.ConfigureDeadLetter(configure); - }); + public PubsubTopicSubscriberConfiguration ConfigurePubsubTopic(Action configure) { + add(e => configure(e.Server.Topic.Options)); return this; } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index 2606206c5..f1ade047a 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -1,5 +1,4 @@ using Wolverine.Transports; -using Wolverine.Pubsub.Internal; using Wolverine.Runtime; using Google.Cloud.PubSub.V1; using JasperFx.Core; @@ -11,22 +10,21 @@ namespace Wolverine.Pubsub; public class PubsubTransport : BrokerTransport, IAsyncDisposable { + internal static Regex NameRegex = new("^(?!goog)[A-Za-z][A-Za-z0-9\\-_.~+%]{2,254}$"); + public const string ProtocolName = "pubsub"; public const string ResponseName = "wlvrn.responses"; public const string DeadLetterName = "wlvrn.dead-letter"; - public static Regex NameRegex = new("^(?!goog)[A-Za-z][A-Za-z0-9\\-_.~+%]{2,254}$"); - internal int AssignedNodeNumber = 0; internal PublisherServiceApiClient? PublisherApiClient = null; internal SubscriberServiceApiClient? SubscriberApiClient = null; - public readonly LightweightCache Topics; public string ProjectId = string.Empty; public EmulatorDetection EmulatorDetection = EmulatorDetection.None; - public bool EnableDeadLettering = false; + public PubsubDeadLetterOptions DeadLetter = new(); /// /// Is this transport connection allowed to build and use response topic and subscription @@ -75,16 +73,18 @@ public override IEnumerable DiagnosticColumns() { protected override IEnumerable explicitEndpoints() => Topics; protected override IEnumerable endpoints() { - if (EnableDeadLettering) { - var dlNames = Topics.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); + var dlNames = Topics.Select(x => x.DeadLetterName).Where(x => x.IsNotEmpty()).Distinct().ToArray(); + + foreach (var dlName in dlNames) { + if (dlName.IsEmpty()) continue; - foreach (var dlName in dlNames) { - var dl = Topics[dlName!]; + var dl = Topics[dlName]; - dl.DeadLetterName = null; - dl.Server.Subscription.Options.DeadLetterPolicy = null; - dl.IsDeadLetter = true; - } + dl.DeadLetterName = null; + dl.Server.Subscription.Options.DeadLetterPolicy = null; + dl.IsDeadLetter = true; + dl.Server.Topic.Options = DeadLetter.Topic; + dl.Server.Subscription.Options = DeadLetter.Subscription; } return Topics; diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs index d1c59df94..d018f204d 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs @@ -1,6 +1,5 @@ using JasperFx.Core.Reflection; using Wolverine.Configuration; -using Wolverine.Pubsub.Internal; namespace Wolverine.Pubsub; @@ -49,7 +48,7 @@ public static PubsubConfiguration UsePubsub(this WolverineOptions endpoints, str /// /// The name of the Google Cloud Pub/Sub topic /// - /// Optional configuration for this Google Cloud Pub/Sub endpoint if being initialized by Wolverine. + /// Optional configuration for this Google Cloud Pub/Sub endpoint. /// /// public static PubsubTopicListenerConfiguration ListenToPubsubTopic( @@ -73,6 +72,9 @@ public static PubsubTopicListenerConfiguration ListenToPubsubTopic( /// /// /// + /// + /// Optional configuration for this Google Cloud Pub/Sub endpoint. + /// /// public static PubsubTopicSubscriberConfiguration ToPubsubTopic( this IPublishToExpression publishing, From a4659d6ee6cfd06b5d4d494bfd147bf65b1d45dd Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:06:52 +0200 Subject: [PATCH 11/16] Minor clean-ups --- .../Internal/PubsubEndpointTests.cs | 4 ++-- .../Wolverine.Pubsub/IPubsubEnvelopeMapper.cs | 2 +- .../Internal/BatchedPubsubListener.cs | 2 +- .../Internal/PubsubListener.cs | 6 ++--- .../Wolverine.Pubsub/PubsubConfiguration.cs | 4 ++-- .../GCP/Wolverine.Pubsub/PubsubEndpoint.cs | 22 +++++++++---------- .../PubsubTopicListenerConfiguration.cs | 12 +++++----- .../PubsubTopicSubscriberConfiguration.cs | 6 ++--- .../GCP/Wolverine.Pubsub/PubsubTransport.cs | 4 ++-- .../PubsubTransportExtensions.cs | 16 +++++++------- .../Wolverine.Pubsub/Wolverine.Pubsub.csproj | 2 +- ...erinePubsubInvalidEndpointNameException.cs | 2 +- ...inePubsubTransportNotConnectedException.cs | 2 +- 13 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs index 034a2747e..04ceabda1 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs @@ -17,9 +17,9 @@ public class PubsubEndpointTests { }; [Fact] - public void default_dead_letter_name_is_transport_default() { + public void default_dead_letter_name_is_null() { new PubsubEndpoint("foo", createTransport()) - .DeadLetterName.ShouldBe(PubsubTransport.DeadLetterName); + .DeadLetterName.ShouldBeNull(); } [Fact] diff --git a/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs b/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs index a6b2017f7..dda3b3530 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/IPubsubEnvelopeMapper.cs @@ -4,7 +4,7 @@ namespace Wolverine.Pubsub; /// -/// Pluggable strategy for reading and writing data to Google Cloud Pub/Sub +/// Pluggable strategy for reading and writing data to Google Cloud Platform Pub/Sub /// public interface IPubsubEnvelopeMapper : IEnvelopeMapper { void MapIncomingToEnvelope(PubsubEnvelope envelope, ReceivedMessage incoming); diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs index f2e180f9c..c03cca0d5 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/BatchedPubsubListener.cs @@ -43,7 +43,7 @@ await listenForMessagesAsync(async () => { await streamingPull.WriteCompleteAsync(); } catch (Exception ex) { - _logger.LogError(ex, "{Uri}: Error while completing the Google Cloud Pub/Sub streaming pull.", _endpoint.Uri); + _logger.LogError(ex, "{Uri}: Error while completing the Google Cloud Platform Pub/Sub streaming pull.", _endpoint.Uri); } } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs index e6e7c6fc8..6082748f7 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -152,7 +152,7 @@ protected async Task listenForMessagesAsync(Func listenAsync) { _logger.LogError( ex, - "{Uri}: Error while trying to retrieve messages from Google Cloud Pub/Sub, attempting to restart stream ({RetryCount}/{MaxRetryCount})...", + "{Uri}: Error while trying to retrieve messages from Google Cloud Platform Pub/Sub, attempting to restart stream ({RetryCount}/{MaxRetryCount})...", _endpoint.Uri, retryCount, _endpoint.Client.RetryPolicy.MaxRetryCount @@ -189,7 +189,7 @@ protected async Task handleMessagesAsync(RepeatedField messages await _complete.PostAsync([envelope]); } catch (Exception ex) { - _logger.LogError(ex, "{Uri}: Error while acknowledging Google Cloud Pub/Sub ping message \"{AckId}\".", _endpoint.Uri, message.AckId); + _logger.LogError(ex, "{Uri}: Error while acknowledging Google Cloud Platform Pub/Sub ping message \"{AckId}\".", _endpoint.Uri, message.AckId); } continue; @@ -198,7 +198,7 @@ protected async Task handleMessagesAsync(RepeatedField messages envelopes.Add(envelope); } catch (Exception ex) { - _logger.LogError(ex, "{Uri}: Error while mapping Google Cloud Pub/Sub message {AckId}.", _endpoint.Uri, message.AckId); + _logger.LogError(ex, "{Uri}: Error while mapping Google Cloud Platform Pub/Sub message {AckId}.", _endpoint.Uri, message.AckId); } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs index 64ed9f77a..4937556a5 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubConfiguration.cs @@ -14,7 +14,7 @@ public class PubsubConfiguration : BrokerExpression< public PubsubConfiguration(PubsubTransport transport, WolverineOptions options) : base(transport, options) { } /// - /// Set emulator detection for the Google Cloud Pub/Sub transport + /// Set emulator detection for the Google Cloud Platform Pub/Sub transport /// /// /// Remember to set the environment variable `PUBSUB_EMULATOR_HOST` to the emulator's host and port @@ -58,7 +58,7 @@ public PubsubConfiguration EnableSystemEndpoints() { } /// - /// Enable dead lettering with Google Cloud Pub/Sub within this entire + /// Enable dead lettering with Google Cloud Platform Pub/Sub within this entire /// application /// /// diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs index 32b6abb53..d9074864f 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs @@ -24,13 +24,13 @@ public class PubsubEndpoint : Endpoint, IBrokerQueue { public PubsubClientOptions Client = new(); /// - /// Name of the dead letter for this Google Cloud Pub/Sub subcription where failed messages will be moved + /// Name of the dead letter for this Google Cloud Platform Pub/Sub subcription where failed messages will be moved /// public string? DeadLetterName = null; /// - /// Pluggable strategy for interoperability with non-Wolverine systems. Customizes how the incoming Google Cloud Pub/Sub messages - /// are read and how outgoing messages are written to Google Cloud Pub/Sub. + /// Pluggable strategy for interoperability with non-Wolverine systems. Customizes how the incoming Google Cloud Platform Pub/Sub messages + /// are read and how outgoing messages are written to Google Cloud Platform Pub/Sub. /// public IPubsubEnvelopeMapper Mapper { get { @@ -78,7 +78,7 @@ public override async ValueTask InitializeAsync(ILogger logger) { if (_transport.AutoPurgeAllQueues) await PurgeAsync(logger); } catch (Exception ex) { - throw new WolverinePubsubTransportException($"{Uri}: Error trying to initialize Google Cloud Pub/Sub endpoint", ex); + throw new WolverinePubsubTransportException($"{Uri}: Error trying to initialize Google Cloud Platform Pub/Sub endpoint", ex); } _hasInitialized = true; @@ -95,15 +95,15 @@ await _transport.PublisherApiClient.CreateTopicAsync(new Topic { } catch (RpcException ex) { if (ex.StatusCode != StatusCode.AlreadyExists) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Uri, Server.Topic.Name); + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Platform Pub/Sub topic \"{Topic}\"", Uri, Server.Topic.Name); throw; } - logger.LogInformation("{Uri}: Google Cloud Pub/Sub topic \"{Topic}\" already exists", Uri, Server.Topic.Name); + logger.LogInformation("{Uri}: Google Cloud Platform Pub/Sub topic \"{Topic}\" already exists", Uri, Server.Topic.Name); } catch (Exception ex) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub topic \"{Topic}\"", Uri, Server.Topic.Name); + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Platform Pub/Sub topic \"{Topic}\"", Uri, Server.Topic.Name); throw; } @@ -134,15 +134,15 @@ await _transport.PublisherApiClient.CreateTopicAsync(new Topic { } catch (RpcException ex) { if (ex.StatusCode != StatusCode.AlreadyExists) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Platform Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); throw; } - logger.LogInformation("{Uri}: Google Cloud Pub/Sub subscription \"{Subscription}\" already exists", Uri, Server.Subscription.Name); + logger.LogInformation("{Uri}: Google Cloud Platform Pub/Sub subscription \"{Subscription}\" already exists", Uri, Server.Subscription.Name); } catch (Exception ex) { - logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); + logger.LogError(ex, "{Uri}: Error trying to initialize Google Cloud Platform Pub/Sub subscription \"{Subscription}\" to topic \"{Topic}\"", Uri, Server.Subscription.Name, Server.Topic.Name); throw; } @@ -218,7 +218,7 @@ await _transport.SubscriberApiClient.AcknowledgeAsync( catch (Exception ex) { logger.LogDebug( ex, - "{Uri}: Error trying to purge Google Cloud Pub/Sub subscription {Subscription}", + "{Uri}: Error trying to purge Google Cloud Platform Pub/Sub subscription {Subscription}", Uri, Server.Subscription.Name ); diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs index 1731fcacb..cc07ba3ce 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicListenerConfiguration.cs @@ -22,7 +22,7 @@ public PubsubTopicListenerConfiguration CircuitBreaker(Action - /// Configure the underlying Google Cloud Pub/Sub topic. This is only applicable when + /// Configure the underlying Google Cloud Platform Pub/Sub topic. This is only applicable when /// Wolverine is creating the topic. /// /// @@ -34,7 +34,7 @@ public PubsubTopicListenerConfiguration ConfigurePubsubTopic(Action - /// Configure the underlying Google Cloud Pub/Sub subscription. This is only applicable when + /// Configure the underlying Google Cloud Platform Pub/Sub subscription. This is only applicable when /// Wolverine is creating the subscription. /// /// @@ -46,7 +46,7 @@ public PubsubTopicListenerConfiguration ConfigurePubsubSubscription(Action - /// Configure the underlying Google Cloud Pub/Sub subscriber. + /// Configure the underlying Google Cloud Platform Pub/Sub subscriber. /// /// /// @@ -57,7 +57,7 @@ public PubsubTopicListenerConfiguration ConfigureListener(Action - /// Completely disable all Google Cloud Pub/Sub dead lettering for just this endpoint + /// Completely disable all Google Cloud Platform Pub/Sub dead lettering for just this endpoint /// /// public PubsubTopicListenerConfiguration DisableDeadLettering() { @@ -90,7 +90,7 @@ public PubsubTopicListenerConfiguration ConfigureDeadLettering( } /// - /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// Utilize custom envelope mapping for Google Cloud Platform Pub/Sub interoperability with external non-Wolverine systems /// /// /// @@ -101,7 +101,7 @@ public PubsubTopicListenerConfiguration InteropWith(IPubsubEnvelopeMapper mapper } /// - /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// Utilize custom envelope mapping for Google Cloud Platform Pub/Sub interoperability with external non-Wolverine systems /// /// /// diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs index 9fcb82509..9e64d98a4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTopicSubscriberConfiguration.cs @@ -6,7 +6,7 @@ public class PubsubTopicSubscriberConfiguration : SubscriberConfiguration - /// Configure the underlying Google Cloud Pub/Sub topic. This is only applicable when + /// Configure the underlying Google Cloud Platform Pub/Sub topic. This is only applicable when /// Wolverine is creating the topic. /// /// @@ -18,7 +18,7 @@ public PubsubTopicSubscriberConfiguration ConfigurePubsubTopic(Action - /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// Utilize custom envelope mapping for Google Cloud Platform Pub/Sub interoperability with external non-Wolverine systems /// /// /// @@ -29,7 +29,7 @@ public PubsubTopicSubscriberConfiguration InteropWith(IPubsubEnvelopeMapper mapp } /// - /// Utilize custom envelope mapping for Google Cloud Pub/Sub interoperability with external non-Wolverine systems + /// Utilize custom envelope mapping for Google Cloud Platform Pub/Sub interoperability with external non-Wolverine systems /// /// /// diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs index f1ade047a..49d732469 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs @@ -32,7 +32,7 @@ public class PubsubTransport : BrokerTransport, IAsyncDisposable /// public bool SystemEndpointsEnabled = false; - public PubsubTransport() : base(ProtocolName, "Google Cloud Pub/Sub") { + public PubsubTransport() : base(ProtocolName, "Google Cloud Platform Pub/Sub") { IdentifierDelimiter = "."; Topics = new(name => new(name, this)); } @@ -49,7 +49,7 @@ public override async ValueTask ConnectAsync(IWolverineRuntime runtime) { EmulatorDetection = EmulatorDetection, }; - if (string.IsNullOrWhiteSpace(ProjectId)) throw new InvalidOperationException("Google Cloud Pub/Sub project id must be set before connecting"); + if (string.IsNullOrWhiteSpace(ProjectId)) throw new InvalidOperationException("Google Cloud Platform Pub/Sub project id must be set before connecting"); AssignedNodeNumber = runtime.DurabilitySettings.AssignedNodeNumber; PublisherApiClient = await pubBuilder.BuildAsync(); diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs index d018f204d..51834664c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubTransportExtensions.cs @@ -6,7 +6,7 @@ namespace Wolverine.Pubsub; public static class PubsubTransportExtensions { /// - /// Quick access to the Google Cloud Pub/Sub Transport within this application. + /// Quick access to the Google Cloud Platform Pub/Sub Transport within this application. /// This is for advanced usage. /// /// @@ -18,14 +18,14 @@ internal static PubsubTransport PubsubTransport(this WolverineOptions endpoints) } /// - /// Additive configuration to the Google Cloud Pub/Sub integration for this Wolverine application. + /// Additive configuration to the Google Cloud Platform Pub/Sub integration for this Wolverine application. /// /// /// public static PubsubConfiguration ConfigurePubsub(this WolverineOptions endpoints) => new PubsubConfiguration(endpoints.PubsubTransport(), endpoints); /// - /// Connect to Google Cloud Pub/Sub with a project id. + /// Connect to Google Cloud Platform Pub/Sub with a project id. /// /// /// @@ -43,12 +43,12 @@ public static PubsubConfiguration UsePubsub(this WolverineOptions endpoints, str } /// - /// Listen for incoming messages at the designated Google Cloud Pub/Sub topic by name. + /// Listen for incoming messages at the designated Google Cloud Platform Pub/Sub topic by name. /// /// - /// The name of the Google Cloud Pub/Sub topic + /// The name of the Google Cloud Platform Pub/Sub topic /// - /// Optional configuration for this Google Cloud Pub/Sub endpoint. + /// Optional configuration for this Google Cloud Platform Pub/Sub endpoint. /// /// public static PubsubTopicListenerConfiguration ListenToPubsubTopic( @@ -68,12 +68,12 @@ public static PubsubTopicListenerConfiguration ListenToPubsubTopic( } /// - /// Publish the designated messages to a Google Cloud Pub/Sub topic. + /// Publish the designated messages to a Google Cloud Platform Pub/Sub topic. /// /// /// /// - /// Optional configuration for this Google Cloud Pub/Sub endpoint. + /// Optional configuration for this Google Cloud Platform Pub/Sub endpoint. /// /// public static PubsubTopicSubscriberConfiguration ToPubsubTopic( diff --git a/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj b/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj index 5dcace3a7..97416b803 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj +++ b/src/Transports/GCP/Wolverine.Pubsub/Wolverine.Pubsub.csproj @@ -2,7 +2,7 @@ WolverineFx.Pubsub - Google Cloud Pub/Sub transport for Wolverine applications + Google Cloud Platform Pub/Sub transport for Wolverine applications diff --git a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs index 308ff3b4c..8e65f31e2 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubInvalidEndpointNameException.cs @@ -1,5 +1,5 @@ namespace Wolverine.Pubsub; public class WolverinePubsubInvalidEndpointNameException : Exception { - public WolverinePubsubInvalidEndpointNameException(string topicName, string? message = null, Exception? innerException = null) : base(message ?? $"Google Cloud Pub/Sub endpoint name \"{topicName}\" is invalid.", innerException) { } + public WolverinePubsubInvalidEndpointNameException(string topicName, string? message = null, Exception? innerException = null) : base(message ?? $"Google Cloud Platform Pub/Sub endpoint name \"{topicName}\" is invalid.", innerException) { } } diff --git a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs index 9136d4f68..1b91d2701 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/WolverinePubsubTransportNotConnectedException.cs @@ -1,5 +1,5 @@ namespace Wolverine.Pubsub; public class WolverinePubsubTransportNotConnectedException : Exception { - public WolverinePubsubTransportNotConnectedException(string message = "Google Cloud Pub/Sub transport has not been connected", Exception? innerException = null) : base(message, innerException) { } + public WolverinePubsubTransportNotConnectedException(string message = "Google Cloud Platform Pub/Sub transport has not been connected", Exception? innerException = null) : base(message, innerException) { } } From f2e9d026f2dd28ffd8a4b7671127b5b1c6dfdc33 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:40:53 +0200 Subject: [PATCH 12/16] Test renamed --- .../when_discovering_a_sender_with_all_defaults.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs index a174b2256..6ed5bf6ed 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs @@ -19,7 +19,7 @@ public void should_have_exactly_one_route() { } [Fact] - public void routed_to_azure_service_bus_queue() { + public void routed_to_pubsub_endpoint() { var endpoint = theRoute.Sender.Endpoint.ShouldBeOfType(); endpoint.Server.Topic.Name.TopicId.ShouldBe("published-message"); From 037981392dabe7b6070d9f440ecdd11d17b41ea8 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:12:37 +0200 Subject: [PATCH 13/16] Minor dead letter fix --- .../Internal/InlinePubsubListener.cs | 20 ++++++++----------- .../GCP/Wolverine.Pubsub/PubsubEndpoint.cs | 14 +++++++++++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs index 0208e1548..efe89bbd9 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/InlinePubsubListener.cs @@ -11,19 +11,15 @@ public InlinePubsubListener( IWolverineRuntime runtime ) : base(endpoint, transport, receiver, runtime) { } - public override async Task StartAsync() { + public override async Task StartAsync() => await listenForMessagesAsync(async () => { if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); - await listenForMessagesAsync(async () => { - if (_transport.SubscriberApiClient is null) throw new WolverinePubsubTransportNotConnectedException(); + var response = await _transport.SubscriberApiClient.PullAsync( + _endpoint.Server.Subscription.Name, + maxMessages: 1, + _cancellation.Token + ); - var response = await _transport.SubscriberApiClient.PullAsync( - _endpoint.Server.Subscription.Name, - maxMessages: 1, - _cancellation.Token - ); - - await handleMessagesAsync(response.ReceivedMessages); - }); - } + await handleMessagesAsync(response.ReceivedMessages); + }); } diff --git a/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs b/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs index d9074864f..41a56e8c3 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/PubsubEndpoint.cs @@ -184,8 +184,18 @@ public override ValueTask BuildListenerAsync(IWolverineRuntime runtim public override bool TryBuildDeadLetterSender(IWolverineRuntime runtime, out ISender? deadLetterSender) { if (DeadLetterName.IsNotEmpty()) { + var initialized = _transport.Topics.Contains(DeadLetterName); var dl = _transport.Topics[DeadLetterName]; + if (!initialized) { + dl.Server.Topic.Options = _transport.DeadLetter.Topic; + dl.Server.Subscription.Options = _transport.DeadLetter.Subscription; + } + + dl.DeadLetterName = null; + dl.Server.Subscription.Options.DeadLetterPolicy = null; + dl.IsDeadLetter = true; + deadLetterSender = new InlinePubsubSender(dl, runtime); return true; @@ -250,10 +260,10 @@ await _transport.PublisherApiClient.PublishAsync(new() { internal void ConfigureDeadLetter(Action configure) { if (DeadLetterName.IsEmpty()) return; - var initialized = !_transport.Topics.Contains(DeadLetterName); + var initialized = _transport.Topics.Contains(DeadLetterName); var dl = _transport.Topics[DeadLetterName]; - if (initialized) { + if (!initialized) { dl.Server.Topic.Options = _transport.DeadLetter.Topic; dl.Server.Subscription.Options = _transport.DeadLetter.Subscription; } From 72a07fe04e07344ac2b4fa53f06f613fac50d124 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:25:55 +0200 Subject: [PATCH 14/16] Moved PubsubEndpointTests.cs out from internal tests. Removed empty folders. --- .../{Internal => }/PubsubEndpointTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Transports/GCP/Wolverine.Pubsub.Tests/{Internal => }/PubsubEndpointTests.cs (98%) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubEndpointTests.cs similarity index 98% rename from src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs rename to src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubEndpointTests.cs index 04ceabda1..98d59d82c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/Internal/PubsubEndpointTests.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/PubsubEndpointTests.cs @@ -7,7 +7,7 @@ using Wolverine.Configuration; using Xunit; -namespace Wolverine.Pubsub.Tests.Internal; +namespace Wolverine.Pubsub.Tests; public class PubsubEndpointTests { private PubsubTransport createTransport() => new("wolverine") { From 4051cd2bd0b3a0aa9502fe47c2f39159658f6300 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:26:27 +0200 Subject: [PATCH 15/16] Minor change in test --- .../DurableSendingAndReceivingCompliance.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs index 3f95c78e8..6a5958cf4 100644 --- a/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs +++ b/src/Transports/GCP/Wolverine.Pubsub.Tests/DurableSendingAndReceivingCompliance.cs @@ -27,7 +27,7 @@ await SenderIs(opts => { .EnableDeadLettering() .EnableSystemEndpoints() .ConfigureListeners(x => x.UseDurableInbox()) - .ConfigureListeners(x => x.UseDurableInbox()); + .ConfigureSenders(x => x.UseDurableOutbox()); opts.Services .AddMarten(store => { @@ -47,7 +47,7 @@ await ReceiverIs(opts => { .EnableDeadLettering() .EnableSystemEndpoints() .ConfigureListeners(x => x.UseDurableInbox()) - .ConfigureListeners(x => x.UseDurableInbox()); + .ConfigureSenders(x => x.UseDurableOutbox()); opts.Services.AddMarten(store => { store.Connection(Servers.PostgresConnectionString); From b0ea38545c45b990b006e29be9f1bb12f99763a2 Mon Sep 17 00:00:00 2001 From: jay-zahiri <11631617+jay-zahiri@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:30:29 +0200 Subject: [PATCH 16/16] Fixed a GCP Pub/Sub bug --- .../GCP/Wolverine.Pubsub/Internal/PubsubListener.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs index 6082748f7..de4e5591c 100644 --- a/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs +++ b/src/Transports/GCP/Wolverine.Pubsub/Internal/PubsubListener.cs @@ -136,6 +136,17 @@ protected async Task listenForMessagesAsync(Func listenAsync) { break; } + // This is a know issue at the moment: + // https://github.com/googleapis/google-cloud-java/issues/4220 + // https://stackoverflow.com/questions/60012138/google-cloud-function-pulling-from-pub-sub-subscription-throws-exception-deadl + catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded) { + _logger.LogError(ex, "{Uri}: Google Cloud Platform Pub/Sub returned \"DEADLINE_EXCEEDED\", attempting to restart listener.", _endpoint.Uri); + + _task.SafeDispose(); + _task = StartAsync(); + + break; + } catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled) { _logger.LogInformation("{Uri}: Listener canceled, shutting down listener...", _endpoint.Uri); @@ -152,7 +163,7 @@ protected async Task listenForMessagesAsync(Func listenAsync) { _logger.LogError( ex, - "{Uri}: Error while trying to retrieve messages from Google Cloud Platform Pub/Sub, attempting to restart stream ({RetryCount}/{MaxRetryCount})...", + "{Uri}: Error while trying to retrieve messages from Google Cloud Platform Pub/Sub, attempting to restart listener ({RetryCount}/{MaxRetryCount})...", _endpoint.Uri, retryCount, _endpoint.Client.RetryPolicy.MaxRetryCount