diff --git a/src/NATS.Client.Core/Internal/ReplyTask.cs b/src/NATS.Client.Core/Internal/ReplyTask.cs index ae02c85af..526318a4e 100644 --- a/src/NATS.Client.Core/Internal/ReplyTask.cs +++ b/src/NATS.Client.Core/Internal/ReplyTask.cs @@ -17,12 +17,13 @@ internal sealed class ReplyTask : ReplyTaskBase, IDisposable private readonly NatsConnection _connection; private readonly INatsDeserialize _deserializer; private readonly TimeSpan _requestTimeout; + private readonly bool _throwIfNoResponders; private readonly TaskCompletionSource _tcs; private NatsMsg _msg; private long _replyBytes; private bool _isNoResponders; - public ReplyTask(ReplyTaskFactory factory, long id, string subject, NatsConnection connection, INatsDeserialize deserializer, TimeSpan requestTimeout) + public ReplyTask(ReplyTaskFactory factory, long id, string subject, NatsConnection connection, INatsDeserialize deserializer, TimeSpan requestTimeout, bool throwIfNoResponders) { _factory = factory; _id = id; @@ -30,6 +31,7 @@ public ReplyTask(ReplyTaskFactory factory, long id, string subject, NatsConnecti _connection = connection; _deserializer = deserializer; _requestTimeout = TimeoutValidation.Validate(requestTimeout, nameof(requestTimeout), Timeout.InfiniteTimeSpan); + _throwIfNoResponders = throwIfNoResponders; _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); #if NET9_0_OR_GREATER _gate = new System.Threading.Lock(); @@ -63,6 +65,13 @@ await _tcs.Task isNoResponders = _isNoResponders; } + // Match the SharedInbox path: when ThrowIfNoResponders is set, a 503 sentinel + // surfaces as NatsNoRespondersException rather than an empty message. + if (isNoResponders && _throwIfNoResponders) + { + throw new NatsNoRespondersException(); + } + // Count only messages actually delivered to the caller. Late replies that arrive // after a timeout still hit SetResult, but the user never sees them, so the // counters belong here on the success path, not in SetResult. 503 NoResponders @@ -123,7 +132,7 @@ public ReplyTaskFactory(NatsConnection connection) _replies = new ConcurrentDictionary(); } - public ReplyTask CreateReplyTask(INatsDeserialize? deserializer, TimeSpan? requestTimeout) + public ReplyTask CreateReplyTask(INatsDeserialize? deserializer, TimeSpan? requestTimeout, bool throwIfNoResponders) { deserializer ??= _serializerRegistry.GetDeserializer(); var id = Interlocked.Increment(ref _nextId); @@ -149,7 +158,7 @@ public ReplyTask CreateReplyTask(INatsDeserialize? deser subject = _inboxPrefixString + id; } - var rt = new ReplyTask(this, id, subject, _connection, deserializer, requestTimeout ?? _requestTimeout); + var rt = new ReplyTask(this, id, subject, _connection, deserializer, requestTimeout ?? _requestTimeout, throwIfNoResponders); _replies.TryAdd(id, rt); return rt; } diff --git a/src/NATS.Client.Core/NatsConnection.RequestReply.cs b/src/NATS.Client.Core/NatsConnection.RequestReply.cs index 466a429ea..d164bac1a 100644 --- a/src/NATS.Client.Core/NatsConnection.RequestReply.cs +++ b/src/NATS.Client.Core/NatsConnection.RequestReply.cs @@ -7,16 +7,16 @@ namespace NATS.Client.Core; public partial class NatsConnection { + // ThrowIfNoResponders is intentionally left unset here so it falls through to the + // default derived in SetBaseReplyOptsDefaults (throw unless Direct was set explicitly). private static readonly NatsSubOpts ReplyOptsDefault = new NatsSubOpts { MaxMsgs = 1, - ThrowIfNoResponders = true, }; private static readonly NatsSubOpts ReplyManyOptsDefault = new NatsSubOpts { StopOnEmptyMsg = true, - ThrowIfNoResponders = true, }; /// @@ -53,7 +53,7 @@ public async ValueTask> RequestAsync( if (Opts.RequestReplyMode == NatsRequestReplyMode.Direct) { - using var rt = _replyTaskFactory.CreateReplyTask(replySerializer, replyOpts.Timeout); + using var rt = _replyTaskFactory.CreateReplyTask(replySerializer, replyOpts.Timeout, replyOpts.ThrowIfNoResponders ?? true); requestSerializer ??= Opts.SerializerRegistry.GetSerializer(); await PublishAsync(subject, data, headers, rt.Subject, requestSerializer, requestOpts, cancellationToken).ConfigureAwait(false); var msg = await rt.GetResultAsync(cancellationToken).ConfigureAwait(false); @@ -85,7 +85,7 @@ public async ValueTask> RequestAsync( if (Opts.RequestReplyMode == NatsRequestReplyMode.Direct) { - using var rt = _replyTaskFactory.CreateReplyTask(replySerializer, replyOpts.Timeout); + using var rt = _replyTaskFactory.CreateReplyTask(replySerializer, replyOpts.Timeout, replyOpts.ThrowIfNoResponders ?? true); requestSerializer ??= Opts.SerializerRegistry.GetSerializer(); await PublishAsync(subject, data, headers, rt.Subject, requestSerializer, requestOpts, cancellationToken).ConfigureAwait(false); return await rt.GetResultAsync(cancellationToken).ConfigureAwait(false); @@ -214,6 +214,13 @@ private NatsSubOpts SetReplyManyOptsDefaults(NatsSubOpts? replyOpts) opts = opts with { StopOnEmptyMsg = true }; } + // RequestManyAsync always uses the shared inbox path, so it is unaffected by the + // Direct intentional-selection opt-out and keeps throwing on no-responders by default. + if (!opts.ThrowIfNoResponders.HasValue) + { + opts = opts with { ThrowIfNoResponders = true }; + } + return SetBaseReplyOptsDefaults(opts); } @@ -226,7 +233,9 @@ private NatsSubOpts SetBaseReplyOptsDefaults(NatsSubOpts opts) if (!opts.ThrowIfNoResponders.HasValue) { - opts = opts with { ThrowIfNoResponders = true }; + // Throw on no-responders by default, except when Direct was selected explicitly, which + // preserves Direct's pre-3.x behavior of returning the sentinel as a message. + opts = opts with { ThrowIfNoResponders = !Opts.DirectSetIntentionally }; } return opts; diff --git a/src/NATS.Client.Core/NatsOpts.cs b/src/NATS.Client.Core/NatsOpts.cs index d5c59e9f7..0cd4ce0ef 100644 --- a/src/NATS.Client.Core/NatsOpts.cs +++ b/src/NATS.Client.Core/NatsOpts.cs @@ -38,6 +38,12 @@ public sealed record NatsOpts AuthOpts = NatsAuthOpts.Default, }; + // Backing fields for RequestReplyMode. _directSetIntentionally records whether Direct was set + // explicitly (vs inherited as the default) so no-responders behavior can be preserved across + // the 3.x default flip; see the RequestReplyMode init accessor. + private readonly NatsRequestReplyMode _requestReplyMode = NatsRequestReplyMode.Direct; + private readonly bool _directSetIntentionally; + /// /// NATS server URL to connect to. (default: nats://localhost:4222) /// @@ -260,8 +266,30 @@ public sealed record NatsOpts /// /// The setting determines which mode is used during message exchanges /// initiated by or other related methods. + /// Defaults to . + /// always uses the shared inbox path regardless of this setting. + /// + /// No-responders handling depends on whether this is set explicitly. Left at its default, request-reply + /// throws on a 503 no-responders sentinel. Setting it explicitly to + /// instead returns the sentinel as a message with + /// set, preserving the behavior of Direct mode before 3.x. Use the + /// per-call to override either way. + /// /// - public NatsRequestReplyMode RequestReplyMode { get; init; } = NatsRequestReplyMode.SharedInbox; + public NatsRequestReplyMode RequestReplyMode + { + get => _requestReplyMode; + init + { + _requestReplyMode = value; + + // Explicitly selecting Direct (as opposed to inheriting it as the default) opts out of + // throwing on no-responders, matching Direct's pre-3.x behavior. The _requestReplyMode + // field initializer bypasses this accessor, so an unset RequestReplyMode keeps the + // throwing default. + _directSetIntentionally = value == NatsRequestReplyMode.Direct; + } + } /// /// Factory for creating socket connections to the NATS server. @@ -337,6 +365,12 @@ public sealed record NatsOpts /// public bool SuppressSlowConsumerWarnings { get; init; } = false; + // True when RequestReplyMode was set to Direct explicitly (vs inherited as the default). + // NatsConnection.SetBaseReplyOptsDefaults derives the default no-responders behavior from this: + // explicit Direct returns the 503 sentinel as a message, everything else throws. A per-call + // NatsSubOpts.ThrowIfNoResponders still takes precedence. + internal bool DirectSetIntentionally => _directSetIntentionally; + internal NatsUri[] GetSeedUris(bool suppressRandomization = false) { var urls = Url.Split(','); diff --git a/src/NATS.Client.JetStream/NatsJSContext.cs b/src/NATS.Client.JetStream/NatsJSContext.cs index b83815399..bcd4679a9 100644 --- a/src/NATS.Client.JetStream/NatsJSContext.cs +++ b/src/NATS.Client.JetStream/NatsJSContext.cs @@ -181,6 +181,8 @@ public async ValueTask> TryPublishAsync( NatsMsg msg; try { + // ThrowIfNoResponders=false: the 503 sentinel comes back as a message so the + // retry loop below can handle it, matching the shared-inbox path. msg = await Connection.RequestAsync( subject: subject, data: data, @@ -188,7 +190,7 @@ public async ValueTask> TryPublishAsync( requestSerializer: serializer, replySerializer: NatsJSJsonSerializer.Default, requestOpts: opts, - replyOpts: new NatsSubOpts { Timeout = Opts.RequestTimeout }, + replyOpts: new NatsSubOpts { Timeout = Opts.RequestTimeout, ThrowIfNoResponders = false }, cancellationToken).ConfigureAwait(false); } catch (NatsNoReplyException) @@ -432,11 +434,13 @@ internal async ValueTask>> TryJSRequestAsyn NatsMsg> msg; try { + // ThrowIfNoResponders=false: inspect the 503 no-responders sentinel below + // rather than letting RequestAsync throw, matching the shared-inbox path. msg = await Connection.RequestAsync>( subject: subject, data: request, headers: null, - replyOpts: new NatsSubOpts { Timeout = Opts.RequestTimeout }, + replyOpts: new NatsSubOpts { Timeout = Opts.RequestTimeout, ThrowIfNoResponders = false }, requestSerializer: NatsJSJsonSerializer.Default, replySerializer: NatsJSJsonDocumentSerializer.Default, cancellationToken: cancellationToken).ConfigureAwait(false); diff --git a/tests/NATS.Client.Core.Tests/SubscriptionTest.cs b/tests/NATS.Client.Core.Tests/SubscriptionTest.cs index 489515a5c..2124f9822 100644 --- a/tests/NATS.Client.Core.Tests/SubscriptionTest.cs +++ b/tests/NATS.Client.Core.Tests/SubscriptionTest.cs @@ -31,7 +31,10 @@ public async Task Subscription_periodic_cleanup_test() { await using var server = await NatsServerProcess.StartAsync(); var proxy = new NatsProxy(server.Port); - var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", SubscriptionCleanUpInterval = TimeSpan.FromSeconds(1) }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would make the SUB frame count below never settle at 1. + var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", SubscriptionCleanUpInterval = TimeSpan.FromSeconds(1), RequestReplyMode = NatsRequestReplyMode.SharedInbox }); async Task Isolator() { @@ -68,7 +71,10 @@ public async Task Subscription_cleanup_on_message_receive_test() { await using var server = await NatsServerProcess.StartAsync(); var proxy = new NatsProxy(server.Port); - var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", SubscriptionCleanUpInterval = TimeSpan.MaxValue }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would make the SUB frame count below never settle at 1. + var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", SubscriptionCleanUpInterval = TimeSpan.MaxValue, RequestReplyMode = NatsRequestReplyMode.SharedInbox }); async Task Isolator() { diff --git a/tests/NATS.Client.Core2.Tests/ProtocolParserSizeCheckTest.cs b/tests/NATS.Client.Core2.Tests/ProtocolParserSizeCheckTest.cs index 35261ee52..1d26bb219 100644 --- a/tests/NATS.Client.Core2.Tests/ProtocolParserSizeCheckTest.cs +++ b/tests/NATS.Client.Core2.Tests/ProtocolParserSizeCheckTest.cs @@ -204,7 +204,10 @@ public async Task Valid_hmsg_still_works() await using var server = new FakeServer(output); await server.Ready; - await using var nats = new NatsConnection(new NatsOpts { Url = server.Url, ConnectTimeout = TimeSpan.FromSeconds(10) }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect; + // the test injects an HMSG with sid 1, which must map to the "foo" subscription. + await using var nats = new NatsConnection(new NatsOpts { Url = server.Url, ConnectTimeout = TimeSpan.FromSeconds(10), RequestReplyMode = NatsRequestReplyMode.SharedInbox }); await nats.ConnectAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); diff --git a/tests/NATS.Client.Core2.Tests/ProtocolTest.cs b/tests/NATS.Client.Core2.Tests/ProtocolTest.cs index 4092e34d3..05b6e0747 100644 --- a/tests/NATS.Client.Core2.Tests/ProtocolTest.cs +++ b/tests/NATS.Client.Core2.Tests/ProtocolTest.cs @@ -22,7 +22,10 @@ public async Task Subscription_with_same_subject() { var nats1 = new NatsConnection(new NatsOpts { Url = _server.Url }); var proxy = new NatsProxy(_server.Port); - var nats2 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10) }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would add an extra SUB frame to the proxy capture asserted below. + var nats2 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10), RequestReplyMode = NatsRequestReplyMode.SharedInbox }); var sub1 = await nats2.SubscribeCoreAsync("foo.bar"); var sub2 = await nats2.SubscribeCoreAsync("foo.bar"); @@ -112,7 +115,10 @@ await Retry.Until( public async Task Subscription_queue_group() { var proxy = new NatsProxy(_server.Port); - var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10) }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would shift the SUB frames asserted by index below. + var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10), RequestReplyMode = NatsRequestReplyMode.SharedInbox }); var subject = $"{_server.GetNextId()}.foo"; await using var sub1 = await nats.SubscribeCoreAsync(subject, queueGroup: "group1"); @@ -213,7 +219,10 @@ void Log(string text) // Use a single server to test multiple scenarios to make test runs more efficient var proxy = new NatsProxy(_server.Port); - var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10) }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would consume sid 1 and break the sid sequence asserted below. + var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10), RequestReplyMode = NatsRequestReplyMode.SharedInbox }); var sid = 0; Log("### Auto-unsubscribe after consuming max-msgs"); @@ -337,7 +346,10 @@ await Retry.Until( public async Task Reconnect_with_sub_and_additional_commands() { var proxy = new NatsProxy(_server.Port); - var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10) }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would add an extra SUB frame to the proxy capture asserted below. + var nats = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{proxy.Port}", ConnectTimeout = TimeSpan.FromSeconds(10), RequestReplyMode = NatsRequestReplyMode.SharedInbox }); var subject = $"{_server.GetNextId()}.foo"; var cmdSubject = $"{_server.GetNextId()}.bar"; diff --git a/tests/NATS.Client.Core2.Tests/RequestReplyTest.cs b/tests/NATS.Client.Core2.Tests/RequestReplyTest.cs index 7bc2627fd..7c0cfae0e 100644 --- a/tests/NATS.Client.Core2.Tests/RequestReplyTest.cs +++ b/tests/NATS.Client.Core2.Tests/RequestReplyTest.cs @@ -85,7 +85,9 @@ await Assert.ThrowsAsync(async () => }); await nats.PingAsync(); - await Assert.ThrowsAsync(async () => + // Direct mode surfaces cancellation as TaskCanceledException (a subclass of + // OperationCanceledException), SharedInbox as OperationCanceledException; accept either. + await Assert.ThrowsAnyAsync(async () => { await nats.RequestAsync("foo", 0, cancellationToken: cts.Token); }); @@ -96,13 +98,49 @@ await Assert.ThrowsAsync(async () => } [Fact] - public async Task Request_reply_no_responders_test() + public async Task Request_reply_no_responders_default_throws_test() { - // Enable no responders, and do not set a timeout. We should get a response with a 503-header code. - { - await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url }); - await Assert.ThrowsAsync(async () => await nats.RequestAsync(Guid.NewGuid().ToString(), 0)); - } + // Default mode (Direct transport, not set explicitly) throws on no-responders, + // preserving the pre-3.x default behavior. + await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url }); + await Assert.ThrowsAsync(async () => await nats.RequestAsync(Guid.NewGuid().ToString(), 0)); + } + + [Fact] + public async Task Request_reply_no_responders_shared_inbox_throws_test() + { + await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url, RequestReplyMode = NatsRequestReplyMode.SharedInbox }); + await Assert.ThrowsAsync(async () => await nats.RequestAsync(Guid.NewGuid().ToString(), 0)); + } + + [Fact] + public async Task Request_reply_no_responders_explicit_direct_returns_message_test() + { + // Explicitly selecting Direct preserves the pre-3.x Direct behavior: the 503 sentinel + // comes back as a message with HasNoResponders set instead of throwing. + await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url, RequestReplyMode = NatsRequestReplyMode.Direct }); + var reply = await nats.RequestAsync(Guid.NewGuid().ToString(), 0); + Assert.True(reply.HasNoResponders); + } + + [Theory] + [InlineData(NatsRequestReplyMode.SharedInbox)] + [InlineData(NatsRequestReplyMode.Direct)] + public async Task Request_reply_no_responders_per_call_suppressed_test(NatsRequestReplyMode mode) + { + // Per-call ThrowIfNoResponders=false returns the sentinel as a message in either mode. + await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url, RequestReplyMode = mode }); + var reply = await nats.RequestAsync(Guid.NewGuid().ToString(), 0, replyOpts: new NatsSubOpts { ThrowIfNoResponders = false }); + Assert.True(reply.HasNoResponders); + } + + [Fact] + public async Task Request_reply_no_responders_per_call_throw_overrides_explicit_direct_test() + { + // Per-call ThrowIfNoResponders=true forces a throw even when Direct was selected explicitly. + await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url, RequestReplyMode = NatsRequestReplyMode.Direct }); + await Assert.ThrowsAsync(async () => + await nats.RequestAsync(Guid.NewGuid().ToString(), 0, replyOpts: new NatsSubOpts { ThrowIfNoResponders = true })); } [Fact] @@ -438,9 +476,29 @@ public async Task Direct_request_reply_test() } [Fact] - public async Task Default_SharedInbox_request_reply_test() + public async Task Default_mode_is_direct_test() { + // Default RequestReplyMode is Direct, so the reply-to inbox carries a numeric id + // e.g. _INBOX.Hu5HPpWesrJhvQq2NG3YJ6.1 await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url }); + Assert.Equal(NatsRequestReplyMode.Direct, nats.Opts.RequestReplyMode); + + var reply = await nats.RequestAsync("$JS.API.INFO", cancellationToken: default); + + reply.Subject.Length.Should().BeLessThan("_INBOX..".Length + (2 * 22)); + Assert.True(long.TryParse(reply.Subject.Split('.')[2], out var id)); + Assert.True(id > 0); + + // simple response check + var json = JsonNode.Parse(reply.Data!)!; + var type = json["type"]!.GetValue(); + Assert.Equal("io.nats.jetstream.api.v1.account_info_response", type); + } + + [Fact] + public async Task SharedInbox_request_reply_test() + { + await using var nats = new NatsConnection(new NatsOpts { Url = _server.Url, RequestReplyMode = NatsRequestReplyMode.SharedInbox }); var reply = await nats.RequestAsync("$JS.API.INFO", cancellationToken: default); // reply-to should be inbox diff --git a/tests/NATS.Client.Core2.Tests/SendBufferTest.cs b/tests/NATS.Client.Core2.Tests/SendBufferTest.cs index c995a720d..3230e9c98 100644 --- a/tests/NATS.Client.Core2.Tests/SendBufferTest.cs +++ b/tests/NATS.Client.Core2.Tests/SendBufferTest.cs @@ -39,7 +39,10 @@ void Log(string m) Log("__________________________________"); - await using var nats = new NatsConnection(new NatsOpts { Url = server.Url }); + // SharedInbox so the connection doesn't open an inbox subscription at connect; + // otherwise dispose tries to UNSUB it through the deliberately wedged send buffer + // and times out before the test can assert send cancellation. + await using var nats = new NatsConnection(new NatsOpts { Url = server.Url, RequestReplyMode = NatsRequestReplyMode.SharedInbox }); await server.Ready; Log($"[C] connect {server.Url}"); diff --git a/tests/NATS.Client.CoreUnit.Tests/NatsOptsRequestReplyModeTests.cs b/tests/NATS.Client.CoreUnit.Tests/NatsOptsRequestReplyModeTests.cs new file mode 100644 index 000000000..05bb0457a --- /dev/null +++ b/tests/NATS.Client.CoreUnit.Tests/NatsOptsRequestReplyModeTests.cs @@ -0,0 +1,50 @@ +namespace NATS.Client.Core.Tests; + +public class NatsOptsRequestReplyModeTests +{ + [Fact] + public void Default_mode_is_direct_but_not_set_intentionally() + { + // The default is Direct transport, but because it comes from the field initializer + // (not the init accessor) it is not flagged as intentional, so no-responders still throws. + var opts = new NatsOpts(); + + Assert.Equal(NatsRequestReplyMode.Direct, opts.RequestReplyMode); + Assert.False(opts.DirectSetIntentionally); + } + + [Fact] + public void Explicit_direct_is_set_intentionally() + { + var opts = new NatsOpts { RequestReplyMode = NatsRequestReplyMode.Direct }; + + Assert.Equal(NatsRequestReplyMode.Direct, opts.RequestReplyMode); + Assert.True(opts.DirectSetIntentionally); + } + + [Fact] + public void Explicit_shared_inbox_is_not_set_intentionally() + { + var opts = new NatsOpts { RequestReplyMode = NatsRequestReplyMode.SharedInbox }; + + Assert.Equal(NatsRequestReplyMode.SharedInbox, opts.RequestReplyMode); + Assert.False(opts.DirectSetIntentionally); + } + + [Fact] + public void With_setting_direct_flags_intentional() + { + var opts = new NatsOpts() with { RequestReplyMode = NatsRequestReplyMode.Direct }; + + Assert.True(opts.DirectSetIntentionally); + } + + [Fact] + public void With_not_touching_mode_preserves_default_flag() + { + var opts = new NatsOpts() with { Name = "x" }; + + Assert.Equal(NatsRequestReplyMode.Direct, opts.RequestReplyMode); + Assert.False(opts.DirectSetIntentionally); + } +} diff --git a/tests/NATS.Net.OpenTelemetry.Tests/OpenTelemetryTest.cs b/tests/NATS.Net.OpenTelemetry.Tests/OpenTelemetryTest.cs index 1046fdc9c..32ba7645c 100644 --- a/tests/NATS.Net.OpenTelemetry.Tests/OpenTelemetryTest.cs +++ b/tests/NATS.Net.OpenTelemetry.Tests/OpenTelemetryTest.cs @@ -183,7 +183,10 @@ public async Task Active_subscriptions_updown_counter() { using var meter = new MeterTracker(); await using var server = await NatsServerProcess.StartAsync(); - await using var nats = new NatsConnection(new NatsOpts { Url = server.Url }); + + // SharedInbox so the connection doesn't open an inbox subscription at connect, + // which would itself count as an active subscription and offset the sums below. + await using var nats = new NatsConnection(new NatsOpts { Url = server.Url, RequestReplyMode = NatsRequestReplyMode.SharedInbox }); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); diff --git a/tests/xunit.runsettings b/tests/xunit.runsettings index 1bc835271..b64f9a84c 100644 --- a/tests/xunit.runsettings +++ b/tests/xunit.runsettings @@ -4,6 +4,9 @@ 600000 - 1.0x + + false