-
-
Notifications
You must be signed in to change notification settings - Fork 342
Fix NATS JetStream native scheduled send (err 10190) and add configurable schedule-subject suffix. #3017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Fix NATS JetStream native scheduled send (err 10190) and add configurable schedule-subject suffix. #3017
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b5752c9
Fix NATS JetStream native scheduled send (err 10190) and add configur…
8033fe7
Makes UseScheduleSubjectSuffix throw when suffix is null or whitespace.
430770c
Generate new unique stream in ScheduledMessageDeliveryTests and clean…
724f47c
Update NATS scheduling docs.
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
src/Transports/NATS/Wolverine.Nats.Tests/NatsScheduleSubjectSuffixTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| using Shouldly; | ||
| using Wolverine.Nats.Configuration; | ||
| using Wolverine.Nats.Internal; | ||
| using Xunit; | ||
|
|
||
| namespace Wolverine.Nats.Tests; | ||
|
|
||
| public class NatsScheduleSubjectSuffixTests | ||
| { | ||
| private static NatsEndpoint EndpointFor(string subject = "orders.created") | ||
| { | ||
| var transport = new NatsTransport(); | ||
| return (NatsEndpoint)transport.GetOrCreateEndpoint(NatsEndpointUri.Subject(subject)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void default_suffix_is_scheduled() | ||
| { | ||
| EndpointFor().ScheduleSubjectSuffix.ShouldBe(".scheduled"); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(".scheduled")] | ||
| [InlineData(".override-scheduled")] | ||
| [InlineData(".control")] | ||
| public void use_schedule_subject_suffix_round_trips_to_endpoint(string suffix) | ||
| { | ||
| var endpoint = EndpointFor(); | ||
| var configuration = new NatsSubscriberConfiguration(endpoint); | ||
|
|
||
| configuration.UseScheduleSubjectSuffix(suffix); | ||
| // Callbacks are buffered in the IDelayedEndpointConfiguration base; Apply() mirrors what the | ||
| // runtime does at endpoint compile time. | ||
| ((Wolverine.Configuration.IDelayedEndpointConfiguration)configuration).Apply(); | ||
|
|
||
| endpoint.ScheduleSubjectSuffix.ShouldBe(suffix); | ||
| } | ||
| } |
138 changes: 138 additions & 0 deletions
138
src/Transports/NATS/Wolverine.Nats.Tests/ScheduledMessageDeliveryTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| using FluentAssertions; | ||
| using JasperFx.Core; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Wolverine.Nats.Internal; | ||
| using Wolverine.Runtime; | ||
| using Wolverine.Tracking; | ||
| using Xunit; | ||
|
|
||
| namespace Wolverine.Nats.Tests; | ||
|
|
||
| /// <summary> | ||
| /// Reproduces the NATS JetStream native scheduled-delivery failure (err 10190). Unlike the compliance | ||
| /// <c>schedule_send</c> test, the publishing endpoint here uses <c>.UseJetStream(stream)</c> — that is what | ||
| /// engages the native scheduled-send path; without it Wolverine falls back to durable scheduling and the | ||
| /// bug is never exercised. | ||
| /// </summary> | ||
| [Collection("NATS Integration Tests")] | ||
| [Trait("Category", "Integration")] | ||
| public class ScheduledMessageDeliveryTests : IAsyncLifetime | ||
| { | ||
| private IHost? _sender; | ||
| private IHost? _receiver; | ||
| private string _receiverSubject = ""; | ||
| private string _streamName = ""; | ||
|
|
||
| public async Task InitializeAsync() | ||
| { | ||
| var natsUrl = Environment.GetEnvironmentVariable("NATS_URL") ?? "nats://localhost:4222"; | ||
|
|
||
| // Unique per run (GUID, not an in-process counter) so repeated runs against a persistent NATS | ||
| // instance never collide on stream name / subjects. The stream is torn down in DisposeAsync. | ||
| var id = Guid.NewGuid().ToString("N"); | ||
| _streamName = $"SCHEDULED_{id}"; | ||
| _receiverSubject = $"test.scheduled.{id}.receiver"; | ||
| var streamSubjects = $"test.scheduled.{id}.>"; | ||
|
|
||
| _sender = await Host.CreateDefaultBuilder() | ||
| .UseWolverine(opts => | ||
| { | ||
| opts.ServiceName = "ScheduleSender"; | ||
| opts.UseNats(natsUrl) | ||
| .AutoProvision() | ||
| .UseJetStream(js => js.MaxDeliver = 5) | ||
| .DefineWorkQueueStream(_streamName, s => s.EnableScheduledDelivery(), streamSubjects); | ||
|
|
||
| // .UseJetStream on the PUBLISHING endpoint is what forces the native scheduled-send path. | ||
| opts.PublishMessage<ScheduledPing>() | ||
| .ToNatsSubject(_receiverSubject) | ||
| .UseJetStream(_streamName); | ||
| }) | ||
| .StartAsync(); | ||
|
|
||
| _receiver = await Host.CreateDefaultBuilder() | ||
| .UseWolverine(opts => | ||
| { | ||
| opts.ServiceName = "ScheduleReceiver"; | ||
| opts.UseNats(natsUrl) | ||
| .AutoProvision() | ||
| .UseJetStream(js => js.MaxDeliver = 5) | ||
| .DefineWorkQueueStream(_streamName, s => s.EnableScheduledDelivery(), streamSubjects); | ||
|
|
||
| opts.ListenToNatsSubject(_receiverSubject) | ||
| .Named("receiver") | ||
| .UseJetStream(_streamName, $"receiver-consumer-{id}"); | ||
| }) | ||
| .StartAsync(); | ||
| } | ||
|
|
||
| public async Task DisposeAsync() | ||
| { | ||
| // Delete the run's stream while a connection is still open so persistent NATS instances | ||
| // don't accumulate long-lived test artifacts. Best-effort: the stream may never have been | ||
| // provisioned (e.g. startup failed), so swallow any error. | ||
| if (_sender != null && _streamName.IsNotEmpty()) | ||
| { | ||
| try | ||
| { | ||
| var runtime = _sender.Services.GetRequiredService<IWolverineRuntime>(); | ||
| var transport = runtime.Options.Transports.GetOrCreate<NatsTransport>(); | ||
| await transport.JetStreamContext.DeleteStreamAsync(_streamName); | ||
| } | ||
| catch | ||
| { | ||
| // ignore — best-effort cleanup | ||
| } | ||
| } | ||
|
|
||
| if (_sender != null) | ||
| { | ||
| await _sender.StopAsync(); | ||
| } | ||
|
|
||
| if (_receiver != null) | ||
| { | ||
| await _receiver.StopAsync(); | ||
| } | ||
|
|
||
| _sender?.Dispose(); | ||
| _receiver?.Dispose(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task schedule_send_over_native_jetstream_scheduling() | ||
| { | ||
| // Native scheduled delivery requires NATS Server 2.12+. Skip on older images rather than fail. | ||
| var runtime = _sender!.Services.GetRequiredService<IWolverineRuntime>(); | ||
| var transport = runtime.Options.Transports.GetOrCreate<NatsTransport>(); | ||
| if (!transport.ServerSupportsScheduledSend) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Guard: confirm the native path is engaged, not the durable-scheduling fallback that would mask the bug. | ||
| var agent = runtime.Endpoints.GetOrBuildSendingAgent(new Uri($"nats://subject/{_receiverSubject}")); | ||
| agent.SupportsNativeScheduledSend.Should().BeTrue(); | ||
|
|
||
| var message = new ScheduledPing(Guid.NewGuid(), "scheduled"); | ||
|
|
||
| var session = await _sender | ||
| .TrackActivity() | ||
| .AlsoTrack(_receiver) | ||
| .Timeout(30.Seconds()) | ||
| .WaitForMessageToBeReceivedAt<ScheduledPing>(_receiver!) | ||
| .ExecuteAndWaitAsync(c => c.ScheduleAsync(message, 5.Seconds()).AsTask()); | ||
|
|
||
| session.Received.SingleMessage<ScheduledPing>().Should().BeEquivalentTo(message); | ||
| } | ||
| } | ||
|
|
||
| public record ScheduledPing(Guid Id, string Text); | ||
|
|
||
| public class ScheduledPingHandler | ||
| { | ||
| public void Handle(ScheduledPing message) | ||
| { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.