diff --git a/NATS.Client.sln b/NATS.Client.sln index 47b7ec337..fd628ecfc 100644 --- a/NATS.Client.sln +++ b/NATS.Client.sln @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "sandbox\Conso EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NATS.Client.Core.Tests", "tests\NATS.Client.Core.Tests\NATS.Client.Core.Tests.csproj", "{D60B2ADF-450D-4F8F-A3C5-7803D36FE06B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NatsBenchmark", "sandbox\NatsBenchmark\NatsBenchmark.csproj", "{FDBED61E-DA84-46E4-B4AE-1E4ACFDAE21C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroBenchmark", "sandbox\MicroBenchmark\MicroBenchmark.csproj", "{A10F0D89-13F3-49B3-ACF7-66E45DC95225}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "files", "files", "{899BE3EA-C5CA-4394-9B62-C45CBFF3AF4E}" @@ -119,10 +117,6 @@ Global {D60B2ADF-450D-4F8F-A3C5-7803D36FE06B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D60B2ADF-450D-4F8F-A3C5-7803D36FE06B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D60B2ADF-450D-4F8F-A3C5-7803D36FE06B}.Release|Any CPU.Build.0 = Release|Any CPU - {FDBED61E-DA84-46E4-B4AE-1E4ACFDAE21C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FDBED61E-DA84-46E4-B4AE-1E4ACFDAE21C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FDBED61E-DA84-46E4-B4AE-1E4ACFDAE21C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FDBED61E-DA84-46E4-B4AE-1E4ACFDAE21C}.Release|Any CPU.Build.0 = Release|Any CPU {A10F0D89-13F3-49B3-ACF7-66E45DC95225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A10F0D89-13F3-49B3-ACF7-66E45DC95225}.Debug|Any CPU.Build.0 = Debug|Any CPU {A10F0D89-13F3-49B3-ACF7-66E45DC95225}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -275,7 +269,6 @@ Global {702B4B02-7FEC-42A1-A399-6F39D4E2CA29} = {4827B3EC-73D8-436D-AE2A-5E29AC95FD0C} {7FE6D43B-181E-485E-8C14-35EAA0EBF09A} = {95A69671-16CA-4133-981C-CC381B7AAA30} {D60B2ADF-450D-4F8F-A3C5-7803D36FE06B} = {C526E8AB-739A-48D7-8FC4-048978C9B650} - {FDBED61E-DA84-46E4-B4AE-1E4ACFDAE21C} = {95A69671-16CA-4133-981C-CC381B7AAA30} {A10F0D89-13F3-49B3-ACF7-66E45DC95225} = {95A69671-16CA-4133-981C-CC381B7AAA30} {D3F09B30-1ED5-48C2-81CD-A2AD88E751AC} = {4827B3EC-73D8-436D-AE2A-5E29AC95FD0C} {44881DEE-8B49-44EA-B0BA-8BDA4F706E1A} = {95A69671-16CA-4133-981C-CC381B7AAA30} diff --git a/sandbox/MicroBenchmark/.gitignore b/sandbox/MicroBenchmark/.gitignore new file mode 100644 index 000000000..b0f1a2c50 --- /dev/null +++ b/sandbox/MicroBenchmark/.gitignore @@ -0,0 +1 @@ +/BenchmarkDotNet.Artifacts diff --git a/sandbox/MicroBenchmark/SerializationBuffersBench.cs b/sandbox/MicroBenchmark/SerializationBuffersBench.cs index 4bd864473..8e15c07be 100644 --- a/sandbox/MicroBenchmark/SerializationBuffersBench.cs +++ b/sandbox/MicroBenchmark/SerializationBuffersBench.cs @@ -10,8 +10,6 @@ namespace MicroBenchmark; public class SerializationBuffersBench { private static readonly string Data = new('0', 126); - private static readonly NatsPubOpts OptsWaitUntilSentTrue = new() { WaitUntilSent = true }; - private static readonly NatsPubOpts OptsWaitUntilSentFalse = new() { WaitUntilSent = false }; private NatsConnection _nats; @@ -22,22 +20,11 @@ public class SerializationBuffersBench public void Setup() => _nats = new NatsConnection(); [Benchmark] - public async ValueTask WaitUntilSentTrue() + public async ValueTask PublishAsync() { for (var i = 0; i < Iter; i++) { - await _nats.PublishAsync("foo", Data, opts: OptsWaitUntilSentTrue); - } - - return await _nats.PingAsync(); - } - - [Benchmark] - public async ValueTask WaitUntilSentFalse() - { - for (var i = 0; i < Iter; i++) - { - await _nats.PublishAsync("foo", Data, opts: OptsWaitUntilSentFalse); + await _nats.PublishAsync("foo", Data); } return await _nats.PingAsync(); diff --git a/sandbox/NatsBenchmark/Benchmark.cs b/sandbox/NatsBenchmark/Benchmark.cs deleted file mode 100644 index f346dad88..000000000 --- a/sandbox/NatsBenchmark/Benchmark.cs +++ /dev/null @@ -1,474 +0,0 @@ -// Originally this benchmark code is borrowd from https://github.com/nats-io/nats.net -#nullable disable - -// Copyright 2015-2018 The NATS Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using NATS.Client; -using ZLogger; - -namespace NatsBenchmark -{ - public partial class Benchmark - { - private static readonly long DEFAULTCOUNT = 10000000; - - private string _url = null; - private long _count = DEFAULTCOUNT; - private long _payloadSize = 0; - private string _subject = "s"; - private bool _useOldRequestStyle = true; - private string _creds = null; - - private BenchType _btype = BenchType.SUITE; - - public Benchmark(string[] args) - { - if (!ParseArgs(args)) - return; - - switch (_btype) - { - case BenchType.SUITE: - RunSuite(); - break; - case BenchType.PUB: - RunPub("PUB", _count, _payloadSize); - break; - case BenchType.PUBSUB: - RunPubSub("PUBSUB", _count, _payloadSize); - break; - case BenchType.REQREPLY: - RunReqReply("REQREP", _count, _payloadSize); - break; - case BenchType.REQREPLYASYNC: - RunReqReplyAsync("REQREPASYNC", _count, _payloadSize).Wait(); - break; - default: - throw new Exception("Invalid Type."); - } - } - - private enum BenchType - { - PUB = 0, - PUBSUB, - REQREPLY, - SUITE, - REQREPLYASYNC, - } - - private void SetBenchType(string value) - { - switch (value) - { - case "PUB": - _btype = BenchType.PUB; - break; - case "PUBSUB": - _btype = BenchType.PUBSUB; - break; - case "REQREP": - _btype = BenchType.REQREPLY; - break; - case "REQREPASYNC": - _btype = BenchType.REQREPLYASYNC; - break; - case "SUITE": - _btype = BenchType.SUITE; - break; - default: - _btype = BenchType.PUB; - Console.WriteLine("No type specified. Defaulting to PUB."); - break; - } - } - - private void Usage() - { - Console.WriteLine("benchmark [-h] -type -url -count -creds -size "); - } - - private string GetValue(IDictionary values, string key, string defaultValue) - { - if (values.ContainsKey(key)) - return values[key]; - - return defaultValue; - } - - private bool ParseArgs(string[] args) - { - try - { - // defaults - if (args == null || args.Length == 0) - return true; - - IDictionary strArgs = new Dictionary(); - - for (var i = 0; i < args.Length; i++) - { - if (i + 1 > args.Length) - throw new Exception("Missing argument after " + args[i]); - - if ("-h".Equals(args[i].ToLower()) || - "/?".Equals(args[i].ToLower())) - { - Usage(); - return false; - } - - strArgs.Add(args[i], args[i + 1]); - i++; - } - - SetBenchType(GetValue(strArgs, "-type", "PUB")); - - _url = GetValue(strArgs, "-url", "nats://localhost:4222"); - _count = Convert.ToInt64(GetValue(strArgs, "-count", "10000")); - _payloadSize = Convert.ToInt64(GetValue(strArgs, "-size", "0")); - _useOldRequestStyle = Convert.ToBoolean(GetValue(strArgs, "-old", "false")); - _creds = GetValue(strArgs, "-creds", null); - - Console.WriteLine("Running NATS Custom benchmark:"); - Console.WriteLine(" URL: " + _url); - Console.WriteLine(" Count: " + _count); - Console.WriteLine(" Size: " + _payloadSize); - Console.WriteLine(" Type: " + GetValue(strArgs, "-type", "PUB")); - Console.WriteLine(string.Empty); - - return true; - } - catch (Exception e) - { - Console.WriteLine("Unable to parse command line args: " + e.Message); - return false; - } - } - - private void PrintResults(string testPrefix, Stopwatch sw, long testCount, long msgSize) - { - var msgRate = (int)(testCount / sw.Elapsed.TotalSeconds); - - Console.WriteLine( - "{0}\t{1,10}\t{2,10} msgs/s\t{3,8} kb/s", - testPrefix, - testCount, - msgRate, - msgRate * msgSize / 1024); - } - - private byte[] GeneratePayload(long size) - { - byte[] data = null; - - if (size == 0) - return null; - - data = new byte[size]; - for (var i = 0; i < size; i++) - { - data[i] = (byte)'a'; - } - - return data; - } - - private void RunPub(string testName, long testCount, long testSize) - { - var payload = GeneratePayload(testSize); - - var opts = ConnectionFactory.GetDefaultOptions(); - opts.Url = _url; - if (_creds != null) - { - opts.SetUserCredentials(_creds); - } - - using (var c = new ConnectionFactory().CreateConnection(opts)) - { - Stopwatch sw = sw = Stopwatch.StartNew(); - - for (var i = 0; i < testCount; i++) - { - c.Publish(_subject, payload); - } - - sw.Stop(); - - PrintResults(testName, sw, testCount, testSize); - } - } - - private void RunPubSub(string testName, long testCount, long testSize) - { - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - var payload = GeneratePayload(testSize); - - var cf = new ConnectionFactory(); - - var o = ConnectionFactory.GetDefaultOptions(); - o.ClosedEventHandler = (_, __) => { }; - o.DisconnectedEventHandler = (_, __) => { }; - - o.Url = _url; - o.SubChannelLength = 10000000; - if (_creds != null) - { - o.SetUserCredentials(_creds); - } - - o.AsyncErrorEventHandler += (sender, obj) => - { - Console.WriteLine("Error: " + obj.Error); - }; - - var subConn = cf.CreateConnection(o); - var pubConn = cf.CreateConnection(o); - - var s = subConn.SubscribeAsync(_subject, (sender, args) => - { - subCount++; - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - }); - s.SetPendingLimits(10000000, 1000000000); - subConn.Flush(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - var sw = Stopwatch.StartNew(); - - for (var i = 0; i < testCount; i++) - { - pubConn.Publish(_subject, payload); - } - - pubConn.Flush(); - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - PrintResults(testName, sw, testCount, testSize); - - pubConn.Close(); - subConn.Close(); - } - - private double ConvertTicksToMicros(long ticks) - { - return ConvertTicksToMicros((double)ticks); - } - - private double ConvertTicksToMicros(double ticks) - { - return ticks / TimeSpan.TicksPerMillisecond * 1000.0; - } - - private void RunPubSubLatency(string testName, long testCount, long testSize) - { - var subcriberLock = new object(); - var subscriberDone = false; - - var measurements = new List((int)testCount); - - var payload = GeneratePayload(testSize); - - var cf = new ConnectionFactory(); - var opts = ConnectionFactory.GetDefaultOptions(); - opts.Url = _url; - if (_creds != null) - { - opts.SetUserCredentials(_creds); - } - - var subConn = cf.CreateConnection(opts); - var pubConn = cf.CreateConnection(opts); - - var sw = new Stopwatch(); - - var subs = subConn.SubscribeAsync(_subject, (sender, args) => - { - sw.Stop(); - - measurements.Add(sw.ElapsedTicks); - - lock (subcriberLock) - { - Monitor.Pulse(subcriberLock); - subscriberDone = true; - } - }); - - subConn.Flush(); - - for (var i = 0; i < testCount; i++) - { - lock (subcriberLock) - { - subscriberDone = false; - } - - sw.Reset(); - sw.Start(); - - pubConn.Publish(_subject, payload); - pubConn.Flush(); - - // block on the subscriber finishing - we do not want any - // overlap in measurements. - lock (subcriberLock) - { - if (!subscriberDone) - { - Monitor.Wait(subcriberLock); - } - } - } - - var latencyAvg = measurements.Average(); - - var stddev = Math.Sqrt( - measurements.Average( - v => Math.Pow(v - latencyAvg, 2))); - - Console.WriteLine( - "{0} (us)\t{1} msgs, {2:F2} avg, {3:F2} min, {4:F2} max, {5:F2} stddev", - testName, - testCount, - ConvertTicksToMicros(latencyAvg), - ConvertTicksToMicros(measurements.Min()), - ConvertTicksToMicros(measurements.Max()), - ConvertTicksToMicros(stddev)); - - pubConn.Close(); - subConn.Close(); - } - - private void RunReqReply(string testName, long testCount, long testSize) - { - var payload = GeneratePayload(testSize); - - var cf = new ConnectionFactory(); - - var opts = ConnectionFactory.GetDefaultOptions(); - opts.Url = _url; - opts.UseOldRequestStyle = _useOldRequestStyle; - if (_creds != null) - { - opts.SetUserCredentials(_creds); - } - - var subConn = cf.CreateConnection(opts); - var pubConn = cf.CreateConnection(opts); - - var t = new Thread(() => - { - var s = subConn.SubscribeSync(_subject); - for (var i = 0; i < testCount; i++) - { - var m = s.NextMessage(); - subConn.Publish(m.Reply, payload); - subConn.Flush(); - } - }); - t.IsBackground = true; - t.Start(); - - Thread.Sleep(1000); - - var sw = Stopwatch.StartNew(); - for (var i = 0; i < testCount; i++) - { - pubConn.Request(_subject, payload); - } - - sw.Stop(); - - PrintResults(testName, sw, testCount, testSize); - - pubConn.Close(); - subConn.Close(); - } - - private async Task RunReqReplyAsync(string testName, long testCount, long testSize) - { - var payload = GeneratePayload(testSize); - - var cf = new ConnectionFactory(); - - var opts = ConnectionFactory.GetDefaultOptions(); - opts.Url = _url; - opts.UseOldRequestStyle = _useOldRequestStyle; - if (_creds != null) - { - opts.SetUserCredentials(_creds); - } - - var subConn = cf.CreateConnection(opts); - var pubConn = cf.CreateConnection(opts); - - var t = new Thread(() => - { - var s = subConn.SubscribeSync(_subject); - for (var i = 0; i < testCount; i++) - { - var m = s.NextMessage(); - subConn.Publish(m.Reply, payload); - subConn.Flush(); - } - }); - t.IsBackground = true; - t.Start(); - - Thread.Sleep(1000); - - var sw = Stopwatch.StartNew(); - for (var i = 0; i < testCount; i++) - { - await pubConn.RequestAsync(_subject, payload).ConfigureAwait(false); - } - - sw.Stop(); - - PrintResults(testName, sw, testCount, testSize); - - pubConn.Close(); - subConn.Close(); - } - } -} diff --git a/sandbox/NatsBenchmark/NatsBenchmark.csproj b/sandbox/NatsBenchmark/NatsBenchmark.csproj deleted file mode 100644 index b12d54eca..000000000 --- a/sandbox/NatsBenchmark/NatsBenchmark.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net6.0 - enable - enable - false - - - - - - - - - - - - - - - - diff --git a/sandbox/NatsBenchmark/Program.cs b/sandbox/NatsBenchmark/Program.cs deleted file mode 100644 index bac7c4ada..000000000 --- a/sandbox/NatsBenchmark/Program.cs +++ /dev/null @@ -1,845 +0,0 @@ -using System.Diagnostics; -using System.Runtime; -using System.Text; -using MessagePack; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using NATS.Client; -using NATS.Client.Core; -using NatsBenchmark; -using ZLogger; - -var isPortableThreadPool = await IsRunOnPortableThreadPoolAsync(); -Console.WriteLine($"RunOnPortableThreadPool:{isPortableThreadPool}"); -Console.WriteLine(new { GCSettings.IsServerGC, GCSettings.LatencyMode }); - -ThreadPool.SetMinThreads(1000, 1000); - -// var p = PublishCommand.Create(key, new Vector3(), serializer); -// (p as ICommand).Return(); -try -{ - // use only pubsub suite - new Benchmark(args); -} -catch (Exception e) -{ - Console.WriteLine("Error: " + e.Message); - Console.WriteLine(e); -} - -// COMPlus_ThreadPool_UsePortableThreadPool=0 -> false -static Task IsRunOnPortableThreadPoolAsync() -{ - var tcs = new TaskCompletionSource(); - ThreadPool.QueueUserWorkItem(_ => - { - var st = new StackTrace().ToString(); - tcs.TrySetResult(st.Contains("PortableThreadPool")); - }); - return tcs.Task; -} - -namespace NatsBenchmark -{ - public partial class Benchmark - { - private async Task RunPubSubBenchmark(string testName, long testCount, long testSize, bool disableShow = false) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Information); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - var options = NatsOpts.Default with - { - // LoggerFactory = loggerFactory, - UseThreadPoolCallback = false, - Echo = false, - Verbose = false, - }; - - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - var payload = GeneratePayload(testSize); - - var pubConn = new NATS.Client.Core.NatsConnection(options); - var subConn = new NATS.Client.Core.NatsConnection(options); - - pubConn.ConnectAsync().AsTask().Wait(); - subConn.ConnectAsync().AsTask().Wait(); - - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here:{0}", subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - } - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - var sw = Stopwatch.StartNew(); - - for (var i = 0; i < testCount; i++) - { - await pubConn.PublishAsync(_subject, payload); - } - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - if (!disableShow) - { - PrintResults(testName, sw, testCount, testSize); - } - - pubConn.DisposeAsync().AsTask().Wait(); - subConn.DisposeAsync().AsTask().Wait(); - } - - private void RunPubSubBenchmarkBatch(string testName, long testCount, long testSize, bool disableShow = false) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Trace); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - var options = NatsOpts.Default with - { - // LoggerFactory = loggerFactory, - UseThreadPoolCallback = false, - Echo = false, - Verbose = false, - }; - - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - var payload = GeneratePayload(testSize); - - var pubConn = new NATS.Client.Core.NatsConnection(options); - var subConn = new NATS.Client.Core.NatsConnection(options); - - pubConn.ConnectAsync().AsTask().Wait(); - subConn.ConnectAsync().AsTask().Wait(); - - Task.Run(async () => - { - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here:{0}", subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - } - }).GetAwaiter().GetResult(); - - var data = Enumerable.Range(0, 1000) - .Select(x => (_subject, payload)) - .ToArray(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - var sw = Stopwatch.StartNew(); - - var to = testCount / data.Length; - for (var i = 0; i < to; i++) - { - // pubConn.PostPublishBatch(data!); - foreach (var (subject, bytes) in data) - { - pubConn.PublishAsync(subject, bytes); - } - } - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - if (!disableShow) - { - PrintResults(testName, sw, testCount, testSize); - } - - pubConn.DisposeAsync().AsTask().Wait(); - subConn.DisposeAsync().AsTask().Wait(); - } - - private void ProfilingRunPubSubBenchmarkAsync(string testName, long testCount, long testSize, bool disableShow = false) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Trace); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - var options = NatsOpts.Default with - { - // LoggerFactory = loggerFactory, - UseThreadPoolCallback = false, - Echo = false, - Verbose = false, - }; - - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - var payload = GeneratePayload(testSize); - - var pubConn = new NATS.Client.Core.NatsConnection(options); - var subConn = new NATS.Client.Core.NatsConnection(options); - - pubConn.ConnectAsync().AsTask().Wait(); - subConn.ConnectAsync().AsTask().Wait(); - - Task.Run(async () => - { - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here:{0}", subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - } - }).GetAwaiter().GetResult(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - JetBrains.Profiler.Api.MemoryProfiler.ForceGc(); - JetBrains.Profiler.Api.MemoryProfiler.CollectAllocations(true); - JetBrains.Profiler.Api.MemoryProfiler.GetSnapshot("Before"); - var sw = Stopwatch.StartNew(); - - for (var i = 0; i < testCount; i++) - { - pubConn.PublishAsync(_subject, payload); - } - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - JetBrains.Profiler.Api.MemoryProfiler.GetSnapshot("Finished"); - - if (!disableShow) - { - PrintResults(testName, sw, testCount, testSize); - } - - pubConn.DisposeAsync().AsTask().Wait(); - subConn.DisposeAsync().AsTask().Wait(); - } - - private void RunPubSubBenchmarkBatchRaw(string testName, long testCount, long testSize, int batchSize = 1000, bool disableShow = false) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Information); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - var options = NatsOpts.Default with - { - // LoggerFactory = loggerFactory, - UseThreadPoolCallback = false, - Echo = false, - Verbose = false, - }; - - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - var payload = GeneratePayload(testSize); - - var pubConn = new NATS.Client.Core.NatsConnection(options); - var subConn = new NATS.Client.Core.NatsConnection(options); - - pubConn.ConnectAsync().AsTask().Wait(); - subConn.ConnectAsync().AsTask().Wait(); - - Task.Run(async () => - { - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here:{0}", subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - } - }).GetAwaiter().GetResult(); - - var command = new NATS.Client.Core.Commands.DirectWriteCommand(BuildCommand(testSize), batchSize); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - var sw = Stopwatch.StartNew(); - - var to = testCount / batchSize; - for (var i = 0; i < to; i++) - { - pubConn.PostDirectWrite(command); - } - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - if (!disableShow) - { - PrintResults(testName, sw, testCount, testSize); - } - - pubConn.DisposeAsync().AsTask().Wait(); - subConn.DisposeAsync().AsTask().Wait(); - } - - private string BuildCommand(long testSize) - { - var sb = new StringBuilder(); - sb.Append("PUB "); - sb.Append(_subject); - sb.Append(" "); - sb.Append(testSize); - if (testSize > 0) - { - sb.AppendLine(); - for (var i = 0; i < testSize; i++) - { - sb.Append('a'); - } - } - - return sb.ToString(); - } - - private void RunPubSubBenchmarkPubSub2(string testName, long testCount, long testSize, bool disableShow = false) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Trace); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - var options = NatsOpts.Default with - { - // LoggerFactory = loggerFactory, - UseThreadPoolCallback = false, - Echo = false, - Verbose = false, - }; - - var pubSubLock = new object(); - var pubSubLock2 = new object(); - var finished = false; - var finished2 = false; - var subCount = 0; - var subCount2 = 0; - - var payload = GeneratePayload(testSize); - - var pubConn = new NATS.Client.Core.NatsConnection(options); - var subConn = new NATS.Client.Core.NatsConnection(options); - var pubConn2 = new NATS.Client.Core.NatsConnection(options); - var subConn2 = new NATS.Client.Core.NatsConnection(options); - - pubConn.ConnectAsync().AsTask().Wait(); - subConn.ConnectAsync().AsTask().Wait(); - pubConn2.ConnectAsync().AsTask().Wait(); - subConn2.ConnectAsync().AsTask().Wait(); - - Task.Run(async () => - { - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here:{0}", subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - } - }).GetAwaiter().GetResult(); - Task.Run(async () => - { - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount2); - - // logger.LogInformation("here:{0}", subCount); - if (subCount2 == testCount) - { - lock (pubSubLock2) - { - finished2 = true; - Monitor.Pulse(pubSubLock2); - } - } - } - }).GetAwaiter().GetResult(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - var sw = Stopwatch.StartNew(); - var sw2 = Stopwatch.StartNew(); - var publishCount = testCount / 2; - for (var i = 0; i < publishCount; i++) - { - pubConn.PublishAsync(_subject, payload); - pubConn2.PublishAsync(_subject, payload); - } - - var t1 = Task.Run(() => - { - lock (pubSubLock) - { - if (!finished) - { - Monitor.Wait(pubSubLock); - sw.Stop(); - } - } - }); - - var t2 = Task.Run(() => - { - lock (pubSubLock2) - { - if (!finished2) - { - Monitor.Wait(pubSubLock2); - sw2.Stop(); - } - } - }); - - Task.WaitAll(t1, t2); - - if (!disableShow) - { - PrintResults(testName, sw, testCount, testSize); - PrintResults(testName, sw2, testCount, testSize); - } - - pubConn.DisposeAsync().AsTask().Wait(); - subConn.DisposeAsync().AsTask().Wait(); - } - - private void RunPubSubBenchmarkVector3(string testName, long testCount, bool disableShow = false) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Trace); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - var options = NatsOpts.Default with - { - // LoggerFactory = loggerFactory, - UseThreadPoolCallback = false, - Echo = false, - Verbose = false, - }; - - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - // byte[] payload = generatePayload(testSize); - var pubConn = new NATS.Client.Core.NatsConnection(options); - var subConn = new NATS.Client.Core.NatsConnection(options); - - pubConn.ConnectAsync().AsTask().Wait(); - subConn.ConnectAsync().AsTask().Wait(); - - Task.Run(async () => - { - await foreach (var unused in subConn.SubscribeAsync(_subject)) - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here:{0}", subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - - // JetBrains.Profiler.Api.MemoryProfiler.GetSnapshot("After"); - Monitor.Pulse(pubSubLock); - } - } - } - }).GetAwaiter().GetResult(); - - MessagePackSerializer.Serialize(default(Vector3)); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - var sw = Stopwatch.StartNew(); - - // JetBrains.Profiler.Api.MemoryProfiler.ForceGc(); - // JetBrains.Profiler.Api.MemoryProfiler.CollectAllocations(true); - // JetBrains.Profiler.Api.MemoryProfiler.GetSnapshot("Before"); - for (var i = 0; i < testCount; i++) - { - pubConn.PublishAsync(_subject, default(Vector3)); - } - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - if (!disableShow) - { - PrintResults(testName, sw, testCount, 16); - } - - pubConn.DisposeAsync().AsTask().Wait(); - subConn.DisposeAsync().AsTask().Wait(); - } - - private void RunPubSubVector3(string testName, long testCount) - { - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - // byte[] payload = generatePayload(testSize); - var cf = new ConnectionFactory(); - - var o = ConnectionFactory.GetDefaultOptions(); - o.ClosedEventHandler = (_, __) => { }; - o.DisconnectedEventHandler = (_, __) => { }; - - o.Url = _url; - o.SubChannelLength = 10000000; - if (_creds != null) - { - o.SetUserCredentials(_creds); - } - - o.AsyncErrorEventHandler += (sender, obj) => - { - Console.WriteLine("Error: " + obj.Error); - }; - - var subConn = cf.CreateConnection(o); - var pubConn = cf.CreateConnection(o); - - var s = subConn.SubscribeAsync(_subject, (sender, args) => - { - MessagePackSerializer.Deserialize(args.Message.Data); - - subCount++; - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - }); - s.SetPendingLimits(10000000, 1000000000); - subConn.Flush(); - - MessagePackSerializer.Serialize(default(Vector3)); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - var sw = Stopwatch.StartNew(); - - for (var i = 0; i < testCount; i++) - { - pubConn.Publish(_subject, MessagePackSerializer.Serialize(default(Vector3))); - } - - pubConn.Flush(); - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - PrintResults(testName, sw, testCount, 16); - - pubConn.Close(); - subConn.Close(); - } - - private void RunPubSubRedis(string testName, long testCount, long testSize) - { - var provider = new ServiceCollection() - .AddLogging(x => - { - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Information); - x.AddZLoggerConsole(); - }) - .BuildServiceProvider(); - - var loggerFactory = provider.GetRequiredService(); - var logger = loggerFactory.CreateLogger>(); - - var pubSubLock = new object(); - var finished = false; - var subCount = 0; - - var payload = GeneratePayload(testSize); - - var pubConn = StackExchange.Redis.ConnectionMultiplexer.Connect("localhost"); - var subConn = StackExchange.Redis.ConnectionMultiplexer.Connect("localhost"); - - subConn.GetSubscriber().Subscribe(_subject, (channel, v) => - { - Interlocked.Increment(ref subCount); - - // logger.LogInformation("here?:" + subCount); - if (subCount == testCount) - { - lock (pubSubLock) - { - finished = true; - Monitor.Pulse(pubSubLock); - } - } - }); - - var sw = Stopwatch.StartNew(); - - for (var i = 0; i < testCount; i++) - { - _ = pubConn.GetDatabase().PublishAsync(_subject, payload, StackExchange.Redis.CommandFlags.FireAndForget); - } - - lock (pubSubLock) - { - if (!finished) - Monitor.Wait(pubSubLock); - } - - sw.Stop(); - - PrintResults(testName, sw, testCount, testSize); - - Console.WriteLine("COMPLETE"); - - pubConn.Dispose(); - subConn.Dispose(); - } - - private void RunSuite() - { - // RunPubSubBenchmark("Benchmark8b", 10000000, 8, disableShow: true); - // RunPubSubBenchmarkVector3("BenchmarkV3", 10000000, disableShow: true); - - // ProfilingRunPubSubBenchmarkAsync("BenchmarkProfiling", 10000000, 0); - - // - // RunPubSubBenchmarkBatchRaw("Benchmark", 10000000, 8, disableShow: true); // warmup - // RunPubSubBenchmarkBatchRaw("Benchmark", 500000, 1024 * 4, disableShow: true); // warmup - // RunPubSubBenchmarkBatchRaw("Benchmark", 100000, 1024 * 8, disableShow: true); // warmup - // RunPubSubBenchmarkBatchRaw("Benchmark8b_Opt", 10000000, 8); - // RunPubSubBenchmarkBatchRaw("Benchmark4k_Opt", 500000, 1024 * 4); - RunPubSubBenchmarkBatchRaw("Benchmark8k_Opt", 100000, 1024 * 8, batchSize: 10, disableShow: true); - RunPubSubBenchmarkBatchRaw("Benchmark8k_Opt", 100000, 1024 * 8, batchSize: 10); - RunPubSubBenchmark("Benchmark8k", 100000, 1024 * 8, disableShow: true).GetAwaiter().GetResult(); - RunPubSubBenchmark("Benchmark8k", 100000, 1024 * 8).GetAwaiter().GetResult(); - - // RunPubSubBenchmark("Benchmark8b", 10000000, 8, disableShow: true); - // RunPubSubBenchmark("Benchmark8b", 10000000, 8); - - // runPubSubVector3("PubSubVector3", 10000000); - // runPubSub("PubSubNo", 10000000, 0); - // runPubSub("PubSub8b", 10000000, 8); - - // runPubSub("PubSub8b", 10000000, 8); - - // runPubSub("PubSub32b", 10000000, 32); - // runPubSub("PubSub100b", 10000000, 100); - // runPubSub("PubSub256b", 10000000, 256); - // runPubSub("PubSub512b", 500000, 512); - // runPubSub("PubSub1k", 500000, 1024); - // runPubSub("PubSub4k", 500000, 1024 * 4); - RunPubSub("PubSub8k", 100000, 1024 * 8); - - // RunPubSubBenchmarkVector3("BenchmarkV3", 10000000); - // RunPubSubBenchmark("BenchmarkNo", 10000000, 0); - // RunPubSubBenchmark("Benchmark8b", 10000000, 8); - // RunPubSubBenchmarkBatch("Benchmark8bBatch", 10000000, 8); - // RunPubSubBenchmarkPubSub2("Benchmark8b 2", 10000000, 8); - - // RunPubSubBenchmark("Benchmark32b", 10000000, 32); - // RunPubSubBenchmark("Benchmark100b", 10000000, 100); - // RunPubSubBenchmark("Benchmark256b", 10000000, 256); - // RunPubSubBenchmark("Benchmark512b", 500000, 512); - // RunPubSubBenchmark("Benchmark1k", 500000, 1024); - // RunPubSubBenchmark("Benchmark4k", 500000, 1024 * 4); - // RunPubSubBenchmark("Benchmark8k", 100000, 1024 * 8); - - // Redis? - // RunPubSubRedis("StackExchange.Redis", 10000000, 8); - // RunPubSubRedis("Redis 100", 10000000, 100); - - // These run significantly slower. - // req->server->reply->server->req - // runReqReply("ReqReplNo", 20000, 0); - // runReqReply("ReqRepl8b", 10000, 8); - // runReqReply("ReqRepl32b", 10000, 32); - // runReqReply("ReqRepl256b", 5000, 256); - // runReqReply("ReqRepl512b", 5000, 512); - // runReqReply("ReqRepl1k", 5000, 1024); - // runReqReply("ReqRepl4k", 5000, 1024 * 4); - // runReqReply("ReqRepl8k", 5000, 1024 * 8); - - // runReqReplyAsync("ReqReplAsyncNo", 20000, 0).Wait(); - // runReqReplyAsync("ReqReplAsync8b", 10000, 8).Wait(); - // runReqReplyAsync("ReqReplAsync32b", 10000, 32).Wait(); - // runReqReplyAsync("ReqReplAsync256b", 5000, 256).Wait(); - // runReqReplyAsync("ReqReplAsync512b", 5000, 512).Wait(); - // runReqReplyAsync("ReqReplAsync1k", 5000, 1024).Wait(); - // runReqReplyAsync("ReqReplAsync4k", 5000, 1024 * 4).Wait(); - // runReqReplyAsync("ReqReplAsync8k", 5000, 1024 * 8).Wait(); - - // runPubSubLatency("LatNo", 500, 0); - // runPubSubLatency("Lat8b", 500, 8); - // runPubSubLatency("Lat32b", 500, 32); - // runPubSubLatency("Lat256b", 500, 256); - // runPubSubLatency("Lat512b", 500, 512); - // runPubSubLatency("Lat1k", 500, 1024); - // runPubSubLatency("Lat4k", 500, 1024 * 4); - // runPubSubLatency("Lat8k", 500, 1024 * 8); - } - } -} - -[MessagePackObject] -public struct Vector3 -{ - [Key(0)] - public float X; - [Key(1)] - public float Y; - [Key(2)] - public float Z; -} - -internal static class NatsMsgTestUtils -{ - internal static NatsSub? Register(this NatsSub? sub, Action> action) - { - if (sub == null) - return null; - Task.Run(async () => - { - await foreach (var natsMsg in sub.Msgs.ReadAllAsync()) - { - action(natsMsg); - } - }); - return sub; - } -} diff --git a/sandbox/NatsBenchmark/Properties/launchSettings.json b/sandbox/NatsBenchmark/Properties/launchSettings.json deleted file mode 100644 index 381d40f38..000000000 --- a/sandbox/NatsBenchmark/Properties/launchSettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "profiles": { - "NatsBenchmark": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_TieredPGO": "1", - "DOTNET_ReadyToRun": "0", - "DOTNET_TC_QuickJitForLoops": "1", - "DOTNET_gcServer": "1" - } - } - } -} diff --git a/src/NATS.Client.Core/Commands/CommandBase.cs b/src/NATS.Client.Core/Commands/CommandBase.cs deleted file mode 100644 index 32b0df559..000000000 --- a/src/NATS.Client.Core/Commands/CommandBase.cs +++ /dev/null @@ -1,371 +0,0 @@ -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading.Tasks.Sources; -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal abstract class CommandBase : ICommand, IObjectPoolNode - where TSelf : class, IObjectPoolNode -{ - private static readonly Action CancelAction = SetCancel; - - private TSelf? _next; - private CancellationTokenRegistration _timerRegistration; - private CancellationTimer? _timer; - - public virtual bool IsCanceled { get; private set; } - - public ref TSelf? NextNode => ref _next; - - void ICommand.Return(ObjectPool pool) - { - _timerRegistration.Dispose(); // wait for cancel callback complete - _timerRegistration = default; - - // if failed to return timer, maybe invoked timer callback so avoid race condition, does not return command itself to pool. - if (!IsCanceled && (_timer == null || _timer.TryReturn())) - { - _timer = null; - Reset(); - pool.Return(Unsafe.As(this)); - } - } - - public abstract void Write(ProtocolWriter writer); - - public void SetCancellationTimer(CancellationTimer timer) - { - _timer = timer; - _timerRegistration = timer.Token.UnsafeRegister(CancelAction, this); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static bool TryRent(ObjectPool pool, [NotNullWhen(true)] out TSelf? self) - { - return pool.TryRent(out self!); - } - - protected abstract void Reset(); - - private static void SetCancel(object? state) - { - var self = (CommandBase)state!; - self.IsCanceled = true; - } -} - -internal abstract class AsyncCommandBase : ICommand, IAsyncCommand, IObjectPoolNode, IValueTaskSource, IPromise, IThreadPoolWorkItem - where TSelf : class, IObjectPoolNode -{ - private static readonly Action CancelAction = SetCancel; - - private TSelf? _next; - private CancellationTokenRegistration _timerRegistration; - private CancellationTimer? _timer; - - private ObjectPool? _objectPool; - private bool _noReturn; - - private ManualResetValueTaskSourceCore _core; - - public bool IsCanceled { get; private set; } - - public ref TSelf? NextNode => ref _next; - - void ICommand.Return(ObjectPool pool) - { - // don't return manually, only allows from await. - // however, set pool on this timing. - _objectPool = pool; - } - - public abstract void Write(ProtocolWriter writer); - - public ValueTask AsValueTask() - { - return new ValueTask(this, _core.Version); - } - - public void SetResult() - { - // succeed operation, remove canceler - _timerRegistration.Dispose(); - _timerRegistration = default; - - if (IsCanceled) - return; // already called Canceled, it invoked SetCanceled. - - if (_timer != null) - { - if (!_timer.TryReturn()) - { - // cancel is called. don't set result. - return; - } - - _timer = null; - } - - ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); - } - - public void SetCanceled() - { - if (_noReturn) - return; - - _timerRegistration.Dispose(); - _timerRegistration = default; - - _noReturn = true; - ThreadPool.UnsafeQueueUserWorkItem( - state => - { - var ex = state._timer != null - ? state._timer.GetExceptionWhenCanceled() - : new OperationCanceledException(); - - state._core.SetException(ex); - }, - this, - preferLocal: false); - } - - public void SetException(Exception exception) - { - if (_noReturn) - return; - - _timerRegistration.Dispose(); - _timerRegistration = default; - - _noReturn = true; - ThreadPool.UnsafeQueueUserWorkItem( - state => - { - state.self._core.SetException(state.exception); - }, - (self: this, exception), - preferLocal: false); - } - - void IValueTaskSource.GetResult(short token) - { - try - { - _core.GetResult(token); - } - finally - { - _core.Reset(); - Reset(); - var p = _objectPool; - _objectPool = null; - _timer = null; - _timerRegistration = default; - - // canceled object don't return pool to avoid call SetResult/Exception after await - if (p != null && !_noReturn) - { - p.Return(Unsafe.As(this)); - } - } - } - - ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) - { - return _core.GetStatus(token); - } - - void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) - { - _core.OnCompleted(continuation, state, token, flags); - } - - void IThreadPoolWorkItem.Execute() - { - _core.SetResult(null!); - } - - public void SetCancellationTimer(CancellationTimer timer) - { - _timer = timer; - _timerRegistration = timer.Token.UnsafeRegister(CancelAction, this); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static bool TryRent(ObjectPool pool, [NotNullWhen(true)] out TSelf? self) - { - return pool.TryRent(out self!); - } - - protected abstract void Reset(); - - private static void SetCancel(object? state) - { - var self = (AsyncCommandBase)state!; - self.IsCanceled = true; - self.SetCanceled(); - } -} - -internal abstract class AsyncCommandBase : ICommand, IAsyncCommand, IObjectPoolNode, IValueTaskSource, IPromise, IPromise, IThreadPoolWorkItem - where TSelf : class, IObjectPoolNode -{ - private static readonly Action CancelAction = SetCancel; - - private TSelf? _next; - private CancellationTokenRegistration _timerRegistration; - private CancellationTimer? _timer; - private ManualResetValueTaskSourceCore _core; - private TResponse? _response; - private ObjectPool? _objectPool; - private bool _noReturn; - - public bool IsCanceled { get; private set; } - - public ref TSelf? NextNode => ref _next; - - void ICommand.Return(ObjectPool pool) - { - // don't return manually, only allows from await. - // however, set pool on this timing. - _objectPool = pool; - } - - public abstract void Write(ProtocolWriter writer); - - public ValueTask AsValueTask() - { - return new ValueTask(this, _core.Version); - } - - void IPromise.SetResult() - { - // called when SocketWriter.Flush, however continuation should run on response received. - } - - public void SetResult(TResponse result) - { - _response = result; - - if (IsCanceled) - return; // already called Canceled, it invoked SetCanceled. - - _timerRegistration.Dispose(); - _timerRegistration = default; - - if (_timer != null && _objectPool != null) - { - if (!_timer.TryReturn()) - { - // cancel is called. don't set result. - return; - } - - _timer = null; - } - - ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); - } - - public void SetCanceled() - { - _noReturn = true; - - _timerRegistration.Dispose(); - _timerRegistration = default; - - ThreadPool.UnsafeQueueUserWorkItem( - state => - { - var ex = state._timer != null - ? state._timer.GetExceptionWhenCanceled() - : new OperationCanceledException(); - state._core.SetException(ex); - }, - this, - preferLocal: false); - } - - public void SetException(Exception exception) - { - if (_noReturn) - return; - - _timerRegistration.Dispose(); - _timerRegistration = default; - - _noReturn = true; - ThreadPool.UnsafeQueueUserWorkItem( - state => - { - state.self._core.SetException(state.exception); - }, - (self: this, exception), - preferLocal: false); - } - - TResponse IValueTaskSource.GetResult(short token) - { - try - { - return _core.GetResult(token); - } - finally - { - _core.Reset(); - _response = default!; - Reset(); - var p = _objectPool; - _objectPool = null; - _timer = null; - _timerRegistration = default; - - // canceled object don't return pool to avoid call SetResult/Exception after await - if (p != null && !_noReturn) - { - p.Return(Unsafe.As(this)); - } - } - } - - ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) - { - return _core.GetStatus(token); - } - - void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) - { - _core.OnCompleted(continuation, state, token, flags); - } - - void IThreadPoolWorkItem.Execute() - { - _core.SetResult(_response!); - } - - public void SetCancellationTimer(CancellationTimer timer) - { - _timer = timer; - _timerRegistration = timer.Token.UnsafeRegister(CancelAction, this); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static bool TryRent(ObjectPool pool, [NotNullWhen(true)] out TSelf? self) - { - return pool.TryRent(out self!); - } - - protected abstract void Reset(); - - private static void SetCancel(object? state) - { - var self = (AsyncCommandBase)state!; - self.IsCanceled = true; - self.SetCanceled(); - } -} diff --git a/src/NATS.Client.Core/Commands/CommandWriter.cs b/src/NATS.Client.Core/Commands/CommandWriter.cs new file mode 100644 index 000000000..818b7b766 --- /dev/null +++ b/src/NATS.Client.Core/Commands/CommandWriter.cs @@ -0,0 +1,658 @@ +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using NATS.Client.Core.Internal; + +namespace NATS.Client.Core.Commands; + +/// +/// Used to track commands that have been enqueued to the PipeReader +/// +internal readonly record struct QueuedCommand(int Size, int Trim = 0); + +/// +/// Sets up a Pipe, and provides methods to write to the PipeWriter +/// When methods complete, they have been queued for sending +/// and further cancellation is not possible +/// +/// +/// These methods are in the hot path, and have all been +/// optimized to eliminate allocations and make an initial attempt +/// to run synchronously without the async state machine +/// +internal sealed class CommandWriter : IAsyncDisposable +{ + private readonly ConnectionStatsCounter _counter; + private readonly TimeSpan _defaultCommandTimeout; + private readonly Action _enqueuePing; + private readonly NatsOpts _opts; + private readonly PipeWriter _pipeWriter; + private readonly ProtocolWriter _protocolWriter; + private readonly ChannelWriter _queuedCommandsWriter; + private readonly SemaphoreSlim _semLock; + private Task? _flushTask; + private bool _disposed; + + public CommandWriter(NatsOpts opts, ConnectionStatsCounter counter, Action enqueuePing, TimeSpan? overrideCommandTimeout = default) + { + _counter = counter; + _defaultCommandTimeout = overrideCommandTimeout ?? opts.CommandTimeout; + _enqueuePing = enqueuePing; + _opts = opts; + var pipe = new Pipe(new PipeOptions( + pauseWriterThreshold: opts.WriterBufferSize, // flush will block after hitting + resumeWriterThreshold: opts.WriterBufferSize / 2, // will start flushing again after catching up + minimumSegmentSize: 16384, // segment that is part of an uninterrupted payload can be sent using socket.send + useSynchronizationContext: false)); + PipeReader = pipe.Reader; + _pipeWriter = pipe.Writer; + _protocolWriter = new ProtocolWriter(_pipeWriter, opts.SubjectEncoding, opts.HeaderEncoding); + var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleWriter = true, SingleReader = true }); + _semLock = new SemaphoreSlim(1); + QueuedCommandsReader = channel.Reader; + _queuedCommandsWriter = channel.Writer; + } + + public PipeReader PipeReader { get; } + + public ChannelReader QueuedCommandsReader { get; } + + public Queue InFlightCommands { get; } = new(); + + public async ValueTask DisposeAsync() + { + await _semLock.WaitAsync().ConfigureAwait(false); + try + { + if (_disposed) + { + return; + } + + _disposed = true; + _queuedCommandsWriter.Complete(); + await _pipeWriter.CompleteAsync().ConfigureAwait(false); + } + finally + { + _semLock.Release(); + } + } + + public NatsPipeliningWriteProtocolProcessor CreateNatsPipeliningWriteProtocolProcessor(ISocketConnection socketConnection) => new(socketConnection, this, _opts, _counter); + + public ValueTask ConnectAsync(ClientOpts connectOpts, CancellationToken cancellationToken) + { +#pragma warning disable CA2016 +#pragma warning disable VSTHRD103 + if (!_semLock.Wait(0)) +#pragma warning restore VSTHRD103 +#pragma warning restore CA2016 + { + return ConnectStateMachineAsync(false, connectOpts, cancellationToken); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + return ConnectStateMachineAsync(true, connectOpts, cancellationToken); + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + var success = false; + try + { + _protocolWriter.WriteConnect(connectOpts); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + + return ValueTask.CompletedTask; + } + + public ValueTask PingAsync(PingCommand pingCommand, CancellationToken cancellationToken) + { +#pragma warning disable CA2016 +#pragma warning disable VSTHRD103 + if (!_semLock.Wait(0)) +#pragma warning restore VSTHRD103 +#pragma warning restore CA2016 + { + return PingStateMachineAsync(false, pingCommand, cancellationToken); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + return PingStateMachineAsync(true, pingCommand, cancellationToken); + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + var success = false; + try + { + _protocolWriter.WritePing(); + _enqueuePing(pingCommand); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + + return ValueTask.CompletedTask; + } + + public ValueTask PongAsync(CancellationToken cancellationToken = default) + { +#pragma warning disable CA2016 +#pragma warning disable VSTHRD103 + if (!_semLock.Wait(0)) +#pragma warning restore VSTHRD103 +#pragma warning restore CA2016 + { + return PongStateMachineAsync(false, cancellationToken); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + return PongStateMachineAsync(true, cancellationToken); + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + var success = false; + try + { + _protocolWriter.WritePong(); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + + return ValueTask.CompletedTask; + } + + public ValueTask PublishAsync(string subject, T? value, NatsHeaders? headers, string? replyTo, INatsSerialize serializer, CancellationToken cancellationToken) + { +#pragma warning disable CA2016 +#pragma warning disable VSTHRD103 + if (!_semLock.Wait(0)) +#pragma warning restore VSTHRD103 +#pragma warning restore CA2016 + { + return PublishStateMachineAsync(false, subject, value, headers, replyTo, serializer, cancellationToken); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + return PublishStateMachineAsync(true, subject, value, headers, replyTo, serializer, cancellationToken); + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + var trim = 0; + var success = false; + try + { + trim = _protocolWriter.WritePublish(subject, value, headers, replyTo, serializer); + success = true; + } + finally + { + EnqueueCommand(success, trim: trim); + } + } + finally + { + _semLock.Release(); + } + + return ValueTask.CompletedTask; + } + + public ValueTask SubscribeAsync(int sid, string subject, string? queueGroup, int? maxMsgs, CancellationToken cancellationToken) + { +#pragma warning disable CA2016 +#pragma warning disable VSTHRD103 + if (!_semLock.Wait(0)) +#pragma warning restore VSTHRD103 +#pragma warning restore CA2016 + { + return SubscribeStateMachineAsync(false, sid, subject, queueGroup, maxMsgs, cancellationToken); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + return SubscribeStateMachineAsync(true, sid, subject, queueGroup, maxMsgs, cancellationToken); + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + var success = false; + try + { + _protocolWriter.WriteSubscribe(sid, subject, queueGroup, maxMsgs); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + + return ValueTask.CompletedTask; + } + + public ValueTask UnsubscribeAsync(int sid, CancellationToken cancellationToken) + { +#pragma warning disable CA2016 +#pragma warning disable VSTHRD103 + if (!_semLock.Wait(0)) +#pragma warning restore VSTHRD103 +#pragma warning restore CA2016 + { + return UnsubscribeStateMachineAsync(false, sid, cancellationToken); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + return UnsubscribeStateMachineAsync(true, sid, cancellationToken); + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + var success = false; + try + { + _protocolWriter.WriteUnsubscribe(sid, null); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + + return ValueTask.CompletedTask; + } + + // only used for internal testing + internal async Task TestStallFlushAsync(TimeSpan timeSpan) + { + await _semLock.WaitAsync().ConfigureAwait(false); + + try + { + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.ConfigureAwait(false); + } + + _flushTask = Task.Delay(timeSpan); + } + finally + { + _semLock.Release(); + } + } + + private async ValueTask ConnectStateMachineAsync(bool lockHeld, ClientOpts connectOpts, CancellationToken cancellationToken) + { + if (!lockHeld) + { + if (!await _semLock.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false)) + { + throw new TimeoutException(); + } + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false); + } + + var success = false; + try + { + _protocolWriter.WriteConnect(connectOpts); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + } + + private async ValueTask PingStateMachineAsync(bool lockHeld, PingCommand pingCommand, CancellationToken cancellationToken) + { + if (!lockHeld) + { + if (!await _semLock.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false)) + { + throw new TimeoutException(); + } + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false); + } + + var success = false; + try + { + _protocolWriter.WritePing(); + _enqueuePing(pingCommand); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + } + + private async ValueTask PongStateMachineAsync(bool lockHeld, CancellationToken cancellationToken) + { + if (!lockHeld) + { + if (!await _semLock.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false)) + { + throw new TimeoutException(); + } + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false); + } + + var success = false; + try + { + _protocolWriter.WritePong(); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + } + + private async ValueTask PublishStateMachineAsync(bool lockHeld, string subject, T? value, NatsHeaders? headers, string? replyTo, INatsSerialize serializer, CancellationToken cancellationToken) + { + if (!lockHeld) + { + if (!await _semLock.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false)) + { + throw new TimeoutException(); + } + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false); + } + + var trim = 0; + var success = false; + try + { + trim = _protocolWriter.WritePublish(subject, value, headers, replyTo, serializer); + success = true; + } + finally + { + EnqueueCommand(success, trim: trim); + } + } + finally + { + _semLock.Release(); + } + } + + private async ValueTask SubscribeStateMachineAsync(bool lockHeld, int sid, string subject, string? queueGroup, int? maxMsgs, CancellationToken cancellationToken) + { + if (!lockHeld) + { + if (!await _semLock.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false)) + { + throw new TimeoutException(); + } + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false); + } + + var success = false; + try + { + _protocolWriter.WriteSubscribe(sid, subject, queueGroup, maxMsgs); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + } + + private async ValueTask UnsubscribeStateMachineAsync(bool lockHeld, int sid, CancellationToken cancellationToken) + { + if (!lockHeld) + { + if (!await _semLock.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false)) + { + throw new TimeoutException(); + } + } + + try + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CommandWriter)); + } + + if (_flushTask is { IsCompletedSuccessfully: false }) + { + await _flushTask.WaitAsync(_defaultCommandTimeout, cancellationToken).ConfigureAwait(false); + } + + var success = false; + try + { + _protocolWriter.WriteUnsubscribe(sid, null); + success = true; + } + finally + { + EnqueueCommand(success); + } + } + finally + { + _semLock.Release(); + } + } + + /// + /// Enqueues a command, and kicks off a flush + /// + /// + /// Whether the command was successful + /// If true, it will be sent on the wire + /// If false, it will be thrown out + /// + /// + /// Number of bytes to skip from beginning of message + /// when sending on the wire + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnqueueCommand(bool success, int trim = 0) + { + var size = (int)_pipeWriter.UnflushedBytes; + if (size == 0) + { + // no unflushed bytes means no command was produced + _flushTask = null; + return; + } + + if (success) + { + Interlocked.Add(ref _counter.PendingMessages, 1); + } + + _queuedCommandsWriter.TryWrite(new QueuedCommand(Size: size, Trim: success ? trim : size)); + var flush = _pipeWriter.FlushAsync(); + _flushTask = flush.IsCompletedSuccessfully ? null : flush.AsTask(); + } +} + +internal sealed class PriorityCommandWriter : IAsyncDisposable +{ + private readonly NatsPipeliningWriteProtocolProcessor _natsPipeliningWriteProtocolProcessor; + private int _disposed; + + public PriorityCommandWriter(ISocketConnection socketConnection, NatsOpts opts, ConnectionStatsCounter counter, Action enqueuePing) + { + CommandWriter = new CommandWriter(opts, counter, enqueuePing, overrideCommandTimeout: TimeSpan.MaxValue); + _natsPipeliningWriteProtocolProcessor = CommandWriter.CreateNatsPipeliningWriteProtocolProcessor(socketConnection); + } + + public CommandWriter CommandWriter { get; } + + public async ValueTask DisposeAsync() + { + if (Interlocked.Increment(ref _disposed) == 1) + { + // disposing command writer marks pipe writer as complete + await CommandWriter.DisposeAsync().ConfigureAwait(false); + try + { + // write loop will complete once pipe reader completes + await _natsPipeliningWriteProtocolProcessor.WriteLoop.ConfigureAwait(false); + } + finally + { + await _natsPipeliningWriteProtocolProcessor.DisposeAsync().ConfigureAwait(false); + } + } + } +} diff --git a/src/NATS.Client.Core/Commands/ConnectCommand.cs b/src/NATS.Client.Core/Commands/ConnectCommand.cs deleted file mode 100644 index 9cfe438c2..000000000 --- a/src/NATS.Client.Core/Commands/ConnectCommand.cs +++ /dev/null @@ -1,35 +0,0 @@ -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal sealed class AsyncConnectCommand : AsyncCommandBase -{ - private ClientOpts? _clientOpts; - - private AsyncConnectCommand() - { - } - - public static AsyncConnectCommand Create(ObjectPool pool, ClientOpts connectOpts, CancellationTimer timer) - { - if (!TryRent(pool, out var result)) - { - result = new AsyncConnectCommand(); - } - - result._clientOpts = connectOpts; - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WriteConnect(_clientOpts!); - } - - protected override void Reset() - { - _clientOpts = null; - } -} diff --git a/src/NATS.Client.Core/Commands/DirectWriteCommand.cs b/src/NATS.Client.Core/Commands/DirectWriteCommand.cs deleted file mode 100644 index 433d20a37..000000000 --- a/src/NATS.Client.Core/Commands/DirectWriteCommand.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Text; -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -// public for optimize reusing -internal sealed class DirectWriteCommand : ICommand -{ - private readonly byte[] _protocol; - - /// raw command without \r\n - /// repeating count. - public DirectWriteCommand(string protocol, int repeatCount) - { - if (repeatCount < 1) - throw new ArgumentException("repeatCount should >= 1, repeatCount:" + repeatCount); - - if (repeatCount == 1) - { - _protocol = Encoding.UTF8.GetBytes(protocol + "\r\n"); - } - else - { - var bin = Encoding.UTF8.GetBytes(protocol + "\r\n"); - _protocol = new byte[bin.Length * repeatCount]; - var span = _protocol.AsSpan(); - for (var i = 0; i < repeatCount; i++) - { - bin.CopyTo(span); - span = span.Slice(bin.Length); - } - } - } - - /// raw command protocol, requires \r\n. - public DirectWriteCommand(byte[] protocol) - { - _protocol = protocol; - } - - bool ICommand.IsCanceled => false; - - void ICommand.Return(ObjectPool pool) - { - } - - void ICommand.Write(ProtocolWriter writer) - { - writer.WriteRaw(_protocol); - } - - void ICommand.SetCancellationTimer(CancellationTimer timer) - { - // direct write is not supporting cancellationtimer. - } -} diff --git a/src/NATS.Client.Core/Commands/ICommand.cs b/src/NATS.Client.Core/Commands/ICommand.cs deleted file mode 100644 index 498cd4561..000000000 --- a/src/NATS.Client.Core/Commands/ICommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.Extensions.Logging; -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal interface ICommand -{ - bool IsCanceled { get; } - - void SetCancellationTimer(CancellationTimer timer); - - void Return(ObjectPool pool); - - void Write(ProtocolWriter writer); -} - -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - -internal interface IAsyncCommand : ICommand -{ - ValueTask AsValueTask(); -} - -internal interface IAsyncCommand : ICommand -{ - ValueTask AsValueTask(); -} - -internal interface IBatchCommand : ICommand -{ - new int Write(ProtocolWriter writer); -} diff --git a/src/NATS.Client.Core/Commands/IPromise.cs b/src/NATS.Client.Core/Commands/IPromise.cs deleted file mode 100644 index 52cce41ef..000000000 --- a/src/NATS.Client.Core/Commands/IPromise.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace NATS.Client.Core.Commands; - -internal interface IPromise -{ - void SetResult(); - - void SetCanceled(); - - void SetException(Exception exception); -} - -internal interface IPromise : IPromise -{ - void SetResult(T result); -} diff --git a/src/NATS.Client.Core/Commands/PingCommand.cs b/src/NATS.Client.Core/Commands/PingCommand.cs index 610bcac06..1861adc0b 100644 --- a/src/NATS.Client.Core/Commands/PingCommand.cs +++ b/src/NATS.Client.Core/Commands/PingCommand.cs @@ -1,68 +1,12 @@ -using NATS.Client.Core.Internal; - namespace NATS.Client.Core.Commands; -internal sealed class PingCommand : CommandBase -{ - private PingCommand() - { - } - - public static PingCommand Create(ObjectPool pool, CancellationTimer timer) - { - if (!TryRent(pool, out var result)) - { - result = new PingCommand(); - } - - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WritePing(); - } - - protected override void Reset() - { - } -} - -internal sealed class AsyncPingCommand : AsyncCommandBase +internal struct PingCommand { - private NatsConnection? _connection; - - private AsyncPingCommand() - { - } - - public DateTimeOffset? WriteTime { get; private set; } - - public static AsyncPingCommand Create(NatsConnection connection, ObjectPool pool, CancellationTimer timer) + public PingCommand() { - if (!TryRent(pool, out var result)) - { - result = new AsyncPingCommand(); - } - - result._connection = connection; - result.SetCancellationTimer(timer); - - return result; } - public override void Write(ProtocolWriter writer) - { - WriteTime = DateTimeOffset.UtcNow; - _connection!.EnqueuePing(this); - writer.WritePing(); - } + public DateTimeOffset WriteTime { get; } = DateTimeOffset.UtcNow; - protected override void Reset() - { - WriteTime = null; - _connection = null; - } + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); } diff --git a/src/NATS.Client.Core/Commands/PongCommand.cs b/src/NATS.Client.Core/Commands/PongCommand.cs deleted file mode 100644 index b7e9d5ad0..000000000 --- a/src/NATS.Client.Core/Commands/PongCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal sealed class PongCommand : CommandBase -{ - private PongCommand() - { - } - - public static PongCommand Create(ObjectPool pool, CancellationTimer timer) - { - if (!TryRent(pool, out var result)) - { - result = new PongCommand(); - } - - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WritePong(); - } - - protected override void Reset() - { - } -} diff --git a/src/NATS.Client.Core/Commands/ProtocolWriter.cs b/src/NATS.Client.Core/Commands/ProtocolWriter.cs index 107fea8a1..aaf224365 100644 --- a/src/NATS.Client.Core/Commands/ProtocolWriter.cs +++ b/src/NATS.Client.Core/Commands/ProtocolWriter.cs @@ -1,25 +1,25 @@ -using System.Buffers; using System.Buffers.Text; +using System.IO.Pipelines; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using NATS.Client.Core.Internal; namespace NATS.Client.Core.Commands; internal sealed class ProtocolWriter { - private const int MaxIntStringLength = 10; // int.MaxValue.ToString().Length + private const int MaxIntStringLength = 9; // https://github.com/nats-io/nats-server/blob/28a2a1000045b79927ebf6b75eecc19c1b9f1548/server/util.go#L85C8-L85C23 private const int NewLineLength = 2; // \r\n - private readonly FixedArrayBufferWriter _writer; // where T : IBufferWriter - private readonly FixedArrayBufferWriter _bufferHeaders = new(); - private readonly FixedArrayBufferWriter _bufferPayload = new(); - private readonly HeaderWriter _headerWriter = new(Encoding.UTF8); + private readonly PipeWriter _writer; + private readonly HeaderWriter _headerWriter; + private readonly Encoding _subjectEncoding; - public ProtocolWriter(FixedArrayBufferWriter writer) + public ProtocolWriter(PipeWriter writer, Encoding subjectEncoding, Encoding headerEncoding) { _writer = writer; + _subjectEncoding = subjectEncoding; + _headerWriter = new HeaderWriter(writer, headerEncoding); } // https://docs.nats.io/reference/reference-protocols/nats-protocol#connect @@ -48,70 +48,131 @@ public void WritePong() // https://docs.nats.io/reference/reference-protocols/nats-protocol#pub // PUB [reply-to] <#bytes>\r\n[payload]\r\n - public void WritePublish(string subject, string? replyTo, NatsHeaders? headers, ReadOnlySequence payload) + // or + // https://docs.nats.io/reference/reference-protocols/nats-protocol#hpub + // HPUB [reply-to] <#header bytes> <#total bytes>\r\n[headers]\r\n\r\n[payload]\r\n + // + // returns the number of bytes that should be skipped when writing to the wire + public int WritePublish(string subject, T? value, NatsHeaders? headers, string? replyTo, INatsSerialize serializer) { - // We use a separate buffer to write the headers so that we can calculate the - // size before we write to the output buffer '_writer'. - if (headers != null) + int ctrlLen; + if (headers == null) { - _bufferHeaders.Reset(); - _headerWriter.Write(_bufferHeaders, headers); + // 'PUB ' + subject +' '+ payload len +'\r\n' + ctrlLen = 4 + _subjectEncoding.GetByteCount(subject) + 1 + MaxIntStringLength + 2; + } + else + { + // 'HPUB ' + subject +' '+ header len +' '+ payload len +'\r\n' + ctrlLen = 5 + _subjectEncoding.GetByteCount(subject) + 1 + MaxIntStringLength + 1 + MaxIntStringLength + 2; } - - // Start writing the message to buffer: - // PUP / HPUB - _writer.WriteSpan(headers == null ? CommandConstants.PubWithPadding : CommandConstants.HPubWithPadding); - _writer.WriteASCIIAndSpace(subject); if (replyTo != null) { - _writer.WriteASCIIAndSpace(replyTo); + // len += replyTo +' ' + ctrlLen += _subjectEncoding.GetByteCount(replyTo) + 1; } + var ctrlSpan = _writer.GetSpan(ctrlLen); + var span = ctrlSpan; if (headers == null) { - _writer.WriteNumber(payload.Length); + span[0] = (byte)'P'; + span[1] = (byte)'U'; + span[2] = (byte)'B'; + span[3] = (byte)' '; + span = span[4..]; } else { - var headersLength = _bufferHeaders.WrittenSpan.Length; - _writer.WriteNumber(CommandConstants.NatsHeaders10NewLine.Length + headersLength); - _writer.WriteSpace(); - var total = CommandConstants.NatsHeaders10NewLine.Length + headersLength + payload.Length; - _writer.WriteNumber(total); + span[0] = (byte)'H'; + span[1] = (byte)'P'; + span[2] = (byte)'U'; + span[3] = (byte)'B'; + span[4] = (byte)' '; + span = span[5..]; } - // End of message first line - _writer.WriteNewLine(); + var written = _subjectEncoding.GetBytes(subject, span); + span[written] = (byte)' '; + span = span[(written + 1)..]; - if (headers != null) + if (replyTo != null) { - _writer.WriteSpan(CommandConstants.NatsHeaders10NewLine); - _writer.WriteSpan(_bufferHeaders.WrittenSpan); + written = _subjectEncoding.GetBytes(replyTo, span); + span[written] = (byte)' '; + span = span[(written + 1)..]; } - if (payload.Length != 0) + Span lenSpan; + if (headers == null) { - _writer.WriteSequence(payload); + // len = payload len + lenSpan = span[..MaxIntStringLength]; + span = span[lenSpan.Length..]; + } + else + { + // len = header len +' '+ payload len + lenSpan = span[..(MaxIntStringLength + 1 + MaxIntStringLength)]; + span = span[lenSpan.Length..]; } - _writer.WriteNewLine(); - } + span[0] = (byte)'\r'; + span[1] = (byte)'\n'; + _writer.Advance(ctrlLen); - public void WritePublish(string subject, string? replyTo, NatsHeaders? headers, T? value, INatsSerialize serializer) - { - _bufferPayload.Reset(); + var headersLength = 0L; + var totalLength = 0L; + if (headers != null) + { + headersLength = _headerWriter.Write(headers); + totalLength += headersLength; + } // Consider null as empty payload. This way we are able to transmit null values as sentinels. // Another point is serializer behaviour. For instance JSON serializer seems to serialize null // as a string "null", others might throw exception. if (value != null) { - serializer.Serialize(_bufferPayload, value); + var initialCount = _writer.UnflushedBytes; + serializer.Serialize(_writer, value); + totalLength += _writer.UnflushedBytes - initialCount; + } + + span = _writer.GetSpan(2); + span[0] = (byte)'\r'; + span[1] = (byte)'\n'; + _writer.Advance(2); + + // write the length + var lenWritten = 0; + if (headers != null) + { + if (!Utf8Formatter.TryFormat(headersLength, lenSpan, out lenWritten)) + { + throw new NatsException("Can not format integer."); + } + + lenSpan[lenWritten] = (byte)' '; + lenWritten += 1; + } + + if (!Utf8Formatter.TryFormat(totalLength, lenSpan[lenWritten..], out var tLen)) + { + throw new NatsException("Can not format integer."); + } + + lenWritten += tLen; + var trim = lenSpan.Length - lenWritten; + if (trim > 0) + { + // shift right + ctrlSpan[..(ctrlLen - trim - 2)].CopyTo(ctrlSpan[trim..]); + ctrlSpan[..trim].Clear(); } - var payload = new ReadOnlySequence(_bufferPayload.WrittenMemory); - WritePublish(subject, replyTo, headers, payload); + return trim; } // https://docs.nats.io/reference/reference-protocols/nats-protocol#sub diff --git a/src/NATS.Client.Core/Commands/PublishCommand.cs b/src/NATS.Client.Core/Commands/PublishCommand.cs deleted file mode 100644 index 40349ea7c..000000000 --- a/src/NATS.Client.Core/Commands/PublishCommand.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System.Buffers; -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal sealed class PublishCommand : CommandBase> -{ - private string? _subject; - private string? _replyTo; - private NatsHeaders? _headers; - private T? _value; - private INatsSerialize? _serializer; - private Action? _errorHandler; - private CancellationToken _cancellationToken; - - private PublishCommand() - { - } - - public override bool IsCanceled => _cancellationToken.IsCancellationRequested; - - public static PublishCommand Create(ObjectPool pool, string subject, string? replyTo, NatsHeaders? headers, T? value, INatsSerialize serializer, Action? errorHandler, CancellationToken cancellationToken) - { - if (!TryRent(pool, out var result)) - { - result = new PublishCommand(); - } - - result._subject = subject; - result._replyTo = replyTo; - result._headers = headers; - result._value = value; - result._serializer = serializer; - result._errorHandler = errorHandler; - result._cancellationToken = cancellationToken; - - return result; - } - - public override void Write(ProtocolWriter writer) - { - try - { - writer.WritePublish(_subject!, _replyTo, _headers, _value, _serializer!); - } - catch (Exception e) - { - if (_errorHandler is { } errorHandler) - { - ThreadPool.UnsafeQueueUserWorkItem( - state => - { - try - { - state.handler(state.exception); - } - catch - { - // ignore - } - }, - (handler: errorHandler, exception: e), - preferLocal: false); - } - - throw; - } - } - - protected override void Reset() - { - _subject = default; - _headers = default; - _value = default; - _serializer = null; - _errorHandler = default; - _cancellationToken = default; - } -} - -internal sealed class PublishBytesCommand : CommandBase -{ - private string? _subject; - private string? _replyTo; - private NatsHeaders? _headers; - private ReadOnlySequence _payload; - private CancellationToken _cancellationToken; - - private PublishBytesCommand() - { - } - - public override bool IsCanceled => _cancellationToken.IsCancellationRequested; - - public static PublishBytesCommand Create(ObjectPool pool, string subject, string? replyTo, NatsHeaders? headers, ReadOnlySequence payload, CancellationToken cancellationToken) - { - if (!TryRent(pool, out var result)) - { - result = new PublishBytesCommand(); - } - - result._subject = subject; - result._replyTo = replyTo; - result._headers = headers; - result._payload = payload; - result._cancellationToken = cancellationToken; - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WritePublish(_subject!, _replyTo, _headers, _payload); - } - - protected override void Reset() - { - _subject = default; - _replyTo = default; - _headers = default; - _payload = default; - _cancellationToken = default; - } -} - -internal sealed class AsyncPublishCommand : AsyncCommandBase> -{ - private string? _subject; - private string? _replyTo; - private NatsHeaders? _headers; - private T? _value; - private INatsSerialize? _serializer; - - private AsyncPublishCommand() - { - } - - public static AsyncPublishCommand Create(ObjectPool pool, CancellationTimer timer, string subject, string? replyTo, NatsHeaders? headers, T? value, INatsSerialize serializer) - { - if (!TryRent(pool, out var result)) - { - result = new AsyncPublishCommand(); - } - - result._subject = subject; - result._replyTo = replyTo; - result._headers = headers; - result._value = value; - result._serializer = serializer; - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WritePublish(_subject!, _replyTo, _headers, _value, _serializer!); - } - - protected override void Reset() - { - _subject = default; - _headers = default; - _value = default; - _serializer = null; - } -} - -internal sealed class AsyncPublishBytesCommand : AsyncCommandBase -{ - private string? _subject; - private string? _replyTo; - private NatsHeaders? _headers; - private ReadOnlySequence _payload; - - private AsyncPublishBytesCommand() - { - } - - public static AsyncPublishBytesCommand Create(ObjectPool pool, CancellationTimer timer, string subject, string? replyTo, NatsHeaders? headers, ReadOnlySequence payload) - { - if (!TryRent(pool, out var result)) - { - result = new AsyncPublishBytesCommand(); - } - - result._subject = subject; - result._replyTo = replyTo; - result._headers = headers; - result._payload = payload; - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WritePublish(_subject!, _replyTo, _headers, _payload); - } - - protected override void Reset() - { - _subject = default; - _replyTo = default; - _headers = default; - _payload = default; - } -} diff --git a/src/NATS.Client.Core/Commands/SubscribeCommand.cs b/src/NATS.Client.Core/Commands/SubscribeCommand.cs deleted file mode 100644 index cee69cc4a..000000000 --- a/src/NATS.Client.Core/Commands/SubscribeCommand.cs +++ /dev/null @@ -1,90 +0,0 @@ -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal sealed class AsyncSubscribeCommand : AsyncCommandBase -{ - private string? _subject; - private string? _queueGroup; - private int _sid; - private int? _maxMsgs; - - private AsyncSubscribeCommand() - { - } - - public static AsyncSubscribeCommand Create(ObjectPool pool, CancellationTimer timer, int sid, string subject, string? queueGroup, int? maxMsgs) - { - if (!TryRent(pool, out var result)) - { - result = new AsyncSubscribeCommand(); - } - - result._subject = subject; - result._sid = sid; - result._queueGroup = queueGroup; - result._maxMsgs = maxMsgs; - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WriteSubscribe(_sid, _subject!, _queueGroup, _maxMsgs); - } - - protected override void Reset() - { - _subject = default; - _queueGroup = default; - _sid = 0; - } -} - -internal sealed class AsyncSubscribeBatchCommand : AsyncCommandBase, IBatchCommand -{ - private (int sid, string subject, string? queueGroup, int? maxMsgs)[]? _subscriptions; - - private AsyncSubscribeBatchCommand() - { - } - - public static AsyncSubscribeBatchCommand Create(ObjectPool pool, CancellationTimer timer, (int sid, string subject, string? queueGroup, int? maxMsgs)[]? subscriptions) - { - if (!TryRent(pool, out var result)) - { - result = new AsyncSubscribeBatchCommand(); - } - - result._subscriptions = subscriptions; - result.SetCancellationTimer(timer); - - return result; - } - - public override void Write(ProtocolWriter writer) - { - (this as IBatchCommand).Write(writer); - } - - int IBatchCommand.Write(ProtocolWriter writer) - { - var i = 0; - if (_subscriptions != null) - { - foreach (var (id, subject, queue, maxMsgs) in _subscriptions) - { - i++; - writer.WriteSubscribe(id, subject, queue, maxMsgs); - } - } - - return i; - } - - protected override void Reset() - { - _subscriptions = default; - } -} diff --git a/src/NATS.Client.Core/Commands/UnsubscribeCommand.cs b/src/NATS.Client.Core/Commands/UnsubscribeCommand.cs deleted file mode 100644 index 7b44c0ec6..000000000 --- a/src/NATS.Client.Core/Commands/UnsubscribeCommand.cs +++ /dev/null @@ -1,35 +0,0 @@ -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core.Commands; - -internal sealed class UnsubscribeCommand : CommandBase -{ - private int _sid; - - private UnsubscribeCommand() - { - } - - // Unsubscribe is fire-and-forget, don't use CancellationTimer. - public static UnsubscribeCommand Create(ObjectPool pool, int sid) - { - if (!TryRent(pool, out var result)) - { - result = new UnsubscribeCommand(); - } - - result._sid = sid; - - return result; - } - - public override void Write(ProtocolWriter writer) - { - writer.WriteUnsubscribe(_sid, null); - } - - protected override void Reset() - { - _sid = 0; - } -} diff --git a/src/NATS.Client.Core/Internal/BufferWriterExtensions.cs b/src/NATS.Client.Core/Internal/BufferWriterExtensions.cs index 797cca547..782be2583 100644 --- a/src/NATS.Client.Core/Internal/BufferWriterExtensions.cs +++ b/src/NATS.Client.Core/Internal/BufferWriterExtensions.cs @@ -1,17 +1,16 @@ using System.Buffers; using System.Buffers.Text; using System.Runtime.CompilerServices; -using System.Text; using NATS.Client.Core.Commands; namespace NATS.Client.Core.Internal; internal static class BufferWriterExtensions { - private const int MaxIntStringLength = 10; // int.MaxValue.ToString().Length + private const int MaxIntStringLength = 9; // https://github.com/nats-io/nats-server/blob/28a2a1000045b79927ebf6b75eecc19c1b9f1548/server/util.go#L85C8-L85C23 [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteNewLine(this FixedArrayBufferWriter writer) + public static void WriteNewLine(this IBufferWriter writer) { var span = writer.GetSpan(CommandConstants.NewLine.Length); CommandConstants.NewLine.CopyTo(span); @@ -19,7 +18,7 @@ public static void WriteNewLine(this FixedArrayBufferWriter writer) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteNumber(this FixedArrayBufferWriter writer, long number) + public static void WriteNumber(this IBufferWriter writer, long number) { var span = writer.GetSpan(MaxIntStringLength); if (!Utf8Formatter.TryFormat(number, span, out var writtenLength)) @@ -31,7 +30,7 @@ public static void WriteNumber(this FixedArrayBufferWriter writer, long number) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteSpace(this FixedArrayBufferWriter writer) + public static void WriteSpace(this IBufferWriter writer) { var span = writer.GetSpan(1); span[0] = (byte)' '; @@ -39,7 +38,7 @@ public static void WriteSpace(this FixedArrayBufferWriter writer) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteSpan(this FixedArrayBufferWriter writer, ReadOnlySpan span) + public static void WriteSpan(this IBufferWriter writer, ReadOnlySpan span) { var writerSpan = writer.GetSpan(span.Length); span.CopyTo(writerSpan); @@ -47,7 +46,7 @@ public static void WriteSpan(this FixedArrayBufferWriter writer, ReadOnlySpan sequence) + public static void WriteSequence(this IBufferWriter writer, ReadOnlySequence sequence) { var len = (int)sequence.Length; var span = writer.GetSpan(len); @@ -56,7 +55,7 @@ public static void WriteSequence(this FixedArrayBufferWriter writer, ReadOnlySeq } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteASCIIAndSpace(this FixedArrayBufferWriter writer, string ascii) + public static void WriteASCIIAndSpace(this IBufferWriter writer, string ascii) { var span = writer.GetSpan(ascii.Length + 1); ascii.WriteASCIIBytes(span); diff --git a/src/NATS.Client.Core/Internal/HeaderWriter.cs b/src/NATS.Client.Core/Internal/HeaderWriter.cs index c76b806eb..47b0d8c2e 100644 --- a/src/NATS.Client.Core/Internal/HeaderWriter.cs +++ b/src/NATS.Client.Core/Internal/HeaderWriter.cs @@ -1,5 +1,7 @@ using System.Buffers; +using System.IO.Pipelines; using System.Text; +using NATS.Client.Core.Commands; namespace NATS.Client.Core.Internal; @@ -10,17 +12,24 @@ internal class HeaderWriter private const byte ByteColon = (byte)':'; private const byte ByteSpace = (byte)' '; private const byte ByteDel = 127; + private readonly PipeWriter _pipeWriter; private readonly Encoding _encoding; - public HeaderWriter(Encoding encoding) => _encoding = encoding; + public HeaderWriter(PipeWriter pipeWriter, Encoding encoding) + { + _pipeWriter = pipeWriter; + _encoding = encoding; + } private static ReadOnlySpan CrLf => new[] { ByteCr, ByteLf }; private static ReadOnlySpan ColonSpace => new[] { ByteColon, ByteSpace }; - internal int Write(in FixedArrayBufferWriter bufferWriter, NatsHeaders headers) + internal long Write(NatsHeaders headers) { - var initialCount = bufferWriter.WrittenCount; + var initialCount = _pipeWriter.UnflushedBytes; + _pipeWriter.WriteSpan(CommandConstants.NatsHeaders10NewLine); + foreach (var kv in headers) { foreach (var value in kv.Value) @@ -29,7 +38,7 @@ internal int Write(in FixedArrayBufferWriter bufferWriter, NatsHeaders headers) { // write key var keyLength = _encoding.GetByteCount(kv.Key); - var keySpan = bufferWriter.GetSpan(keyLength); + var keySpan = _pipeWriter.GetSpan(keyLength); _encoding.GetBytes(kv.Key, keySpan); if (!ValidateKey(keySpan.Slice(0, keyLength))) { @@ -37,20 +46,20 @@ internal int Write(in FixedArrayBufferWriter bufferWriter, NatsHeaders headers) $"Invalid header key '{kv.Key}': contains colon, space, or other non-printable ASCII characters"); } - bufferWriter.Advance(keyLength); - bufferWriter.Write(ColonSpace); + _pipeWriter.Advance(keyLength); + _pipeWriter.Write(ColonSpace); // write values var valueLength = _encoding.GetByteCount(value); - var valueSpan = bufferWriter.GetSpan(valueLength); + var valueSpan = _pipeWriter.GetSpan(valueLength); _encoding.GetBytes(value, valueSpan); if (!ValidateValue(valueSpan.Slice(0, valueLength))) { throw new NatsException($"Invalid header value for key '{kv.Key}': contains CRLF"); } - bufferWriter.Advance(valueLength); - bufferWriter.Write(CrLf); + _pipeWriter.Advance(valueLength); + _pipeWriter.Write(CrLf); } } } @@ -58,9 +67,9 @@ internal int Write(in FixedArrayBufferWriter bufferWriter, NatsHeaders headers) // Even empty header needs to terminate. // We will send NATS/1.0 version line // even if there are no headers. - bufferWriter.Write(CrLf); + _pipeWriter.Write(CrLf); - return bufferWriter.WrittenCount - initialCount; + return _pipeWriter.UnflushedBytes - initialCount; } // cannot contain ASCII Bytes <=32, 58, or 127 diff --git a/src/NATS.Client.Core/Internal/NatsPipeliningWriteProtocolProcessor.cs b/src/NATS.Client.Core/Internal/NatsPipeliningWriteProtocolProcessor.cs index 65d9aaa31..b26e144ec 100644 --- a/src/NATS.Client.Core/Internal/NatsPipeliningWriteProtocolProcessor.cs +++ b/src/NATS.Client.Core/Internal/NatsPipeliningWriteProtocolProcessor.cs @@ -1,4 +1,6 @@ +using System.Buffers; using System.Diagnostics; +using System.IO.Pipelines; using System.Threading.Channels; using Microsoft.Extensions.Logging; using NATS.Client.Core.Commands; @@ -7,241 +9,299 @@ namespace NATS.Client.Core.Internal; internal sealed class NatsPipeliningWriteProtocolProcessor : IAsyncDisposable { - private readonly ISocketConnection _socketConnection; - private readonly WriterState _state; - private readonly ObjectPool _pool; + private readonly CancellationTokenSource _cts; private readonly ConnectionStatsCounter _counter; - private readonly FixedArrayBufferWriter _bufferWriter; - private readonly Channel _channel; private readonly NatsOpts _opts; - private readonly Task _writeLoop; + private readonly PipeReader _pipeReader; + private readonly Queue _inFlightCommands; + private readonly ChannelReader _queuedCommandReader; + private readonly ISocketConnection _socketConnection; private readonly Stopwatch _stopwatch = new Stopwatch(); - private readonly CancellationTokenSource _cancellationTokenSource; private int _disposed; - public NatsPipeliningWriteProtocolProcessor(ISocketConnection socketConnection, WriterState state, ObjectPool pool, ConnectionStatsCounter counter) + public NatsPipeliningWriteProtocolProcessor(ISocketConnection socketConnection, CommandWriter commandWriter, NatsOpts opts, ConnectionStatsCounter counter) { - _socketConnection = socketConnection; - _state = state; - _pool = pool; + _cts = new CancellationTokenSource(); _counter = counter; - _bufferWriter = state.BufferWriter; - _channel = state.CommandBuffer; - _opts = state.Opts; - _cancellationTokenSource = new CancellationTokenSource(); - _writeLoop = Task.Run(WriteLoopAsync); + _inFlightCommands = commandWriter.InFlightCommands; + _opts = opts; + _pipeReader = commandWriter.PipeReader; + _queuedCommandReader = commandWriter.QueuedCommandsReader; + _socketConnection = socketConnection; + WriteLoop = Task.Run(WriteLoopAsync); } + public Task WriteLoop { get; } + public async ValueTask DisposeAsync() { if (Interlocked.Increment(ref _disposed) == 1) { #if NET6_0 - _cancellationTokenSource.Cancel(); + _cts.Cancel(); #else - await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); + await _cts.CancelAsync().ConfigureAwait(false); #endif - await _writeLoop.ConfigureAwait(false); // wait for drain writer + try + { + await WriteLoop.ConfigureAwait(false); // wait to drain writer + } + catch + { + // ignore + } } } private async Task WriteLoopAsync() { - var reader = _channel.Reader; - var protocolWriter = new ProtocolWriter(_bufferWriter); var logger = _opts.LoggerFactory.CreateLogger(); - var writerBufferSize = _opts.WriterBufferSize; - var promiseList = new List(100); var isEnabledTraceLogging = logger.IsEnabled(LogLevel.Trace); + var cancellationToken = _cts.Token; + var pending = 0; + var trimming = 0; + var examinedOffset = 0; + + // memory segment used to consolidate multiple small memory chunks + // should <= (minimumSegmentSize * 0.5) in CommandWriter + // 8520 should fit into 6 packets on 1500 MTU TLS connection or 1 packet on 9000 MTU TLS connection + // assuming 40 bytes TCP overhead + 40 bytes TLS overhead per packet + var consolidateMem = new Memory(new byte[8520]); + + // add up in flight command sum + var inFlightSum = 0; + foreach (var command in _inFlightCommands) + { + inFlightSum += command.Size; + } try { - // at first, send priority lane(initial command). + while (true) { - var firstCommands = _state.PriorityCommands; - if (firstCommands.Count != 0) + var result = await _pipeReader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (result.IsCanceled) { - var count = firstCommands.Count; - var tempBuffer = new FixedArrayBufferWriter(); - var tempWriter = new ProtocolWriter(tempBuffer); - foreach (var command in firstCommands) - { - command.Write(tempWriter); + break; + } - if (command is IPromise p) + var consumedPos = result.Buffer.Start; + var examinedPos = result.Buffer.Start; + try + { + var buffer = result.Buffer.Slice(examinedOffset); + while (inFlightSum < result.Buffer.Length) + { + QueuedCommand queuedCommand; + while (!_queuedCommandReader.TryRead(out queuedCommand)) { - promiseList.Add(p); + await _queuedCommandReader.WaitToReadAsync(cancellationToken).ConfigureAwait(false); } - command.Return(_pool); // Promise does not Return but set ObjectPool here. + _inFlightCommands.Enqueue(queuedCommand); + inFlightSum += queuedCommand.Size; } - _state.PriorityCommands.Clear(); - - try - { - var memory = tempBuffer.WrittenMemory; - while (memory.Length > 0) - { - _stopwatch.Restart(); - var sent = await _socketConnection.SendAsync(memory).ConfigureAwait(false); - _stopwatch.Stop(); - if (isEnabledTraceLogging) - { - logger.LogTrace("Socket.SendAsync. Size: {0} BatchSize: {1} Elapsed: {2}ms", sent, count, _stopwatch.Elapsed.TotalMilliseconds); - } - - Interlocked.Add(ref _counter.SentBytes, sent); - memory = memory.Slice(sent); - } - } - catch (Exception ex) + while (buffer.Length > 0) { - _socketConnection.SignalDisconnected(ex); - foreach (var item in promiseList) + if (pending == 0) { - item.SetException(ex); // signal failed + var peek = _inFlightCommands.Peek(); + pending = peek.Size; + trimming = peek.Trim; } - return; // when socket closed, finish writeloop. - } - - foreach (var item in promiseList) - { - item.SetResult(); - } - - promiseList.Clear(); - } - } - - // restore promise(command is exist in bufferWriter) when enter from reconnecting. - promiseList.AddRange(_state.PendingPromises); - _state.PendingPromises.Clear(); - - // main writer loop - while ((_bufferWriter.WrittenCount != 0) || (await reader.WaitToReadAsync(_cancellationTokenSource.Token).ConfigureAwait(false))) - { - try - { - var count = 0; - while (_bufferWriter.WrittenCount < writerBufferSize && reader.TryRead(out var command)) - { - Interlocked.Decrement(ref _counter.PendingMessages); - if (command.IsCanceled) + if (trimming > 0) { + var trimmed = Math.Min(trimming, (int)buffer.Length); + consumedPos = buffer.GetPosition(trimmed); + examinedPos = buffer.GetPosition(trimmed); + examinedOffset = 0; + buffer = buffer.Slice(trimmed); + pending -= trimmed; + trimming -= trimmed; + if (pending == 0) + { + // the entire command was trimmed (canceled) + inFlightSum -= _inFlightCommands.Dequeue().Size; + } + continue; } - try + var sendMem = buffer.First; + var maxSize = 0; + var maxSizeCap = Math.Max(sendMem.Length, consolidateMem.Length); + var doTrim = false; + foreach (var command in _inFlightCommands) { - if (command is IBatchCommand batch) + if (maxSize == 0) { - count += batch.Write(protocolWriter); + // first command; set to pending + maxSize = pending; + continue; } - else + + if (maxSize > maxSizeCap) { - command.Write(protocolWriter); - count++; + // over cap + break; } - } - catch (Exception e) - { - // flag potential serialization exceptions - if (command is IPromise promise) + + if (command.Trim > 0) { - promise.SetException(e); + // will have to trim + doTrim = true; + break; } - throw; + maxSize += command.Size; } - if (command is IPromise p) + if (sendMem.Length > maxSize) { - promiseList.Add(p); + sendMem = sendMem[..maxSize]; } - command.Return(_pool); // Promise does not Return but set ObjectPool here. - } - - try - { - // SendAsync(ReadOnlyMemory) is very efficient, internally using AwaitableAsyncSocketEventArgs - // should use cancellation token?, currently no, wait for flush complete. - var memory = _bufferWriter.WrittenMemory; - while (memory.Length != 0) + var bufferIter = buffer; + if (doTrim || (bufferIter.Length > sendMem.Length && sendMem.Length < consolidateMem.Length)) { - _stopwatch.Restart(); - var sent = await _socketConnection.SendAsync(memory).ConfigureAwait(false); - _stopwatch.Stop(); - if (isEnabledTraceLogging) + var memIter = consolidateMem; + var trimmedSize = 0; + foreach (var command in _inFlightCommands) { - logger.LogTrace("Socket.SendAsync. Size: {0} BatchSize: {1} Elapsed: {2}ms", sent, count, _stopwatch.Elapsed.TotalMilliseconds); + if (bufferIter.Length == 0 || memIter.Length == 0) + { + break; + } + + int write; + if (trimmedSize == 0) + { + // first command, only write pending data + write = pending; + } + else if (command.Trim == 0) + { + write = command.Size; + } + else + { + if (bufferIter.Length < command.Trim + 1) + { + // not enough bytes to start writing the next command + break; + } + + bufferIter = bufferIter.Slice(command.Trim); + write = command.Size - command.Trim; + if (write == 0) + { + // the entire command was trimmed (canceled) + continue; + } + } + + write = Math.Min(memIter.Length, write); + write = Math.Min((int)bufferIter.Length, write); + bufferIter.Slice(0, write).CopyTo(memIter.Span); + memIter = memIter[write..]; + bufferIter = bufferIter.Slice(write); + trimmedSize += write; } - if (sent == 0) + sendMem = consolidateMem[..trimmedSize]; + } + + // perform send + _stopwatch.Restart(); + var sent = await _socketConnection.SendAsync(sendMem).ConfigureAwait(false); + _stopwatch.Stop(); + Interlocked.Add(ref _counter.SentBytes, sent); + if (isEnabledTraceLogging) + { + logger.LogTrace("Socket.SendAsync. Size: {0} Elapsed: {1}ms", sent, _stopwatch.Elapsed.TotalMilliseconds); + } + + var consumed = 0; + var sentAndTrimmed = sent; + while (consumed < sentAndTrimmed) + { + if (pending == 0) { - throw new SocketClosedException(null); + var peek = _inFlightCommands.Peek(); + pending = peek.Size - peek.Trim; + consumed += peek.Trim; + sentAndTrimmed += peek.Trim; + + if (pending == 0) + { + // the entire command was trimmed (canceled) + inFlightSum -= _inFlightCommands.Dequeue().Size; + continue; + } } - Interlocked.Add(ref _counter.SentBytes, sent); + if (pending <= sentAndTrimmed - consumed) + { + // the entire command was sent + inFlightSum -= _inFlightCommands.Dequeue().Size; + Interlocked.Add(ref _counter.PendingMessages, -1); + Interlocked.Add(ref _counter.SentMessages, 1); - memory = memory.Slice(sent); + // mark the bytes as consumed, and reset pending + consumed += pending; + pending = 0; + } + else + { + // the entire command was not sent; decrement pending by + // the number of bytes from the command that was sent + pending += consumed - sentAndTrimmed; + break; + } } - Interlocked.Add(ref _counter.SentMessages, count); - - _bufferWriter.Reset(); - foreach (var item in promiseList) + if (consumed > 0) { - item.SetResult(); + // mark fully sent commands as consumed + consumedPos = buffer.GetPosition(consumed); + examinedOffset = sentAndTrimmed - consumed; + } + else + { + // no commands were consumed + examinedOffset += sentAndTrimmed; } - promiseList.Clear(); - } - catch (Exception ex) - { - // may receive from socket.SendAsync - - // when error, command is dequeued and written buffer is still exists in state.BufferWriter - // store current pending promises to state. - _state.PendingPromises.AddRange(promiseList); - _socketConnection.SignalDisconnected(ex); - return; // when socket closed, finish writeloop. + // lop off sent bytes for next iteration + examinedPos = buffer.GetPosition(sentAndTrimmed); + buffer = buffer.Slice(sentAndTrimmed); } } - catch (Exception ex) + finally { - if (ex is SocketClosedException) - { - return; - } + _pipeReader.AdvanceTo(consumedPos, examinedPos); + } - try - { - logger.LogError(ex, "Internal error occured on WriteLoop."); - } - catch - { - } + if (result.IsCompleted) + { + break; } } } catch (OperationCanceledException) { + // ignore, intentionally disposed } - finally + catch (SocketClosedException) { - try - { - if (_bufferWriter.WrittenMemory.Length != 0) - { - await _socketConnection.SendAsync(_bufferWriter.WrittenMemory).ConfigureAwait(false); - } - } - catch - { - } + // ignore, will be handled in read loop + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error occured in write loop"); + throw; } logger.LogDebug("WriteLoop finished."); diff --git a/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs b/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs index 6bd7fc852..05c9dda23 100644 --- a/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs +++ b/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs @@ -19,7 +19,7 @@ internal sealed class NatsReadProtocolProcessor : IAsyncDisposable private readonly TaskCompletionSource _waitForInfoSignal; private readonly TaskCompletionSource _waitForPongOrErrorSignal; // wait for initial connection private readonly Task _infoParsed; // wait for an upgrade - private readonly ConcurrentQueue _pingCommands; // wait for pong + private readonly ConcurrentQueue _pingCommands; // wait for pong private readonly ILogger _logger; private readonly bool _trace; private int _disposed; @@ -32,12 +32,12 @@ public NatsReadProtocolProcessor(ISocketConnection socketConnection, NatsConnect _waitForInfoSignal = waitForInfoSignal; _waitForPongOrErrorSignal = waitForPongOrErrorSignal; _infoParsed = infoParsed; - _pingCommands = new ConcurrentQueue(); + _pingCommands = new ConcurrentQueue(); _socketReader = new SocketReader(socketConnection, connection.Opts.ReaderBufferSize, connection.Counter, connection.Opts.LoggerFactory); _readLoop = Task.Run(ReadLoopAsync); } - public bool TryEnqueuePing(AsyncPingCommand ping) + public bool TryEnqueuePing(PingCommand ping) { if (_disposed != 0) return false; @@ -52,7 +52,7 @@ public async ValueTask DisposeAsync() await _readLoop.ConfigureAwait(false); // wait for drain buffer. foreach (var item in _pingCommands) { - item.SetCanceled(); + item.TaskCompletionSource.SetCanceled(); } _waitForInfoSignal.TrySetCanceled(); @@ -309,7 +309,7 @@ private async ValueTask> DispatchCommandAsync(int code, R { const int PingSize = 6; // PING\r\n - await _connection.PostPongAsync().ConfigureAwait(false); // return pong to server + await _connection.PongAsync().ConfigureAwait(false); // return pong to server if (length < PingSize) { @@ -329,9 +329,7 @@ private async ValueTask> DispatchCommandAsync(int code, R if (_pingCommands.TryDequeue(out var pingCommand)) { - var start = pingCommand.WriteTime; - var elapsed = DateTimeOffset.UtcNow - start; - pingCommand.SetResult(elapsed ?? TimeSpan.Zero); + pingCommand.TaskCompletionSource.SetResult(DateTimeOffset.UtcNow - pingCommand.WriteTime); } if (length < PongSize) diff --git a/src/NATS.Client.Core/Internal/SubscriptionManager.cs b/src/NATS.Client.Core/Internal/SubscriptionManager.cs index 397d244ab..38c8fee4e 100644 --- a/src/NATS.Client.Core/Internal/SubscriptionManager.cs +++ b/src/NATS.Client.Core/Internal/SubscriptionManager.cs @@ -47,7 +47,7 @@ public SubscriptionManager(NatsConnection connection, string inboxPrefix) internal InboxSubBuilder InboxSubBuilder { get; } - public async ValueTask SubscribeAsync(NatsSubBase sub, CancellationToken cancellationToken) + public ValueTask SubscribeAsync(NatsSubBase sub, CancellationToken cancellationToken) { if (IsInboxSubject(sub.Subject)) { @@ -56,12 +56,10 @@ public async ValueTask SubscribeAsync(NatsSubBase sub, CancellationToken cancell throw new NatsException("Inbox subscriptions don't support queue groups"); } - await SubscribeInboxAsync(sub, cancellationToken).ConfigureAwait(false); - } - else - { - await SubscribeInternalAsync(sub.Subject, sub.QueueGroup, sub.Opts, sub, cancellationToken).ConfigureAwait(false); + return SubscribeInboxAsync(sub, cancellationToken); } + + return SubscribeInternalAsync(sub.Subject, sub.QueueGroup, sub.Opts, sub, cancellationToken); } public ValueTask PublishToClientHandlersAsync(string subject, string? replyTo, int sid, in ReadOnlySequence? headersBuffer, in ReadOnlySequence payloadBuffer) @@ -128,7 +126,9 @@ public ValueTask RemoveAsync(NatsSubBase sub) { if (!_bySub.TryGetValue(sub, out var subMetadata)) { - throw new NatsException("subscription is not registered with the manager"); + // this can happen when a call to SubscribeAsync is canceled or timed out before subscribing + // in that case, return as there is nothing to unsubscribe + return ValueTask.CompletedTask; } lock (_gate) @@ -147,7 +147,7 @@ public ValueTask RemoveAsync(NatsSubBase sub) /// Commands returned form all the subscriptions will be run as a priority right after reconnection is established. /// /// Enumerable list of commands - public IEnumerable GetReconnectCommands() + public async ValueTask WriteReconnectCommandsAsync(CommandWriter commandWriter) { var subs = new List<(NatsSubBase, int)>(); lock (_gate) @@ -163,8 +163,7 @@ public IEnumerable GetReconnectCommands() foreach (var (sub, sid) in subs) { - foreach (var command in sub.GetReconnectCommands(sid)) - yield return command; + await sub.WriteReconnectCommandsAsync(commandWriter, sid).ConfigureAwait(false); } } @@ -219,8 +218,7 @@ private async ValueTask SubscribeInternalAsync(string subject, string? queueGrou try { - await _connection.SubscribeCoreAsync(sid, subject, queueGroup, opts?.MaxMsgs, cancellationToken) - .ConfigureAwait(false); + await _connection.SubscribeCoreAsync(sid, subject, queueGroup, opts?.MaxMsgs, cancellationToken).ConfigureAwait(false); await sub.ReadyAsync().ConfigureAwait(false); } catch diff --git a/src/NATS.Client.Core/Internal/TcpConnection.cs b/src/NATS.Client.Core/Internal/TcpConnection.cs index f05f9cd59..4e71805a1 100644 --- a/src/NATS.Client.Core/Internal/TcpConnection.cs +++ b/src/NATS.Client.Core/Internal/TcpConnection.cs @@ -31,12 +31,6 @@ public TcpConnection(ILogger logger) } _socket.NoDelay = true; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _socket.SendBufferSize = 0; - _socket.ReceiveBufferSize = 0; - } } public Task WaitForClosed => _waitForClosedSource.Task; diff --git a/src/NATS.Client.Core/NATS.Client.Core.csproj b/src/NATS.Client.Core/NATS.Client.Core.csproj index 122376783..58767fd80 100644 --- a/src/NATS.Client.Core/NATS.Client.Core.csproj +++ b/src/NATS.Client.Core/NATS.Client.Core.csproj @@ -20,7 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/NATS.Client.Core/NatsConnection.LowLevelApi.cs b/src/NATS.Client.Core/NatsConnection.LowLevelApi.cs index 0fa8d380f..9cbcf23e7 100644 --- a/src/NATS.Client.Core/NatsConnection.LowLevelApi.cs +++ b/src/NATS.Client.Core/NatsConnection.LowLevelApi.cs @@ -1,116 +1,15 @@ -using System.Buffers; -using NATS.Client.Core.Commands; - namespace NATS.Client.Core; public partial class NatsConnection { - /// - /// Publishes and yields immediately unless the command channel is full in which case - /// waits until there is space in command channel. - /// - internal ValueTask PubPostAsync(string subject, string? replyTo = default, ReadOnlySequence payload = default, NatsHeaders? headers = default, CancellationToken cancellationToken = default) - { - headers?.SetReadOnly(); - - if (ConnectionState == NatsConnectionState.Open) - { - var command = PublishBytesCommand.Create(_pool, subject, replyTo, headers, payload, cancellationToken); - return EnqueueCommandAsync(command); - } - else - { - return WithConnectAsync(subject, replyTo, headers, payload, cancellationToken, static (self, s, r, h, p, c) => - { - var command = PublishBytesCommand.Create(self._pool, s, r, h, p, c); - return self.EnqueueCommandAsync(command); - }); - } - } - - internal ValueTask PubModelPostAsync(string subject, T? data, INatsSerialize serializer, string? replyTo = default, NatsHeaders? headers = default, Action? errorHandler = default, CancellationToken cancellationToken = default) - { - headers?.SetReadOnly(); - - if (ConnectionState == NatsConnectionState.Open) - { - var command = PublishCommand.Create(_pool, subject, replyTo, headers, data, serializer, errorHandler, cancellationToken); - return EnqueueCommandAsync(command); - } - else - { - return WithConnectAsync(subject, replyTo, headers, data, serializer, errorHandler, cancellationToken, static (self, s, r, h, d, ser, eh, c) => - { - var command = PublishCommand.Create(self._pool, s, r, h, d, ser, eh, c); - return self.EnqueueCommandAsync(command); - }); - } - } - - internal ValueTask PubAsync(string subject, string? replyTo = default, ReadOnlySequence payload = default, NatsHeaders? headers = default, CancellationToken cancellationToken = default) - { - headers?.SetReadOnly(); - - if (ConnectionState == NatsConnectionState.Open) - { - var command = AsyncPublishBytesCommand.Create(_pool, GetCancellationTimer(cancellationToken), subject, replyTo, headers, payload); - if (TryEnqueueCommand(command)) - { - return command.AsValueTask(); - } - else - { - return EnqueueAndAwaitCommandAsync(command); - } - } - else - { - return WithConnectAsync(subject, replyTo, headers, payload, cancellationToken, static (self, s, r, h, p, token) => - { - var command = AsyncPublishBytesCommand.Create(self._pool, self.GetCancellationTimer(token), s, r, h, p); - return self.EnqueueAndAwaitCommandAsync(command); - }); - } - } - - internal ValueTask PubModelAsync(string subject, T? data, INatsSerialize serializer, string? replyTo = default, NatsHeaders? headers = default, CancellationToken cancellationToken = default) - { - headers?.SetReadOnly(); - - if (ConnectionState == NatsConnectionState.Open) - { - var command = AsyncPublishCommand.Create(_pool, GetCancellationTimer(cancellationToken), subject, replyTo, headers, data, serializer); - if (TryEnqueueCommand(command)) - { - return command.AsValueTask(); - } - else - { - return EnqueueAndAwaitCommandAsync(command); - } - } - else - { - return WithConnectAsync(subject, replyTo, headers, data, serializer, cancellationToken, static (self, s, r, h, v, ser, token) => - { - var command = AsyncPublishCommand.Create(self._pool, self.GetCancellationTimer(token), s, r, h, v, ser); - return self.EnqueueAndAwaitCommandAsync(command); - }); - } - } + internal ValueTask SubAsync(NatsSubBase sub, CancellationToken cancellationToken = default) => + ConnectionState != NatsConnectionState.Open + ? ConnectAndSubAsync(sub, cancellationToken) + : SubscriptionManager.SubscribeAsync(sub, cancellationToken); - internal ValueTask SubAsync(NatsSubBase sub, CancellationToken cancellationToken = default) + private async ValueTask ConnectAndSubAsync(NatsSubBase sub, CancellationToken cancellationToken = default) { - if (ConnectionState == NatsConnectionState.Open) - { - return SubscriptionManager.SubscribeAsync(sub, cancellationToken); - } - else - { - return WithConnectAsync(sub, cancellationToken, static (self, s, token) => - { - return self.SubscriptionManager.SubscribeAsync(s, token); - }); - } + await ConnectAsync().AsTask().WaitAsync(cancellationToken).ConfigureAwait(false); + await SubscriptionManager.SubscribeAsync(sub, cancellationToken).ConfigureAwait(false); } } diff --git a/src/NATS.Client.Core/NatsConnection.Ping.cs b/src/NATS.Client.Core/NatsConnection.Ping.cs index 6639f433d..9dac84b51 100644 --- a/src/NATS.Client.Core/NatsConnection.Ping.cs +++ b/src/NATS.Client.Core/NatsConnection.Ping.cs @@ -5,28 +5,16 @@ namespace NATS.Client.Core; public partial class NatsConnection { /// - public ValueTask PingAsync(CancellationToken cancellationToken = default) + public async ValueTask PingAsync(CancellationToken cancellationToken = default) { - if (ConnectionState == NatsConnectionState.Open) + if (ConnectionState != NatsConnectionState.Open) { - var command = AsyncPingCommand.Create(this, _pool, GetCancellationTimer(cancellationToken)); - if (TryEnqueueCommand(command)) - { - return command.AsValueTask(); - } - else - { - return EnqueueAndAwaitCommandAsync(command); - } - } - else - { - return WithConnectAsync(cancellationToken, static (self, token) => - { - var command = AsyncPingCommand.Create(self, self._pool, self.GetCancellationTimer(token)); - return self.EnqueueAndAwaitCommandAsync(command); - }); + await ConnectAsync().AsTask().WaitAsync(cancellationToken).ConfigureAwait(false); } + + var pingCommand = new PingCommand(); + await CommandWriter.PingAsync(pingCommand, cancellationToken).ConfigureAwait(false); + return await pingCommand.TaskCompletionSource.Task.ConfigureAwait(false); } /// @@ -37,13 +25,8 @@ public ValueTask PingAsync(CancellationToken cancellationToken = defau /// /// Cancels the Ping command /// representing the asynchronous operation - private ValueTask PingOnlyAsync(CancellationToken cancellationToken = default) - { - if (ConnectionState == NatsConnectionState.Open) - { - return EnqueueCommandAsync(PingCommand.Create(_pool, GetCancellationTimer(cancellationToken))); - } - - return ValueTask.CompletedTask; - } + private ValueTask PingOnlyAsync(CancellationToken cancellationToken = default) => + ConnectionState == NatsConnectionState.Open + ? CommandWriter.PingAsync(new PingCommand(), cancellationToken) + : ValueTask.CompletedTask; } diff --git a/src/NATS.Client.Core/NatsConnection.Publish.cs b/src/NATS.Client.Core/NatsConnection.Publish.cs index a81b54aae..9f5abedf0 100644 --- a/src/NATS.Client.Core/NatsConnection.Publish.cs +++ b/src/NATS.Client.Core/NatsConnection.Publish.cs @@ -5,33 +5,29 @@ public partial class NatsConnection /// public ValueTask PublishAsync(string subject, NatsHeaders? headers = default, string? replyTo = default, NatsPubOpts? opts = default, CancellationToken cancellationToken = default) { - if (opts?.WaitUntilSent ?? false) - { - return PubAsync(subject, replyTo, payload: default, headers, cancellationToken); - } - else - { - return PubPostAsync(subject, replyTo, payload: default, headers, cancellationToken); - } + headers?.SetReadOnly(); + return ConnectionState != NatsConnectionState.Open + ? ConnectAndPublishAsync(subject, default, headers, replyTo, NatsRawSerializer.Default, cancellationToken) + : CommandWriter.PublishAsync(subject, default, headers, replyTo, NatsRawSerializer.Default, cancellationToken); } /// public ValueTask PublishAsync(string subject, T? data, NatsHeaders? headers = default, string? replyTo = default, INatsSerialize? serializer = default, NatsPubOpts? opts = default, CancellationToken cancellationToken = default) { serializer ??= Opts.SerializerRegistry.GetSerializer(); - if (opts?.WaitUntilSent ?? false) - { - return PubModelAsync(subject, data, serializer, replyTo, headers, cancellationToken); - } - else - { - return PubModelPostAsync(subject, data, serializer, replyTo, headers, opts?.ErrorHandler, cancellationToken); - } + headers?.SetReadOnly(); + return ConnectionState != NatsConnectionState.Open + ? ConnectAndPublishAsync(subject, data, headers, replyTo, serializer, cancellationToken) + : CommandWriter.PublishAsync(subject, data, headers, replyTo, serializer, cancellationToken); } /// - public ValueTask PublishAsync(in NatsMsg msg, INatsSerialize? serializer = default, NatsPubOpts? opts = default, CancellationToken cancellationToken = default) + public ValueTask PublishAsync(in NatsMsg msg, INatsSerialize? serializer = default, NatsPubOpts? opts = default, CancellationToken cancellationToken = default) => + PublishAsync(msg.Subject, msg.Data, msg.Headers, msg.ReplyTo, serializer, opts, cancellationToken); + + private async ValueTask ConnectAndPublishAsync(string subject, T? data, NatsHeaders? headers, string? replyTo, INatsSerialize serializer, CancellationToken cancellationToken) { - return PublishAsync(msg.Subject, msg.Data, msg.Headers, msg.ReplyTo, serializer, opts, cancellationToken); + await ConnectAsync().AsTask().WaitAsync(cancellationToken).ConfigureAwait(false); + await CommandWriter.PublishAsync(subject, data, headers, replyTo, serializer, cancellationToken).ConfigureAwait(false); } } diff --git a/src/NATS.Client.Core/NatsConnection.RequestSub.cs b/src/NATS.Client.Core/NatsConnection.RequestSub.cs index 514c37ff5..a3f94c24f 100644 --- a/src/NATS.Client.Core/NatsConnection.RequestSub.cs +++ b/src/NATS.Client.Core/NatsConnection.RequestSub.cs @@ -19,15 +19,7 @@ internal async ValueTask> RequestSubAsync( await SubAsync(sub, cancellationToken).ConfigureAwait(false); requestSerializer ??= Opts.SerializerRegistry.GetSerializer(); - - if (requestOpts?.WaitUntilSent == true) - { - await PubModelAsync(subject, data, requestSerializer, replyTo, headers, cancellationToken).ConfigureAwait(false); - } - else - { - await PubModelPostAsync(subject, data, requestSerializer, replyTo, headers, requestOpts?.ErrorHandler, cancellationToken).ConfigureAwait(false); - } + await PublishAsync(subject, data, headers, replyTo, requestSerializer, requestOpts, cancellationToken).ConfigureAwait(false); return sub; } diff --git a/src/NATS.Client.Core/NatsConnection.Util.cs b/src/NATS.Client.Core/NatsConnection.Util.cs deleted file mode 100644 index a5635a9e1..000000000 --- a/src/NATS.Client.Core/NatsConnection.Util.cs +++ /dev/null @@ -1,96 +0,0 @@ -using NATS.Client.Core.Commands; -using NATS.Client.Core.Internal; - -namespace NATS.Client.Core; - -public partial class NatsConnection -{ - internal void PostDirectWrite(ICommand command) - { - if (ConnectionState == NatsConnectionState.Open) - { - EnqueueCommandSync(command); - } - else - { - WithConnect(command, static (self, command) => - { - self.EnqueueCommandSync(command); - }); - } - } - - // DirectWrite is not supporting CancellationTimer - internal void PostDirectWrite(string protocol, int repeatCount = 1) - { - if (ConnectionState == NatsConnectionState.Open) - { - EnqueueCommandSync(new DirectWriteCommand(protocol, repeatCount)); - } - else - { - WithConnect(protocol, repeatCount, static (self, protocol, repeatCount) => - { - self.EnqueueCommandSync(new DirectWriteCommand(protocol, repeatCount)); - }); - } - } - - internal void PostDirectWrite(byte[] protocol) - { - if (ConnectionState == NatsConnectionState.Open) - { - EnqueueCommandSync(new DirectWriteCommand(protocol)); - } - else - { - WithConnect(protocol, static (self, protocol) => - { - self.EnqueueCommandSync(new DirectWriteCommand(protocol)); - }); - } - } - - internal void PostDirectWrite(DirectWriteCommand command) - { - if (ConnectionState == NatsConnectionState.Open) - { - EnqueueCommandSync(command); - } - else - { - WithConnect(command, static (self, command) => - { - self.EnqueueCommandSync(command); - }); - } - } - - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - internal CancellationTimer GetCancellationTimer(CancellationToken cancellationToken, TimeSpan timeout = default) - { - if (timeout == default) - timeout = Opts.CommandTimeout; - return _cancellationTimerPool.Start(timeout, cancellationToken); - } - - private async ValueTask EnqueueAndAwaitCommandAsync(IAsyncCommand command) - { - await EnqueueCommandAsync(command).ConfigureAwait(false); - await command.AsValueTask().ConfigureAwait(false); - } - - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - private bool TryEnqueueCommand(ICommand command) - { - if (_commandWriter.TryWrite(command)) - { - Interlocked.Increment(ref Counter.PendingMessages); - return true; - } - else - { - return false; - } - } -} diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs index 4eaecd915..847be4610 100644 --- a/src/NATS.Client.Core/NatsConnection.cs +++ b/src/NATS.Client.Core/NatsConnection.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics; +using System.IO.Pipelines; using System.Threading.Channels; using Microsoft.Extensions.Logging; using NATS.Client.Core.Commands; @@ -29,8 +30,6 @@ public partial class NatsConnection : INatsConnection #pragma warning restore SA1401 private readonly object _gate = new object(); - private readonly WriterState _writerState; - private readonly ChannelWriter _commandWriter; private readonly ILogger _logger; private readonly ObjectPool _pool; private readonly CancellationTimerPool _cancellationTimerPool; @@ -72,8 +71,7 @@ public NatsConnection(NatsOpts opts) _cancellationTimerPool = new CancellationTimerPool(_pool, _disposedCancellationTokenSource.Token); _name = opts.Name; Counter = new ConnectionStatsCounter(); - _writerState = new WriterState(opts); - _commandWriter = _writerState.CommandBuffer.Writer; + CommandWriter = new CommandWriter(Opts, Counter, EnqueuePing); InboxPrefix = NewInbox(opts.InboxPrefix); SubscriptionManager = new SubscriptionManager(this, InboxPrefix); _logger = opts.LoggerFactory.CreateLogger(); @@ -111,6 +109,8 @@ public NatsConnectionState ConnectionState internal SubscriptionManager SubscriptionManager { get; } + internal CommandWriter CommandWriter { get; } + internal string InboxPrefix { get; } internal ObjectPool ObjectPool => _pool; @@ -143,11 +143,9 @@ public async ValueTask ConnectAsync() await waiter.Task.ConfigureAwait(false); return; } - else - { - // Only Closed(initial) state, can run initial connect. - await InitialConnectAsync().ConfigureAwait(false); - } + + // Only Closed(initial) state, can run initial connect. + await InitialConnectAsync().ConfigureAwait(false); } public virtual async ValueTask DisposeAsync() @@ -167,12 +165,8 @@ public virtual async ValueTask DisposeAsync() #endif } - foreach (var item in _writerState.PendingPromises) - { - item.SetCanceled(); - } - await SubscriptionManager.DisposeAsync().ConfigureAwait(false); + await CommandWriter.DisposeAsync().ConfigureAwait(false); _waitForOpenConnection.TrySetCanceled(); #if NET6_0 _disposedCancellationTokenSource.Cancel(); @@ -184,22 +178,6 @@ public virtual async ValueTask DisposeAsync() internal NatsStats GetStats() => Counter.ToStats(); - internal void EnqueuePing(AsyncPingCommand pingCommand) - { - // Enqueue Ping Command to current working reader. - var reader = _socketReader; - if (reader != null) - { - if (reader.TryEnqueuePing(pingCommand)) - { - return; - } - } - - // Can not add PING, set fail. - pingCommand.SetCanceled(); - } - internal ValueTask PublishToClientHandlersAsync(string subject, string? replyTo, int sid, in ReadOnlySequence? headersBuffer, in ReadOnlySequence payloadBuffer) { return SubscriptionManager.PublishToClientHandlersAsync(subject, replyTo, sid, headersBuffer, payloadBuffer); @@ -210,29 +188,16 @@ internal void ResetPongCount() Interlocked.Exchange(ref _pongCount, 0); } - internal async ValueTask EnqueueAndAwaitCommandAsync(IAsyncCommand command) - { - await EnqueueCommandAsync(command).ConfigureAwait(false); - return await command.AsValueTask().ConfigureAwait(false); - } - - internal ValueTask PostPongAsync() - { - return EnqueueCommandAsync(PongCommand.Create(_pool, GetCancellationTimer(CancellationToken.None))); - } + internal ValueTask PongAsync() => CommandWriter.PongAsync(CancellationToken.None); // called only internally - internal ValueTask SubscribeCoreAsync(int sid, string subject, string? queueGroup, int? maxMsgs, CancellationToken cancellationToken) - { - var command = AsyncSubscribeCommand.Create(_pool, GetCancellationTimer(cancellationToken), sid, subject, queueGroup, maxMsgs); - return EnqueueAndAwaitCommandAsync(command); - } + internal ValueTask SubscribeCoreAsync(int sid, string subject, string? queueGroup, int? maxMsgs, CancellationToken cancellationToken) => CommandWriter.SubscribeAsync(sid, subject, queueGroup, maxMsgs, cancellationToken); internal ValueTask UnsubscribeAsync(int sid) { try { - return EnqueueCommandAsync(UnsubscribeCommand.Create(_pool, sid)); + return CommandWriter.UnsubscribeAsync(sid, CancellationToken.None); } catch (Exception ex) { @@ -445,26 +410,31 @@ private async ValueTask SetupReaderWriterAsync(bool reconnect) // Authentication _userCredentials?.Authenticate(_clientOpts, WritableServerInfo); - // add CONNECT and PING command to priority lane - _writerState.PriorityCommands.Clear(); - var connectCommand = AsyncConnectCommand.Create(_pool, _clientOpts, GetCancellationTimer(CancellationToken.None)); - _writerState.PriorityCommands.Add(connectCommand); - _writerState.PriorityCommands.Add(PingCommand.Create(_pool, GetCancellationTimer(CancellationToken.None))); - - if (reconnect) + await using (var priorityCommandWriter = new PriorityCommandWriter(_socket!, Opts, Counter, EnqueuePing)) { - // Reestablish subscriptions and consumers - _writerState.PriorityCommands.AddRange(SubscriptionManager.GetReconnectCommands()); - } + // add CONNECT and PING command to priority lane + await priorityCommandWriter.CommandWriter.ConnectAsync(_clientOpts, CancellationToken.None).ConfigureAwait(false); + await priorityCommandWriter.CommandWriter.PingAsync(new PingCommand(), CancellationToken.None).ConfigureAwait(false); - // create the socket writer - _socketWriter = new NatsPipeliningWriteProtocolProcessor(_socket!, _writerState, _pool, Counter); + Task? reconnectTask = null; + if (reconnect) + { + // Reestablish subscriptions and consumers + reconnectTask = SubscriptionManager.WriteReconnectCommandsAsync(priorityCommandWriter.CommandWriter).AsTask(); + } + + // receive COMMAND response (PONG or ERROR) + await waitForPongOrErrorSignal.Task.ConfigureAwait(false); - // wait for COMMAND to send - await connectCommand.AsValueTask().ConfigureAwait(false); + if (reconnectTask != null) + { + // wait for reconnect commands to complete + await reconnectTask.ConfigureAwait(false); + } + } - // receive COMMAND response (PONG or ERROR) - await waitForPongOrErrorSignal.Task.ConfigureAwait(false); + // create the socket writer + _socketWriter = CommandWriter.CreateNatsPipeliningWriteProtocolProcessor(_socket!); lock (_gate) { @@ -727,37 +697,26 @@ private async void StartPingTimer(CancellationToken cancellationToken) } } - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - private CancellationTimer GetRequestCommandTimer(CancellationToken cancellationToken) + private void EnqueuePing(PingCommand pingCommand) { - return _cancellationTimerPool.Start(Opts.RequestTimeout, cancellationToken); - } - - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - private void EnqueueCommandSync(ICommand command) - { - if (_commandWriter.TryWrite(command)) - { - Interlocked.Increment(ref Counter.PendingMessages); - } - else + // Enqueue Ping Command to current working reader. + var reader = _socketReader; + if (reader != null) { - throw new NatsException("Can't write to command channel"); + if (reader.TryEnqueuePing(pingCommand)) + { + return; + } } + + // Can not add PING, set fail. + pingCommand.TaskCompletionSource.SetCanceled(); } - private async ValueTask EnqueueCommandAsync(ICommand command) + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private CancellationTimer GetRequestCommandTimer(CancellationToken cancellationToken) { - RETRY: - if (_commandWriter.TryWrite(command)) - { - Interlocked.Increment(ref Counter.PendingMessages); - } - else - { - await _commandWriter.WaitToWriteAsync(_disposedCancellationTokenSource.Token).ConfigureAwait(false); - goto RETRY; - } + return _cancellationTimerPool.Start(Opts.RequestTimeout, cancellationToken); } // catch and log all exceptions, enforcing the socketComponentDisposeTimeout @@ -815,191 +774,4 @@ private void ThrowIfDisposed() if (IsDisposed) throw new ObjectDisposedException(null); } - - private async void WithConnect(Action core) - { - try - { - await ConnectAsync().ConfigureAwait(false); - } - catch - { - // log will shown on ConnectAsync failed - return; - } - - core(this); - } - - private async void WithConnect(T1 item1, Action core) - { - try - { - await ConnectAsync().ConfigureAwait(false); - } - catch - { - // log will shown on ConnectAsync failed - return; - } - - core(this, item1); - } - - private async void WithConnect(T1 item1, T2 item2, Action core) - { - try - { - await ConnectAsync().ConfigureAwait(false); - } - catch - { - // log will shown on ConnectAsync failed - return; - } - - core(this, item1, item2); - } - - private async void WithConnect(T1 item1, T2 item2, T3 item3, Action core) - { - try - { - await ConnectAsync().ConfigureAwait(false); - } - catch - { - // log will shown on ConnectAsync failed - return; - } - - core(this, item1, item2, item3); - } - - private async ValueTask WithConnectAsync(Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1, item2).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1, item2, item3).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, T4 item4, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1, item2, item3, item4).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1, item2, item3, item4, item5).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1, item2, item3, item4, item5, item6).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7, Func coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - await coreAsync(this, item1, item2, item3, item4, item5, item6, item7).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(Func> coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - return await coreAsync(this).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, Func> coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - return await coreAsync(this, item1).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, Func> coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - return await coreAsync(this, item1, item2).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, Func> coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - return await coreAsync(this, item1, item2, item3).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, T4 item4, Func> coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - return await coreAsync(this, item1, item2, item3, item4).ConfigureAwait(false); - } - - private async ValueTask WithConnectAsync(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, Func> coreAsync) - { - await ConnectAsync().ConfigureAwait(false); - return await coreAsync(this, item1, item2, item3, item4, item5).ConfigureAwait(false); - } -} - -// This writer state is reused when reconnecting. -internal sealed class WriterState -{ - public WriterState(NatsOpts opts) - { - Opts = opts; - BufferWriter = new FixedArrayBufferWriter(); - - if (opts.WriterCommandBufferLimit == null) - { - CommandBuffer = Channel.CreateUnbounded(new UnboundedChannelOptions - { - AllowSynchronousContinuations = false, // always should be in async loop. - SingleWriter = false, - SingleReader = true, - }); - } - else - { - CommandBuffer = Channel.CreateBounded(new BoundedChannelOptions(opts.WriterCommandBufferLimit.Value) - { - FullMode = BoundedChannelFullMode.Wait, - AllowSynchronousContinuations = false, // always should be in async loop. - SingleWriter = false, - SingleReader = true, - }); - } - - PriorityCommands = new List(); - PendingPromises = new List(); - } - - public FixedArrayBufferWriter BufferWriter { get; } - - public Channel CommandBuffer { get; } - - public NatsOpts Opts { get; } - - public List PriorityCommands { get; } - - public List PendingPromises { get; } } diff --git a/src/NATS.Client.Core/NatsOpts.cs b/src/NATS.Client.Core/NatsOpts.cs index d22c7a042..c44ffda56 100644 --- a/src/NATS.Client.Core/NatsOpts.cs +++ b/src/NATS.Client.Core/NatsOpts.cs @@ -31,7 +31,7 @@ public sealed record NatsOpts public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; - public int WriterBufferSize { get; init; } = 65534; + public int WriterBufferSize { get; init; } = 1048576; public int ReaderBufferSize { get; init; } = 1048576; @@ -61,14 +61,14 @@ public sealed record NatsOpts public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(5); - public TimeSpan CommandTimeout { get; init; } = TimeSpan.FromMinutes(1); + public TimeSpan CommandTimeout { get; init; } = TimeSpan.FromSeconds(5); public TimeSpan SubscriptionCleanUpInterval { get; init; } = TimeSpan.FromMinutes(5); - public int? WriterCommandBufferLimit { get; init; } = 1_000; - public Encoding HeaderEncoding { get; init; } = Encoding.ASCII; + public Encoding SubjectEncoding { get; init; } = Encoding.ASCII; + public bool WaitUntilSent { get; init; } = false; /// diff --git a/src/NATS.Client.Core/NatsPubOpts.cs b/src/NATS.Client.Core/NatsPubOpts.cs index d7cf2ec38..58d2c4beb 100644 --- a/src/NATS.Client.Core/NatsPubOpts.cs +++ b/src/NATS.Client.Core/NatsPubOpts.cs @@ -3,17 +3,18 @@ namespace NATS.Client.Core; public record NatsPubOpts { /// - /// When set to true, calls to PublishAsync will complete after data has been written to socket - /// Default value is false, and calls to PublishAsync will complete after the publish command has been written to the Command Channel + /// Obsolete option historically used to control when PublishAsync returned + /// No longer has any effect + /// This option method will be removed in a future release /// + [Obsolete("No longer has any effect")] public bool? WaitUntilSent { get; init; } /// - /// Optional callback to handle serialization exceptions. + /// Obsolete callback historically used for handling serialization errors + /// All errors are now thrown as exceptions in PublishAsync + /// This option method will be removed in a future release /// - /// - /// When WaitUntilSent is set to false serialization exceptions won't propagate - /// to the caller but this callback will be called with the exception thrown by the serializer. - /// + [Obsolete("All errors are now thrown as exceptions in PublishAsync")] public Action? ErrorHandler { get; init; } } diff --git a/src/NATS.Client.Core/NatsSubBase.cs b/src/NATS.Client.Core/NatsSubBase.cs index 1ec37c33f..570fdaea3 100644 --- a/src/NATS.Client.Core/NatsSubBase.cs +++ b/src/NATS.Client.Core/NatsSubBase.cs @@ -239,19 +239,17 @@ public virtual async ValueTask ReceiveAsync(string subject, string? replyTo, Rea internal void ClearException() => Interlocked.Exchange(ref _exception, null); /// - /// Collect commands when reconnecting. + /// Write commands when reconnecting. /// /// - /// By default this will yield the required subscription command. - /// When overriden base must be called to yield the re-subscription command. - /// Additional command (e.g. publishing pull requests in case of JetStream consumers) can be yielded as part of the reconnect routine. + /// By default this will write the required subscription command. + /// When overriden base must be called to write the re-subscription command. + /// Additional command (e.g. publishing pull requests in case of JetStream consumers) can be written as part of the reconnect routine. /// + /// command writer used to write reconnect commands /// SID which might be required to create subscription commands - /// IEnumerable list of commands - internal virtual IEnumerable GetReconnectCommands(int sid) - { - yield return AsyncSubscribeCommand.Create(Connection.ObjectPool, Connection.GetCancellationTimer(default), sid, Subject, QueueGroup, PendingMsgs); - } + /// ValueTask + internal virtual ValueTask WriteReconnectCommandsAsync(CommandWriter commandWriter, int sid) => commandWriter.SubscribeAsync(sid, Subject, QueueGroup, PendingMsgs, CancellationToken.None); /// /// Invoked when a MSG or HMSG arrives for the subscription. diff --git a/src/NATS.Client.JetStream/Internal/NatsJSConsume.cs b/src/NATS.Client.JetStream/Internal/NatsJSConsume.cs index 14f660433..d94c9b88b 100644 --- a/src/NATS.Client.JetStream/Internal/NatsJSConsume.cs +++ b/src/NATS.Client.JetStream/Internal/NatsJSConsume.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using NATS.Client.Core; using NATS.Client.Core.Commands; +using NATS.Client.Core.Internal; using NATS.Client.JetStream.Models; namespace NATS.Client.JetStream.Internal; @@ -153,13 +154,12 @@ public ValueTask CallMsgNextAsync(string origin, ConsumerGetnextRequest request, _logger.LogDebug(NatsJSLogEvents.PullRequest, "Sending pull request for {Origin} {Msgs}, {Bytes}", origin, request.Batch, request.MaxBytes); } - return Connection.PubModelAsync( + return Connection.PublishAsync( subject: $"{_context.Opts.Prefix}.CONSUMER.MSG.NEXT.{_stream}.{_consumer}", data: request, - serializer: NatsJSJsonSerializer.Default, replyTo: Subject, - headers: default, - cancellationToken); + serializer: NatsJSJsonSerializer.Default, + cancellationToken: cancellationToken); } public void ResetHeartbeatTimer() => _timer.Change(_hbTimeout, _hbTimeout); @@ -176,11 +176,9 @@ public override async ValueTask DisposeAsync() } } - internal override IEnumerable GetReconnectCommands(int sid) + internal override async ValueTask WriteReconnectCommandsAsync(CommandWriter commandWriter, int sid) { - foreach (var command in base.GetReconnectCommands(sid)) - yield return command; - + await base.WriteReconnectCommandsAsync(commandWriter, sid); ResetPending(); var request = new ConsumerGetnextRequest @@ -192,17 +190,15 @@ internal override IEnumerable GetReconnectCommands(int sid) }; if (_cancellationToken.IsCancellationRequested) - yield break; + return; - yield return PublishCommand.Create( - pool: Connection.ObjectPool, + await commandWriter.PublishAsync( subject: $"{_context.Opts.Prefix}.CONSUMER.MSG.NEXT.{_stream}.{_consumer}", - replyTo: Subject, - headers: default, value: request, + headers: default, + replyTo: Subject, serializer: NatsJSJsonSerializer.Default, - errorHandler: default, - cancellationToken: default); + cancellationToken: CancellationToken.None); } protected override async ValueTask ReceiveInternalAsync( diff --git a/src/NATS.Client.JetStream/Internal/NatsJSFetch.cs b/src/NATS.Client.JetStream/Internal/NatsJSFetch.cs index 9804ef64e..751d2f923 100644 --- a/src/NATS.Client.JetStream/Internal/NatsJSFetch.cs +++ b/src/NATS.Client.JetStream/Internal/NatsJSFetch.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using NATS.Client.Core; using NATS.Client.Core.Commands; +using NATS.Client.Core.Internal; using NATS.Client.JetStream.Models; namespace NATS.Client.JetStream.Internal; @@ -126,13 +127,12 @@ public NatsJSFetch( public ChannelReader> Msgs { get; } public ValueTask CallMsgNextAsync(ConsumerGetnextRequest request, CancellationToken cancellationToken = default) => - Connection.PubModelAsync( + Connection.PublishAsync( subject: $"{_context.Opts.Prefix}.CONSUMER.MSG.NEXT.{_stream}.{_consumer}", data: request, - serializer: NatsJSJsonSerializer.Default, replyTo: Subject, - headers: default, - cancellationToken); + serializer: NatsJSJsonSerializer.Default, + cancellationToken: cancellationToken); public void ResetHeartbeatTimer() => _hbTimer.Change(_hbTimeout, Timeout.Infinite); @@ -148,11 +148,9 @@ public override async ValueTask DisposeAsync() } } - internal override IEnumerable GetReconnectCommands(int sid) + internal override async ValueTask WriteReconnectCommandsAsync(CommandWriter commandWriter, int sid) { - foreach (var command in base.GetReconnectCommands(sid)) - yield return command; - + await base.WriteReconnectCommandsAsync(commandWriter, sid); var request = new ConsumerGetnextRequest { Batch = _maxMsgs, @@ -160,15 +158,13 @@ internal override IEnumerable GetReconnectCommands(int sid) Expires = _expires, }; - yield return PublishCommand.Create( - pool: Connection.ObjectPool, + await commandWriter.PublishAsync( subject: $"{_context.Opts.Prefix}.CONSUMER.MSG.NEXT.{_stream}.{_consumer}", - replyTo: Subject, - headers: default, value: request, + headers: default, + replyTo: Subject, serializer: NatsJSJsonSerializer.Default, - errorHandler: default, - cancellationToken: default); + cancellationToken: CancellationToken.None); } protected override async ValueTask ReceiveInternalAsync( diff --git a/src/NATS.Client.JetStream/Internal/NatsJSOrderedConsume.cs b/src/NATS.Client.JetStream/Internal/NatsJSOrderedConsume.cs index 48b500bec..e1a38227b 100644 --- a/src/NATS.Client.JetStream/Internal/NatsJSOrderedConsume.cs +++ b/src/NATS.Client.JetStream/Internal/NatsJSOrderedConsume.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using NATS.Client.Core; using NATS.Client.Core.Commands; +using NATS.Client.Core.Internal; using NATS.Client.JetStream.Models; namespace NATS.Client.JetStream.Internal; @@ -123,13 +124,12 @@ public ValueTask CallMsgNextAsync(string origin, ConsumerGetnextRequest request, _logger.LogDebug(NatsJSLogEvents.PullRequest, "Sending pull request for {Origin} {Msgs}, {Bytes}", origin, request.Batch, request.MaxBytes); } - return Connection.PubModelAsync( + return Connection.PublishAsync( subject: $"{_context.Opts.Prefix}.CONSUMER.MSG.NEXT.{_stream}.{_consumer}", data: request, - serializer: NatsJSJsonSerializer.Default, replyTo: Subject, - headers: default, - cancellationToken); + serializer: NatsJSJsonSerializer.Default, + cancellationToken: cancellationToken); } public void ResetHeartbeatTimer() => _timer.Change(_hbTimeout, Timeout.Infinite); @@ -145,10 +145,10 @@ public override async ValueTask DisposeAsync() await _timer.DisposeAsync().ConfigureAwait(false); } - internal override IEnumerable GetReconnectCommands(int sid) + internal override ValueTask WriteReconnectCommandsAsync(CommandWriter commandWriter, int sid) { // Override normal subscription behavior to resubscribe on reconnect - yield break; + return ValueTask.CompletedTask; } protected override async ValueTask ReceiveInternalAsync( diff --git a/src/NATS.Client.JetStream/NatsJSContext.cs b/src/NATS.Client.JetStream/NatsJSContext.cs index d6779ed2c..447e76db8 100644 --- a/src/NATS.Client.JetStream/NatsJSContext.cs +++ b/src/NATS.Client.JetStream/NatsJSContext.cs @@ -199,50 +199,42 @@ internal async ValueTask> JSRequestAsync( + subject: subject, + data: request, + headers: default, + replyOpts: new NatsSubOpts { Timeout = Connection.Opts.RequestTimeout }, + requestSerializer: NatsJSJsonSerializer.Default, + replySerializer: NatsJSErrorAwareJsonSerializer.Default, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (await sub.Msgs.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { - await using var sub = await Connection.RequestSubAsync( - subject: subject, - data: request, - headers: default, - replyOpts: new NatsSubOpts { Timeout = Connection.Opts.RequestTimeout }, - requestSerializer: NatsJSJsonSerializer.Default, - replySerializer: NatsJSErrorAwareJsonSerializer.Default, - cancellationToken: cancellationTimer.Token) - .ConfigureAwait(false); - - if (await sub.Msgs.WaitToReadAsync(cancellationTimer.Token).ConfigureAwait(false)) + if (sub.Msgs.TryRead(out var msg)) { - if (sub.Msgs.TryRead(out var msg)) + if (msg.Data == null) { - if (msg.Data == null) - { - throw new NatsJSException("No response data received"); - } - - return new NatsJSResponse(msg.Data, default); + throw new NatsJSException("No response data received"); } + + return new NatsJSResponse(msg.Data, default); } + } - if (sub is NatsSubBase { EndReason: NatsSubEndReason.Exception, Exception: not null } sb) + if (sub is NatsSubBase { EndReason: NatsSubEndReason.Exception, Exception: not null } sb) + { + if (sb.Exception is NatsSubException { Exception.SourceException: NatsJSApiErrorException jsError }) { - if (sb.Exception is NatsSubException { Exception.SourceException: NatsJSApiErrorException jsError }) - { - // Clear exception here so that subscription disposal won't throw it. - sb.ClearException(); - - return new NatsJSResponse(default, jsError.Error); - } + // Clear exception here so that subscription disposal won't throw it. + sb.ClearException(); - throw sb.Exception; + return new NatsJSResponse(default, jsError.Error); } - throw new NatsJSApiNoResponseException(); - } - finally - { - cancellationTimer.TryReturn(); + throw sb.Exception; } + + throw new NatsJSApiNoResponseException(); } } diff --git a/src/NATS.Client.JetStream/NatsJSMsg.cs b/src/NATS.Client.JetStream/NatsJSMsg.cs index e0d9aa255..25aef4c76 100644 --- a/src/NATS.Client.JetStream/NatsJSMsg.cs +++ b/src/NATS.Client.JetStream/NatsJSMsg.cs @@ -86,7 +86,7 @@ public interface INatsJSMsg /// Ack options. /// A used to cancel the call. /// A representing the async call. - ValueTask AckAsync(AckOpts opts = default, CancellationToken cancellationToken = default); + ValueTask AckAsync(AckOpts? opts = default, CancellationToken cancellationToken = default); /// /// Signals that the message will not be processed now and processing can move onto the next message. @@ -99,7 +99,7 @@ public interface INatsJSMsg /// Messages rejected using -NAK will be resent by the NATS JetStream server after the configured timeout /// or the delay parameter if it's specified. /// - ValueTask NakAsync(AckOpts opts = default, TimeSpan delay = default, CancellationToken cancellationToken = default); + ValueTask NakAsync(AckOpts? opts = default, TimeSpan delay = default, CancellationToken cancellationToken = default); /// /// Indicates that work is ongoing and the wait period should be extended. @@ -117,7 +117,7 @@ public interface INatsJSMsg /// by another amount of time equal to ack_wait by the NATS JetStream server. /// /// - ValueTask AckProgressAsync(AckOpts opts = default, CancellationToken cancellationToken = default); + ValueTask AckProgressAsync(AckOpts? opts = default, CancellationToken cancellationToken = default); /// /// Instructs the server to stop redelivery of the message without acknowledging it as successfully processed. @@ -125,7 +125,7 @@ public interface INatsJSMsg /// Ack options. /// A used to cancel the call. /// A representing the async call. - ValueTask AckTerminateAsync(AckOpts opts = default, CancellationToken cancellationToken = default); + ValueTask AckTerminateAsync(AckOpts? opts = default, CancellationToken cancellationToken = default); } /// @@ -203,7 +203,7 @@ public ValueTask ReplyAsync(NatsHeaders? headers = default, string? replyTo = de /// Ack options. /// A used to cancel the call. /// A representing the async call. - public ValueTask AckAsync(AckOpts opts = default, CancellationToken cancellationToken = default) => SendAckAsync(NatsJSConstants.Ack, opts, cancellationToken); + public ValueTask AckAsync(AckOpts? opts = default, CancellationToken cancellationToken = default) => SendAckAsync(NatsJSConstants.Ack, opts, cancellationToken); /// /// Signals that the message will not be processed now and processing can move onto the next message. @@ -216,7 +216,7 @@ public ValueTask ReplyAsync(NatsHeaders? headers = default, string? replyTo = de /// Messages rejected using -NAK will be resent by the NATS JetStream server after the configured timeout /// or the delay parameter if it's specified. /// - public ValueTask NakAsync(AckOpts opts = default, TimeSpan delay = default, CancellationToken cancellationToken = default) + public ValueTask NakAsync(AckOpts? opts = default, TimeSpan delay = default, CancellationToken cancellationToken = default) { if (delay == default) { @@ -245,7 +245,7 @@ public ValueTask NakAsync(AckOpts opts = default, TimeSpan delay = default, Canc /// by another amount of time equal to ack_wait by the NATS JetStream server. /// /// - public ValueTask AckProgressAsync(AckOpts opts = default, CancellationToken cancellationToken = default) => SendAckAsync(NatsJSConstants.AckProgress, opts, cancellationToken); + public ValueTask AckProgressAsync(AckOpts? opts = default, CancellationToken cancellationToken = default) => SendAckAsync(NatsJSConstants.AckProgress, opts, cancellationToken); /// /// Instructs the server to stop redelivery of the message without acknowledging it as successfully processed. @@ -253,16 +253,16 @@ public ValueTask NakAsync(AckOpts opts = default, TimeSpan delay = default, Canc /// Ack options. /// A used to cancel the call. /// A representing the async call. - public ValueTask AckTerminateAsync(AckOpts opts = default, CancellationToken cancellationToken = default) => SendAckAsync(NatsJSConstants.AckTerminate, opts, cancellationToken); + public ValueTask AckTerminateAsync(AckOpts? opts = default, CancellationToken cancellationToken = default) => SendAckAsync(NatsJSConstants.AckTerminate, opts, cancellationToken); - private async ValueTask SendAckAsync(ReadOnlySequence payload, AckOpts opts = default, CancellationToken cancellationToken = default) + private async ValueTask SendAckAsync(ReadOnlySequence payload, AckOpts? opts = default, CancellationToken cancellationToken = default) { CheckPreconditions(); if (_msg == default) throw new NatsJSException("No user message, can't acknowledge"); - if ((opts.DoubleAck ?? _context.Opts.AckOpts.DoubleAck) == true) + if (opts?.DoubleAck ?? _context.Opts.DoubleAck) { await Connection.RequestAsync, object?>( subject: ReplyTo, @@ -275,10 +275,6 @@ private async ValueTask SendAckAsync(ReadOnlySequence payload, AckOpts opt { await _msg.ReplyAsync( data: payload, - opts: new NatsPubOpts - { - WaitUntilSent = opts.WaitUntilSent ?? _context.Opts.AckOpts.WaitUntilSent, - }, serializer: NatsRawSerializer>.Default, cancellationToken: cancellationToken); } @@ -303,6 +299,10 @@ private void CheckPreconditions() /// /// Options to be used when acknowledging messages received from a stream using a consumer. /// -/// Wait for the publish to be flushed down to the network. -/// Ask server for an acknowledgment. -public readonly record struct AckOpts(bool? WaitUntilSent = false, bool? DoubleAck = false); +public readonly record struct AckOpts +{ + /// + /// Ask server for an acknowledgment + /// + public bool? DoubleAck { get; init; } +} diff --git a/src/NATS.Client.JetStream/NatsJSOpts.cs b/src/NATS.Client.JetStream/NatsJSOpts.cs index 0a65791fb..48d56ef53 100644 --- a/src/NATS.Client.JetStream/NatsJSOpts.cs +++ b/src/NATS.Client.JetStream/NatsJSOpts.cs @@ -16,7 +16,6 @@ public NatsJSOpts(NatsOpts opts, string? apiPrefix = default, string? domain = d } ApiPrefix = apiPrefix ?? "$JS.API"; - AckOpts = ackOpts ?? new AckOpts(opts.WaitUntilSent); Domain = domain; } @@ -36,12 +35,12 @@ public NatsJSOpts(NatsOpts opts, string? apiPrefix = default, string? domain = d public string? Domain { get; } /// - /// Message ACK options . + /// Ask server for an acknowledgment. /// /// - /// These options are used as the defaults when acknowledging messages received from a stream using a consumer. + /// Defaults to false. /// - public AckOpts AckOpts { get; init; } + public bool DoubleAck { get; init; } = false; /// /// Default consume options to be used in consume calls in this context. diff --git a/src/NATS.Client.Serializers.Json/NatsJsonSerializer.cs b/src/NATS.Client.Serializers.Json/NatsJsonSerializer.cs index 8266eadaf..9c0244718 100644 --- a/src/NATS.Client.Serializers.Json/NatsJsonSerializer.cs +++ b/src/NATS.Client.Serializers.Json/NatsJsonSerializer.cs @@ -31,7 +31,7 @@ public sealed class NatsJsonSerializer : INatsSerialize, INatsDeserialize< /// public static NatsJsonSerializer Default { get; } = new(new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); - /// + /// flush public void Serialize(IBufferWriter bufferWriter, T? value) { Utf8JsonWriter writer; diff --git a/src/NATS.Client.Services/Internal/SvcListener.cs b/src/NATS.Client.Services/Internal/SvcListener.cs index 7220155a2..88fdd234d 100644 --- a/src/NATS.Client.Services/Internal/SvcListener.cs +++ b/src/NATS.Client.Services/Internal/SvcListener.cs @@ -10,9 +10,8 @@ internal class SvcListener : IAsyncDisposable private readonly SvcMsgType _type; private readonly string _subject; private readonly string? _queueGroup; - private readonly CancellationToken _cancellationToken; + private readonly CancellationTokenSource _cts; private Task? _readLoop; - private CancellationTokenSource? _cts; public SvcListener(NatsConnection nats, Channel channel, SvcMsgType type, string subject, string? queueGroup, CancellationToken cancellationToken) { @@ -21,27 +20,37 @@ public SvcListener(NatsConnection nats, Channel channel, SvcMsgType type _type = type; _subject = subject; _queueGroup = queueGroup; - _cancellationToken = cancellationToken; + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); } - public ValueTask StartAsync() + public async ValueTask StartAsync() { - _cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken); + var sub = await _nats.SubscribeCoreAsync(_subject, _queueGroup, serializer: NatsRawSerializer>.Default, cancellationToken: _cts.Token); _readLoop = Task.Run(async () => { - await foreach (var msg in _nats.SubscribeAsync>(_subject, _queueGroup, serializer: NatsRawSerializer>.Default, cancellationToken: _cts.Token)) + await using (sub) { - await _channel.Writer.WriteAsync(new SvcMsg(_type, msg), _cancellationToken).ConfigureAwait(false); + await foreach (var msg in sub.Msgs.ReadAllAsync()) + { + await _channel.Writer.WriteAsync(new SvcMsg(_type, msg), _cts.Token).ConfigureAwait(false); + } } }); - return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() { - _cts?.Cancel(); - + _cts.Cancel(); if (_readLoop != null) - await _readLoop; + { + try + { + await _readLoop; + } + catch (OperationCanceledException) + { + // intentionally canceled + } + } } } diff --git a/src/NATS.Client.Services/NatsSvcServer.cs b/src/NATS.Client.Services/NatsSvcServer.cs index f99efbeaf..a0ad59e51 100644 --- a/src/NATS.Client.Services/NatsSvcServer.cs +++ b/src/NATS.Client.Services/NatsSvcServer.cs @@ -18,7 +18,6 @@ public class NatsSvcServer : INatsSvcServer private readonly string _id; private readonly NatsConnection _nats; private readonly NatsSvcConfig _config; - private readonly CancellationToken _cancellationToken; private readonly Channel _channel; private readonly Task _taskMsgLoop; private readonly List _svcListeners = new(); @@ -39,7 +38,6 @@ public NatsSvcServer(NatsConnection nats, NatsSvcConfig config, CancellationToke _nats = nats; _config = config; _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _cancellationToken = _cts.Token; _channel = Channel.CreateBounded(32); _taskMsgLoop = Task.Run(MsgLoop); _started = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"); @@ -72,8 +70,14 @@ public async ValueTask StopAsync(CancellationToken cancellationToken = default) _channel.Writer.TryComplete(); _cts.Cancel(); - - await _taskMsgLoop; + try + { + await _taskMsgLoop; + } + catch (OperationCanceledException) + { + // intentionally canceled + } } /// @@ -180,7 +184,7 @@ public InfoResponse GetInfo() /// public async ValueTask DisposeAsync() { - await StopAsync(_cancellationToken); + await StopAsync(_cts.Token); GC.SuppressFinalize(this); } @@ -194,7 +198,7 @@ internal async ValueTask StartAsync() foreach (var subject in new[] { $"$SRV.{type}", $"$SRV.{type}.{name}", $"$SRV.{type}.{name}.{_id}" }) { // for discovery subjects do not use a queue group - var svcListener = new SvcListener(_nats, _channel, svcType, subject, default, _cancellationToken); + var svcListener = new SvcListener(_nats, _channel, svcType, subject, default, _cts.Token); await svcListener.StartAsync(); _svcListeners.Add(svcListener); } @@ -223,7 +227,7 @@ private async ValueTask AddEndpointInternalAsync(Func, ValueTas private async Task MsgLoop() { - await foreach (var svcMsg in _channel.Reader.ReadAllAsync(_cancellationToken)) + await foreach (var svcMsg in _channel.Reader.ReadAllAsync(_cts.Token)) { try { @@ -246,7 +250,7 @@ await svcMsg.Msg.ReplyAsync( Metadata = _config.Metadata!, }, serializer: NatsSrvJsonSerializer.Default, - cancellationToken: _cancellationToken); + cancellationToken: _cts.Token); } else if (type == SvcMsgType.Info) { @@ -258,7 +262,7 @@ await svcMsg.Msg.ReplyAsync( await svcMsg.Msg.ReplyAsync( data: GetInfo(), serializer: NatsSrvJsonSerializer.Default, - cancellationToken: _cancellationToken); + cancellationToken: _cts.Token); } else if (type == SvcMsgType.Stats) { @@ -270,7 +274,7 @@ await svcMsg.Msg.ReplyAsync( await svcMsg.Msg.ReplyAsync( data: GetStats(), serializer: NatsSrvJsonSerializer.Default, - cancellationToken: _cancellationToken); + cancellationToken: _cts.Token); } } catch (Exception ex) @@ -286,7 +290,6 @@ await svcMsg.Msg.ReplyAsync( public class Group { private readonly NatsSvcServer _server; - private readonly CancellationToken _cancellationToken; private readonly string _dot; /// @@ -302,7 +305,6 @@ public Group(NatsSvcServer server, string groupName, string? queueGroup = defaul _server = server; GroupName = groupName; QueueGroup = queueGroup; - _cancellationToken = cancellationToken; _dot = GroupName.Length == 0 ? string.Empty : "."; } diff --git a/tests/NATS.Client.CheckNativeAot/Program.cs b/tests/NATS.Client.CheckNativeAot/Program.cs index dead43d03..91462152e 100644 --- a/tests/NATS.Client.CheckNativeAot/Program.cs +++ b/tests/NATS.Client.CheckNativeAot/Program.cs @@ -135,7 +135,7 @@ async Task JetStreamTests() // Only ACK one message so we can consume again if (messages.Count == 1) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cancellationToken: cts2.Token); + await msg.AckAsync(cancellationToken: cts2.Token); } if (messages.Count == 2) diff --git a/tests/NATS.Client.Core.Tests/CancellationTest.cs b/tests/NATS.Client.Core.Tests/CancellationTest.cs index e6ee72ef3..f29431931 100644 --- a/tests/NATS.Client.Core.Tests/CancellationTest.cs +++ b/tests/NATS.Client.Core.Tests/CancellationTest.cs @@ -1,5 +1,3 @@ -using System.Text; - namespace NATS.Client.Core.Tests; public class CancellationTest @@ -8,66 +6,63 @@ public class CancellationTest public CancellationTest(ITestOutputHelper output) => _output = output; - // should check - // timeout via command-timeout(request-timeout) - // timeout via connection dispose - // cancel manually + // check CommandTimeout [Fact] public async Task CommandTimeoutTest() { - await using var server = NatsServer.Start(_output, TransportType.Tcp); - - await using var subConnection = server.CreateClientConnection(NatsOpts.Default with { CommandTimeout = TimeSpan.FromSeconds(1) }); - await using var pubConnection = server.CreateClientConnection(NatsOpts.Default with { CommandTimeout = TimeSpan.FromSeconds(1) }); - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var cancellationToken = cts.Token; - - await pubConnection.ConnectAsync(); + var server = NatsServer.Start(_output, TransportType.Tcp); - await subConnection.SubscribeCoreAsync("foo", cancellationToken: cancellationToken); + await using var conn = server.CreateClientConnection(NatsOpts.Default with { CommandTimeout = TimeSpan.FromMilliseconds(1) }); + await conn.ConnectAsync(); - var cmd = new SleepWriteCommand("PUB foo 5\r\naiueo", TimeSpan.FromSeconds(10)); - pubConnection.PostDirectWrite(cmd); + // stall the flush task + await conn.CommandWriter.TestStallFlushAsync(TimeSpan.FromSeconds(5)); - var timeoutException = await Assert.ThrowsAsync(async () => + // commands that call ConnectAsync throw OperationCanceledException + await Assert.ThrowsAsync(() => conn.PingAsync().AsTask()); + await Assert.ThrowsAsync(() => conn.PublishAsync("test").AsTask()); + await Assert.ThrowsAsync(async () => { - await pubConnection.PublishAsync("foo", "aiueo", opts: new NatsPubOpts { WaitUntilSent = true }, cancellationToken: cancellationToken); + await foreach (var unused in conn.SubscribeAsync("test")) + { + } }); - - timeoutException.Message.Should().Contain("1 seconds elapsing"); } - // Queue-full - - // External Cancellation -} - -// writer queue can't consume when sleeping -internal class SleepWriteCommand : ICommand -{ - private readonly byte[] _protocol; - private readonly TimeSpan _sleepTime; - - public SleepWriteCommand(string protocol, TimeSpan sleepTime) + // check that cancellation works on commands that call ConnectAsync + [Fact] + public async Task CommandConnectCancellationTest() { - _protocol = Encoding.UTF8.GetBytes(protocol + "\r\n"); - _sleepTime = sleepTime; - } + var server = NatsServer.Start(_output, TransportType.Tcp); - public bool IsCanceled => false; + await using var conn = server.CreateClientConnection(); + await conn.ConnectAsync(); - public void Return(ObjectPool pool) - { - } + // kill the server + await server.DisposeAsync(); - public void SetCancellationTimer(CancellationTimer timer) - { - } + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var cancellationToken = cts.Token; - public void Write(ProtocolWriter writer) - { - Thread.Sleep(_sleepTime); - writer.WriteRaw(_protocol); + // wait for reconnect loop to kick in + while (conn.ConnectionState != NatsConnectionState.Reconnecting) + { + await Task.Delay(1, cancellationToken); + } + + // cancel cts + cts.Cancel(); + + // commands that call ConnectAsync throw TaskCanceledException + await Assert.ThrowsAsync(() => conn.PingAsync(cancellationToken).AsTask()); + await Assert.ThrowsAsync(() => conn.PublishAsync("test", cancellationToken: cancellationToken).AsTask()); + + // todo: https://github.com/nats-io/nats.net.v2/issues/323 + // await Assert.ThrowsAsync(async () => + // { + // await foreach (var unused in conn.SubscribeAsync("test", cancellationToken: cancellationToken)) + // { + // } + // }); } } diff --git a/tests/NATS.Client.Core.Tests/JsonSerializerTests.cs b/tests/NATS.Client.Core.Tests/JsonSerializerTests.cs index 419e30b1c..47acefc42 100644 --- a/tests/NATS.Client.Core.Tests/JsonSerializerTests.cs +++ b/tests/NATS.Client.Core.Tests/JsonSerializerTests.cs @@ -34,15 +34,11 @@ public async Task Serialize_any_type() // Default serializer won't work with random types await using var nats1 = server.CreateClientConnection(); - var signal = new WaitSignal(); - - await nats1.PublishAsync( + var exception = await Assert.ThrowsAsync(() => nats1.PublishAsync( subject: "would.not.work", data: new SomeTestData { Name = "won't work" }, - opts: new NatsPubOpts { ErrorHandler = e => signal.Pulse(e) }, - cancellationToken: cancellationToken); + cancellationToken: cancellationToken).AsTask()); - var exception = await signal; Assert.Matches(@"Can't serialize .*SomeTestData", exception.Message); } diff --git a/tests/NATS.Client.Core.Tests/LowLevelApiTest.cs b/tests/NATS.Client.Core.Tests/LowLevelApiTest.cs index a7807df13..3d7efe29b 100644 --- a/tests/NATS.Client.Core.Tests/LowLevelApiTest.cs +++ b/tests/NATS.Client.Core.Tests/LowLevelApiTest.cs @@ -23,15 +23,15 @@ public async Task Sub_custom_builder_test() await Retry.Until( "subscription is ready", () => builder.IsSynced, - async () => await nats.PubAsync("foo.sync")); + async () => await nats.PublishAsync("foo.sync")); for (var i = 0; i < 10; i++) { var headers = new NatsHeaders { { "X-Test", $"value-{i}" } }; - await nats.PubModelAsync($"foo.data{i}", i, NatsDefaultSerializer.Default, "bar", headers); + await nats.PublishAsync($"foo.data{i}", i, headers, "bar", NatsDefaultSerializer.Default); } - await nats.PubAsync("foo.done"); + await nats.PublishAsync("foo.done"); await builder.Done; Assert.Equal(10, builder.Messages.Count()); diff --git a/tests/NATS.Client.Core.Tests/MessageInterfaceTest.cs b/tests/NATS.Client.Core.Tests/MessageInterfaceTest.cs index 1acba090b..0291b679e 100644 --- a/tests/NATS.Client.Core.Tests/MessageInterfaceTest.cs +++ b/tests/NATS.Client.Core.Tests/MessageInterfaceTest.cs @@ -34,13 +34,13 @@ public async Task Sub_custom_builder_test() await Retry.Until( reason: "subscription is ready", condition: () => Volatile.Read(ref sync) > 0, - action: async () => await nats.PubAsync("foo.sync"), + action: async () => await nats.PublishAsync("foo.sync"), retryDelay: TimeSpan.FromSeconds(1)); for (var i = 0; i < 10; i++) await nats.PublishAsync(subject: $"foo.{i}", data: $"test_msg_{i}"); - await nats.PubAsync("foo.end"); + await nats.PublishAsync("foo.end"); await sub; } diff --git a/tests/NATS.Client.Core.Tests/NatsHeaderTest.cs b/tests/NATS.Client.Core.Tests/NatsHeaderTest.cs index 59ca96f51..6b74776af 100644 --- a/tests/NATS.Client.Core.Tests/NatsHeaderTest.cs +++ b/tests/NATS.Client.Core.Tests/NatsHeaderTest.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.IO.Pipelines; using System.Text; namespace NATS.Client.Core.Tests; @@ -10,7 +11,7 @@ public class NatsHeaderTest public NatsHeaderTest(ITestOutputHelper output) => _output = output; [Fact] - public void WriterTests() + public async Task WriterTests() { var headers = new NatsHeaders { @@ -19,16 +20,19 @@ public void WriterTests() ["a-long-header-key"] = "value", ["key"] = "a-long-header-value", }; - var writer = new HeaderWriter(Encoding.UTF8); - var buffer = new FixedArrayBufferWriter(); - var written = writer.Write(buffer, headers); + var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: 0)); + var writer = new HeaderWriter(pipe.Writer, Encoding.UTF8); + var written = writer.Write(headers); - var text = "k1: v1\r\nk2: v2-0\r\nk2: v2-1\r\na-long-header-key: value\r\nkey: a-long-header-value\r\n\r\n"; - var expected = new Span(Encoding.UTF8.GetBytes(text)); + var text = "NATS/1.0\r\nk1: v1\r\nk2: v2-0\r\nk2: v2-1\r\na-long-header-key: value\r\nkey: a-long-header-value\r\n\r\n"; + var expected = new ReadOnlySequence(Encoding.UTF8.GetBytes(text)); Assert.Equal(expected.Length, written); - Assert.True(expected.SequenceEqual(buffer.WrittenSpan)); - _output.WriteLine($"Buffer:\n{buffer.WrittenSpan.Dump()}"); + await pipe.Writer.FlushAsync(); + var result = await pipe.Reader.ReadAtLeastAsync((int)written); + + Assert.True(expected.ToSpan().SequenceEqual(result.Buffer.ToSpan())); + _output.WriteLine($"Buffer:\n{result.Buffer.FirstSpan.Dump()}"); } [Fact] diff --git a/tests/NATS.Client.Core.Tests/ProtocolTest.cs b/tests/NATS.Client.Core.Tests/ProtocolTest.cs index 01702108c..030ece422 100644 --- a/tests/NATS.Client.Core.Tests/ProtocolTest.cs +++ b/tests/NATS.Client.Core.Tests/ProtocolTest.cs @@ -346,16 +346,14 @@ internal NatsSubReconnectTest(NatsConnection connection, string subject, Action< : base(connection, connection.SubscriptionManager, subject, queueGroup: default, opts: default) => _callback = callback; - internal override IEnumerable GetReconnectCommands(int sid) + internal override async ValueTask WriteReconnectCommandsAsync(CommandWriter commandWriter, int sid) { - // Yield re-subscription - foreach (var command in base.GetReconnectCommands(sid)) - yield return command; + await base.WriteReconnectCommandsAsync(commandWriter, sid); // Any additional commands to send on reconnect - yield return PublishBytesCommand.Create(Connection.ObjectPool, "bar1", default, default, default, default); - yield return PublishBytesCommand.Create(Connection.ObjectPool, "bar2", default, default, default, default); - yield return PublishBytesCommand.Create(Connection.ObjectPool, "bar3", default, default, default, default); + await commandWriter.PublishAsync("bar1", default, default, default, NatsRawSerializer.Default, default); + await commandWriter.PublishAsync("bar2", default, default, default, NatsRawSerializer.Default, default); + await commandWriter.PublishAsync("bar3", default, default, default, NatsRawSerializer.Default, default); } protected override ValueTask ReceiveInternalAsync(string subject, string? replyTo, ReadOnlySequence? headersBuffer, ReadOnlySequence payloadBuffer) diff --git a/tests/NATS.Client.Core.Tests/SerializerTest.cs b/tests/NATS.Client.Core.Tests/SerializerTest.cs index 7c439675b..2b499621d 100644 --- a/tests/NATS.Client.Core.Tests/SerializerTest.cs +++ b/tests/NATS.Client.Core.Tests/SerializerTest.cs @@ -14,36 +14,11 @@ public async Task Serializer_exceptions() await using var server = NatsServer.Start(); await using var nats = server.CreateClientConnection(); - await Assert.ThrowsAsync(async () => - { - var signal = new WaitSignal(); - - var opts = new NatsPubOpts - { - WaitUntilSent = false, - ErrorHandler = e => - { - signal.Pulse(e); - }, - }; - - await nats.PublishAsync( - "foo", - 0, - serializer: new TestSerializer(), - opts: opts); - - throw await signal; - }); - - await Assert.ThrowsAsync(async () => - { - await nats.PublishAsync( + await Assert.ThrowsAsync(() => + nats.PublishAsync( "foo", 0, - serializer: new TestSerializer(), - opts: new NatsPubOpts { WaitUntilSent = true }); - }); + serializer: new TestSerializer()).AsTask()); // Check that our connection isn't affected by the exceptions await using var sub = await nats.SubscribeCoreAsync("foo"); diff --git a/tests/NATS.Client.JetStream.Tests/ConsumerConsumeTest.cs b/tests/NATS.Client.JetStream.Tests/ConsumerConsumeTest.cs index ff226ad27..477ecf44c 100644 --- a/tests/NATS.Client.JetStream.Tests/ConsumerConsumeTest.cs +++ b/tests/NATS.Client.JetStream.Tests/ConsumerConsumeTest.cs @@ -40,7 +40,7 @@ public async Task Consume_msgs_test() await using var cc = await consumer.ConsumeInternalAsync(serializer: TestDataJsonSerializer.Default, consumerOpts, cancellationToken: cts.Token); await foreach (var msg in cc.Msgs.ReadAllAsync(cts.Token)) { - await msg.AckAsync(new AckOpts(true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(count, msg.Data!.Test); count++; if (count == 30) @@ -113,7 +113,7 @@ public async Task Consume_idle_heartbeat_test() var cc = await consumer.ConsumeInternalAsync(serializer: TestDataJsonSerializer.Default, consumerOpts, cancellationToken: cts.Token); await foreach (var msg in cc.Msgs.ReadAllAsync(cts.Token)) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(count, msg.Data!.Test); await signal; break; @@ -183,7 +183,7 @@ public async Task Consume_reconnect_test() var count = 0; await foreach (var msg in cc.Msgs.ReadAllAsync(cts.Token)) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(count, msg.Data!.Test); count++; diff --git a/tests/NATS.Client.JetStream.Tests/ConsumerFetchTest.cs b/tests/NATS.Client.JetStream.Tests/ConsumerFetchTest.cs index f7aed90ac..fb79e3fe9 100644 --- a/tests/NATS.Client.JetStream.Tests/ConsumerFetchTest.cs +++ b/tests/NATS.Client.JetStream.Tests/ConsumerFetchTest.cs @@ -31,7 +31,7 @@ public async Task Fetch_test() await consumer.FetchInternalAsync(serializer: TestDataJsonSerializer.Default, opts: new NatsJSFetchOpts { MaxMsgs = 10 }, cancellationToken: cts.Token); await foreach (var msg in fc.Msgs.ReadAllAsync(cts.Token)) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(count, msg.Data!.Test); count++; } @@ -59,7 +59,7 @@ public async Task FetchNoWait_test() var count = 0; await foreach (var msg in consumer.FetchNoWaitAsync(serializer: TestDataJsonSerializer.Default, opts: new NatsJSFetchOpts { MaxMsgs = 10 }, cancellationToken: cts.Token)) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(count, msg.Data!.Test); count++; } diff --git a/tests/NATS.Client.JetStream.Tests/ConsumerNextTest.cs b/tests/NATS.Client.JetStream.Tests/ConsumerNextTest.cs index 7ec2489f9..7df97ddce 100644 --- a/tests/NATS.Client.JetStream.Tests/ConsumerNextTest.cs +++ b/tests/NATS.Client.JetStream.Tests/ConsumerNextTest.cs @@ -27,7 +27,7 @@ public async Task Next_test() var next = await consumer.NextAsync(serializer: TestDataJsonSerializer.Default, cancellationToken: cts.Token); if (next is { } msg) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(i, msg.Data!.Test); } } diff --git a/tests/NATS.Client.JetStream.Tests/CustomSerializerTest.cs b/tests/NATS.Client.JetStream.Tests/CustomSerializerTest.cs index a3906707c..0135ae0c9 100644 --- a/tests/NATS.Client.JetStream.Tests/CustomSerializerTest.cs +++ b/tests/NATS.Client.JetStream.Tests/CustomSerializerTest.cs @@ -45,7 +45,7 @@ public async Task When_consuming_ack_should_be_serialized_normally_if_custom_ser if (next is { } msg) { Assert.Equal(new byte[] { 42 }, msg.Data); - await msg.AckAsync(opts: new AckOpts(DoubleAck: true), cts.Token); + await msg.AckAsync(new AckOpts { DoubleAck = true }, cancellationToken: cts.Token); } else { diff --git a/tests/NATS.Client.JetStream.Tests/DoubleAckNakDelayTests.cs b/tests/NATS.Client.JetStream.Tests/DoubleAckNakDelayTests.cs index 80a8ccc7f..3b345edef 100644 --- a/tests/NATS.Client.JetStream.Tests/DoubleAckNakDelayTests.cs +++ b/tests/NATS.Client.JetStream.Tests/DoubleAckNakDelayTests.cs @@ -27,7 +27,7 @@ public async Task Double_ack_received_messages() var next = await consumer.NextAsync(cancellationToken: cts.Token); if (next is { } msg) { - await msg.AckAsync(new AckOpts(DoubleAck: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); Assert.Equal(42, msg.Data); await Retry.Until("seen ACK", () => proxy.Frames.Any(f => f.Message.StartsWith("PUB $JS.ACK"))); diff --git a/tests/NATS.Client.JetStream.Tests/DoubleAckTest.cs b/tests/NATS.Client.JetStream.Tests/DoubleAckTest.cs index 9c22ff4f7..fd0030d82 100644 --- a/tests/NATS.Client.JetStream.Tests/DoubleAckTest.cs +++ b/tests/NATS.Client.JetStream.Tests/DoubleAckTest.cs @@ -36,7 +36,7 @@ public async Task Fetch_should_not_block_socket() { // double ack will use the same TCP stream to wait for the ACK from the server // fetch must not block the socket so that the ACK can be received - await msg.AckAsync(new AckOpts(DoubleAck: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); count++; } @@ -58,7 +58,7 @@ public async Task Fetch_should_not_block_socket() { // double ack will use the same TCP stream to wait for the ACK from the server // fetch must not block the socket so that the ACK can be received - await msg.AckAsync(new AckOpts(DoubleAck: true), cts.Token); + await msg.AckAsync(cancellationToken: cts.Token); count++; } diff --git a/tests/NATS.Client.JetStream.Tests/JetStreamTest.cs b/tests/NATS.Client.JetStream.Tests/JetStreamTest.cs index 826bf09a9..ccf7cb009 100644 --- a/tests/NATS.Client.JetStream.Tests/JetStreamTest.cs +++ b/tests/NATS.Client.JetStream.Tests/JetStreamTest.cs @@ -93,7 +93,7 @@ public async Task Create_stream_test() // Only ACK one message so we can consume again if (messages.Count == 1) { - await msg.AckAsync(new AckOpts(WaitUntilSent: true), cancellationToken: cts2.Token); + await msg.AckAsync(cancellationToken: cts2.Token); } if (messages.Count == 2) diff --git a/tests/NATS.Client.Perf/NATS.Client.Perf.csproj b/tests/NATS.Client.Perf/NATS.Client.Perf.csproj index 52a84f3bd..383754112 100644 --- a/tests/NATS.Client.Perf/NATS.Client.Perf.csproj +++ b/tests/NATS.Client.Perf/NATS.Client.Perf.csproj @@ -1,15 +1,16 @@ - - Exe - net8.0 - enable - enable - false - + + Exe + net8.0 + enable + enable + false + true + - - - + + + diff --git a/tests/NATS.Client.Perf/Program.cs b/tests/NATS.Client.Perf/Program.cs index 9a3d83357..5efe47749 100644 --- a/tests/NATS.Client.Perf/Program.cs +++ b/tests/NATS.Client.Perf/Program.cs @@ -24,12 +24,8 @@ Console.WriteLine("\nRunning nats bench"); var natsBenchTotalMsgs = RunNatsBench(server.ClientUrl, t); -await using var nats1 = server.CreateClientConnection(new NatsOpts -{ - // don't drop messages - SubPendingChannelFullMode = BoundedChannelFullMode.Wait, -}); -await using var nats2 = server.CreateClientConnection(); +await using var nats1 = server.CreateClientConnection(NatsOpts.Default with { SubPendingChannelFullMode = BoundedChannelFullMode.Wait }, testLogger: false); +await using var nats2 = server.CreateClientConnection(testLogger: false); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); @@ -71,16 +67,26 @@ var stopwatch = Stopwatch.StartNew(); var payload = new ReadOnlySequence(new byte[t.Size]); +var pubSync = 0; +var pubAsync = 0; for (var i = 0; i < t.Msgs; i++) { - await nats2.PublishAsync(t.Subject, payload, cancellationToken: cts.Token); + var vt = nats2.PublishAsync(t.Subject, payload, cancellationToken: cts.Token); + if (vt.IsCompletedSuccessfully) + { + pubSync++; + vt.GetAwaiter().GetResult(); + } + else + { + pubAsync++; + await vt; + } } -Console.WriteLine($"[{stopwatch.Elapsed}]"); - +Console.WriteLine("pub time: {0}, sync: {1}, async: {2}", stopwatch.Elapsed, pubSync, pubAsync); await subReader; - -Console.WriteLine($"[{stopwatch.Elapsed}]"); +Console.WriteLine("sub time: {0}", stopwatch.Elapsed); var seconds = stopwatch.Elapsed.TotalSeconds; diff --git a/tests/NATS.Client.Services.Tests/ServicesSerializationTest.cs b/tests/NATS.Client.Services.Tests/ServicesSerializationTest.cs index 6844ab9c2..68d478ef9 100644 --- a/tests/NATS.Client.Services.Tests/ServicesSerializationTest.cs +++ b/tests/NATS.Client.Services.Tests/ServicesSerializationTest.cs @@ -1,4 +1,3 @@ -using System.Buffers; using NATS.Client.Core.Tests; using NATS.Client.Serializers.Json; using NATS.Client.Services.Internal; diff --git a/tests/NATS.Client.TestUtilities/NatsServer.cs b/tests/NATS.Client.TestUtilities/NatsServer.cs index 2cea17b80..b88c6c57f 100644 --- a/tests/NATS.Client.TestUtilities/NatsServer.cs +++ b/tests/NATS.Client.TestUtilities/NatsServer.cs @@ -318,13 +318,13 @@ public async ValueTask DisposeAsync() return (client, proxy); } - public NatsConnection CreateClientConnection(NatsOpts? options = default, int reTryCount = 10, bool ignoreAuthorizationException = false) + public NatsConnection CreateClientConnection(NatsOpts? options = default, int reTryCount = 10, bool ignoreAuthorizationException = false, bool testLogger = true) { for (var i = 0; i < reTryCount; i++) { try { - var nats = new NatsConnection(ClientOpts(options ?? NatsOpts.Default)); + var nats = new NatsConnection(ClientOpts(options ?? NatsOpts.Default, testLogger: testLogger)); try { @@ -359,7 +359,7 @@ public NatsConnectionPool CreatePooledClientConnection(NatsOpts opts) return new NatsConnectionPool(4, ClientOpts(opts)); } - public NatsOpts ClientOpts(NatsOpts opts) + public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true) { var natsTlsOpts = Opts.EnableTls ? opts.TlsOpts with @@ -373,11 +373,7 @@ public NatsOpts ClientOpts(NatsOpts opts) return opts with { - LoggerFactory = _loggerFactory, - - // ConnectTimeout = TimeSpan.FromSeconds(1), - // ReconnectWait = TimeSpan.Zero, - // ReconnectJitter = TimeSpan.Zero, + LoggerFactory = testLogger ? _loggerFactory : opts.LoggerFactory, TlsOpts = natsTlsOpts, Url = ClientUrl, };