From 9d80ed0a5732d350f48d3de7f0308fbbb54388f3 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 13:58:56 +0100 Subject: [PATCH 01/87] Add ConcurrentBufferReaderWriter --- src/Common/ConcurrentBufferReaderWriter.cs | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/Common/ConcurrentBufferReaderWriter.cs diff --git a/src/Common/ConcurrentBufferReaderWriter.cs b/src/Common/ConcurrentBufferReaderWriter.cs new file mode 100644 index 00000000..ebf7cc4c --- /dev/null +++ b/src/Common/ConcurrentBufferReaderWriter.cs @@ -0,0 +1,88 @@ +using System.Buffers; + +namespace SurrealDB.Common; + +public sealed class ConcurrentBufferReaderWriter { + private readonly ArrayBufferWriter _buf = new(); + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + /// State used by multiple threads. Only interact via methods! + private int _readerPosition; + + public WriteHandle Write() => new(this); + + public ReadHandle Read() => new(this); + + /// Allows writing the buffer. Proxy for . + public readonly ref struct WriteHandle { + private readonly ConcurrentBufferReaderWriter _owner; + + internal WriteHandle(ConcurrentBufferReaderWriter owner) { + _owner = owner; + _owner._lock.EnterWriteLock(); + } + + public ReadOnlyMemory WrittenMemory => _owner._buf.WrittenMemory; + public ReadOnlySpan WrittenSpan => _owner._buf.WrittenSpan; + public int WrittenCount => _owner._buf.WrittenCount; + public int Capacity => _owner._buf.Capacity; + public int FreeCapacity => _owner._buf.FreeCapacity; + + public void Clear() => _owner._buf.Clear(); + + public void Advance(int count) => _owner._buf.Advance(count); + + public Memory GetMemory(int sizeHint = 0) => _owner._buf.GetMemory(sizeHint); + + public Span GetSpan(int sizeHint = 0) => _owner._buf.GetSpan(sizeHint); + + public void Dispose() { + _owner._lock.ExitWriteLock(); + } + } + + /// Allows concurrent reading from the buffer. + public readonly ref struct ReadHandle { + private readonly ConcurrentBufferReaderWriter _owner; + + internal ReadHandle(ConcurrentBufferReaderWriter owner) { + _owner = owner; + _owner._lock.EnterReadLock(); + } + + /// Reads a section of memory from the buffer + /// The maximum expected amount of memory read. + public ReadOnlyMemory ReadMemory(int expectedSize) { + // [THEADSAFE] increment the position + int newPosition = Interlocked.Add(ref _owner._readerPosition, expectedSize); + ReadOnlyMemory available = _owner._buf.WrittenMemory; + int start = newPosition - expectedSize; + int end = Math.Min(available.Length, newPosition); + return (nuint)start <= (nuint)available.Length ? available.Slice(start, end - start) : default; + } + + /// + public ReadOnlySpan ReadSpan(int expectedSize) { + // [THEADSAFE] increment the position + int newPosition = Interlocked.Add(ref _owner._readerPosition, expectedSize); + ReadOnlySpan available = _owner._buf.WrittenSpan; + int start = newPosition - expectedSize; + int end = Math.Min(available.Length, newPosition); + return (nuint)start <= (nuint)available.Length ? available.Slice(start, end - start) : default; + } + + /// Reads at most the size of from the buffer, and writes it to the . + /// Where the elements are written to. + /// The number of elements read and also written to the . + public int CopyTo(Span destination) { + var source = ReadSpan(destination.Length); + source.CopyTo(destination); + return source.Length; + } + + public void Dispose() { + _owner._lock.ExitReadLock(); + } + } +} + + From 140b9958bace817ecb44df1c57f35bb4f9ef35ad Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 14:27:24 +0100 Subject: [PATCH 02/87] Rename WsTx -> WsManager --- src/Ws/Handler.cs | 18 +++++++++--------- src/Ws/Ws.cs | 6 +++--- src/Ws/{WsTx.cs => WsManager.cs} | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) rename src/Ws/{WsTx.cs => WsManager.cs} (99%) diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index cc36fe91..4ebe4e07 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -6,11 +6,11 @@ internal interface IHandler : IDisposable { public bool Persistent { get; } - public void Handle(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm); + public void Handle(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm); } internal sealed class ResponseHandler : IHandler { - private readonly TaskCompletionSource<(WsTx.RspHeader, WsTx.NtyHeader, Stream)> _tcs = new(); + private readonly TaskCompletionSource<(WsManager.RspHeader, WsManager.NtyHeader, Stream)> _tcs = new(); private readonly string _id; private readonly CancellationToken _ct; @@ -19,13 +19,13 @@ public ResponseHandler(string id, CancellationToken ct) { _ct = ct; } - public Task<(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm)> Task => _tcs!.Task; + public Task<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> Task => _tcs!.Task; public string Id => _id; public bool Persistent => false; - public void Handle(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm) { + public void Handle(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm) { _tcs.SetResult((rsp, nty, stm)); } @@ -35,10 +35,10 @@ public void Dispose() { } -internal class NotificationHandler : IHandler, IAsyncEnumerable<(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm)> { +internal class NotificationHandler : IHandler, IAsyncEnumerable<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> { private readonly Ws _mediator; private readonly CancellationToken _ct; - private TaskCompletionSource<(WsTx.RspHeader, WsTx.NtyHeader, Stream)> _tcs = new(); + private TaskCompletionSource<(WsManager.RspHeader, WsManager.NtyHeader, Stream)> _tcs = new(); public NotificationHandler(Ws mediator, string id, CancellationToken ct) { _mediator = mediator; Id = id; @@ -48,7 +48,7 @@ public NotificationHandler(Ws mediator, string id, CancellationToken ct) { public string Id { get; } public bool Persistent => true; - public void Handle(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm) { + public void Handle(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm) { _tcs.SetResult((rsp, nty, stm)); _tcs = new(); } @@ -57,9 +57,9 @@ public void Dispose() { _tcs.TrySetCanceled(); } - public async IAsyncEnumerator<(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm)> GetAsyncEnumerator(CancellationToken cancellationToken = default) { + public async IAsyncEnumerator<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> GetAsyncEnumerator(CancellationToken cancellationToken = default) { while (!_ct.IsCancellationRequested) { - (WsTx.RspHeader, WsTx.NtyHeader, Stream) res; + (WsManager.RspHeader, WsManager.NtyHeader, Stream) res; try { res = await _tcs.Task; } catch (OperationCanceledException) { diff --git a/src/Ws/Ws.cs b/src/Ws/Ws.cs index 6486e0c8..cd2e1a34 100644 --- a/src/Ws/Ws.cs +++ b/src/Ws/Ws.cs @@ -6,7 +6,7 @@ namespace SurrealDB.Ws; public sealed class Ws : IDisposable, IAsyncDisposable { private readonly CancellationTokenSource _cts = new(); - private readonly WsTx _tx = new(); + private readonly WsManager _tx = new(); private readonly ConcurrentDictionary _handlers = new(); private Task _recv = Task.CompletedTask; @@ -29,7 +29,7 @@ public async Task Close(CancellationToken ct = default) { /// /// Sends the request and awaits a response from the server /// - public async Task<(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm)> RequestOnce(string id, Stream request, CancellationToken ct = default) { + public async Task<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> RequestOnce(string id, Stream request, CancellationToken ct = default) { ResponseHandler handler = new(id, ct); Register(handler); await _tx.Tw(request, ct); @@ -39,7 +39,7 @@ public async Task Close(CancellationToken ct = default) { /// /// Sends the request and awaits responses from the server until manually canceled using the cancellation token /// - public async IAsyncEnumerable<(WsTx.RspHeader rsp, WsTx.NtyHeader nty, Stream stm)> RequestPersists(string id, Stream request, [EnumeratorCancellation] CancellationToken ct = default) { + public async IAsyncEnumerable<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> RequestPersists(string id, Stream request, [EnumeratorCancellation] CancellationToken ct = default) { NotificationHandler handler = new(this, id, ct); Register(handler); await _tx.Tw(request, ct); diff --git a/src/Ws/WsTx.cs b/src/Ws/WsManager.cs similarity index 99% rename from src/Ws/WsTx.cs rename to src/Ws/WsManager.cs index 03893a29..cd08f5bf 100644 --- a/src/Ws/WsTx.cs +++ b/src/Ws/WsManager.cs @@ -9,7 +9,7 @@ namespace SurrealDB.Ws; -public sealed class WsTx : IDisposable { +public sealed class WsManager : IDisposable { private readonly ClientWebSocket _ws = new(); public static int DefaultBufferSize => 16 * 1024; From 20681b50bd494c62e592f403dedb952699d5623c Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 14:29:30 +0100 Subject: [PATCH 03/87] Implement BufferedStreamReader --- src/Common/StreamExtensions.cs | 47 +++++++++++ src/Ws/BufferedStreamReader.cs | 140 +++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/Common/StreamExtensions.cs create mode 100644 src/Ws/BufferedStreamReader.cs diff --git a/src/Common/StreamExtensions.cs b/src/Common/StreamExtensions.cs new file mode 100644 index 00000000..43a12663 --- /dev/null +++ b/src/Common/StreamExtensions.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; + +namespace SurrealDB.Common; + +internal static class StreamExtensions { + public static bool TryReadToBuffer(this MemoryStream memoryStream, int expectedSize, out ReadOnlyMemory read) { + if (!memoryStream.TryGetBuffer(out var buffer)) { + read = default; + return false; + } + // negative size -> read to end + expectedSize = expectedSize < 0 ? Int32.MaxValue : expectedSize; + // fake a read call + var pos = (int)memoryStream.Position; + var cap = (int)memoryStream.Length; + var len = Math.Min(expectedSize, cap - pos); + memoryStream.Position += len; + read = buffer.AsMemory(pos, len); + return true; + } + + public static bool TryReadToBuffer(this MemoryStream memoryStream, int expectedSize, out ReadOnlySpan read) { + if (!memoryStream.TryGetBuffer(out var buffer)) { + read = default; + return false; + } + // negative size -> read to end + expectedSize = expectedSize < 0 ? Int32.MaxValue : expectedSize; + // fake a read call + var pos = (int)memoryStream.Position; + var cap = (int)memoryStream.Length; + var len = Math.Min(expectedSize, cap - pos); + memoryStream.Position += len; + read = buffer.AsSpan(pos, len); + return true; + } + + public static async Task> ReadToBufferAsync(this Stream stream, Memory buffer, CancellationToken ct) { + int read = await stream.ReadAsync(buffer, ct); + return buffer.Slice(0, read); + } + + public static ReadOnlySpan ReadToBuffer(this Stream stream, Span buffer) { + int read = stream.Read(buffer); + return buffer.Slice(0, read); + } +} diff --git a/src/Ws/BufferedStreamReader.cs b/src/Ws/BufferedStreamReader.cs new file mode 100644 index 00000000..6a979d0e --- /dev/null +++ b/src/Ws/BufferedStreamReader.cs @@ -0,0 +1,140 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +using Microsoft.IO; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +/// Allows reading a stream efficiently +public struct BufferedStreamReader : IDisposable, IAsyncDisposable { + public const int BUFFER_SIZE = 16 * 1024; + private Stream? _arbitraryStream; + private MemoryStream? _memoryStream; + private byte[]? _poolArray; + + private BufferedStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream) { + _arbitraryStream = arbitraryStream; + _memoryStream = memoryStream; + _poolArray = null; + } + + public Stream Stream => _memoryStream ?? _arbitraryStream!; + + public BufferedStreamReader(Stream stream) { + ThrowArgIfStreamCantRead(stream); + this = stream switch { + RecyclableMemoryStream => new(stream, null), // TryGetBuffer is expensive! + MemoryStream ms => new(null, ms), + _ => new(stream, null) + }; + } + + /// Reads up to bytes from the underlying . + /// The expected number of bytes to read + /// The cancellation token + /// The context bound memory representing the bytes read. + /// The returned memory is invalid outside this instance. Do not reference the memory outside of the scope! + public ValueTask> ReadAsync(int expectedSize, CancellationToken ct = default) { + var memoryStream = _memoryStream; + var stream = _arbitraryStream; + ThrowIfNull(stream is null & memoryStream is null); + + if (memoryStream is not null) { + if (memoryStream.TryReadToBuffer(expectedSize, out ReadOnlyMemory read)) { + return new(read); + } + + // unable to access the memory stream buffer + // handle as regular stream + stream = memoryStream; + } + + Debug.Assert(stream is not null); + // reserve the buffer + var buffer = _poolArray; + if (buffer is null) { + _poolArray = buffer = ArrayPool.Shared.Rent(BUFFER_SIZE); + } + + // negative buffer size -> read as much as possible + expectedSize = expectedSize < 0 ? buffer.Length : expectedSize; + + return new(stream.ReadToBufferAsync(buffer.AsMemory(0, Math.Min(buffer.Length, expectedSize)), ct)); + } + + /// + public ReadOnlySpan Read(int expectedSize) { + var memoryStream = _memoryStream; + var stream = _arbitraryStream; + ThrowIfNull(stream is null & memoryStream is null); + + if (memoryStream is not null) { + if (memoryStream.TryReadToBuffer(expectedSize, out ReadOnlySpan read)) { + return read; + } + + // unable to access the memory stream buffer + // handle as regular stream + stream = memoryStream; + } + + Debug.Assert(stream is not null); + // reserve the buffer + var buffer = _poolArray; + if (buffer is null) { + _poolArray = buffer = ArrayPool.Shared.Rent(BUFFER_SIZE); + } + + return stream.ReadToBuffer(buffer.AsSpan(0, Math.Min(buffer.Length, expectedSize))); + } + + + public void Dispose() { + _arbitraryStream?.Dispose(); + _arbitraryStream = null; + _memoryStream?.Dispose(); + _memoryStream = null; + + var poolArray = _poolArray; + _poolArray = null; + if (poolArray is not null) { + ArrayPool.Shared.Return(poolArray); + } + } + + public async ValueTask DisposeAsync() { + var arbitraryStream = _arbitraryStream; + _arbitraryStream = null; + if (arbitraryStream is not null) { + await arbitraryStream.DisposeAsync().ConfigureAwait(false); + } + + var memoryStream = _memoryStream; + _memoryStream = null; + if (memoryStream is not null) { + await memoryStream.DisposeAsync().ConfigureAwait(false); + } + + var poolArray = _poolArray; + _poolArray = null; + if (poolArray is not null) { + ArrayPool.Shared.Return(poolArray); + } + } + + private static void ThrowIfNull([DoesNotReturnIf(true)] bool isNull, [CallerArgumentExpression(nameof(isNull))] string expression = "") { + if (isNull) { + throw new InvalidOperationException($"The expression cannot be null. `{expression}`"); + } + } + + private static void ThrowArgIfStreamCantRead(Stream stream, [CallerArgumentExpression(nameof(stream))] string argName = "") { + if (!stream.CanRead) { + throw new ArgumentException("The stream must be readable", argName); + } + } +} From 9e6f2f26c0d9edf20e60a2f21bef8fdded3f5d68 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 14:30:42 +0100 Subject: [PATCH 04/87] Implement message based websocket communication Use channels to pump messages --- src/Ws/WsChannels.cs | 207 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/Ws/WsChannels.cs diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs new file mode 100644 index 00000000..949649a9 --- /dev/null +++ b/src/Ws/WsChannels.cs @@ -0,0 +1,207 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Threading.Channels; + +using Microsoft.IO; + +namespace SurrealDB.Ws; + +/// Sends messages from a channel to a websocket server. +internal sealed class WsChannelRx { + private readonly ClientWebSocket _ws; + private readonly ChannelReader _in; + private readonly object _lock = new(); + private CancellationTokenSource? _cts; + private Task? _execute; + + public WsChannelRx(ClientWebSocket ws, ChannelReader @in) { + _ws = ws; + _in = @in; + } + + private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { + Debug.Assert(ct.CanBeCanceled); + while (!ct.IsCancellationRequested) { + var reader = await input.ReadAsync(ct); + + bool isFinalBlock = false; + while (!isFinalBlock && !ct.IsCancellationRequested) { + var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).ConfigureAwait(false); + isFinalBlock = rom.Length != BufferedStreamReader.BUFFER_SIZE; + await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).ConfigureAwait(false); + } + + if (!isFinalBlock) { + // ensure that the message is always terminated + // no not pass a CancellationToken + await output.SendAsync(default, WebSocketMessageType.Text, true, default).ConfigureAwait(false); + } + + await reader.DisposeAsync().ConfigureAwait(false); + ct.ThrowIfCancellationRequested(); + } + } + + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] + public bool Connected => _cts is not null & _execute is not null; + + public void Open() { + lock (_lock) { + ThrowIfConnected(); + _cts = new(); + _execute = Execute(_ws, _in, _cts.Token); + } + } + + public Task Close() { + Task task; + lock (_lock) { + ThrowIfDisconnected(); + _cts.Cancel(); + task = _execute; + _execute = null; + } + return task; + } + + [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } +} + +/// Receives messages from a websocket server and passes them to a channel +internal sealed class WsChannelTx { + private readonly ClientWebSocket _ws; + private readonly ChannelWriter _out; + private readonly object _lock = new(); + private CancellationTokenSource? _cts; + private Task? _execute; + + public WsChannelTx(ClientWebSocket ws, ChannelWriter @out) { + _ws = ws; + _out = @out; + } + + private static async Task Execute(ClientWebSocket input, ChannelWriter output, CancellationToken ct) { + RecyclableMemoryStreamManager memoryManager = new(); + Debug.Assert(ct.CanBeCanceled); + while (!ct.IsCancellationRequested) { + var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); + // receive the first part + var result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); + // create a new message with a RecyclableMemoryStream + // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking + WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); + // begin adding the message to the output + var writeOutput = output.WriteAsync(msg, ct); + using (var h = await msg.LockAsync(ct)) { + // write the first part to the message + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct); + } + + while (!result.EndOfMessage) { + // receive more parts + result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); + using var h = await msg.LockAsync(ct); + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct); + } + + await writeOutput.ConfigureAwait(false); + + ArrayPool.Shared.Return(buffer); + ct.ThrowIfCancellationRequested(); + } + } + + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] + public bool Connected => _cts is not null & _execute is not null; + + public void Open() { + lock (_lock) { + ThrowIfConnected(); + _cts = new(); + _execute = Execute(_ws, _out, _cts.Token); + } + } + + public Task Close() { + Task task; + lock (_lock) { + ThrowIfDisconnected(); + _cts.Cancel(); + task = _execute; + _execute = null; + } + return task; + } + + [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } +} + +public sealed class WsMessage : IDisposable, IAsyncDisposable { + private readonly MemoryStream _buffer; + private readonly SemaphoreSlim _lock = new(1, 1); + private bool _endOfMessage; + + internal WsMessage(MemoryStream buffer) { + _buffer = buffer; + _endOfMessage = false; + } + + public async Task LockAsync(CancellationToken ct) { + await _lock.WaitAsync(ct); + return new(this); + } + + public Handle Lock(CancellationToken ct) { + _lock.Wait(ct); + return new(this); + } + + public void Dispose() { + _lock.Dispose(); + _buffer.Dispose(); + } + + public ValueTask DisposeAsync() { + _lock.Dispose(); + return _buffer.DisposeAsync(); + } + + public readonly struct Handle : IDisposable { + private readonly WsMessage _msg; + + internal Handle(WsMessage msg) { + _msg = msg; + } + + public MemoryStream Stream => _msg._buffer; + public bool EndOfMessage { get => _msg._endOfMessage; set => _msg._endOfMessage = value; } + + public void Dispose() { + _msg._lock.Release(); + } + } +} From d17533dd781cd3d0a886690ccf2ce30cc47aeccc Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 14:45:31 +0100 Subject: [PATCH 05/87] Allow waiting for EndOfMessage --- src/Ws/WsChannels.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index 949649a9..ce9892b7 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -94,8 +94,10 @@ public WsChannelTx(ClientWebSocket ws, ChannelWriter @out) { } private static async Task Execute(ClientWebSocket input, ChannelWriter output, CancellationToken ct) { - RecyclableMemoryStreamManager memoryManager = new(); Debug.Assert(ct.CanBeCanceled); + // the MemoryManager associated with the streams + RecyclableMemoryStreamManager memoryManager = new(); + while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); // receive the first part @@ -115,8 +117,10 @@ private static async Task Execute(ClientWebSocket input, ChannelWriter.Shared.Return(buffer); @@ -163,6 +167,7 @@ private void ThrowIfConnected() { public sealed class WsMessage : IDisposable, IAsyncDisposable { private readonly MemoryStream _buffer; private readonly SemaphoreSlim _lock = new(1, 1); + private readonly TaskCompletionSource _endOfMessageEvent = new(); private bool _endOfMessage; internal WsMessage(MemoryStream buffer) { @@ -181,15 +186,21 @@ public Handle Lock(CancellationToken ct) { } public void Dispose() { + _endOfMessageEvent.TrySetCanceled(); _lock.Dispose(); _buffer.Dispose(); } public ValueTask DisposeAsync() { + _endOfMessageEvent.TrySetCanceled(); _lock.Dispose(); return _buffer.DisposeAsync(); } + public Task EndOfMessageAsync(CancellationToken ct = default) { + return ct.CanBeCanceled ? _endOfMessageEvent.Task.WaitAsync(ct) : _endOfMessageEvent.Task; + } + public readonly struct Handle : IDisposable { private readonly WsMessage _msg; @@ -198,7 +209,17 @@ internal Handle(WsMessage msg) { } public MemoryStream Stream => _msg._buffer; - public bool EndOfMessage { get => _msg._endOfMessage; set => _msg._endOfMessage = value; } + + public bool EndOfMessage { + get => _msg._endOfMessage; + set { + if (!_msg._endOfMessage && value) { + // finish the AwaitEndOfMessage task + _msg._endOfMessageEvent.SetResult(); + } + _msg._endOfMessage = value; + } + } public void Dispose() { _msg._lock.Release(); From 2c6f1d2b2d0983c05dc2f62af64ccf02b1fb0d21 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:07:59 +0100 Subject: [PATCH 06/87] Pass the result for Received Task --- src/Ws/WsChannels.cs | 45 +++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index ce9892b7..39cf7f34 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -110,14 +110,20 @@ private static async Task Execute(ClientWebSocket input, ChannelWriter _endOfMessageEvent = new(); + private TaskCompletionSource _receivedEvent = new(); + private int _endOfMessage; internal WsMessage(MemoryStream buffer) { _buffer = buffer; - _endOfMessage = false; + _endOfMessage = 0; } public async Task LockAsync(CancellationToken ct) { @@ -197,10 +204,27 @@ public ValueTask DisposeAsync() { return _buffer.DisposeAsync(); } + internal void SetEndOfMessage() { + var endOfMessage = Interlocked.Exchange(ref _endOfMessage, 1); + if (endOfMessage == 0) { + // finish the AwaitEndOfMessage task + _endOfMessageEvent.SetResult(null); + } + } + + internal void SetReceived(WebSocketReceiveResult count) { + var receivedEvent = Interlocked.Exchange(ref _receivedEvent, new()); + receivedEvent.SetResult(count); + } + public Task EndOfMessageAsync(CancellationToken ct = default) { return ct.CanBeCanceled ? _endOfMessageEvent.Task.WaitAsync(ct) : _endOfMessageEvent.Task; } + public Task ReceivedAsync(CancellationToken ct = default) { + return ct.CanBeCanceled ? _receivedEvent.Task.WaitAsync(ct) : _receivedEvent.Task; + } + public readonly struct Handle : IDisposable { private readonly WsMessage _msg; @@ -210,16 +234,7 @@ internal Handle(WsMessage msg) { public MemoryStream Stream => _msg._buffer; - public bool EndOfMessage { - get => _msg._endOfMessage; - set { - if (!_msg._endOfMessage && value) { - // finish the AwaitEndOfMessage task - _msg._endOfMessageEvent.SetResult(); - } - _msg._endOfMessage = value; - } - } + public bool EndOfMessage => _msg._endOfMessage == 0; public void Dispose() { _msg._lock.Release(); From 1f934a18bcc73a3f201b1a470866e8e9e31638c6 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:08:18 +0100 Subject: [PATCH 07/87] Add nullability attributes for legacy frameworks --- src/Common/NullableAttributes.cs | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/Common/NullableAttributes.cs diff --git a/src/Common/NullableAttributes.cs b/src/Common/NullableAttributes.cs new file mode 100644 index 00000000..673fab35 --- /dev/null +++ b/src/Common/NullableAttributes.cs @@ -0,0 +1,66 @@ +// ReSharper disable CheckNamespace +#if !(NET6_0 || NET_5_0 || NET5_0_OR_GREATER) + +#pragma warning disable IDE0130 +namespace System.Diagnostics.CodeAnalysis; +#pragma warning restore IDE0130 + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +internal sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } +} + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +internal sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } +} + + +#endif From 6e04ae8e8bb90a8b2558a4826deb491ab7fd9cdb Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:08:34 +0100 Subject: [PATCH 08/87] add System.Theading.Channels for netstd --- src/Ws/Ws.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ws/Ws.csproj b/src/Ws/Ws.csproj index dd553b1f..51ef5f4f 100644 --- a/src/Ws/Ws.csproj +++ b/src/Ws/Ws.csproj @@ -20,6 +20,7 @@ + From d0cd73a9579e93a05b392aa7a7e5606e4f31193b Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:09:14 +0100 Subject: [PATCH 09/87] Remove useless CancellationToken overload --- src/Ws/WsChannels.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index 39cf7f34..59630f9d 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -217,12 +217,12 @@ internal void SetReceived(WebSocketReceiveResult count) { receivedEvent.SetResult(count); } - public Task EndOfMessageAsync(CancellationToken ct = default) { - return ct.CanBeCanceled ? _endOfMessageEvent.Task.WaitAsync(ct) : _endOfMessageEvent.Task; + public Task EndOfMessageAsync() { + return _endOfMessageEvent.Task; } - public Task ReceivedAsync(CancellationToken ct = default) { - return ct.CanBeCanceled ? _receivedEvent.Task.WaitAsync(ct) : _receivedEvent.Task; + public Task ReceivedAsync() { + return _receivedEvent.Task; } public readonly struct Handle : IDisposable { From f2de839c85fd784eac3cb2a5a687c6521bd29647 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:40:23 +0100 Subject: [PATCH 10/87] Move ws jsonrpc headers to different file --- src/Ws/Handler.cs | 18 +-- src/Ws/Headers.cs | 259 ++++++++++++++++++++++++++++++++++++++++++++ src/Ws/Ws.cs | 4 +- src/Ws/WsManager.cs | 253 ------------------------------------------- 4 files changed, 270 insertions(+), 264 deletions(-) create mode 100644 src/Ws/Headers.cs diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index 4ebe4e07..15f393b4 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -6,11 +6,11 @@ internal interface IHandler : IDisposable { public bool Persistent { get; } - public void Handle(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm); + public void Handle(RspHeader rsp, NtyHeader nty, Stream stm); } internal sealed class ResponseHandler : IHandler { - private readonly TaskCompletionSource<(WsManager.RspHeader, WsManager.NtyHeader, Stream)> _tcs = new(); + private readonly TaskCompletionSource<(RspHeader, NtyHeader, Stream)> _tcs = new(); private readonly string _id; private readonly CancellationToken _ct; @@ -19,13 +19,13 @@ public ResponseHandler(string id, CancellationToken ct) { _ct = ct; } - public Task<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> Task => _tcs!.Task; + public Task<(RspHeader rsp, NtyHeader nty, Stream stm)> Task => _tcs!.Task; public string Id => _id; public bool Persistent => false; - public void Handle(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm) { + public void Handle(RspHeader rsp, NtyHeader nty, Stream stm) { _tcs.SetResult((rsp, nty, stm)); } @@ -35,10 +35,10 @@ public void Dispose() { } -internal class NotificationHandler : IHandler, IAsyncEnumerable<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> { +internal class NotificationHandler : IHandler, IAsyncEnumerable<(RspHeader rsp, NtyHeader nty, Stream stm)> { private readonly Ws _mediator; private readonly CancellationToken _ct; - private TaskCompletionSource<(WsManager.RspHeader, WsManager.NtyHeader, Stream)> _tcs = new(); + private TaskCompletionSource<(RspHeader, NtyHeader, Stream)> _tcs = new(); public NotificationHandler(Ws mediator, string id, CancellationToken ct) { _mediator = mediator; Id = id; @@ -48,7 +48,7 @@ public NotificationHandler(Ws mediator, string id, CancellationToken ct) { public string Id { get; } public bool Persistent => true; - public void Handle(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm) { + public void Handle(RspHeader rsp, NtyHeader nty, Stream stm) { _tcs.SetResult((rsp, nty, stm)); _tcs = new(); } @@ -57,9 +57,9 @@ public void Dispose() { _tcs.TrySetCanceled(); } - public async IAsyncEnumerator<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> GetAsyncEnumerator(CancellationToken cancellationToken = default) { + public async IAsyncEnumerator<(RspHeader rsp, NtyHeader nty, Stream stm)> GetAsyncEnumerator(CancellationToken cancellationToken = default) { while (!_ct.IsCancellationRequested) { - (WsManager.RspHeader, WsManager.NtyHeader, Stream) res; + (RspHeader, NtyHeader, Stream) res; try { res = await _tcs.Task; } catch (OperationCanceledException) { diff --git a/src/Ws/Headers.cs b/src/Ws/Headers.cs new file mode 100644 index 00000000..7ebf9cc5 --- /dev/null +++ b/src/Ws/Headers.cs @@ -0,0 +1,259 @@ +using System.Text.Json; + +using SurrealDB.Json; + +namespace SurrealDB.Ws; + +public readonly record struct NtyHeader(string? id, string? method, WsClient.Error err) { + public bool IsDefault => default == this; + + /// + /// Parses the head including the result propertyname, excluding the result array. + /// + internal static (NtyHeader head, long off, string? err) Parse(in ReadOnlySpan utf8) { + Fsm fsm = new() { + Lexer = new(utf8, false, new JsonReaderState(new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true })), + State = Fsms.Start, + }; + while (fsm.MoveNext()) {} + + if (!fsm.Success) { + return (default, fsm.Lexer.BytesConsumed, $"Error while parsing {nameof(RspHeader)} at {fsm.Lexer.TokenStartIndex}: {fsm.Err}"); + } + return (new(fsm.Id, fsm.Method, fsm.Error), default, default); + } + + private enum Fsms { + Start, // -> Prop + Prop, // -> PropId | PropAsync | PropMethod | ProsResult + PropId, // -> Prop | End + PropMethod, // -> Prop | End + PropError, // -> End + PropParams, // -> End + End + } + + private ref struct Fsm { + public Fsms State; + public Utf8JsonReader Lexer; + public string? Err; + public bool Success; + + public string? Name; + public string? Id; + public WsClient.Error Error; + public string? Method; + + public bool MoveNext() { + return State switch { + Fsms.Start => Start(), + Fsms.Prop => Prop(), + Fsms.PropId => PropId(), + Fsms.PropMethod => PropMethod(), + Fsms.PropError => PropError(), + Fsms.PropParams => PropParams(), + Fsms.End => End(), + _ => false + }; + } + + private bool Start() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.StartObject) { + Err = "Unable to read token StartObject"; + return false; + } + + State = Fsms.Prop; + return true; + + } + + private bool End() { + Success = !String.IsNullOrEmpty(Id) && !String.IsNullOrEmpty(Method); + return false; + } + + private bool Prop() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.PropertyName) { + Err = "Unable to read PropertyName"; + return false; + } + + Name = Lexer.GetString(); + if ("id".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropId; + return true; + } + if ("method".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropMethod; + return true; + } + if ("error".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropError; + return true; + } + if ("params".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropParams; + return true; + } + + Err = $"Unknown PropertyName `{Name}`"; + return false; + } + + private bool PropId() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.String) { + Err = "Unable to read `id` property value"; + return false; + } + + State = Fsms.Prop; + Id = Lexer.GetString(); + return true; + } + + private bool PropError() { + Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); + State = Fsms.End; + return true; + } + + private bool PropMethod() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.String) { + Err = "Unable to read `method` property value"; + return false; + } + + State = Fsms.Prop; + Method = Lexer.GetString(); + return true; + } + + private bool PropParams() { + // Do not parse the result! + // The complete result is not present in the buffer! + // The result is returned as a unevaluated asynchronous stream! + State = Fsms.End; + return true; + } + } +} + +public readonly record struct RspHeader(string? id, WsClient.Error err) { + public bool IsDefault => default == this; + + /// + /// Parses the head including the result propertyname, excluding the result array. + /// + internal static (RspHeader head, long off, string? err) Parse(in ReadOnlySpan utf8) { + Fsm fsm = new() { + Lexer = new(utf8, false, new JsonReaderState(new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true })), + State = Fsms.Start, + }; + while (fsm.MoveNext()) {} + + if (!fsm.Success) { + return (default, fsm.Lexer.BytesConsumed, $"Error while parsing {nameof(RspHeader)} at {fsm.Lexer.TokenStartIndex}: {fsm.Err}"); + } + return (new(fsm.Id, fsm.Error), default, default); + } + + private enum Fsms { + Start, // -> Prop + Prop, // -> PropId | PropError | ProsResult + PropId, // -> Prop | End + PropError, // -> End + PropResult, // -> End + End + } + + private ref struct Fsm { + public Fsms State; + public Utf8JsonReader Lexer; + public string? Err; + public bool Success; + + public string? Name; + public string? Id; + public WsClient.Error Error; + + public bool MoveNext() { + return State switch { + Fsms.Start => Start(), + Fsms.Prop => Prop(), + Fsms.PropId => PropId(), + Fsms.PropError => PropError(), + Fsms.PropResult => PropResult(), + Fsms.End => End(), + _ => false + }; + } + + private bool Start() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.StartObject) { + Err = "Unable to read token StartObject"; + return false; + } + + State = Fsms.Prop; + return true; + + } + + private bool End() { + Success = !String.IsNullOrEmpty(Id); + return false; + } + + private bool Prop() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.PropertyName) { + Err = "Unable to read PropertyName"; + return false; + } + + Name = Lexer.GetString(); + if ("id".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropId; + return true; + } + if ("result".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropResult; + return true; + } + if ("error".Equals(Name, StringComparison.OrdinalIgnoreCase)) { + State = Fsms.PropError; + return true; + } + + Err = $"Unknown PropertyName `{Name}`"; + return false; + } + + private bool PropId() { + if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.String) { + Err = "Unable to read `id` property value"; + return false; + } + + State = Fsms.Prop; + Id = Lexer.GetString(); + return true; + } + + private bool PropError() { + Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); + State = Fsms.End; + return true; + } + + + private bool PropResult() { + // Do not parse the result! + // The complete result is not present in the buffer! + // The result is returned as a unevaluated asynchronous stream! + State = Fsms.End; + return true; + } + } +} + diff --git a/src/Ws/Ws.cs b/src/Ws/Ws.cs index cd2e1a34..4c101377 100644 --- a/src/Ws/Ws.cs +++ b/src/Ws/Ws.cs @@ -29,7 +29,7 @@ public async Task Close(CancellationToken ct = default) { /// /// Sends the request and awaits a response from the server /// - public async Task<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> RequestOnce(string id, Stream request, CancellationToken ct = default) { + public async Task<(RspHeader rsp, NtyHeader nty, Stream stm)> RequestOnce(string id, Stream request, CancellationToken ct = default) { ResponseHandler handler = new(id, ct); Register(handler); await _tx.Tw(request, ct); @@ -39,7 +39,7 @@ public async Task Close(CancellationToken ct = default) { /// /// Sends the request and awaits responses from the server until manually canceled using the cancellation token /// - public async IAsyncEnumerable<(WsManager.RspHeader rsp, WsManager.NtyHeader nty, Stream stm)> RequestPersists(string id, Stream request, [EnumeratorCancellation] CancellationToken ct = default) { + public async IAsyncEnumerable<(RspHeader rsp, NtyHeader nty, Stream stm)> RequestPersists(string id, Stream request, [EnumeratorCancellation] CancellationToken ct = default) { NotificationHandler handler = new(this, id, ct); Register(handler); await _tx.Tw(request, ct); diff --git a/src/Ws/WsManager.cs b/src/Ws/WsManager.cs index cd08f5bf..c38a885b 100644 --- a/src/Ws/WsManager.cs +++ b/src/Ws/WsManager.cs @@ -148,257 +148,4 @@ private void ThrowIfConnected() { private static void ThrowParseHead(string err, long off) { throw new JsonException(err, default, default, off); } - - public readonly record struct NtyHeader(string? id, string? method, WsClient.Error err) { - public bool IsDefault => default == this; - - /// - /// Parses the head including the result propertyname, excluding the result array. - /// - internal static (NtyHeader head, long off, string? err) Parse(in ReadOnlySpan utf8) { - Fsm fsm = new() { - Lexer = new(utf8, false, new JsonReaderState(new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true })), - State = Fsms.Start, - }; - while (fsm.MoveNext()) {} - - if (!fsm.Success) { - return (default, fsm.Lexer.BytesConsumed, $"Error while parsing {nameof(RspHeader)} at {fsm.Lexer.TokenStartIndex}: {fsm.Err}"); - } - return (new(fsm.Id, fsm.Method, fsm.Error), default, default); - } - - private enum Fsms { - Start, // -> Prop - Prop, // -> PropId | PropAsync | PropMethod | ProsResult - PropId, // -> Prop | End - PropMethod, // -> Prop | End - PropError, // -> End - PropParams, // -> End - End - } - - private ref struct Fsm { - public Fsms State; - public Utf8JsonReader Lexer; - public string? Err; - public bool Success; - - public string? Name; - public string? Id; - public WsClient.Error Error; - public string? Method; - - public bool MoveNext() { - return State switch { - Fsms.Start => Start(), - Fsms.Prop => Prop(), - Fsms.PropId => PropId(), - Fsms.PropMethod => PropMethod(), - Fsms.PropError => PropError(), - Fsms.PropParams => PropParams(), - Fsms.End => End(), - _ => false - }; - } - - private bool Start() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.StartObject) { - Err = "Unable to read token StartObject"; - return false; - } - - State = Fsms.Prop; - return true; - - } - - private bool End() { - Success = !String.IsNullOrEmpty(Id) && !String.IsNullOrEmpty(Method); - return false; - } - - private bool Prop() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.PropertyName) { - Err = "Unable to read PropertyName"; - return false; - } - - Name = Lexer.GetString(); - if ("id".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropId; - return true; - } - if ("method".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropMethod; - return true; - } - if ("error".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropError; - return true; - } - if ("params".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropParams; - return true; - } - - Err = $"Unknown PropertyName `{Name}`"; - return false; - } - - private bool PropId() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.String) { - Err = "Unable to read `id` property value"; - return false; - } - - State = Fsms.Prop; - Id = Lexer.GetString(); - return true; - } - - private bool PropError() { - Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); - State = Fsms.End; - return true; - } - - private bool PropMethod() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.String) { - Err = "Unable to read `method` property value"; - return false; - } - - State = Fsms.Prop; - Method = Lexer.GetString(); - return true; - } - - private bool PropParams() { - // Do not parse the result! - // The complete result is not present in the buffer! - // The result is returned as a unevaluated asynchronous stream! - State = Fsms.End; - return true; - } - } - } - - public readonly record struct RspHeader(string? id, WsClient.Error err) { - public bool IsDefault => default == this; - - /// - /// Parses the head including the result propertyname, excluding the result array. - /// - internal static (RspHeader head, long off, string? err) Parse(in ReadOnlySpan utf8) { - Fsm fsm = new() { - Lexer = new(utf8, false, new JsonReaderState(new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true })), - State = Fsms.Start, - }; - while (fsm.MoveNext()) {} - - if (!fsm.Success) { - return (default, fsm.Lexer.BytesConsumed, $"Error while parsing {nameof(RspHeader)} at {fsm.Lexer.TokenStartIndex}: {fsm.Err}"); - } - return (new(fsm.Id, fsm.Error), default, default); - } - - private enum Fsms { - Start, // -> Prop - Prop, // -> PropId | PropError | ProsResult - PropId, // -> Prop | End - PropError, // -> End - PropResult, // -> End - End - } - - private ref struct Fsm { - public Fsms State; - public Utf8JsonReader Lexer; - public string? Err; - public bool Success; - - public string? Name; - public string? Id; - public WsClient.Error Error; - - public bool MoveNext() { - return State switch { - Fsms.Start => Start(), - Fsms.Prop => Prop(), - Fsms.PropId => PropId(), - Fsms.PropError => PropError(), - Fsms.PropResult => PropResult(), - Fsms.End => End(), - _ => false - }; - } - - private bool Start() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.StartObject) { - Err = "Unable to read token StartObject"; - return false; - } - - State = Fsms.Prop; - return true; - - } - - private bool End() { - Success = !String.IsNullOrEmpty(Id); - return false; - } - - private bool Prop() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.PropertyName) { - Err = "Unable to read PropertyName"; - return false; - } - - Name = Lexer.GetString(); - if ("id".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropId; - return true; - } - if ("result".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropResult; - return true; - } - if ("error".Equals(Name, StringComparison.OrdinalIgnoreCase)) { - State = Fsms.PropError; - return true; - } - - Err = $"Unknown PropertyName `{Name}`"; - return false; - } - - private bool PropId() { - if (!Lexer.Read() || Lexer.TokenType != JsonTokenType.String) { - Err = "Unable to read `id` property value"; - return false; - } - - State = Fsms.Prop; - Id = Lexer.GetString(); - return true; - } - - private bool PropError() { - Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); - State = Fsms.End; - return true; - } - - - private bool PropResult() { - // Do not parse the result! - // The complete result is not present in the buffer! - // The result is returned as a unevaluated asynchronous stream! - State = Fsms.End; - return true; - } - } - } } From c2f8ed42a87eaf81257cf2ea8a0efe53cadace22 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:57:53 +0100 Subject: [PATCH 11/87] Pass RecyclableMemoryStreamManager to WsChannelTx --- src/Ws/WsChannels.cs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index 59630f9d..23340ac5 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -9,7 +9,7 @@ namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -internal sealed class WsChannelRx { +public sealed class WsChannelRx { private readonly ClientWebSocket _ws; private readonly ChannelReader _in; private readonly object _lock = new(); @@ -24,7 +24,7 @@ public WsChannelRx(ClientWebSocket ws, ChannelReader @in) private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - var reader = await input.ReadAsync(ct); + var reader = await input.ReadAsync(ct).ConfigureAwait(false); bool isFinalBlock = false; while (!isFinalBlock && !ct.IsCancellationRequested) { @@ -81,23 +81,26 @@ private void ThrowIfConnected() { } /// Receives messages from a websocket server and passes them to a channel -internal sealed class WsChannelTx { +public sealed class WsChannelTx { private readonly ClientWebSocket _ws; private readonly ChannelWriter _out; + private readonly RecyclableMemoryStreamManager _memoryManager; private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsChannelTx(ClientWebSocket ws, ChannelWriter @out) { + public WsChannelTx(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { _ws = ws; _out = @out; + _memoryManager = memoryManager; } - private static async Task Execute(ClientWebSocket input, ChannelWriter output, CancellationToken ct) { + private static async Task Execute( + RecyclableMemoryStreamManager memoryManager, + ClientWebSocket input, + ChannelWriter output, + CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); - // the MemoryManager associated with the streams - RecyclableMemoryStreamManager memoryManager = new(); - while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); // receive the first part @@ -107,9 +110,9 @@ private static async Task Execute(ClientWebSocket input, ChannelWriter LockAsync(CancellationToken ct) { - await _lock.WaitAsync(ct); + await _lock.WaitAsync(ct).ConfigureAwait(false); return new(this); } From 0003150fb622cf89b82f251926e9a8dab6eb0ee8 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 15:58:13 +0100 Subject: [PATCH 12/87] Add Task.Inv == Task.ConfigureAwait(false) --- src/Common/TaskExtensions.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Common/TaskExtensions.cs diff --git a/src/Common/TaskExtensions.cs b/src/Common/TaskExtensions.cs new file mode 100644 index 00000000..7d4a6f4c --- /dev/null +++ b/src/Common/TaskExtensions.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; + +namespace SurrealDB.Common; + +/// Extension methods for Tasks +public static class TaskExtensions { + /// The task is invariant. + /// Equivalent to Task.ConfigureAwait(false). + public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); + + /// The task is invariant. + /// Equivalent to Task.ConfigureAwait(false). + public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); + + /// The task is invariant. + /// Equivalent to Task.ConfigureAwait(false). + public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); + + /// The task is invariant. + /// Equivalent to Task.ConfigureAwait(false). + public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); +} From 91184379226fc8c8a95856a62796b2fa91904645 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 16:01:15 +0100 Subject: [PATCH 13/87] Use .Inv() instead of .ConfigureAwait(false) --- src/Ws/BufferedStreamReader.cs | 4 +-- src/Ws/WsChannels.cs | 28 ++++++++++--------- .../Driver.Tests/Queries/GeneralQueryTests.cs | 12 ++++---- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/Ws/BufferedStreamReader.cs b/src/Ws/BufferedStreamReader.cs index 6a979d0e..c178ff5b 100644 --- a/src/Ws/BufferedStreamReader.cs +++ b/src/Ws/BufferedStreamReader.cs @@ -110,13 +110,13 @@ public async ValueTask DisposeAsync() { var arbitraryStream = _arbitraryStream; _arbitraryStream = null; if (arbitraryStream is not null) { - await arbitraryStream.DisposeAsync().ConfigureAwait(false); + await arbitraryStream.DisposeAsync().Inv(); } var memoryStream = _memoryStream; _memoryStream = null; if (memoryStream is not null) { - await memoryStream.DisposeAsync().ConfigureAwait(false); + await memoryStream.DisposeAsync().Inv(); } var poolArray = _poolArray; diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index 23340ac5..fa8e8870 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -6,6 +6,8 @@ using Microsoft.IO; +using SurrealDB.Common; + namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. @@ -24,22 +26,22 @@ public WsChannelRx(ClientWebSocket ws, ChannelReader @in) private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - var reader = await input.ReadAsync(ct).ConfigureAwait(false); + var reader = await input.ReadAsync(ct).Inv(); bool isFinalBlock = false; while (!isFinalBlock && !ct.IsCancellationRequested) { - var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).ConfigureAwait(false); + var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).Inv(); isFinalBlock = rom.Length != BufferedStreamReader.BUFFER_SIZE; - await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).ConfigureAwait(false); + await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); } if (!isFinalBlock) { // ensure that the message is always terminated // no not pass a CancellationToken - await output.SendAsync(default, WebSocketMessageType.Text, true, default).ConfigureAwait(false); + await output.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); } - await reader.DisposeAsync().ConfigureAwait(false); + await reader.DisposeAsync().Inv(); ct.ThrowIfCancellationRequested(); } } @@ -104,24 +106,24 @@ private static async Task Execute( while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); // receive the first part - var result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); + var result = await input.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); // begin adding the message to the output var writeOutput = output.WriteAsync(msg, ct); - using (var h = await msg.LockAsync(ct).ConfigureAwait(false)) { + using (var h = await msg.LockAsync(ct).Inv()) { // write the first part to the message - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).Inv(); // indicate, that a message has been received msg.SetReceived(result); } while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts - result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); - using var h = await msg.LockAsync(ct).ConfigureAwait(false); - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); + result = await input.ReceiveAsync(buffer, ct).Inv(); + using var h = await msg.LockAsync(ct).Inv(); + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).Inv(); msg.SetReceived(result); if (result.EndOfMessage) { msg.SetEndOfMessage(); @@ -130,7 +132,7 @@ private static async Task Execute( } // finish adding the message to the output - await writeOutput.ConfigureAwait(false); + await writeOutput.Inv(); ArrayPool.Shared.Return(buffer); ct.ThrowIfCancellationRequested(); @@ -186,7 +188,7 @@ internal WsMessage(MemoryStream buffer) { } public async Task LockAsync(CancellationToken ct) { - await _lock.WaitAsync(ct).ConfigureAwait(false); + await _lock.WaitAsync(ct).Inv(); return new(this); } diff --git a/tests/Driver.Tests/Queries/GeneralQueryTests.cs b/tests/Driver.Tests/Queries/GeneralQueryTests.cs index ca1d8e3a..f5fe7031 100644 --- a/tests/Driver.Tests/Queries/GeneralQueryTests.cs +++ b/tests/Driver.Tests/Queries/GeneralQueryTests.cs @@ -28,7 +28,7 @@ public abstract class GeneralQueryTests public GeneralQueryTests(ITestOutputHelper logger) { Logger = logger; } - + private static readonly List CarRecords = new List { new Car( Brand: "Car 1", @@ -149,7 +149,7 @@ public async Task ExpressionAsAnAliasQueryTest() => await DbHandle.WithDataba doc.Should().BeEquivalentTo(expectedObject); } ); - + [Fact] public async Task ManuallyGeneratedObjectStructureQueryTest() => await DbHandle.WithDatabase( async db => { @@ -316,7 +316,7 @@ public async Task SimultaneousDatabaseOperations() => await DbHandle.WithData async db => { var taskCount = 50; var tasks = Enumerable.Range(0, taskCount).Select(i => DbTask(i, db)); - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks).Inv(); } ); @@ -326,17 +326,17 @@ private async Task DbTask(int i, T db) { var expectedResult = new TestObject(i, i); Thing thing = Thing.From("object", expectedResult.Key.ToString()); - var createResponse = await db.Create(thing, expectedResult).ConfigureAwait(false); + var createResponse = await db.Create(thing, expectedResult).Inv(); AssertResponse(createResponse, expectedResult); Logger.WriteLine($"Create {i} - Thread ID {Thread.CurrentThread.ManagedThreadId}"); - var selectResponse = await db.Select(thing).ConfigureAwait(false); + var selectResponse = await db.Select(thing).Inv(); AssertResponse(selectResponse, expectedResult); Logger.WriteLine($"Select {i} - Thread ID {Thread.CurrentThread.ManagedThreadId}"); string sql = "SELECT * FROM $record"; Dictionary param = new() { ["record"] = thing }; - var queryResponse = await db.Query(sql, param).ConfigureAwait(false); + var queryResponse = await db.Query(sql, param).Inv(); AssertResponse(queryResponse, expectedResult); Logger.WriteLine($"Query {i} - Thread ID {Thread.CurrentThread.ManagedThreadId}"); From 8acc45fe0fef054a70322f8dd94c9e3905318202 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 16:11:24 +0100 Subject: [PATCH 14/87] Implement IDisposable for WsChannelRx & WsChannelTx --- src/Ws/WsChannels.cs | 45 +++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index fa8e8870..72a9a413 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -6,12 +6,10 @@ using Microsoft.IO; -using SurrealDB.Common; - namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -public sealed class WsChannelRx { +public sealed class WsChannelRx : IDisposable { private readonly ClientWebSocket _ws; private readonly ChannelReader _in; private readonly object _lock = new(); @@ -26,22 +24,22 @@ public WsChannelRx(ClientWebSocket ws, ChannelReader @in) private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - var reader = await input.ReadAsync(ct).Inv(); + var reader = await input.ReadAsync(ct).ConfigureAwait(false); bool isFinalBlock = false; while (!isFinalBlock && !ct.IsCancellationRequested) { - var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).Inv(); + var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).ConfigureAwait(false); isFinalBlock = rom.Length != BufferedStreamReader.BUFFER_SIZE; - await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); + await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).ConfigureAwait(false); } if (!isFinalBlock) { // ensure that the message is always terminated // no not pass a CancellationToken - await output.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); + await output.SendAsync(default, WebSocketMessageType.Text, true, default).ConfigureAwait(false); } - await reader.DisposeAsync().Inv(); + await reader.DisposeAsync().ConfigureAwait(false); ct.ThrowIfCancellationRequested(); } } @@ -80,10 +78,16 @@ private void ThrowIfConnected() { throw new InvalidOperationException("The connection is already open"); } } + + public void Dispose() { + _ws.Dispose(); + _cts?.Dispose(); + _execute?.Dispose(); + } } /// Receives messages from a websocket server and passes them to a channel -public sealed class WsChannelTx { +public sealed class WsChannelTx : IDisposable { private readonly ClientWebSocket _ws; private readonly ChannelWriter _out; private readonly RecyclableMemoryStreamManager _memoryManager; @@ -106,24 +110,24 @@ private static async Task Execute( while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); // receive the first part - var result = await input.ReceiveAsync(buffer, ct).Inv(); + var result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); // begin adding the message to the output var writeOutput = output.WriteAsync(msg, ct); - using (var h = await msg.LockAsync(ct).Inv()) { + using (var h = await msg.LockAsync(ct).ConfigureAwait(false)) { // write the first part to the message - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).Inv(); + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); // indicate, that a message has been received msg.SetReceived(result); } while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts - result = await input.ReceiveAsync(buffer, ct).Inv(); - using var h = await msg.LockAsync(ct).Inv(); - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).Inv(); + result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); + using var h = await msg.LockAsync(ct).ConfigureAwait(false); + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); msg.SetReceived(result); if (result.EndOfMessage) { msg.SetEndOfMessage(); @@ -132,7 +136,7 @@ private static async Task Execute( } // finish adding the message to the output - await writeOutput.Inv(); + await writeOutput.ConfigureAwait(false); ArrayPool.Shared.Return(buffer); ct.ThrowIfCancellationRequested(); @@ -173,6 +177,13 @@ private void ThrowIfConnected() { throw new InvalidOperationException("The connection is already open"); } } + + public void Dispose() { + _ws.Dispose(); + _cts?.Dispose(); + _execute?.Dispose(); + _out.Complete(); + } } public sealed class WsMessage : IDisposable, IAsyncDisposable { @@ -188,7 +199,7 @@ internal WsMessage(MemoryStream buffer) { } public async Task LockAsync(CancellationToken ct) { - await _lock.WaitAsync(ct).Inv(); + await _lock.WaitAsync(ct).ConfigureAwait(false); return new(this); } From ae9e72f3c7b81094d64cc0ef5224fcb29ba8fcee Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:51:21 +0100 Subject: [PATCH 15/87] Remove TaskList --- src/Common/TaskList.cs | 119 ----------------------------------------- 1 file changed, 119 deletions(-) delete mode 100644 src/Common/TaskList.cs diff --git a/src/Common/TaskList.cs b/src/Common/TaskList.cs deleted file mode 100644 index 10d13c9a..00000000 --- a/src/Common/TaskList.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Collections; -using System.Diagnostics; - -namespace SurrealDB.Common; - -public sealed class TaskList { - private readonly object _lock = new(); - private readonly Node _root; - private Node _tail; - private int _len; - - public TaskList() { - _root = _tail = new(Task.CompletedTask); - } - - public void Add(Task task) { - lock (_lock) { - Debug.Assert(_tail.Next is null); - Node tail = new(task) { Prev = _tail }; - _tail.Next = tail; - _tail = tail; - _len += 1; - } - } - - public void Trim() { - lock (_lock) { - Node? pos = _root; - do { - Node cur = pos; - pos = pos.Next; - Task task = cur.Task; - if (task.IsCompleted) { - Remove(cur); - } - } while (pos is not null); - } - } - - public ValueTask WhenAll() { - return _len == 0 ? default : new(Task.WhenAll(Drain())); - } - - public DrainIterator Drain() { - return new DrainIterator(this); - } - - /// - /// Removes the node from the list. Requires _lock! - /// - private bool Remove(Node node) { - if (Object.ReferenceEquals(_root, node)) { - // Do not remove the root! - return false; - } - Node? prev = node.Prev; - Node? next = node.Next; - if (Object.ReferenceEquals(_tail, node)) { - _tail = prev!; // cannot be null because of _root - } - if (prev is not null) { - prev.Next = next; - } - if (next is not null) { - next.Prev = prev; - } - - _len -= 1; - return true; - } - - private sealed class Node { - public readonly Task Task; - public Node? Next; - public Node? Prev; - - public Node(Task task) { - Task = task; - } - } - - public struct DrainIterator : IEnumerable, IEnumerator { - private readonly TaskList _list; - - public DrainIterator(TaskList list) { - _list = list; - } - - public DrainIterator GetEnumerator() { - return new(_list); - } - - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - public bool MoveNext() { - lock (_list._lock) { - return _list.Remove(_list._tail); - } - } - - public void Reset() { - throw new NotSupportedException("Cannot be reset"); - } - - public Task Current => _list._tail.Task; - - object IEnumerator.Current => Current; - - public void Dispose() { - // not needed - } - } -} From 6fa5829fa4f571c058c287edf098c9f751774fba Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:52:43 +0100 Subject: [PATCH 16/87] Introduce WsHeader and WsHeaderWithMessage instead of (RspHeader Response, NtyHeader Notify, int Offset) tuple --- src/Ws/{Headers.cs => HeaderHelper.cs} | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) rename src/Ws/{Headers.cs => HeaderHelper.cs} (87%) diff --git a/src/Ws/Headers.cs b/src/Ws/HeaderHelper.cs similarity index 87% rename from src/Ws/Headers.cs rename to src/Ws/HeaderHelper.cs index 7ebf9cc5..f5eb584a 100644 --- a/src/Ws/Headers.cs +++ b/src/Ws/HeaderHelper.cs @@ -1,9 +1,45 @@ +using System.Diagnostics; +using System.Net.WebSockets; using System.Text.Json; +using SurrealDB.Common; using SurrealDB.Json; namespace SurrealDB.Ws; +public static class HeaderHelper { + public static WsHeader Parse(ReadOnlySpan utf8) { + var (rsp, rspOff, rspErr) = RspHeader.Parse(utf8); + if (rspErr is null) { + return new(rsp, default, (int)rspOff); + } + var (nty, ntyOff, ntyErr) = NtyHeader.Parse(utf8); + if (ntyErr is null) { + return new(default, nty, (int)ntyOff); + } + + throw new JsonException($"Failed to parse RspHeader or NotifyHeader: {rspErr} \n--AND--\n {ntyErr}", null, 0, Math.Max(rspOff, ntyOff)); + } +} + +public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int Offset) { + public string? Id => (Response.IsDefault, Notify.IsDefault) switch { + (true, false) => Notify.id, + (false, true) => Response.id, + _ => null + }; +} + +public readonly record struct WsHeaderWithMessage(WsHeader Header, WsMessage Message) : IDisposable, IAsyncDisposable { + public void Dispose() { + Message.Dispose(); + } + + public ValueTask DisposeAsync() { + return Message.DisposeAsync(); + } +} + public readonly record struct NtyHeader(string? id, string? method, WsClient.Error err) { public bool IsDefault => default == this; From 8844339ebe76d06f5dc37bd58662ad4fa7d9d07f Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:53:13 +0100 Subject: [PATCH 17/87] Add Span.ClipLength clips the length of a span the the maximum length --- src/Common/MemoryExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Common/MemoryExtensions.cs b/src/Common/MemoryExtensions.cs index e2106d58..6309a11c 100644 --- a/src/Common/MemoryExtensions.cs +++ b/src/Common/MemoryExtensions.cs @@ -1,7 +1,10 @@ namespace SurrealDB.Common; internal static class MemoryExtensions { - public static ReadOnlySpan SliceToMin(in this ReadOnlySpan span, int length) { + public static Span ClipLength(in this Span span, int length) { + return span.Length <= length ? span : span.Slice(0, length); + } + public static ReadOnlySpan ClipLength(in this ReadOnlySpan span, int length) { return span.Length <= length ? span : span.Slice(0, length); } } From a7146ad6edde078e4f0fbebb2f66a87325d812e3 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:54:21 +0100 Subject: [PATCH 18/87] Use channel for message portion received events --- src/Ws/WsChannels.cs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs index 72a9a413..b3eb322c 100644 --- a/src/Ws/WsChannels.cs +++ b/src/Ws/WsChannels.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; @@ -6,6 +7,8 @@ using Microsoft.IO; +using SurrealDB.Common; + namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. @@ -116,19 +119,19 @@ private static async Task Execute( WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); // begin adding the message to the output var writeOutput = output.WriteAsync(msg, ct); - using (var h = await msg.LockAsync(ct).ConfigureAwait(false)) { - // write the first part to the message + using (var h = await msg.LockStreamAsync(ct).ConfigureAwait(false)) { + // write the first part to the message and notify listeners await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); - // indicate, that a message has been received - msg.SetReceived(result); + await msg.SetReceivedAsync(result, ct).Inv(); } while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); - using var h = await msg.LockAsync(ct).ConfigureAwait(false); + using var h = await msg.LockStreamAsync(ct).ConfigureAwait(false); await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); - msg.SetReceived(result); + await msg.SetReceivedAsync(result, ct).Inv(); + if (result.EndOfMessage) { msg.SetEndOfMessage(); } @@ -187,10 +190,10 @@ public void Dispose() { } public sealed class WsMessage : IDisposable, IAsyncDisposable { + private readonly Channel _channel = Channel.CreateUnbounded(); private readonly MemoryStream _buffer; private readonly SemaphoreSlim _lock = new(1, 1); private readonly TaskCompletionSource _endOfMessageEvent = new(); - private TaskCompletionSource _receivedEvent = new(); private int _endOfMessage; internal WsMessage(MemoryStream buffer) { @@ -198,12 +201,14 @@ internal WsMessage(MemoryStream buffer) { _endOfMessage = 0; } - public async Task LockAsync(CancellationToken ct) { + public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; + + public async Task LockStreamAsync(CancellationToken ct) { await _lock.WaitAsync(ct).ConfigureAwait(false); return new(this); } - public Handle Lock(CancellationToken ct) { + public Handle LockStream(CancellationToken ct) { _lock.Wait(ct); return new(this); } @@ -228,17 +233,16 @@ internal void SetEndOfMessage() { } } - internal void SetReceived(WebSocketReceiveResult count) { - var receivedEvent = Interlocked.Exchange(ref _receivedEvent, new()); - receivedEvent.SetResult(count); - } - public Task EndOfMessageAsync() { return _endOfMessageEvent.Task; } - public Task ReceivedAsync() { - return _receivedEvent.Task; + internal ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { + return _channel.Writer.WriteAsync(result, ct); + } + + public ValueTask ReceiveAsync(CancellationToken ct) { + return _channel.Reader.ReadAsync(ct); } public readonly struct Handle : IDisposable { From 987cd31d9b637aeb109749b35d3275282db42024 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:54:48 +0100 Subject: [PATCH 19/87] Use new WsHeader... types for handlers --- src/Ws/Handler.cs | 28 +++++++++++++++------------- src/Ws/Ws.cs | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index 15f393b4..a0665687 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -6,11 +6,11 @@ internal interface IHandler : IDisposable { public bool Persistent { get; } - public void Handle(RspHeader rsp, NtyHeader nty, Stream stm); + public void Dispatch(WsHeaderWithMessage header); } internal sealed class ResponseHandler : IHandler { - private readonly TaskCompletionSource<(RspHeader, NtyHeader, Stream)> _tcs = new(); + private readonly TaskCompletionSource _tcs = new(); private readonly string _id; private readonly CancellationToken _ct; @@ -19,14 +19,15 @@ public ResponseHandler(string id, CancellationToken ct) { _ct = ct; } - public Task<(RspHeader rsp, NtyHeader nty, Stream stm)> Task => _tcs!.Task; + public Task Task => _tcs!.Task; public string Id => _id; public bool Persistent => false; - public void Handle(RspHeader rsp, NtyHeader nty, Stream stm) { - _tcs.SetResult((rsp, nty, stm)); + public void Dispatch(WsHeaderWithMessage header) { + _ct.ThrowIfCancellationRequested(); + _tcs.SetResult(header); } public void Dispose() { @@ -35,11 +36,11 @@ public void Dispose() { } -internal class NotificationHandler : IHandler, IAsyncEnumerable<(RspHeader rsp, NtyHeader nty, Stream stm)> { - private readonly Ws _mediator; +internal class NotificationHandler : IHandler, IAsyncEnumerable { + private readonly WsTxMessageMediator _mediator; private readonly CancellationToken _ct; - private TaskCompletionSource<(RspHeader, NtyHeader, Stream)> _tcs = new(); - public NotificationHandler(Ws mediator, string id, CancellationToken ct) { + private TaskCompletionSource _tcs = new(); + public NotificationHandler(WsTxMessageMediator mediator, string id, CancellationToken ct) { _mediator = mediator; Id = id; _ct = ct; @@ -48,8 +49,9 @@ public NotificationHandler(Ws mediator, string id, CancellationToken ct) { public string Id { get; } public bool Persistent => true; - public void Handle(RspHeader rsp, NtyHeader nty, Stream stm) { - _tcs.SetResult((rsp, nty, stm)); + public void Dispatch(WsHeaderWithMessage header) { + _ct.ThrowIfCancellationRequested(); + _tcs.SetResult(header); _tcs = new(); } @@ -57,9 +59,9 @@ public void Dispose() { _tcs.TrySetCanceled(); } - public async IAsyncEnumerator<(RspHeader rsp, NtyHeader nty, Stream stm)> GetAsyncEnumerator(CancellationToken cancellationToken = default) { + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { while (!_ct.IsCancellationRequested) { - (RspHeader, NtyHeader, Stream) res; + WsHeaderWithMessage res; try { res = await _tcs.Task; } catch (OperationCanceledException) { diff --git a/src/Ws/Ws.cs b/src/Ws/Ws.cs index 4c101377..cc51dd25 100644 --- a/src/Ws/Ws.cs +++ b/src/Ws/Ws.cs @@ -79,7 +79,7 @@ private async Task Receive(CancellationToken stoppingToken) { continue; } - handler.Handle(response, notify, stream); + handler.Dispatch(response, notify, stream); if (!handler.Persistent) { // persistent handlers are for notifications and are not removed automatically From 27de306cf3056ed724b3feb0c2b5022bb5aa307e Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:56:31 +0100 Subject: [PATCH 20/87] Implement WsTxMessageMediator Listens for Messages and dispatches them by their headers to different handlers. --- src/Ws/WsTxMessageMediator.cs | 128 ++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/Ws/WsTxMessageMediator.cs diff --git a/src/Ws/WsTxMessageMediator.cs b/src/Ws/WsTxMessageMediator.cs new file mode 100644 index 00000000..287c043c --- /dev/null +++ b/src/Ws/WsTxMessageMediator.cs @@ -0,0 +1,128 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Threading.Channels; + +using Microsoft.IO; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +/// Listens for s and dispatches them by their headers to different s. +internal sealed class WsTxMessageMediator { + private readonly ChannelReader _in; + private readonly WsChannelTx _tx; + private ConcurrentDictionary _handlers = new(); + private readonly object _lock = new(); + private CancellationTokenSource? _cts; + private Task? _execute; + + public static int MaxHeaderBytes => 1024; + + public WsTxMessageMediator(ClientWebSocket ws, Channel channel, RecyclableMemoryStreamManager memoryManager) { + _in = channel.Reader; + _tx = new(ws, channel.Writer, memoryManager); + } + + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] + public bool Connected => _cts is not null & _execute is not null; + + private async Task Execute(CancellationToken ct) { + Debug.Assert(ct.CanBeCanceled); + + while (!ct.IsCancellationRequested) { + var message = await _in.ReadAsync(ct).Inv(); + + // receive the first part of the message + var result = await message.ReceiveAsync(ct).Inv(); + WsHeader header; + using (var h = await message.LockStreamAsync(ct).Inv()) { + // parse the header from the message + header = PeekHeader(h.Stream, result.Count); + } + + // find the handler + string? id = header.Id; + if (id is null || !_handlers.TryGetValue(id, out var handler)) { + // invalid format, or no registered -> discard message + await message.DisposeAsync().Inv(); + return; + } + + // dispatch the message to the handler + try { + handler.Dispatch(new(header, message)); + } catch (OperationCanceledException) { + // handler is canceled -> unregister + Unregister(handler.Id); + } + + if (!handler.Persistent) { + // handler is only used once -> unregister + Unregister(handler.Id); + } + ct.ThrowIfCancellationRequested(); + } + } + + private static WsHeader PeekHeader(MemoryStream stream, int seekLength) { + Span bytes = stackalloc byte[MaxHeaderBytes].ClipLength(seekLength); + int read = stream.Read(bytes); + // peek instead of reading + stream.Position -= read; + Debug.Assert(read == bytes.Length); + return HeaderHelper.Parse(bytes); + } + + public void Unregister(string id) { + if (_handlers.TryRemove(id, out var handler)) + { + handler.Dispose(); + } + } + + public bool TryRegister(IHandler handler) { + return _handlers.TryAdd(handler.Id, handler); + } + + + public void Open() { + lock (_lock) { + ThrowIfConnected(); + _cts = new(); + _execute = Execute(_cts.Token); + } + _tx.Open(); + } + + public async Task Close() { + await _tx.Close().Inv(); + Task task; + lock (_lock) { + ThrowIfDisconnected(); + _cts.Cancel(); + task = _execute; + _execute = null; + } + await task.Inv(); + } + + [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] + private void ThrowIfDisconnected() { + Debug.Assert(_tx.Connected == Connected); + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + Debug.Assert(_tx.Connected == Connected); + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } + +} From e82dd901d5710eeb54458f38e3f647da46b42688 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:57:14 +0100 Subject: [PATCH 21/87] Move WsChannel* and WsMessage to separate files --- src/Ws/WsChannelRx.cs | 84 ++++++++++++++ src/Ws/WsChannelTx.cs | 111 ++++++++++++++++++ src/Ws/WsChannels.cs | 263 ------------------------------------------ src/Ws/WsMessage.cs | 78 +++++++++++++ 4 files changed, 273 insertions(+), 263 deletions(-) create mode 100644 src/Ws/WsChannelRx.cs create mode 100644 src/Ws/WsChannelTx.cs delete mode 100644 src/Ws/WsChannels.cs create mode 100644 src/Ws/WsMessage.cs diff --git a/src/Ws/WsChannelRx.cs b/src/Ws/WsChannelRx.cs new file mode 100644 index 00000000..79d42212 --- /dev/null +++ b/src/Ws/WsChannelRx.cs @@ -0,0 +1,84 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Threading.Channels; + +namespace SurrealDB.Ws; + +/// Sends messages from a channel to a websocket server. +public sealed class WsChannelRx : IDisposable { + private readonly ClientWebSocket _ws; + private readonly ChannelReader _in; + private readonly object _lock = new(); + private CancellationTokenSource? _cts; + private Task? _execute; + + public WsChannelRx(ClientWebSocket ws, ChannelReader @in) { + _ws = ws; + _in = @in; + } + + private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { + Debug.Assert(ct.CanBeCanceled); + while (!ct.IsCancellationRequested) { + var reader = await input.ReadAsync(ct).ConfigureAwait(false); + + bool isFinalBlock = false; + while (!isFinalBlock && !ct.IsCancellationRequested) { + var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).ConfigureAwait(false); + isFinalBlock = rom.Length != BufferedStreamReader.BUFFER_SIZE; + await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).ConfigureAwait(false); + } + + if (!isFinalBlock) { + // ensure that the message is always terminated + // no not pass a CancellationToken + await output.SendAsync(default, WebSocketMessageType.Text, true, default).ConfigureAwait(false); + } + + await reader.DisposeAsync().ConfigureAwait(false); + ct.ThrowIfCancellationRequested(); + } + } + + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] + public bool Connected => _cts is not null & _execute is not null; + + public void Open() { + lock (_lock) { + ThrowIfConnected(); + _cts = new(); + _execute = Execute(_ws, _in, _cts.Token); + } + } + + public Task Close() { + Task task; + lock (_lock) { + ThrowIfDisconnected(); + _cts.Cancel(); + task = _execute; + _execute = null; + } + return task; + } + + [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } + + public void Dispose() { + _ws.Dispose(); + _cts?.Dispose(); + _execute?.Dispose(); + } +} diff --git a/src/Ws/WsChannelTx.cs b/src/Ws/WsChannelTx.cs new file mode 100644 index 00000000..0622392e --- /dev/null +++ b/src/Ws/WsChannelTx.cs @@ -0,0 +1,111 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Threading.Channels; + +using Microsoft.IO; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +/// Receives messages from a websocket server and passes them to a channel +public sealed class WsChannelTx : IDisposable { + private readonly ClientWebSocket _ws; + private readonly ChannelWriter _out; + private readonly RecyclableMemoryStreamManager _memoryManager; + private readonly object _lock = new(); + private CancellationTokenSource? _cts; + private Task? _execute; + + public WsChannelTx(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { + _ws = ws; + _out = @out; + _memoryManager = memoryManager; + } + + private static async Task Execute( + RecyclableMemoryStreamManager memoryManager, + ClientWebSocket input, + ChannelWriter output, + CancellationToken ct) { + Debug.Assert(ct.CanBeCanceled); + while (!ct.IsCancellationRequested) { + var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); + // receive the first part + var result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); + // create a new message with a RecyclableMemoryStream + // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking + WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); + // begin adding the message to the output + var writeOutput = output.WriteAsync(msg, ct); + using (var h = await msg.LockStreamAsync(ct).ConfigureAwait(false)) { + // write the first part to the message and notify listeners + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); + await msg.SetReceivedAsync(result, ct).Inv(); + } + + while (!result.EndOfMessage && !ct.IsCancellationRequested) { + // receive more parts + result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); + using var h = await msg.LockStreamAsync(ct).ConfigureAwait(false); + await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); + await msg.SetReceivedAsync(result, ct).Inv(); + + if (result.EndOfMessage) { + msg.SetEndOfMessage(); + } + ct.ThrowIfCancellationRequested(); + } + + // finish adding the message to the output + await writeOutput.ConfigureAwait(false); + + ArrayPool.Shared.Return(buffer); + ct.ThrowIfCancellationRequested(); + } + } + + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] + public bool Connected => _cts is not null & _execute is not null; + + public void Open() { + lock (_lock) { + ThrowIfConnected(); + _cts = new(); + _execute = Execute(_memoryManager, _ws, _out, _cts.Token); + } + } + + public Task Close() { + Task task; + lock (_lock) { + ThrowIfDisconnected(); + _cts.Cancel(); + task = _execute; + _execute = null; + } + return task; + } + + [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } + + public void Dispose() { + _ws.Dispose(); + _cts?.Dispose(); + _execute?.Dispose(); + _out.Complete(); + } +} diff --git a/src/Ws/WsChannels.cs b/src/Ws/WsChannels.cs deleted file mode 100644 index b3eb322c..00000000 --- a/src/Ws/WsChannels.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System.Buffers; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; -using System.Threading.Channels; - -using Microsoft.IO; - -using SurrealDB.Common; - -namespace SurrealDB.Ws; - -/// Sends messages from a channel to a websocket server. -public sealed class WsChannelRx : IDisposable { - private readonly ClientWebSocket _ws; - private readonly ChannelReader _in; - private readonly object _lock = new(); - private CancellationTokenSource? _cts; - private Task? _execute; - - public WsChannelRx(ClientWebSocket ws, ChannelReader @in) { - _ws = ws; - _in = @in; - } - - private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { - Debug.Assert(ct.CanBeCanceled); - while (!ct.IsCancellationRequested) { - var reader = await input.ReadAsync(ct).ConfigureAwait(false); - - bool isFinalBlock = false; - while (!isFinalBlock && !ct.IsCancellationRequested) { - var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).ConfigureAwait(false); - isFinalBlock = rom.Length != BufferedStreamReader.BUFFER_SIZE; - await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).ConfigureAwait(false); - } - - if (!isFinalBlock) { - // ensure that the message is always terminated - // no not pass a CancellationToken - await output.SendAsync(default, WebSocketMessageType.Text, true, default).ConfigureAwait(false); - } - - await reader.DisposeAsync().ConfigureAwait(false); - ct.ThrowIfCancellationRequested(); - } - } - - [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] - public bool Connected => _cts is not null & _execute is not null; - - public void Open() { - lock (_lock) { - ThrowIfConnected(); - _cts = new(); - _execute = Execute(_ws, _in, _cts.Token); - } - } - - public Task Close() { - Task task; - lock (_lock) { - ThrowIfDisconnected(); - _cts.Cancel(); - task = _execute; - _execute = null; - } - return task; - } - - [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - public void Dispose() { - _ws.Dispose(); - _cts?.Dispose(); - _execute?.Dispose(); - } -} - -/// Receives messages from a websocket server and passes them to a channel -public sealed class WsChannelTx : IDisposable { - private readonly ClientWebSocket _ws; - private readonly ChannelWriter _out; - private readonly RecyclableMemoryStreamManager _memoryManager; - private readonly object _lock = new(); - private CancellationTokenSource? _cts; - private Task? _execute; - - public WsChannelTx(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { - _ws = ws; - _out = @out; - _memoryManager = memoryManager; - } - - private static async Task Execute( - RecyclableMemoryStreamManager memoryManager, - ClientWebSocket input, - ChannelWriter output, - CancellationToken ct) { - Debug.Assert(ct.CanBeCanceled); - while (!ct.IsCancellationRequested) { - var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); - // receive the first part - var result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); - // create a new message with a RecyclableMemoryStream - // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking - WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); - // begin adding the message to the output - var writeOutput = output.WriteAsync(msg, ct); - using (var h = await msg.LockStreamAsync(ct).ConfigureAwait(false)) { - // write the first part to the message and notify listeners - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); - await msg.SetReceivedAsync(result, ct).Inv(); - } - - while (!result.EndOfMessage && !ct.IsCancellationRequested) { - // receive more parts - result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); - using var h = await msg.LockStreamAsync(ct).ConfigureAwait(false); - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); - await msg.SetReceivedAsync(result, ct).Inv(); - - if (result.EndOfMessage) { - msg.SetEndOfMessage(); - } - ct.ThrowIfCancellationRequested(); - } - - // finish adding the message to the output - await writeOutput.ConfigureAwait(false); - - ArrayPool.Shared.Return(buffer); - ct.ThrowIfCancellationRequested(); - } - } - - [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] - public bool Connected => _cts is not null & _execute is not null; - - public void Open() { - lock (_lock) { - ThrowIfConnected(); - _cts = new(); - _execute = Execute(_memoryManager, _ws, _out, _cts.Token); - } - } - - public Task Close() { - Task task; - lock (_lock) { - ThrowIfDisconnected(); - _cts.Cancel(); - task = _execute; - _execute = null; - } - return task; - } - - [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - public void Dispose() { - _ws.Dispose(); - _cts?.Dispose(); - _execute?.Dispose(); - _out.Complete(); - } -} - -public sealed class WsMessage : IDisposable, IAsyncDisposable { - private readonly Channel _channel = Channel.CreateUnbounded(); - private readonly MemoryStream _buffer; - private readonly SemaphoreSlim _lock = new(1, 1); - private readonly TaskCompletionSource _endOfMessageEvent = new(); - private int _endOfMessage; - - internal WsMessage(MemoryStream buffer) { - _buffer = buffer; - _endOfMessage = 0; - } - - public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; - - public async Task LockStreamAsync(CancellationToken ct) { - await _lock.WaitAsync(ct).ConfigureAwait(false); - return new(this); - } - - public Handle LockStream(CancellationToken ct) { - _lock.Wait(ct); - return new(this); - } - - public void Dispose() { - _endOfMessageEvent.TrySetCanceled(); - _lock.Dispose(); - _buffer.Dispose(); - } - - public ValueTask DisposeAsync() { - _endOfMessageEvent.TrySetCanceled(); - _lock.Dispose(); - return _buffer.DisposeAsync(); - } - - internal void SetEndOfMessage() { - var endOfMessage = Interlocked.Exchange(ref _endOfMessage, 1); - if (endOfMessage == 0) { - // finish the AwaitEndOfMessage task - _endOfMessageEvent.SetResult(null); - } - } - - public Task EndOfMessageAsync() { - return _endOfMessageEvent.Task; - } - - internal ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { - return _channel.Writer.WriteAsync(result, ct); - } - - public ValueTask ReceiveAsync(CancellationToken ct) { - return _channel.Reader.ReadAsync(ct); - } - - public readonly struct Handle : IDisposable { - private readonly WsMessage _msg; - - internal Handle(WsMessage msg) { - _msg = msg; - } - - public MemoryStream Stream => _msg._buffer; - - public bool EndOfMessage => _msg._endOfMessage == 0; - - public void Dispose() { - _msg._lock.Release(); - } - } -} diff --git a/src/Ws/WsMessage.cs b/src/Ws/WsMessage.cs new file mode 100644 index 00000000..e0280d2f --- /dev/null +++ b/src/Ws/WsMessage.cs @@ -0,0 +1,78 @@ +using System.Collections.ObjectModel; +using System.Net.WebSockets; +using System.Threading.Channels; + +namespace SurrealDB.Ws; + +public sealed class WsMessage : IDisposable, IAsyncDisposable { + private readonly Channel _channel = Channel.CreateUnbounded(); + private readonly MemoryStream _buffer; + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly TaskCompletionSource _endOfMessageEvent = new(); + private int _endOfMessage; + + internal WsMessage(MemoryStream buffer) { + _buffer = buffer; + _endOfMessage = 0; + } + + public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; + + public async Task LockStreamAsync(CancellationToken ct) { + await _lock.WaitAsync(ct).ConfigureAwait(false); + return new(this); + } + + public Handle LockStream(CancellationToken ct) { + _lock.Wait(ct); + return new(this); + } + + public void Dispose() { + _endOfMessageEvent.TrySetCanceled(); + _lock.Dispose(); + _buffer.Dispose(); + } + + public ValueTask DisposeAsync() { + _endOfMessageEvent.TrySetCanceled(); + _lock.Dispose(); + return _buffer.DisposeAsync(); + } + + internal void SetEndOfMessage() { + var endOfMessage = Interlocked.Exchange(ref _endOfMessage, 1); + if (endOfMessage == 0) { + // finish the AwaitEndOfMessage task + _endOfMessageEvent.SetResult(null); + } + } + + public Task EndOfMessageAsync() { + return _endOfMessageEvent.Task; + } + + internal ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { + return _channel.Writer.WriteAsync(result, ct); + } + + public ValueTask ReceiveAsync(CancellationToken ct) { + return _channel.Reader.ReadAsync(ct); + } + + public readonly struct Handle : IDisposable { + private readonly WsMessage _msg; + + internal Handle(WsMessage msg) { + _msg = msg; + } + + public MemoryStream Stream => _msg._buffer; + + public bool EndOfMessage => _msg._endOfMessage == 0; + + public void Dispose() { + _msg._lock.Release(); + } + } +} From afebf84f5495c285972632e353c3d2a73dd849bf Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 17:58:14 +0100 Subject: [PATCH 22/87] Move BufferedStreamReader to Common --- src/{Ws => Common}/BufferedStreamReader.cs | 4 +--- src/Common/Common.csproj | 1 + src/Ws/WsChannelRx.cs | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) rename src/{Ws => Common}/BufferedStreamReader.cs (99%) diff --git a/src/Ws/BufferedStreamReader.cs b/src/Common/BufferedStreamReader.cs similarity index 99% rename from src/Ws/BufferedStreamReader.cs rename to src/Common/BufferedStreamReader.cs index c178ff5b..bc00d15f 100644 --- a/src/Ws/BufferedStreamReader.cs +++ b/src/Common/BufferedStreamReader.cs @@ -5,9 +5,7 @@ using Microsoft.IO; -using SurrealDB.Common; - -namespace SurrealDB.Ws; +namespace SurrealDB.Common; /// Allows reading a stream efficiently public struct BufferedStreamReader : IDisposable, IAsyncDisposable { diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index 4aeee446..68bee439 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Ws/WsChannelRx.cs b/src/Ws/WsChannelRx.cs index 79d42212..c832ca77 100644 --- a/src/Ws/WsChannelRx.cs +++ b/src/Ws/WsChannelRx.cs @@ -3,6 +3,8 @@ using System.Net.WebSockets; using System.Threading.Channels; +using SurrealDB.Common; + namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. From 7debd5edd618c56092651c2c2a5fd3887bbec1e9 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 18:20:05 +0100 Subject: [PATCH 23/87] Rename WsClient - WsClientSync --- src/Driver/Rpc/DatabaseRpc.cs | 18 +++++++++--------- src/Driver/Rpc/RpcClientExtensions.cs | 10 +++++----- src/Ws/Handler.cs | 2 +- src/Ws/HeaderHelper.cs | 12 ++++++------ src/Ws/{WsClient.cs => JsonRpcClientSync.cs} | 4 ++-- 5 files changed, 23 insertions(+), 23 deletions(-) rename src/Ws/{WsClient.cs => JsonRpcClientSync.cs} (97%) diff --git a/src/Driver/Rpc/DatabaseRpc.cs b/src/Driver/Rpc/DatabaseRpc.cs index 29eca395..2060cf30 100644 --- a/src/Driver/Rpc/DatabaseRpc.cs +++ b/src/Driver/Rpc/DatabaseRpc.cs @@ -8,7 +8,7 @@ namespace SurrealDB.Driver.Rpc; public sealed class DatabaseRpc : IDatabase { - private readonly WsClient _client = new(); + private readonly WsClientSync _client = new(); private Config _config; private bool _configured; @@ -86,7 +86,7 @@ public async Task Use( string? ns, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClient.Response rsp = await _client.Send(new() { method = "use", parameters = new(){ db, ns } }, ct); + WsClientSync.Response rsp = await _client.Send(new() { method = "use", parameters = new(){ db, ns } }, ct); if (rsp.error == default) { _config.Database = db; @@ -109,7 +109,7 @@ public async Task Signin( TRequest auth, CancellationToken ct = default) where TRequest : IAuth { ThrowIfInvalidConnection(); - WsClient.Response rsp = await _client.Send(new() { method = "signin", parameters = new() { auth } }, ct); + WsClientSync.Response rsp = await _client.Send(new() { method = "signin", parameters = new() { auth } }, ct); return rsp.ToSurreal(); } @@ -155,8 +155,8 @@ public async Task Query( IReadOnlyDictionary? vars, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClient.Request req = new() { method = "query", parameters = new() { sql, vars, }, }; - WsClient.Response rsp = await _client.Send(req, ct); + WsClientSync.Request req = new() { method = "query", parameters = new() { sql, vars, }, }; + WsClientSync.Response rsp = await _client.Send(req, ct); return rsp.ToSurreal(); } @@ -165,8 +165,8 @@ public async Task Select( Thing thing, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClient.Request req = new() { method = "select", parameters = new() { thing, }, }; - WsClient.Response rsp = await _client.Send(req, ct); + WsClientSync.Request req = new() { method = "select", parameters = new() { thing, }, }; + WsClientSync.Response rsp = await _client.Send(req, ct); return rsp.ToSurreal(); } @@ -176,8 +176,8 @@ public async Task Create( object data, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClient.Request req = new() { method = "create", async = true, parameters = new() { thing, data, }, }; - WsClient.Response rsp = await _client.Send(req, ct); + WsClientSync.Request req = new() { method = "create", async = true, parameters = new() { thing, data, }, }; + WsClientSync.Response rsp = await _client.Send(req, ct); return rsp.ToSurreal(); } diff --git a/src/Driver/Rpc/RpcClientExtensions.cs b/src/Driver/Rpc/RpcClientExtensions.cs index 29dfaa8d..761310e5 100644 --- a/src/Driver/Rpc/RpcClientExtensions.cs +++ b/src/Driver/Rpc/RpcClientExtensions.cs @@ -13,8 +13,8 @@ namespace SurrealDB.Driver.Rpc; internal static class RpcClientExtensions { - internal static async Task ToSurreal(this Task rsp) => ToSurreal(await rsp); - internal static DriverResponse ToSurreal(this WsClient.Response rsp){ + internal static async Task ToSurreal(this Task rsp) => ToSurreal(await rsp); + internal static DriverResponse ToSurreal(this WsClientSync.Response rsp){ if (rsp.id is null) { ThrowIdMissing(); } @@ -26,7 +26,7 @@ internal static DriverResponse ToSurreal(this WsClient.Response rsp){ return UnpackFromStatusDocument(in rsp); } - private static DriverResponse UnpackFromStatusDocument(in WsClient.Response rsp) { + private static DriverResponse UnpackFromStatusDocument(in WsClientSync.Response rsp) { // Some results come as a simple object or an array of objects or even and empty string // [ { }, { }, ... ] // Others come embedded into a 'status document' that can have multiple result sets @@ -62,7 +62,7 @@ private static DriverResponse UnpackFromStatusDocument(in WsClient.Response rsp) return new(RawResult.Ok(default, rsp.result)); } - private static DriverResponse ToSingleAny(in WsClient.Response rsp) { + private static DriverResponse ToSingleAny(in WsClientSync.Response rsp) { JsonElement root = rsp.result; if (root.ValueKind == JsonValueKind.Object) { var okOrErr = root.Deserialize(SerializerOptions.Shared); @@ -80,7 +80,7 @@ private static DriverResponse ToSingleAny(in WsClient.Response rsp) { return new(RawResult.Ok(default, root)); } - private static DriverResponse FromNestedStatus(in WsClient.Response rsp) { + private static DriverResponse FromNestedStatus(in WsClientSync.Response rsp) { ArrayBuilder builder = new(); foreach (JsonElement e in rsp.result.EnumerateArray()) { OkOrErrorResult res = e.Deserialize(SerializerOptions.Shared); diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index a0665687..88cff311 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -73,7 +73,7 @@ public async IAsyncEnumerator GetAsyncEnumerator(Cancellati // unregister before throwing if (_ct.IsCancellationRequested) { - _mediator.Unregister(this); + _mediator.Unregister(Id); } } } diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index f5eb584a..a1d2a525 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -40,7 +40,7 @@ public ValueTask DisposeAsync() { } } -public readonly record struct NtyHeader(string? id, string? method, WsClient.Error err) { +public readonly record struct NtyHeader(string? id, string? method, WsClientSync.Error err) { public bool IsDefault => default == this; /// @@ -77,7 +77,7 @@ private ref struct Fsm { public string? Name; public string? Id; - public WsClient.Error Error; + public WsClientSync.Error Error; public string? Method; public bool MoveNext() { @@ -149,7 +149,7 @@ private bool PropId() { } private bool PropError() { - Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); + Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); State = Fsms.End; return true; } @@ -175,7 +175,7 @@ private bool PropParams() { } } -public readonly record struct RspHeader(string? id, WsClient.Error err) { +public readonly record struct RspHeader(string? id, WsClientSync.Error err) { public bool IsDefault => default == this; /// @@ -211,7 +211,7 @@ private ref struct Fsm { public string? Name; public string? Id; - public WsClient.Error Error; + public WsClientSync.Error Error; public bool MoveNext() { return State switch { @@ -277,7 +277,7 @@ private bool PropId() { } private bool PropError() { - Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); + Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); State = Fsms.End; return true; } diff --git a/src/Ws/WsClient.cs b/src/Ws/JsonRpcClientSync.cs similarity index 97% rename from src/Ws/WsClient.cs rename to src/Ws/JsonRpcClientSync.cs index 661584d2..eb7a2b36 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/JsonRpcClientSync.cs @@ -12,10 +12,10 @@ namespace SurrealDB.Ws; /// /// The client used to connect to the Surreal server via JSON RPC. /// -public sealed class WsClient : IDisposable, IAsyncDisposable { +public sealed class WsClientSync : IDisposable, IAsyncDisposable { private static readonly Lazy s_manager = new(static () => new()); // Do not get any funny ideas and fill this fucker up. - public static readonly List EmptyList = new(); + private static readonly List EmptyList = new(); private readonly Ws _ws = new(); From 7d7306559b10f29b3e9e50a58917e01dfdedf9ad Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 18:20:41 +0100 Subject: [PATCH 24/87] Rename EmptyList --- src/Driver/Rpc/RpcClientExtensions.cs | 1 - src/Ws/{JsonRpcClientSync.cs => WsClientSync.cs} | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) rename src/Ws/{JsonRpcClientSync.cs => WsClientSync.cs} (97%) diff --git a/src/Driver/Rpc/RpcClientExtensions.cs b/src/Driver/Rpc/RpcClientExtensions.cs index 761310e5..1b43f280 100644 --- a/src/Driver/Rpc/RpcClientExtensions.cs +++ b/src/Driver/Rpc/RpcClientExtensions.cs @@ -12,7 +12,6 @@ namespace SurrealDB.Driver.Rpc; internal static class RpcClientExtensions { - internal static async Task ToSurreal(this Task rsp) => ToSurreal(await rsp); internal static DriverResponse ToSurreal(this WsClientSync.Response rsp){ if (rsp.id is null) { diff --git a/src/Ws/JsonRpcClientSync.cs b/src/Ws/WsClientSync.cs similarity index 97% rename from src/Ws/JsonRpcClientSync.cs rename to src/Ws/WsClientSync.cs index eb7a2b36..36ed77c5 100644 --- a/src/Ws/JsonRpcClientSync.cs +++ b/src/Ws/WsClientSync.cs @@ -15,7 +15,7 @@ namespace SurrealDB.Ws; public sealed class WsClientSync : IDisposable, IAsyncDisposable { private static readonly Lazy s_manager = new(static () => new()); // Do not get any funny ideas and fill this fucker up. - private static readonly List EmptyList = new(); + private static readonly List s_emptyList = new(); private readonly Ws _ws = new(); @@ -64,7 +64,7 @@ public ValueTask DisposeAsync() { public async Task Send(Request req, CancellationToken ct = default) { ThrowIfDisconnected(); req.id ??= GetRandomId(6); - req.parameters ??= EmptyList; + req.parameters ??= s_emptyList; await using RecyclableMemoryStream stream = new(s_manager.Value); From c202e1c2c2fc1edba194b180404ca620e793fef7 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 20:09:32 +0100 Subject: [PATCH 25/87] idk --- ...dStreamReader.cs => BufferStreamReader.cs} | 6 +- src/Common/TaskExtensions.cs | 8 +- src/Driver/Rpc/DatabaseRpc.cs | 22 +-- src/Driver/Rpc/RpcClientExtensions.cs | 10 +- src/Ws/Handler.cs | 4 +- src/Ws/HeaderHelper.cs | 21 ++- src/Ws/Ws.cs | 123 ------------- src/Ws/WsChannelTx.cs | 111 ------------ src/Ws/WsClient.cs | 167 ++++++++++++++++++ src/Ws/WsClientOptions.cs | 14 ++ src/Ws/WsClientSync.cs | 145 --------------- src/Ws/WsManager.cs | 151 ---------------- src/Ws/WsMessage.cs | 41 ++--- src/Ws/WsRxClient.cs | 54 ++++++ src/Ws/{WsChannelRx.cs => WsRxWriter.cs} | 21 ++- .../{WsTxMessageMediator.cs => WsTxClient.cs} | 27 +-- src/Ws/WsTxReader.cs | 116 ++++++++++++ 17 files changed, 427 insertions(+), 614 deletions(-) rename src/Common/{BufferedStreamReader.cs => BufferStreamReader.cs} (95%) delete mode 100644 src/Ws/Ws.cs delete mode 100644 src/Ws/WsChannelTx.cs create mode 100644 src/Ws/WsClient.cs create mode 100644 src/Ws/WsClientOptions.cs delete mode 100644 src/Ws/WsClientSync.cs delete mode 100644 src/Ws/WsManager.cs create mode 100644 src/Ws/WsRxClient.cs rename src/Ws/{WsChannelRx.cs => WsRxWriter.cs} (75%) rename src/Ws/{WsTxMessageMediator.cs => WsTxClient.cs} (82%) create mode 100644 src/Ws/WsTxReader.cs diff --git a/src/Common/BufferedStreamReader.cs b/src/Common/BufferStreamReader.cs similarity index 95% rename from src/Common/BufferedStreamReader.cs rename to src/Common/BufferStreamReader.cs index bc00d15f..2268db0b 100644 --- a/src/Common/BufferedStreamReader.cs +++ b/src/Common/BufferStreamReader.cs @@ -8,13 +8,13 @@ namespace SurrealDB.Common; /// Allows reading a stream efficiently -public struct BufferedStreamReader : IDisposable, IAsyncDisposable { +public struct BufferStreamReader : IDisposable, IAsyncDisposable { public const int BUFFER_SIZE = 16 * 1024; private Stream? _arbitraryStream; private MemoryStream? _memoryStream; private byte[]? _poolArray; - private BufferedStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream) { + private BufferStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream) { _arbitraryStream = arbitraryStream; _memoryStream = memoryStream; _poolArray = null; @@ -22,7 +22,7 @@ private BufferedStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream public Stream Stream => _memoryStream ?? _arbitraryStream!; - public BufferedStreamReader(Stream stream) { + public BufferStreamReader(Stream stream) { ThrowArgIfStreamCantRead(stream); this = stream switch { RecyclableMemoryStream => new(stream, null), // TryGetBuffer is expensive! diff --git a/src/Common/TaskExtensions.cs b/src/Common/TaskExtensions.cs index 7d4a6f4c..998905a6 100644 --- a/src/Common/TaskExtensions.cs +++ b/src/Common/TaskExtensions.cs @@ -6,17 +6,17 @@ namespace SurrealDB.Common; public static class TaskExtensions { /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); + public static ConfiguredTaskAwaitable Inv(this Task t) => t.Inv(); /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); + public static ConfiguredTaskAwaitable Inv(this Task t) => t.Inv(); /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); + public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.Inv(); /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); + public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.Inv(); } diff --git a/src/Driver/Rpc/DatabaseRpc.cs b/src/Driver/Rpc/DatabaseRpc.cs index 2060cf30..2b48e2f1 100644 --- a/src/Driver/Rpc/DatabaseRpc.cs +++ b/src/Driver/Rpc/DatabaseRpc.cs @@ -8,7 +8,7 @@ namespace SurrealDB.Driver.Rpc; public sealed class DatabaseRpc : IDatabase { - private readonly WsClientSync _client = new(); + private readonly WsClient _client = new(); private Config _config; private bool _configured; @@ -55,7 +55,7 @@ public async Task Open(CancellationToken ct = default) { // Open connection InvalidConfigException.ThrowIfNull(_config.RpcEndpoint); - await _client.Open(_config.RpcEndpoint!, ct); + await _client.OpenAsync(_config.RpcEndpoint!, ct); // Authenticate if (_config.Username != null && _config.Password != null) { @@ -70,7 +70,7 @@ public async Task Open(CancellationToken ct = default) { public async Task Close(CancellationToken ct = default) { _configured = false; - await _client.Close(ct); + await _client.CloseAsync(ct); } /// @@ -86,7 +86,7 @@ public async Task Use( string? ns, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClientSync.Response rsp = await _client.Send(new() { method = "use", parameters = new(){ db, ns } }, ct); + WsClient.Response rsp = await _client.Send(new() { method = "use", parameters = new(){ db, ns } }, ct); if (rsp.error == default) { _config.Database = db; @@ -109,7 +109,7 @@ public async Task Signin( TRequest auth, CancellationToken ct = default) where TRequest : IAuth { ThrowIfInvalidConnection(); - WsClientSync.Response rsp = await _client.Send(new() { method = "signin", parameters = new() { auth } }, ct); + WsClient.Response rsp = await _client.Send(new() { method = "signin", parameters = new() { auth } }, ct); return rsp.ToSurreal(); } @@ -155,8 +155,8 @@ public async Task Query( IReadOnlyDictionary? vars, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClientSync.Request req = new() { method = "query", parameters = new() { sql, vars, }, }; - WsClientSync.Response rsp = await _client.Send(req, ct); + WsClient.Request req = new() { method = "query", parameters = new() { sql, vars, }, }; + WsClient.Response rsp = await _client.Send(req, ct); return rsp.ToSurreal(); } @@ -165,8 +165,8 @@ public async Task Select( Thing thing, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClientSync.Request req = new() { method = "select", parameters = new() { thing, }, }; - WsClientSync.Response rsp = await _client.Send(req, ct); + WsClient.Request req = new() { method = "select", parameters = new() { thing, }, }; + WsClient.Response rsp = await _client.Send(req, ct); return rsp.ToSurreal(); } @@ -176,8 +176,8 @@ public async Task Create( object data, CancellationToken ct = default) { ThrowIfInvalidConnection(); - WsClientSync.Request req = new() { method = "create", async = true, parameters = new() { thing, data, }, }; - WsClientSync.Response rsp = await _client.Send(req, ct); + WsClient.Request req = new() { method = "create", async = true, parameters = new() { thing, data, }, }; + WsClient.Response rsp = await _client.Send(req, ct); return rsp.ToSurreal(); } diff --git a/src/Driver/Rpc/RpcClientExtensions.cs b/src/Driver/Rpc/RpcClientExtensions.cs index 1b43f280..21e7f892 100644 --- a/src/Driver/Rpc/RpcClientExtensions.cs +++ b/src/Driver/Rpc/RpcClientExtensions.cs @@ -12,8 +12,8 @@ namespace SurrealDB.Driver.Rpc; internal static class RpcClientExtensions { - internal static async Task ToSurreal(this Task rsp) => ToSurreal(await rsp); - internal static DriverResponse ToSurreal(this WsClientSync.Response rsp){ + internal static async Task ToSurreal(this Task rsp) => ToSurreal(await rsp); + internal static DriverResponse ToSurreal(this WsClient.Response rsp){ if (rsp.id is null) { ThrowIdMissing(); } @@ -25,7 +25,7 @@ internal static DriverResponse ToSurreal(this WsClientSync.Response rsp){ return UnpackFromStatusDocument(in rsp); } - private static DriverResponse UnpackFromStatusDocument(in WsClientSync.Response rsp) { + private static DriverResponse UnpackFromStatusDocument(in WsClient.Response rsp) { // Some results come as a simple object or an array of objects or even and empty string // [ { }, { }, ... ] // Others come embedded into a 'status document' that can have multiple result sets @@ -61,7 +61,7 @@ private static DriverResponse UnpackFromStatusDocument(in WsClientSync.Response return new(RawResult.Ok(default, rsp.result)); } - private static DriverResponse ToSingleAny(in WsClientSync.Response rsp) { + private static DriverResponse ToSingleAny(in WsClient.Response rsp) { JsonElement root = rsp.result; if (root.ValueKind == JsonValueKind.Object) { var okOrErr = root.Deserialize(SerializerOptions.Shared); @@ -79,7 +79,7 @@ private static DriverResponse ToSingleAny(in WsClientSync.Response rsp) { return new(RawResult.Ok(default, root)); } - private static DriverResponse FromNestedStatus(in WsClientSync.Response rsp) { + private static DriverResponse FromNestedStatus(in WsClient.Response rsp) { ArrayBuilder builder = new(); foreach (JsonElement e in rsp.result.EnumerateArray()) { OkOrErrorResult res = e.Deserialize(SerializerOptions.Shared); diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index 88cff311..03466812 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -37,10 +37,10 @@ public void Dispose() { } internal class NotificationHandler : IHandler, IAsyncEnumerable { - private readonly WsTxMessageMediator _mediator; + private readonly WsTxClient _mediator; private readonly CancellationToken _ct; private TaskCompletionSource _tcs = new(); - public NotificationHandler(WsTxMessageMediator mediator, string id, CancellationToken ct) { + public NotificationHandler(WsTxClient mediator, string id, CancellationToken ct) { _mediator = mediator; Id = id; _ct = ct; diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index a1d2a525..6bd335db 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -8,6 +8,13 @@ namespace SurrealDB.Ws; public static class HeaderHelper { + /// Generates a random base64 string of the length specified. + public static string GetRandomId(int length) { + Span buf = stackalloc byte[length]; + ThreadRng.Shared.NextBytes(buf); + return Convert.ToBase64String(buf); + } + public static WsHeader Parse(ReadOnlySpan utf8) { var (rsp, rspOff, rspErr) = RspHeader.Parse(utf8); if (rspErr is null) { @@ -22,7 +29,7 @@ public static WsHeader Parse(ReadOnlySpan utf8) { } } -public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int Offset) { +public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int BytesLength) { public string? Id => (Response.IsDefault, Notify.IsDefault) switch { (true, false) => Notify.id, (false, true) => Response.id, @@ -40,7 +47,7 @@ public ValueTask DisposeAsync() { } } -public readonly record struct NtyHeader(string? id, string? method, WsClientSync.Error err) { +public readonly record struct NtyHeader(string? id, string? method, WsClient.Error err) { public bool IsDefault => default == this; /// @@ -77,7 +84,7 @@ private ref struct Fsm { public string? Name; public string? Id; - public WsClientSync.Error Error; + public WsClient.Error Error; public string? Method; public bool MoveNext() { @@ -149,7 +156,7 @@ private bool PropId() { } private bool PropError() { - Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); + Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); State = Fsms.End; return true; } @@ -175,7 +182,7 @@ private bool PropParams() { } } -public readonly record struct RspHeader(string? id, WsClientSync.Error err) { +public readonly record struct RspHeader(string? id, WsClient.Error err) { public bool IsDefault => default == this; /// @@ -211,7 +218,7 @@ private ref struct Fsm { public string? Name; public string? Id; - public WsClientSync.Error Error; + public WsClient.Error Error; public bool MoveNext() { return State switch { @@ -277,7 +284,7 @@ private bool PropId() { } private bool PropError() { - Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); + Error = JsonSerializer.Deserialize(ref Lexer, SerializerOptions.Shared); State = Fsms.End; return true; } diff --git a/src/Ws/Ws.cs b/src/Ws/Ws.cs deleted file mode 100644 index cc51dd25..00000000 --- a/src/Ws/Ws.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace SurrealDB.Ws; - -public sealed class Ws : IDisposable, IAsyncDisposable { - private readonly CancellationTokenSource _cts = new(); - private readonly WsManager _tx = new(); - private readonly ConcurrentDictionary _handlers = new(); - private Task _recv = Task.CompletedTask; - - public bool Connected => _tx.Connected; - - public async Task Open(Uri remote, CancellationToken ct = default) { - await _tx.Open(remote, ct); - _recv = Task.Run(async () => await Receive(_cts.Token), _cts.Token); - } - - public async Task Close(CancellationToken ct = default) { - Task t1 = _tx.Close(ct); - Task t2 = Task.Run(ClearHandlers, ct); - _cts.Cancel(); - - await t1; - await t2; - } - - /// - /// Sends the request and awaits a response from the server - /// - public async Task<(RspHeader rsp, NtyHeader nty, Stream stm)> RequestOnce(string id, Stream request, CancellationToken ct = default) { - ResponseHandler handler = new(id, ct); - Register(handler); - await _tx.Tw(request, ct); - return await handler.Task; - } - - /// - /// Sends the request and awaits responses from the server until manually canceled using the cancellation token - /// - public async IAsyncEnumerable<(RspHeader rsp, NtyHeader nty, Stream stm)> RequestPersists(string id, Stream request, [EnumeratorCancellation] CancellationToken ct = default) { - NotificationHandler handler = new(this, id, ct); - Register(handler); - await _tx.Tw(request, ct); - await foreach (var res in handler) { - yield return res; - } - } - - internal void Register(IHandler handler) { - if (!_handlers.TryAdd(handler.Id, handler)) { - ThrowDuplicateId(handler.Id); - } - } - - internal void Unregister(IHandler handler) { - if (!_handlers.TryRemove(handler.Id, out var h)) { - return; - } - - h.Dispose(); - } - - private async Task Receive(CancellationToken stoppingToken) { - while (!stoppingToken.IsCancellationRequested) { - var (id, response, notify, stream) = await _tx.Tr(stoppingToken); - - stoppingToken.ThrowIfCancellationRequested(); - - if (String.IsNullOrEmpty(id)) { - continue; // Invalid response - } - - if (!_handlers.TryGetValue(id, out IHandler? handler)) { - // assume that unhandled responses belong to other clients - // discard! - await stream.DisposeAsync(); - continue; - } - - handler.Dispatch(response, notify, stream); - - if (!handler.Persistent) { - // persistent handlers are for notifications and are not removed automatically - Unregister(handler); - } - } - } - - private void ClearHandlers() { - foreach (var handler in _handlers.Values) { - Unregister(handler); - } - } - - public void Dispose() { - try { - Close().Wait(); - _tx.Dispose(); - } catch (OperationCanceledException) { - // expected - } catch (AggregateException) { - // wrapping OperationCanceledException - } - } - - public async ValueTask DisposeAsync() { - try { - await Close(); - _tx.Dispose(); - } catch (OperationCanceledException) { - // expected - } catch (AggregateException) { - // wrapping OperationCanceledException for async - } - } - - [DoesNotReturn] - private static void ThrowDuplicateId(string id) { - throw new ArgumentOutOfRangeException(nameof(id), $"A request with the Id `{id}` is already registered"); - } -} diff --git a/src/Ws/WsChannelTx.cs b/src/Ws/WsChannelTx.cs deleted file mode 100644 index 0622392e..00000000 --- a/src/Ws/WsChannelTx.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Buffers; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; -using System.Threading.Channels; - -using Microsoft.IO; - -using SurrealDB.Common; - -namespace SurrealDB.Ws; - -/// Receives messages from a websocket server and passes them to a channel -public sealed class WsChannelTx : IDisposable { - private readonly ClientWebSocket _ws; - private readonly ChannelWriter _out; - private readonly RecyclableMemoryStreamManager _memoryManager; - private readonly object _lock = new(); - private CancellationTokenSource? _cts; - private Task? _execute; - - public WsChannelTx(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { - _ws = ws; - _out = @out; - _memoryManager = memoryManager; - } - - private static async Task Execute( - RecyclableMemoryStreamManager memoryManager, - ClientWebSocket input, - ChannelWriter output, - CancellationToken ct) { - Debug.Assert(ct.CanBeCanceled); - while (!ct.IsCancellationRequested) { - var buffer = ArrayPool.Shared.Rent(BufferedStreamReader.BUFFER_SIZE); - // receive the first part - var result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); - // create a new message with a RecyclableMemoryStream - // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking - WsMessage msg = new(new RecyclableMemoryStream(memoryManager)); - // begin adding the message to the output - var writeOutput = output.WriteAsync(msg, ct); - using (var h = await msg.LockStreamAsync(ct).ConfigureAwait(false)) { - // write the first part to the message and notify listeners - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); - await msg.SetReceivedAsync(result, ct).Inv(); - } - - while (!result.EndOfMessage && !ct.IsCancellationRequested) { - // receive more parts - result = await input.ReceiveAsync(buffer, ct).ConfigureAwait(false); - using var h = await msg.LockStreamAsync(ct).ConfigureAwait(false); - await h.Stream.WriteAsync(buffer.AsMemory(0, result.Count), ct).ConfigureAwait(false); - await msg.SetReceivedAsync(result, ct).Inv(); - - if (result.EndOfMessage) { - msg.SetEndOfMessage(); - } - ct.ThrowIfCancellationRequested(); - } - - // finish adding the message to the output - await writeOutput.ConfigureAwait(false); - - ArrayPool.Shared.Return(buffer); - ct.ThrowIfCancellationRequested(); - } - } - - [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] - public bool Connected => _cts is not null & _execute is not null; - - public void Open() { - lock (_lock) { - ThrowIfConnected(); - _cts = new(); - _execute = Execute(_memoryManager, _ws, _out, _cts.Token); - } - } - - public Task Close() { - Task task; - lock (_lock) { - ThrowIfDisconnected(); - _cts.Cancel(); - task = _execute; - _execute = null; - } - return task; - } - - [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - public void Dispose() { - _ws.Dispose(); - _cts?.Dispose(); - _execute?.Dispose(); - _out.Complete(); - } -} diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs new file mode 100644 index 00000000..d9648207 --- /dev/null +++ b/src/Ws/WsClient.cs @@ -0,0 +1,167 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Channels; + +using Microsoft.IO; + +using SurrealDB.Common; +using SurrealDB.Json; + +namespace SurrealDB.Ws; + +/// The client used to connect to the Surreal server via JSON RPC. +public sealed class WsClient : IDisposable { + private static readonly Lazy s_manager = new(static () => new()); + // Do not get any funny ideas and fill this fucker up. + private static readonly List s_emptyList = new(); + + private readonly ClientWebSocket _ws = new(); + private readonly RecyclableMemoryStreamManager _memoryManager; + private readonly WsRxClient _rx; + private readonly WsTxClient _tx; + + private readonly int _idBytes; + + public WsClient() + : this(WsClientOptions.Default) { + } + + public WsClient(WsClientOptions options) { + _memoryManager = options.MemoryManager ?? new(); + _rx = new(_ws, Channel.CreateBounded(options.ChannelRxMessagesMax)); + _tx = new(_ws, Channel.CreateBounded(options.ChannelTxMessagesMax), _memoryManager, options.HeaderBytesMax); + _idBytes = options.IdBytes; + } + + /// Indicates whether the client is connected or not. + public bool Connected => _ws.State == WebSocketState.Open; + + public WebSocketState State => _ws.State; + + /// Opens the connection to the Surreal server. + public async Task OpenAsync(Uri url, CancellationToken ct = default) { + ThrowIfConnected(); + await _ws.ConnectAsync(url, ct); + } + + /// + /// Closes the connection to the Surreal server. + /// + public async Task CloseAsync(CancellationToken ct = default) { + await _ws.CloseAsync(WebSocketCloseStatus.Empty, "Orderly connection close", ct); + } + + /// + public void Dispose() { + _rx.Dispose(); + _tx.Dispose(); + _ws.Dispose(); + } + + /// + /// Sends the specified request to the Surreal server, and returns the response. + /// + public async Task Send(Request req, CancellationToken ct = default) { + ThrowIfDisconnected(); + req.id ??= HeaderHelper.GetRandomId(_idBytes); + req.parameters ??= s_emptyList; + + // listen for the response + ResponseHandler handler = new(req.id, ct); + if (!_tx.TryRegister(handler)) { + return default; + } + // send request + await using (var stream = await SerializeAsync(req, ct).Inv()) { + await _rx.SendAsync(stream); + } + // await response + var message = await handler.Task.Inv(); + // validate header + var header = message.Header.Response; + if (!message.Header.Notify.IsDefault) { + ThrowExpectResponseGotNotify(); + } + if (header.IsDefault) { + ThrowInvalidResponse(); + } + + // deserialize body + await message.Message.EndOfMessageAsync().Inv(); + JsonDocument? body; + lock (message.Message.Stream) { + var stream = message.Message.Stream; + // move position stream beyond header and deserialize message body + stream.Position += message.Header.BytesLength; + body = JsonSerializer.Deserialize(stream, SerializerOptions.Shared); + } + if (body is null) { + ThrowInvalidResponse(); + } + return new(header.id, header.err, ExtractResult(body)); + } + + private static async Task SerializeAsync(Request req, CancellationToken ct) { + RecyclableMemoryStream stream = new(s_manager.Value); + + await JsonSerializer.SerializeAsync(stream, req, SerializerOptions.Shared, ct).Inv(); + // position = Length = EndOfMessage -> position = 0 + stream.Position = 0; + return stream; + } + + private static JsonElement ExtractResult(JsonDocument root) { + return root.RootElement.TryGetProperty("result", out JsonElement res) ? res : default; + } + + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } + + [DoesNotReturn] + private static void ThrowExpectResponseGotNotify() { + throw new InvalidOperationException("Expected a response, got a notification"); + } + + [DoesNotReturn] + private static void ThrowInvalidResponse() { + throw new InvalidOperationException("Invalid response"); + } + + public record struct Request( + string? id, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] + bool async, + string? method, + [property: JsonPropertyName("params"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] + List? parameters); + + public readonly record struct Response( + string? id, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] + Error error, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] + JsonElement result); + + public readonly record struct Error( + int code, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] + string? message); + + + public record struct Notify( + string? id, + string? method, + [property: JsonPropertyName("params"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] + List? parameters); +} diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs new file mode 100644 index 00000000..02746d16 --- /dev/null +++ b/src/Ws/WsClientOptions.cs @@ -0,0 +1,14 @@ +using Microsoft.IO; + +namespace SurrealDB.Ws; + +public sealed record WsClientOptions { + public int ChannelRxMessagesMax { get; init; } = 256; + public int ChannelTxMessagesMax { get; init; } = 256; + public int HeaderBytesMax { get; init; } = 512; + /// The id is base64 encoded. 6 bytes = 4 characters. Use values in steps of 6. + public int IdBytes { get; init; } = 6; + public RecyclableMemoryStreamManager? MemoryManager { get; init; } + + internal static WsClientOptions Default => new(); +} diff --git a/src/Ws/WsClientSync.cs b/src/Ws/WsClientSync.cs deleted file mode 100644 index 36ed77c5..00000000 --- a/src/Ws/WsClientSync.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; - -using Microsoft.IO; - -using SurrealDB.Common; -using SurrealDB.Json; - -namespace SurrealDB.Ws; - -/// -/// The client used to connect to the Surreal server via JSON RPC. -/// -public sealed class WsClientSync : IDisposable, IAsyncDisposable { - private static readonly Lazy s_manager = new(static () => new()); - // Do not get any funny ideas and fill this fucker up. - private static readonly List s_emptyList = new(); - - private readonly Ws _ws = new(); - - /// - /// Indicates whether the client is connected or not. - /// - public bool Connected => _ws.Connected; - - /// - /// Generates a random base64 string of the length specified. - /// - public static string GetRandomId(int length) { - Span buf = stackalloc byte[length]; - ThreadRng.Shared.NextBytes(buf); - return Convert.ToBase64String(buf); - } - - /// - /// Opens the connection to the Surreal server. - /// - public async Task Open(Uri url, CancellationToken ct = default) { - ThrowIfConnected(); - await _ws.Open(url, ct); - } - - /// - /// Closes the connection to the Surreal server. - /// - public async Task Close(CancellationToken ct = default) { - await _ws.Close(ct); - } - - /// - public void Dispose() { - _ws.Dispose(); - } - - /// - public ValueTask DisposeAsync() { - return _ws.DisposeAsync(); - } - - /// - /// Sends the specified request to the Surreal server, and returns the response. - /// - public async Task Send(Request req, CancellationToken ct = default) { - ThrowIfDisconnected(); - req.id ??= GetRandomId(6); - req.parameters ??= s_emptyList; - - await using RecyclableMemoryStream stream = new(s_manager.Value); - - await JsonSerializer.SerializeAsync(stream, req, SerializerOptions.Shared, ct); - // Now Position = Length = EndOfMessage - // Write the buffer to the websocket - stream.Position = 0; - var (rsp, nty, stm) = await _ws.RequestOnce(req.id, stream, ct); - if (!nty.IsDefault) { - ThrowExpectRspGotNty(); - } - - if (rsp.IsDefault) { - ThrowRspDefault(); - } - - var bdy = await JsonSerializer.DeserializeAsync(stm, SerializerOptions.Shared, ct); - if (bdy is null) { - ThrowRspDefault(); - } - return new(rsp.id, rsp.err, ExtractResult(bdy)); - } - - private static JsonElement ExtractResult(JsonDocument root) { - return root.RootElement.TryGetProperty("result", out JsonElement res) ? res : default; - } - - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - [DoesNotReturn] - private static void ThrowExpectRspGotNty() { - throw new InvalidOperationException("Expected a response, got a notification"); - } - - [DoesNotReturn] - private static void ThrowRspDefault() { - throw new InvalidOperationException("Invalid response"); - } - - public record struct Request( - string? id, - [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] - bool async, - string? method, - [property: JsonPropertyName("params"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] - List? parameters); - - public readonly record struct Response( - string? id, - [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] - Error error, - [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] - JsonElement result); - - public readonly record struct Error( - int code, - [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] - string? message); - - - public record struct Notify( - string? id, - string? method, - [property: JsonPropertyName("params"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault),] - List? parameters); - - -} diff --git a/src/Ws/WsManager.cs b/src/Ws/WsManager.cs deleted file mode 100644 index c38a885b..00000000 --- a/src/Ws/WsManager.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Buffers; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; -using System.Text.Json; - -using SurrealDB.Common; -using SurrealDB.Json; - -namespace SurrealDB.Ws; - -public sealed class WsManager : IDisposable { - private readonly ClientWebSocket _ws = new(); - - public static int DefaultBufferSize => 16 * 1024; - - /// - /// Indicates whether the client is connected or not. - /// - public bool Connected => _ws.State == WebSocketState.Open; - - public async Task Open(Uri remote, CancellationToken ct = default) { - ThrowIfConnected(); - await _ws.ConnectAsync(remote, ct); - } - - public async Task Close(CancellationToken ct = default) { - if (_ws.State == WebSocketState.Closed) { - return; - } - - try { - await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client disconnect", ct); - } catch (OperationCanceledException) { - if (ct.IsCancellationRequested) { - // Catch any canceled exception that is generated during the close, - // but still throw for cancellations that we requested. - throw; - } - } - } - - public void Dispose() { - _ws.Dispose(); - } - - /// - /// Receives a response stream from the socket. - /// Parses the header. - /// The body contains the result array including the end object token `[...]}`. - /// - public async Task<(string? id, RspHeader rsp, NtyHeader nty, Stream body)> Tr(CancellationToken ct) { - ThrowIfDisconnected(); - // this method assumes that the header size never exceeds DefaultBufferSize! - IMemoryOwner owner = MemoryPool.Shared.Rent(DefaultBufferSize); - var r = await _ws.ReceiveAsync(owner.Memory, ct); - - if (r.Count <= 0) { - return (default, default, default, default!); - } - - // parse the header - var (rsp, nty, off) = ParseHeader(owner.Memory.Span.Slice(0, r.Count)); - string? id = rsp.IsDefault ? nty.id : rsp.id; - if (String.IsNullOrEmpty(id)) { - ThrowHeaderId(); - } - // returns a stream over the remainder of the body - Stream body = CreateBody(r, owner, owner.Memory.Slice(off)); - return (id, rsp, nty, body); - } - - private static (RspHeader rsp, NtyHeader nty, int off) ParseHeader(ReadOnlySpan utf8) { - var (rsp, rspOff, rspErr) = RspHeader.Parse(utf8); - if (rspErr is null) { - return (rsp, default, (int)rspOff); - } - var (nty, ntyOff, ntyErr) = NtyHeader.Parse(utf8); - if (ntyErr is null) { - return (default, nty, (int)ntyOff); - } - - throw new JsonException($"Failed to parse RspHeader or NotifyHeader: {rspErr} \n--AND--\n {ntyErr}", null, 0, Math.Max(rspOff, ntyOff)); - } - - private Stream CreateBody(ValueWebSocketReceiveResult res, IDisposable owner, ReadOnlyMemory rem) { - // check if rsp is already completely in the buffer - if (res.EndOfMessage) { - // create a rented stream from the remainder. - MemoryStream s = RentedMemoryStream.FromMemory(owner, rem, true, true); - s.SetLength(res.Count); - return s; - } - - // the rsp is not recv completely! - // create a stream wrapping the websocket - // with the recv portion as a prefix - Debug.Assert(res.Count == rem.Length); - return new WsStream(owner, rem, _ws); - } - - /// - /// Sends the stream over the socket. - /// - /// - /// Fast if used with a with exposed buffer! - /// - public async Task Tw(Stream req, CancellationToken ct) { - ThrowIfDisconnected(); - if (req is MemoryStream ms && ms.TryGetBuffer(out ArraySegment raw)) { - // We can obtain the raw buffer from the request, send it - await _ws.SendAsync(raw, WebSocketMessageType.Text, true, ct); - return; - } - - using IMemoryOwner owner = MemoryPool.Shared.Rent(DefaultBufferSize); - bool end = false; - while (!end && !ct.IsCancellationRequested) { - int read = await req.ReadAsync(owner.Memory, ct); - end = read != owner.Memory.Length; - ReadOnlyMemory used = owner.Memory.Slice(0, read); - await _ws.SendAsync(used, WebSocketMessageType.Text, end, ct); - - ThrowIfDisconnected(); - ct.ThrowIfCancellationRequested(); - } - Debug.Assert(end, "Unfinished message sent!"); - } - - [DoesNotReturn] - private static void ThrowHeaderId() { - throw new InvalidOperationException("Header has no associated id!"); - } - - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - [DoesNotReturn] - private static void ThrowParseHead(string err, long off) { - throw new JsonException(err, default, default, off); - } -} diff --git a/src/Ws/WsMessage.cs b/src/Ws/WsMessage.cs index e0280d2f..5137412f 100644 --- a/src/Ws/WsMessage.cs +++ b/src/Ws/WsMessage.cs @@ -1,43 +1,38 @@ -using System.Collections.ObjectModel; using System.Net.WebSockets; using System.Threading.Channels; +using SurrealDB.Common; + namespace SurrealDB.Ws; public sealed class WsMessage : IDisposable, IAsyncDisposable { private readonly Channel _channel = Channel.CreateUnbounded(); - private readonly MemoryStream _buffer; + private readonly MemoryStream _stream; private readonly SemaphoreSlim _lock = new(1, 1); private readonly TaskCompletionSource _endOfMessageEvent = new(); private int _endOfMessage; - internal WsMessage(MemoryStream buffer) { - _buffer = buffer; + internal WsMessage(MemoryStream stream) { + _stream = stream; _endOfMessage = 0; } public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; - public async Task LockStreamAsync(CancellationToken ct) { - await _lock.WaitAsync(ct).ConfigureAwait(false); - return new(this); - } - - public Handle LockStream(CancellationToken ct) { - _lock.Wait(ct); - return new(this); - } + /// The underlying stream. + /// Use a lock (as in ) before accessing! + public MemoryStream Stream => _stream; public void Dispose() { _endOfMessageEvent.TrySetCanceled(); _lock.Dispose(); - _buffer.Dispose(); + _stream.Dispose(); } public ValueTask DisposeAsync() { _endOfMessageEvent.TrySetCanceled(); _lock.Dispose(); - return _buffer.DisposeAsync(); + return _stream.DisposeAsync(); } internal void SetEndOfMessage() { @@ -59,20 +54,4 @@ internal ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationT public ValueTask ReceiveAsync(CancellationToken ct) { return _channel.Reader.ReadAsync(ct); } - - public readonly struct Handle : IDisposable { - private readonly WsMessage _msg; - - internal Handle(WsMessage msg) { - _msg = msg; - } - - public MemoryStream Stream => _msg._buffer; - - public bool EndOfMessage => _msg._endOfMessage == 0; - - public void Dispose() { - _msg._lock.Release(); - } - } } diff --git a/src/Ws/WsRxClient.cs b/src/Ws/WsRxClient.cs new file mode 100644 index 00000000..e4ad4f2e --- /dev/null +++ b/src/Ws/WsRxClient.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Threading.Channels; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +public sealed class WsRxClient : IDisposable { + private readonly ClientWebSocket _ws; + private readonly Channel _channel; + private readonly WsRxWriter _rx; + + public WsRxClient(ClientWebSocket ws, Channel channel) { + _ws = ws; + _channel = channel; + _rx = new(ws, channel.Reader); + } + + public bool Connected => _rx.Connected; + + + public ValueTask SendAsync(Stream stream) { + BufferStreamReader reader = new(stream); + return _channel.Writer.WriteAsync(reader); + } + + public void Open() { + _rx.Open(); + } + + public async Task Close() { + await _rx.Close().Inv(); + } + + + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } + + public void Dispose() { + _rx.Dispose(); + _channel.Writer.Complete(); + } +} diff --git a/src/Ws/WsChannelRx.cs b/src/Ws/WsRxWriter.cs similarity index 75% rename from src/Ws/WsChannelRx.cs rename to src/Ws/WsRxWriter.cs index c832ca77..882085a9 100644 --- a/src/Ws/WsChannelRx.cs +++ b/src/Ws/WsRxWriter.cs @@ -8,37 +8,37 @@ namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -public sealed class WsChannelRx : IDisposable { +public sealed class WsRxWriter : IDisposable { private readonly ClientWebSocket _ws; - private readonly ChannelReader _in; + private readonly ChannelReader _in; private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsChannelRx(ClientWebSocket ws, ChannelReader @in) { + public WsRxWriter(ClientWebSocket ws, ChannelReader @in) { _ws = ws; _in = @in; } - private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { + private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - var reader = await input.ReadAsync(ct).ConfigureAwait(false); + var reader = await input.ReadAsync(ct).Inv(); bool isFinalBlock = false; while (!isFinalBlock && !ct.IsCancellationRequested) { - var rom = await reader.ReadAsync(BufferedStreamReader.BUFFER_SIZE, ct).ConfigureAwait(false); - isFinalBlock = rom.Length != BufferedStreamReader.BUFFER_SIZE; - await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).ConfigureAwait(false); + var rom = await reader.ReadAsync(BufferStreamReader.BUFFER_SIZE, ct).Inv(); + isFinalBlock = rom.Length != BufferStreamReader.BUFFER_SIZE; + await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); } if (!isFinalBlock) { // ensure that the message is always terminated // no not pass a CancellationToken - await output.SendAsync(default, WebSocketMessageType.Text, true, default).ConfigureAwait(false); + await output.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); } - await reader.DisposeAsync().ConfigureAwait(false); + await reader.DisposeAsync().Inv(); ct.ThrowIfCancellationRequested(); } } @@ -79,7 +79,6 @@ private void ThrowIfConnected() { } public void Dispose() { - _ws.Dispose(); _cts?.Dispose(); _execute?.Dispose(); } diff --git a/src/Ws/WsTxMessageMediator.cs b/src/Ws/WsTxClient.cs similarity index 82% rename from src/Ws/WsTxMessageMediator.cs rename to src/Ws/WsTxClient.cs index 287c043c..ebf31d15 100644 --- a/src/Ws/WsTxMessageMediator.cs +++ b/src/Ws/WsTxClient.cs @@ -12,21 +12,22 @@ namespace SurrealDB.Ws; /// Listens for s and dispatches them by their headers to different s. -internal sealed class WsTxMessageMediator { +internal sealed class WsTxClient : IDisposable { private readonly ChannelReader _in; - private readonly WsChannelTx _tx; - private ConcurrentDictionary _handlers = new(); + private readonly WsTxReader _tx; + private readonly ConcurrentDictionary _handlers = new(); private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public static int MaxHeaderBytes => 1024; - - public WsTxMessageMediator(ClientWebSocket ws, Channel channel, RecyclableMemoryStreamManager memoryManager) { + public WsTxClient(ClientWebSocket ws, Channel channel, RecyclableMemoryStreamManager memoryManager, int maxHeaderBytes) { _in = channel.Reader; _tx = new(ws, channel.Writer, memoryManager); + MaxHeaderBytes = maxHeaderBytes; } + public int MaxHeaderBytes { get; } + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] public bool Connected => _cts is not null & _execute is not null; @@ -34,14 +35,15 @@ private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { + ThrowIfDisconnected(); var message = await _in.ReadAsync(ct).Inv(); // receive the first part of the message var result = await message.ReceiveAsync(ct).Inv(); WsHeader header; - using (var h = await message.LockStreamAsync(ct).Inv()) { + lock (message.Stream) { // parse the header from the message - header = PeekHeader(h.Stream, result.Count); + header = PeekHeader(message.Stream, result.Count); } // find the handler @@ -68,7 +70,7 @@ private async Task Execute(CancellationToken ct) { } } - private static WsHeader PeekHeader(MemoryStream stream, int seekLength) { + private WsHeader PeekHeader(MemoryStream stream, int seekLength) { Span bytes = stackalloc byte[MaxHeaderBytes].ClipLength(seekLength); int read = stream.Read(bytes); // peek instead of reading @@ -88,7 +90,6 @@ public bool TryRegister(IHandler handler) { return _handlers.TryAdd(handler.Id, handler); } - public void Open() { lock (_lock) { ThrowIfConnected(); @@ -125,4 +126,10 @@ private void ThrowIfConnected() { } } + public void Dispose() { + _tx.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); + _execute?.Dispose(); + } } diff --git a/src/Ws/WsTxReader.cs b/src/Ws/WsTxReader.cs new file mode 100644 index 00000000..5bd90d31 --- /dev/null +++ b/src/Ws/WsTxReader.cs @@ -0,0 +1,116 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Threading.Channels; + +using Microsoft.IO; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +/// Receives messages from a websocket server and passes them to a channel +public sealed class WsTxReader : IDisposable { + private readonly ClientWebSocket _ws; + private readonly ChannelWriter _out; + private readonly RecyclableMemoryStreamManager _memoryManager; + private readonly object _lock = new(); + private CancellationTokenSource? _cts; + private Task? _execute; + + public WsTxReader(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { + _ws = ws; + _out = @out; + _memoryManager = memoryManager; + } + + private async Task Execute(CancellationToken ct) { + Debug.Assert(ct.CanBeCanceled); + while (!ct.IsCancellationRequested) { + var buffer = ArrayPool.Shared.Rent(BufferStreamReader.BUFFER_SIZE); + try { + await ReceiveMessage(ct, buffer).Inv(); + } finally { + ArrayPool.Shared.Return(buffer); + } + ct.ThrowIfCancellationRequested(); + } + } + + private async Task ReceiveMessage(CancellationToken ct, byte[] buffer) { + // receive the first part + var result = await _ws.ReceiveAsync(buffer, ct).Inv(); + // create a new message with a RecyclableMemoryStream + // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking + WsMessage msg = new(new RecyclableMemoryStream(_memoryManager)); + // begin adding the message to the output + var writeOutput = _out.WriteAsync(msg, ct); + + await WriteToStream(msg, buffer, result, ct).Inv(); + + while (!result.EndOfMessage && !ct.IsCancellationRequested) { + // receive more parts + result = await _ws.ReceiveAsync(buffer, ct).Inv(); + await WriteToStream(msg, buffer, result, ct).Inv(); + + if (result.EndOfMessage) { + msg.SetEndOfMessage(); + } + + ct.ThrowIfCancellationRequested(); + } + + // finish adding the message to the output + await writeOutput.Inv(); + } + + private static async Task WriteToStream(WsMessage msg, byte[] buffer, WebSocketReceiveResult result, CancellationToken ct) { + lock (msg.Stream) { + msg.Stream.Write(buffer.AsSpan(0, result.Count)); + } + + await msg.SetReceivedAsync(result, ct).Inv(); + } + + [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] + public bool Connected => _cts is not null & _execute is not null; + + public void Open() { + lock (_lock) { + ThrowIfConnected(); + _cts = new(); + _execute = Execute(_cts.Token); + } + } + + public Task Close() { + Task task; + lock (_lock) { + ThrowIfDisconnected(); + _cts.Cancel(); + task = _execute; + _execute = null; + } + return task; + } + + [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] + private void ThrowIfDisconnected() { + if (!Connected) { + throw new InvalidOperationException("The connection is not open."); + } + } + + private void ThrowIfConnected() { + if (Connected) { + throw new InvalidOperationException("The connection is already open"); + } + } + + public void Dispose() { + _cts?.Dispose(); + _execute?.Dispose(); + _out.Complete(); + } +} From 5574b9913944297a643bf0623d29ae9ad9627af5 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 21:22:22 +0100 Subject: [PATCH 26/87] Convert WsMessage into a Stream WsMessageReader --- src/Ws/HeaderHelper.cs | 2 +- src/Ws/WsClient.cs | 19 ++-- src/Ws/WsMessage.cs | 57 ------------ src/Ws/WsMessageReader.cs | 188 ++++++++++++++++++++++++++++++++++++++ src/Ws/WsTxClient.cs | 21 ++--- src/Ws/WsTxReader.cs | 21 +---- 6 files changed, 210 insertions(+), 98 deletions(-) delete mode 100644 src/Ws/WsMessage.cs create mode 100644 src/Ws/WsMessageReader.cs diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index 6bd335db..0f132a22 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -37,7 +37,7 @@ public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int }; } -public readonly record struct WsHeaderWithMessage(WsHeader Header, WsMessage Message) : IDisposable, IAsyncDisposable { +public readonly record struct WsHeaderWithMessage(WsHeader Header, WsMessageReader Message) : IDisposable, IAsyncDisposable { public void Dispose() { Message.Dispose(); } diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index d9648207..65d26bed 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -31,7 +31,7 @@ public WsClient() public WsClient(WsClientOptions options) { _memoryManager = options.MemoryManager ?? new(); _rx = new(_ws, Channel.CreateBounded(options.ChannelRxMessagesMax)); - _tx = new(_ws, Channel.CreateBounded(options.ChannelTxMessagesMax), _memoryManager, options.HeaderBytesMax); + _tx = new(_ws, Channel.CreateBounded(options.ChannelTxMessagesMax), _memoryManager, options.HeaderBytesMax); _idBytes = options.IdBytes; } @@ -78,25 +78,20 @@ public async Task Send(Request req, CancellationToken ct = default) { await _rx.SendAsync(stream); } // await response - var message = await handler.Task.Inv(); + var response = await handler.Task.Inv(); // validate header - var header = message.Header.Response; - if (!message.Header.Notify.IsDefault) { + var header = response.Header.Response; + if (!response.Header.Notify.IsDefault) { ThrowExpectResponseGotNotify(); } if (header.IsDefault) { ThrowInvalidResponse(); } + // move position stream beyond header and deserialize message body + response.Message.Position = response.Header.BytesLength; // deserialize body - await message.Message.EndOfMessageAsync().Inv(); - JsonDocument? body; - lock (message.Message.Stream) { - var stream = message.Message.Stream; - // move position stream beyond header and deserialize message body - stream.Position += message.Header.BytesLength; - body = JsonSerializer.Deserialize(stream, SerializerOptions.Shared); - } + var body = await JsonSerializer.DeserializeAsync(response.Message, SerializerOptions.Shared, ct).Inv(); if (body is null) { ThrowInvalidResponse(); } diff --git a/src/Ws/WsMessage.cs b/src/Ws/WsMessage.cs deleted file mode 100644 index 5137412f..00000000 --- a/src/Ws/WsMessage.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Net.WebSockets; -using System.Threading.Channels; - -using SurrealDB.Common; - -namespace SurrealDB.Ws; - -public sealed class WsMessage : IDisposable, IAsyncDisposable { - private readonly Channel _channel = Channel.CreateUnbounded(); - private readonly MemoryStream _stream; - private readonly SemaphoreSlim _lock = new(1, 1); - private readonly TaskCompletionSource _endOfMessageEvent = new(); - private int _endOfMessage; - - internal WsMessage(MemoryStream stream) { - _stream = stream; - _endOfMessage = 0; - } - - public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; - - /// The underlying stream. - /// Use a lock (as in ) before accessing! - public MemoryStream Stream => _stream; - - public void Dispose() { - _endOfMessageEvent.TrySetCanceled(); - _lock.Dispose(); - _stream.Dispose(); - } - - public ValueTask DisposeAsync() { - _endOfMessageEvent.TrySetCanceled(); - _lock.Dispose(); - return _stream.DisposeAsync(); - } - - internal void SetEndOfMessage() { - var endOfMessage = Interlocked.Exchange(ref _endOfMessage, 1); - if (endOfMessage == 0) { - // finish the AwaitEndOfMessage task - _endOfMessageEvent.SetResult(null); - } - } - - public Task EndOfMessageAsync() { - return _endOfMessageEvent.Task; - } - - internal ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { - return _channel.Writer.WriteAsync(result, ct); - } - - public ValueTask ReceiveAsync(CancellationToken ct) { - return _channel.Reader.ReadAsync(ct); - } -} diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsMessageReader.cs new file mode 100644 index 00000000..ee0f790c --- /dev/null +++ b/src/Ws/WsMessageReader.cs @@ -0,0 +1,188 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +public sealed class WsMessageReader : Stream { + private readonly Channel _channel = Channel.CreateUnbounded(); + private readonly MemoryStream _stream; + private int _endOfMessage; + + internal WsMessageReader(MemoryStream stream) { + _stream = stream; + _endOfMessage = 0; + } + + public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; + + protected override void Dispose(bool disposing) { + if (!disposing) { + return; + } + + lock (_stream) { + _stream.Dispose(); + } + } + + private async ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { + await _channel.Writer.WriteAsync(result, ct).Inv(); + if (result.EndOfMessage) { + Interlocked.Exchange(ref _endOfMessage, 1); + } + } + + public ValueTask ReceiveAsync(CancellationToken ct) { + return _channel.Reader.ReadAsync(ct); + } + + public WebSocketReceiveResult Receive(CancellationToken ct) { + return ReceiveAsync(ct).Inv().GetAwaiter().GetResult(); + } + + internal ValueTask WriteResultAsync(ReadOnlyMemory buffer, WebSocketReceiveResult result, CancellationToken ct) { + ReadOnlySpan span = buffer.Span.Slice(0, result.Count); + lock (_stream) { + _stream.Write(span); + } + + return SetReceivedAsync(result, ct); + } + +#region Stream member + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length { + get { + Interlocked.MemoryBarrierProcessWide(); + return _stream.Length; + } + } + + public override long Position { + get { + Interlocked.MemoryBarrierProcessWide(); + return _stream.Position; + } + set { + lock (_stream) { + _stream.Position = value; + } + } + } + public override void Flush() { + ThrowCantWrite(); + } + + public override int Read(byte[] buffer, int offset, int count) { + return Read(buffer.AsSpan(offset, count)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(Span buffer) { + return Read(buffer, default); + } + + private int Read(Span buffer, CancellationToken ct) { + int read; + lock (_stream) { + // attempt to read from present buffer + read = _stream.Read(buffer); + } + + while (true) { + var result = Receive(ct); + int inc; + lock (_stream) { + inc = _stream.Read(buffer.Slice(read)); + } + + Debug.Assert(inc == result.Count); + read += inc; + + if (result.EndOfMessage) { + break; + } + } + + return read; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) { + return ReadAsync(buffer.AsMemory(offset, count), ct).AsTask(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) { + return ct.IsCancellationRequested ? new(0) : ReadInternalAsync(buffer, ct); + } + + private ValueTask ReadInternalAsync(Memory buffer, CancellationToken ct) { + int read; + lock (_stream) { + // attempt to read from present buffer + read = _stream.Read(buffer.Span); + } + + if (read == buffer.Length || IsEndOfMessage) { + return new(read); + } + + return new(ReadFromChannelAsync(buffer, read, ct)); + } + + private async Task ReadFromChannelAsync(Memory buffer, int read, CancellationToken ct) { + while (true) { + var result = await ReceiveAsync(ct).Inv(); + int inc; + lock (_stream) { + inc = _stream.Read(buffer.Span.Slice(read)); + } + Debug.Assert(inc == result.Count); + read += inc; + + if (result.EndOfMessage) { + break; + } + } + + return read; + } + + public override int ReadByte() { + Span buffer = stackalloc byte[1]; + int read = Read(buffer); + return read == 0 ? -1 : buffer[0]; + } + + public override long Seek(long offset, SeekOrigin origin) { + lock (_stream) { + return _stream.Seek(offset, origin); + } + } + + public override void SetLength(long value) { + lock (_stream) { + _stream.SetLength(value); + } + } + + public override void Write(byte[] buffer, int offset, int count) { + ThrowCantWrite(); + } + + +#endregion + + [DoesNotReturn] + private static void ThrowCantWrite() { + throw new NotSupportedException("The stream does not support writing"); + } +} diff --git a/src/Ws/WsTxClient.cs b/src/Ws/WsTxClient.cs index ebf31d15..9c07006d 100644 --- a/src/Ws/WsTxClient.cs +++ b/src/Ws/WsTxClient.cs @@ -13,14 +13,14 @@ namespace SurrealDB.Ws; /// Listens for s and dispatches them by their headers to different s. internal sealed class WsTxClient : IDisposable { - private readonly ChannelReader _in; + private readonly ChannelReader _in; private readonly WsTxReader _tx; private readonly ConcurrentDictionary _handlers = new(); private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsTxClient(ClientWebSocket ws, Channel channel, RecyclableMemoryStreamManager memoryManager, int maxHeaderBytes) { + public WsTxClient(ClientWebSocket ws, Channel channel, RecyclableMemoryStreamManager memoryManager, int maxHeaderBytes) { _in = channel.Reader; _tx = new(ws, channel.Writer, memoryManager); MaxHeaderBytes = maxHeaderBytes; @@ -40,14 +40,11 @@ private async Task Execute(CancellationToken ct) { // receive the first part of the message var result = await message.ReceiveAsync(ct).Inv(); - WsHeader header; - lock (message.Stream) { - // parse the header from the message - header = PeekHeader(message.Stream, result.Count); - } + // parse the header from the message + WsHeader header = PeekHeader(message, result.Count); - // find the handler - string? id = header.Id; + // find the handler + string? id = header.Id; if (id is null || !_handlers.TryGetValue(id, out var handler)) { // invalid format, or no registered -> discard message await message.DisposeAsync().Inv(); @@ -70,11 +67,11 @@ private async Task Execute(CancellationToken ct) { } } - private WsHeader PeekHeader(MemoryStream stream, int seekLength) { - Span bytes = stackalloc byte[MaxHeaderBytes].ClipLength(seekLength); + private WsHeader PeekHeader(Stream stream, int seekLength) { + Span bytes = stackalloc byte[Math.Min(MaxHeaderBytes, seekLength)]; int read = stream.Read(bytes); // peek instead of reading - stream.Position -= read; + stream.Position = 0; Debug.Assert(read == bytes.Length); return HeaderHelper.Parse(bytes); } diff --git a/src/Ws/WsTxReader.cs b/src/Ws/WsTxReader.cs index 5bd90d31..094b4d75 100644 --- a/src/Ws/WsTxReader.cs +++ b/src/Ws/WsTxReader.cs @@ -13,13 +13,13 @@ namespace SurrealDB.Ws; /// Receives messages from a websocket server and passes them to a channel public sealed class WsTxReader : IDisposable { private readonly ClientWebSocket _ws; - private readonly ChannelWriter _out; + private readonly ChannelWriter _out; private readonly RecyclableMemoryStreamManager _memoryManager; private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsTxReader(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { + public WsTxReader(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { _ws = ws; _out = @out; _memoryManager = memoryManager; @@ -43,20 +43,16 @@ private async Task ReceiveMessage(CancellationToken ct, byte[] buffer) { var result = await _ws.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking - WsMessage msg = new(new RecyclableMemoryStream(_memoryManager)); + WsMessageReader msg = new(new RecyclableMemoryStream(_memoryManager)); // begin adding the message to the output var writeOutput = _out.WriteAsync(msg, ct); - await WriteToStream(msg, buffer, result, ct).Inv(); + await msg.WriteResultAsync(buffer, result, ct).Inv(); while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts result = await _ws.ReceiveAsync(buffer, ct).Inv(); - await WriteToStream(msg, buffer, result, ct).Inv(); - - if (result.EndOfMessage) { - msg.SetEndOfMessage(); - } + msg.WriteResultAsync(buffer, result, ct).Inv(); ct.ThrowIfCancellationRequested(); } @@ -65,13 +61,6 @@ private async Task ReceiveMessage(CancellationToken ct, byte[] buffer) { await writeOutput.Inv(); } - private static async Task WriteToStream(WsMessage msg, byte[] buffer, WebSocketReceiveResult result, CancellationToken ct) { - lock (msg.Stream) { - msg.Stream.Write(buffer.AsSpan(0, result.Count)); - } - - await msg.SetReceivedAsync(result, ct).Inv(); - } [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] public bool Connected => _cts is not null & _execute is not null; From 8e4565120997e4cb7334737117a6eb2fce3d6a55 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 21:30:23 +0100 Subject: [PATCH 27/87] Refactoring Remove useless DisposeAsync --- src/Ws/HeaderHelper.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index 0f132a22..4e7ae8a6 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -37,14 +37,10 @@ public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int }; } -public readonly record struct WsHeaderWithMessage(WsHeader Header, WsMessageReader Message) : IDisposable, IAsyncDisposable { +public readonly record struct WsHeaderWithMessage(WsHeader Header, WsMessageReader Message) : IDisposable { public void Dispose() { Message.Dispose(); } - - public ValueTask DisposeAsync() { - return Message.DisposeAsync(); - } } public readonly record struct NtyHeader(string? id, string? method, WsClient.Error err) { From 582fadfbfde145abc5b121b13c418212106bd974 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Mon, 31 Oct 2022 21:31:06 +0100 Subject: [PATCH 28/87] Refactoring Remove useless DisposeAsync --- src/Ws/WsClient.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 65d26bed..7f03b367 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -77,25 +77,26 @@ public async Task Send(Request req, CancellationToken ct = default) { await using (var stream = await SerializeAsync(req, ct).Inv()) { await _rx.SendAsync(stream); } - // await response - var response = await handler.Task.Inv(); + // await response, dispose message when done + using var response = await handler.Task.Inv(); // validate header - var header = response.Header.Response; + var responseHeader = response.Header.Response; if (!response.Header.Notify.IsDefault) { ThrowExpectResponseGotNotify(); } - if (header.IsDefault) { + if (responseHeader.IsDefault) { ThrowInvalidResponse(); } - // move position stream beyond header and deserialize message body + // position stream beyond header and deserialize message body response.Message.Position = response.Header.BytesLength; // deserialize body var body = await JsonSerializer.DeserializeAsync(response.Message, SerializerOptions.Shared, ct).Inv(); if (body is null) { ThrowInvalidResponse(); } - return new(header.id, header.err, ExtractResult(body)); + + return new(responseHeader.id, responseHeader.err, ExtractResult(body)); } private static async Task SerializeAsync(Request req, CancellationToken ct) { From 574e3346daa862c18cd20680492892084283d8b7 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 10:46:15 +0100 Subject: [PATCH 29/87] Use local memory manager --- src/Ws/WsClient.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 7f03b367..efbfebc5 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -13,7 +13,6 @@ namespace SurrealDB.Ws; /// The client used to connect to the Surreal server via JSON RPC. public sealed class WsClient : IDisposable { - private static readonly Lazy s_manager = new(static () => new()); // Do not get any funny ideas and fill this fucker up. private static readonly List s_emptyList = new(); @@ -99,8 +98,8 @@ public async Task Send(Request req, CancellationToken ct = default) { return new(responseHeader.id, responseHeader.err, ExtractResult(body)); } - private static async Task SerializeAsync(Request req, CancellationToken ct) { - RecyclableMemoryStream stream = new(s_manager.Value); + private async Task SerializeAsync(Request req, CancellationToken ct) { + RecyclableMemoryStream stream = new(_memoryManager); await JsonSerializer.SerializeAsync(stream, req, SerializerOptions.Shared, ct).Inv(); // position = Length = EndOfMessage -> position = 0 From e6abf116c7c2d10af3e802484b3714bc88c2d6ac Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 11:27:26 +0100 Subject: [PATCH 30/87] public constants in UpperCamelCase --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 63f9d967..37b7467e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,7 +20,7 @@ csharp_style_var_for_built_in_types = false:none csharp_style_var_when_type_is_apparent = false:none dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined dotnet_naming_rule.constants_rule.severity = warning -dotnet_naming_rule.constants_rule.style = all_upper_style +dotnet_naming_rule.constants_rule.style = upper_camel_case_style dotnet_naming_rule.constants_rule.symbols = constants_symbols dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_constants_rule.resharper_style = AaBb, NUM_ + AA_BB From 170b11ecfc94cf902d3fc535aff0569c8a906e8c Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 13:01:03 +0100 Subject: [PATCH 31/87] Use ValidateReadonly for WsClientOptions --- src/Common/LatentReadonly.cs | 33 +++++++++++++ src/Common/ValidateReadonly.cs | 74 +++++++++++++++++++++++++++++ src/Ws/WsClientOptions.cs | 86 +++++++++++++++++++++++++++++++--- 3 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/Common/LatentReadonly.cs create mode 100644 src/Common/ValidateReadonly.cs diff --git a/src/Common/LatentReadonly.cs b/src/Common/LatentReadonly.cs new file mode 100644 index 00000000..dc092a67 --- /dev/null +++ b/src/Common/LatentReadonly.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SurrealDB.Common; + +public abstract record LatentReadonly { + private bool _isReadonly; + + protected void MakeReadonly() { + _isReadonly = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool IsReadonly() => _isReadonly; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void Set(out T field, in T value) { + ThrowIfReadonly(); + field = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfReadonly() { + if (_isReadonly) { + ThrowReadonly(); + } + } + + [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowReadonly() { + throw new InvalidOperationException("The object is readonly and cannot be mutated."); + } +} diff --git a/src/Common/ValidateReadonly.cs b/src/Common/ValidateReadonly.cs new file mode 100644 index 00000000..9374b86f --- /dev/null +++ b/src/Common/ValidateReadonly.cs @@ -0,0 +1,74 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Text; + +namespace SurrealDB.Common; + +public abstract record ValidateReadonly : LatentReadonly { + /// Evaluates , if it yields any errors, throws a with the errors + protected void ValidateOrThrow(string? message = null) { + PoolArrayBuilder<(string, string)> errors = new(); + foreach (var error in Validations()) { + errors.Append(error); + } + + if (!errors.IsDefault) { + AggregatePropertyValidationException.Throw(message, errors.ToArray()); + } + } + + /// Performs a sequence of validation on properties. + /// If a validation fails, yields the PropertyName and the corresponding error message. + protected abstract IEnumerable<(string PropertyName, string Message)> Validations(); +} + +[Serializable] +public class AggregatePropertyValidationException : Exception { + public AggregatePropertyValidationException() { + } + + public AggregatePropertyValidationException(string? message) : base(message) { + } + + public AggregatePropertyValidationException(string? message, Exception? inner) : base(message, inner) { + } + + public AggregatePropertyValidationException(string? message, (string Property, string Error)[]? errors, Exception? inner) : base(message, inner) { + Errors = errors; + } + + protected AggregatePropertyValidationException( + SerializationInfo info, + StreamingContext context) : base(info, context) { + } + + public (string Property, string Error)[]? Errors { get; set; } + + public override string ToString() { + ValueStringBuilder sb = new(stackalloc char[512]); + Span<(string PropertyName, string Message)>.Enumerator en = Errors.AsSpan().GetEnumerator(); + sb.Append(Message ?? "Validation failed with the following errors:"); + if (en.MoveNext()) { + sb.Append(Environment.NewLine); + sb.Append("- `"); + sb.Append(en.Current.PropertyName); + sb.Append("`: "); + sb.Append(en.Current.Message); + } + while (en.MoveNext()) { + sb.Append(Environment.NewLine); + sb.Append("- `"); + sb.Append(en.Current.PropertyName); + sb.Append("`: "); + sb.Append(en.Current.Message); + } + + return sb.ToString(); + } + + [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] + public static void Throw(string? message, (string, string)[]? errors = null, Exception? inner = default) { + throw new AggregatePropertyValidationException(message, errors, inner); + } +} diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index 02746d16..fac1a7ae 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -1,14 +1,86 @@ +using System.Text; + using Microsoft.IO; +using SurrealDB.Common; + namespace SurrealDB.Ws; -public sealed record WsClientOptions { - public int ChannelRxMessagesMax { get; init; } = 256; - public int ChannelTxMessagesMax { get; init; } = 256; - public int HeaderBytesMax { get; init; } = 512; - /// The id is base64 encoded. 6 bytes = 4 characters. Use values in steps of 6. - public int IdBytes { get; init; } = 6; - public RecyclableMemoryStreamManager? MemoryManager { get; init; } +public sealed record WsClientOptions : ValidateReadonly { + public const int MaxArraySize = 0X7FFFFFC7; + + /// The maximum number of messages in the client outbound channel. + /// A message may consist of multiple blocks. Only the message counts towards this number. + public int ChannelRxMessagesMax { + get => _channelRxMessagesMax; + set => Set(out _channelRxMessagesMax, in value); + } + /// The maximum number of messages in the client inbound channel. + /// A message may consist of multiple blocks. Only the message counts towards this number. + public int ChannelTxMessagesMax { + get => _channelTxMessagesMax; + set => Set(out _channelTxMessagesMax, in value); + } + /// The maximum number of bytes a received header can consist of. + /// The client receives a message with a and the message. + /// This is the length the socket "peeks" at the beginning of the network stream, in oder to fully deserialize the or . + /// The entire header must be contained within the peeked memory. + /// The length is bound to . + /// Longer lengths introduce additional overhead. + public int ReceiveHeaderBytesMax { + get => _receiveHeaderBytesMax; + set => Set(out _receiveHeaderBytesMax, in value); + } + /// The number of bytes the id consists of. + /// The id is base64 encoded, therefore 6 bytes = 4 characters. Use values in steps of 6. + public int IdBytes { + get => _idBytes; + set => Set(out _idBytes, in value); + } + /// Defines the resize behaviour of the streams used for handling messages. + public RecyclableMemoryStreamManager MemoryManager { + get => _memoryManager!; // Validated not null + set => Set(out _memoryManager, in value); + } + + private RecyclableMemoryStreamManager? _memoryManager; + private int _idBytes = 6; + private int _receiveHeaderBytesMax = 512; + private int _channelTxMessagesMax = 256; + private int _channelRxMessagesMax = 256; + + public void ValidateAndMakeReadonly() { + ValidateOrThrow(); + MakeReadonly(); + } + + protected override IEnumerable<(string PropertyName, string Message)> Validations() { + if (ChannelRxMessagesMax <= 0) { + yield return (nameof(ChannelRxMessagesMax), "cannot be less then or equal to zero"); + } + if (ChannelRxMessagesMax > MaxArraySize) { + yield return (nameof(ChannelRxMessagesMax), $"cannot be greater then {nameof(MaxArraySize)}"); + } + + if (ChannelTxMessagesMax <= 0) { + yield return (nameof(ChannelTxMessagesMax), "cannot be less then or equal to zero"); + } + if (ChannelTxMessagesMax > MaxArraySize) { + yield return (nameof(ChannelTxMessagesMax), $"cannot be greater then {nameof(MaxArraySize)}"); + } + + if (ReceiveHeaderBytesMax <= 0) { + yield return (nameof(ReceiveHeaderBytesMax), "cannot be less then or equal to zero"); + } + + if (ReceiveHeaderBytesMax > (_memoryManager?.BlockSize ?? 0)) { + yield return (nameof(ReceiveHeaderBytesMax), $"cannot be greater then {nameof(MemoryManager)}.{nameof(MemoryManager.BlockSize)}"); + } + + if (_memoryManager is null) { + yield return (nameof(MemoryManager), "cannot be null"); + } + } internal static WsClientOptions Default => new(); } From c969e108732f815c4e8bc9c600302b2f121241ff Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 13:01:23 +0100 Subject: [PATCH 32/87] Add ValueArrayBuilder --- src/Common/ArrayBuilder.cs | 212 +++++++++++++++++++++++++++++++++++-- 1 file changed, 206 insertions(+), 6 deletions(-) diff --git a/src/Common/ArrayBuilder.cs b/src/Common/ArrayBuilder.cs index 87bf5ef4..5c07433a 100644 --- a/src/Common/ArrayBuilder.cs +++ b/src/Common/ArrayBuilder.cs @@ -4,6 +4,7 @@ // Modified for generic types and not backed by a pool +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -63,12 +64,6 @@ public ref T this[int index] } } - public override string ToString() - { - string s = Raw.Slice(0, _pos).ToString(); - return s; - } - /// Returns the underlying storage of the builder. public Span Raw => _array.AsSpan(); @@ -185,3 +180,208 @@ private void Grow(int additionalCapacityBeyondPos) _array = newArray; } } + +internal ref struct PoolArrayBuilder +{ + private T[]? _array; + private Span _raw; + private int _pos; + + public PoolArrayBuilder(Span initialBuffer) + { + _array = null; + _raw = initialBuffer; + _pos = 0; + } + + public PoolArrayBuilder(int initialCapacity) + { + _array = ArrayPool.Shared.Rent(initialCapacity); + _raw = _array; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _raw.Length); + _pos = value; + } + } + + public bool IsEmpty => 0 >= (uint)_pos; + + public bool IsDefault => _raw.IsEmpty && _array is null && _pos == 0; + + public int Capacity => _raw.Length; + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + if ((uint)capacity > (uint)_raw.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref T GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_raw); + } + + public ref T this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _raw[index]; + } + } + + /// Returns the underlying storage of the builder. + public Span Raw => _raw; + + public ReadOnlySpan AsSpan() => _raw.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _raw.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _raw.Slice(start, length); + + public ArraySegment AsSegment() => new(_array ?? Array.Empty()); + public ArraySegment AsSegment(int start) => new(_array ?? Array.Empty(), start, _pos - start); + public ArraySegment AsSegment(int start, int length) => new(_array ?? Array.Empty(), start, length); + + public bool TryCopyTo(Span destination, out int written) { + if (_raw.Slice(0, _pos).TryCopyTo(destination)) + { + written = _pos; + return true; + } + + written = 0; + return false; + } + + public void Insert(int index, in T value, int count) + { + if (_pos > _raw.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _raw.Slice(index, remaining).CopyTo(_raw.Slice(index + count)); + _raw.Slice(index, count).Fill(value); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(T c) + { + int pos = _pos; + if ((uint)pos < (uint)_raw.Length) + { + _raw[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + public void Append(in T c, int count) + { + if (_pos > _raw.Length - count) + { + Grow(count); + } + + Span dst = _raw.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public void Append(ReadOnlySpan value) + { + int pos = _pos; + if (pos > _raw.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_raw.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _raw.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _raw.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(T c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _raw.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + T[] newArray = ArrayPool.Shared.Rent( + (int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_raw.Length * 2) + ); + _raw.Slice(0, _pos).CopyTo(newArray); + _raw = newArray; + if (_array is not null) { + ArrayPool.Shared.Return(_array); + } + _array = newArray; + } + + public void Dispose() { + var array = _array; + _array = null; + _raw = default; + if (array is not null) { + ArrayPool.Shared.Return(array); + } + } + + public T[] ToArray() { + var array = new T[_pos]; + _raw.Slice(0, _pos).CopyTo(array.AsSpan(0, _pos)); + Dispose(); + return array; + } +} From c4d1d418005a354fd8fb3d8a008e343adf859371 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 13:02:57 +0100 Subject: [PATCH 33/87] Dont use string interpolation --- src/Ws/WsClientOptions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index fac1a7ae..73d8c4b7 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -59,14 +59,14 @@ public void ValidateAndMakeReadonly() { yield return (nameof(ChannelRxMessagesMax), "cannot be less then or equal to zero"); } if (ChannelRxMessagesMax > MaxArraySize) { - yield return (nameof(ChannelRxMessagesMax), $"cannot be greater then {nameof(MaxArraySize)}"); + yield return (nameof(ChannelRxMessagesMax), "cannot be greater then MaxArraySize"); } if (ChannelTxMessagesMax <= 0) { yield return (nameof(ChannelTxMessagesMax), "cannot be less then or equal to zero"); } if (ChannelTxMessagesMax > MaxArraySize) { - yield return (nameof(ChannelTxMessagesMax), $"cannot be greater then {nameof(MaxArraySize)}"); + yield return (nameof(ChannelTxMessagesMax), "cannot be greater then MaxArraySize"); } if (ReceiveHeaderBytesMax <= 0) { @@ -74,7 +74,7 @@ public void ValidateAndMakeReadonly() { } if (ReceiveHeaderBytesMax > (_memoryManager?.BlockSize ?? 0)) { - yield return (nameof(ReceiveHeaderBytesMax), $"cannot be greater then {nameof(MemoryManager)}.{nameof(MemoryManager.BlockSize)}"); + yield return (nameof(ReceiveHeaderBytesMax), "cannot be greater then MemoryManager.BlockSize"); } if (_memoryManager is null) { From 1723745bc53ff907859694d425e3b6d6db728ef8 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 14:48:12 +0100 Subject: [PATCH 34/87] idk --- src/Common/BufferStreamReader.cs | 39 +++++----------- src/Ws/Handler.cs | 4 +- src/Ws/WsClient.cs | 38 +++++++++++----- src/Ws/WsClientOptions.cs | 6 ++- src/Ws/WsRxClient.cs | 54 ----------------------- src/Ws/{WsRxWriter.cs => WsRxConsumer.cs} | 22 ++++----- src/Ws/WsRxProducer.cs | 25 +++++++++++ src/Ws/{WsTxClient.cs => WsTxConsumer.cs} | 15 ++----- src/Ws/{WsTxReader.cs => WsTxProducer.cs} | 9 ++-- 9 files changed, 90 insertions(+), 122 deletions(-) delete mode 100644 src/Ws/WsRxClient.cs rename src/Ws/{WsRxWriter.cs => WsRxConsumer.cs} (71%) create mode 100644 src/Ws/WsRxProducer.cs rename src/Ws/{WsTxClient.cs => WsTxConsumer.cs} (85%) rename src/Ws/{WsTxReader.cs => WsTxProducer.cs} (90%) diff --git a/src/Common/BufferStreamReader.cs b/src/Common/BufferStreamReader.cs index 2268db0b..92242d19 100644 --- a/src/Common/BufferStreamReader.cs +++ b/src/Common/BufferStreamReader.cs @@ -8,26 +8,27 @@ namespace SurrealDB.Common; /// Allows reading a stream efficiently -public struct BufferStreamReader : IDisposable, IAsyncDisposable { - public const int BUFFER_SIZE = 16 * 1024; +public struct BufferStreamReader : IDisposable { private Stream? _arbitraryStream; private MemoryStream? _memoryStream; + private readonly int _bufferSize; private byte[]? _poolArray; - private BufferStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream) { + private BufferStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream, int bufferSize) { _arbitraryStream = arbitraryStream; _memoryStream = memoryStream; + _bufferSize = bufferSize; _poolArray = null; } public Stream Stream => _memoryStream ?? _arbitraryStream!; - public BufferStreamReader(Stream stream) { + public BufferStreamReader(Stream stream, int bufferSize) { ThrowArgIfStreamCantRead(stream); this = stream switch { - RecyclableMemoryStream => new(stream, null), // TryGetBuffer is expensive! - MemoryStream ms => new(null, ms), - _ => new(stream, null) + RecyclableMemoryStream => new(stream, null, bufferSize), // TryGetBuffer is expensive! + MemoryStream ms => new(null, ms, bufferSize), + _ => new(stream, null, bufferSize) }; } @@ -55,7 +56,7 @@ public ValueTask> ReadAsync(int expectedSize, CancellationT // reserve the buffer var buffer = _poolArray; if (buffer is null) { - _poolArray = buffer = ArrayPool.Shared.Rent(BUFFER_SIZE); + _poolArray = buffer = ArrayPool.Shared.Rent(_bufferSize); } // negative buffer size -> read as much as possible @@ -84,7 +85,7 @@ public ReadOnlySpan Read(int expectedSize) { // reserve the buffer var buffer = _poolArray; if (buffer is null) { - _poolArray = buffer = ArrayPool.Shared.Rent(BUFFER_SIZE); + _poolArray = buffer = ArrayPool.Shared.Rent(_bufferSize); } return stream.ReadToBuffer(buffer.AsSpan(0, Math.Min(buffer.Length, expectedSize))); @@ -104,26 +105,6 @@ public void Dispose() { } } - public async ValueTask DisposeAsync() { - var arbitraryStream = _arbitraryStream; - _arbitraryStream = null; - if (arbitraryStream is not null) { - await arbitraryStream.DisposeAsync().Inv(); - } - - var memoryStream = _memoryStream; - _memoryStream = null; - if (memoryStream is not null) { - await memoryStream.DisposeAsync().Inv(); - } - - var poolArray = _poolArray; - _poolArray = null; - if (poolArray is not null) { - ArrayPool.Shared.Return(poolArray); - } - } - private static void ThrowIfNull([DoesNotReturnIf(true)] bool isNull, [CallerArgumentExpression(nameof(isNull))] string expression = "") { if (isNull) { throw new InvalidOperationException($"The expression cannot be null. `{expression}`"); diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index 03466812..80807bf4 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -37,10 +37,10 @@ public void Dispose() { } internal class NotificationHandler : IHandler, IAsyncEnumerable { - private readonly WsTxClient _mediator; + private WsTxConsumer _mediator; private readonly CancellationToken _ct; private TaskCompletionSource _tcs = new(); - public NotificationHandler(WsTxClient mediator, string id, CancellationToken ct) { + public NotificationHandler(WsTxConsumer mediator, string id, CancellationToken ct) { _mediator = mediator; Id = id; _ct = ct; diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index efbfebc5..ac7bc7ee 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -18,8 +18,10 @@ public sealed class WsClient : IDisposable { private readonly ClientWebSocket _ws = new(); private readonly RecyclableMemoryStreamManager _memoryManager; - private readonly WsRxClient _rx; - private readonly WsTxClient _tx; + private readonly WsRxProducer _rxProducer; + private WsRxConsumer _rxConsumer; + private WsTxConsumer _txConsumer; + private WsTxProducer _txProducer; private readonly int _idBytes; @@ -28,9 +30,15 @@ public WsClient() } public WsClient(WsClientOptions options) { - _memoryManager = options.MemoryManager ?? new(); - _rx = new(_ws, Channel.CreateBounded(options.ChannelRxMessagesMax)); - _tx = new(_ws, Channel.CreateBounded(options.ChannelTxMessagesMax), _memoryManager, options.HeaderBytesMax); + options.ValidateAndMakeReadonly(); + _memoryManager = options.MemoryManager; + var rx = Channel.CreateBounded(options.ChannelRxMessagesMax); + _rxProducer = new(rx.Writer, _memoryManager.BlockSize); + _rxConsumer = new(_ws, rx.Reader, _memoryManager.BlockSize); + var tx = Channel.CreateBounded(options.ChannelTxMessagesMax); + _txConsumer = new(tx.Reader, options.ReceiveHeaderBytesMax); + _txProducer = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize); + _idBytes = options.IdBytes; } @@ -42,20 +50,28 @@ public WsClient(WsClientOptions options) { /// Opens the connection to the Surreal server. public async Task OpenAsync(Uri url, CancellationToken ct = default) { ThrowIfConnected(); - await _ws.ConnectAsync(url, ct); + await _ws.ConnectAsync(url, ct).Inv(); + _rxConsumer.Open(); + _txConsumer.Open(); + _txProducer.Open(); } /// /// Closes the connection to the Surreal server. /// public async Task CloseAsync(CancellationToken ct = default) { - await _ws.CloseAsync(WebSocketCloseStatus.Empty, "Orderly connection close", ct); + await _ws.CloseAsync(WebSocketCloseStatus.Empty, "client connection closed orderly", ct).Inv(); + await _rxConsumer.Close().Inv(); + await _txProducer.Close().Inv(); + await _txConsumer.Close().Inv(); } /// public void Dispose() { - _rx.Dispose(); - _tx.Dispose(); + _rxProducer.Dispose(); + _rxConsumer.Dispose(); + _txConsumer.Dispose(); + _txProducer.Dispose(); _ws.Dispose(); } @@ -69,12 +85,12 @@ public async Task Send(Request req, CancellationToken ct = default) { // listen for the response ResponseHandler handler = new(req.id, ct); - if (!_tx.TryRegister(handler)) { + if (!_txConsumer.TryRegister(handler)) { return default; } // send request await using (var stream = await SerializeAsync(req, ct).Inv()) { - await _rx.SendAsync(stream); + await _rxProducer.SendAsync(stream); } // await response, dispose message when done using var response = await handler.Task.Inv(); diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index 73d8c4b7..f2a45fe5 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -50,8 +50,10 @@ public RecyclableMemoryStreamManager MemoryManager { private int _channelRxMessagesMax = 256; public void ValidateAndMakeReadonly() { - ValidateOrThrow(); - MakeReadonly(); + if (!IsReadonly()) { + ValidateOrThrow(); + MakeReadonly(); + } } protected override IEnumerable<(string PropertyName, string Message)> Validations() { diff --git a/src/Ws/WsRxClient.cs b/src/Ws/WsRxClient.cs deleted file mode 100644 index e4ad4f2e..00000000 --- a/src/Ws/WsRxClient.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; -using System.Threading.Channels; - -using SurrealDB.Common; - -namespace SurrealDB.Ws; - -public sealed class WsRxClient : IDisposable { - private readonly ClientWebSocket _ws; - private readonly Channel _channel; - private readonly WsRxWriter _rx; - - public WsRxClient(ClientWebSocket ws, Channel channel) { - _ws = ws; - _channel = channel; - _rx = new(ws, channel.Reader); - } - - public bool Connected => _rx.Connected; - - - public ValueTask SendAsync(Stream stream) { - BufferStreamReader reader = new(stream); - return _channel.Writer.WriteAsync(reader); - } - - public void Open() { - _rx.Open(); - } - - public async Task Close() { - await _rx.Close().Inv(); - } - - - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - public void Dispose() { - _rx.Dispose(); - _channel.Writer.Complete(); - } -} diff --git a/src/Ws/WsRxWriter.cs b/src/Ws/WsRxConsumer.cs similarity index 71% rename from src/Ws/WsRxWriter.cs rename to src/Ws/WsRxConsumer.cs index 882085a9..3b9d8ecf 100644 --- a/src/Ws/WsRxWriter.cs +++ b/src/Ws/WsRxConsumer.cs @@ -8,37 +8,39 @@ namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -public sealed class WsRxWriter : IDisposable { +public struct WsRxConsumer : IDisposable { private readonly ClientWebSocket _ws; private readonly ChannelReader _in; private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsRxWriter(ClientWebSocket ws, ChannelReader @in) { + private readonly int _blockSize; + + public WsRxConsumer(ClientWebSocket ws, ChannelReader @in, int blockSize) { _ws = ws; _in = @in; + _blockSize = blockSize; } - private static async Task Execute(ClientWebSocket output, ChannelReader input, CancellationToken ct) { + private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - var reader = await input.ReadAsync(ct).Inv(); + using var reader = await _in.ReadAsync(ct).Inv(); bool isFinalBlock = false; while (!isFinalBlock && !ct.IsCancellationRequested) { - var rom = await reader.ReadAsync(BufferStreamReader.BUFFER_SIZE, ct).Inv(); - isFinalBlock = rom.Length != BufferStreamReader.BUFFER_SIZE; - await output.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); + var rom = await reader.ReadAsync(_blockSize, ct).Inv(); + isFinalBlock = rom.Length != _blockSize; + await _ws.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); } if (!isFinalBlock) { // ensure that the message is always terminated // no not pass a CancellationToken - await output.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); + await _ws.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); } - await reader.DisposeAsync().Inv(); ct.ThrowIfCancellationRequested(); } } @@ -50,7 +52,7 @@ public void Open() { lock (_lock) { ThrowIfConnected(); _cts = new(); - _execute = Execute(_ws, _in, _cts.Token); + _execute = Execute(_cts.Token); } } diff --git a/src/Ws/WsRxProducer.cs b/src/Ws/WsRxProducer.cs new file mode 100644 index 00000000..6d32af8c --- /dev/null +++ b/src/Ws/WsRxProducer.cs @@ -0,0 +1,25 @@ +using System.Threading.Channels; + +using SurrealDB.Common; + +namespace SurrealDB.Ws; + +public readonly struct WsRxProducer : IDisposable { + private readonly ChannelWriter _channel; + private readonly int _bufferSize; + + public WsRxProducer(ChannelWriter channel, int bufferSize) { + _channel = channel; + _bufferSize = bufferSize; + } + + public async Task SendAsync(Stream stream) { + // reader is disposed by the consumber + BufferStreamReader reader = new(stream, _bufferSize); + await _channel.WriteAsync(reader); + } + + public void Dispose() { + _channel.Complete(); + } +} diff --git a/src/Ws/WsTxClient.cs b/src/Ws/WsTxConsumer.cs similarity index 85% rename from src/Ws/WsTxClient.cs rename to src/Ws/WsTxConsumer.cs index 9c07006d..20fff9da 100644 --- a/src/Ws/WsTxClient.cs +++ b/src/Ws/WsTxConsumer.cs @@ -11,18 +11,16 @@ namespace SurrealDB.Ws; -/// Listens for s and dispatches them by their headers to different s. -internal sealed class WsTxClient : IDisposable { +/// Listens for s and dispatches them by their headers to different s. +internal struct WsTxConsumer : IDisposable { private readonly ChannelReader _in; - private readonly WsTxReader _tx; private readonly ConcurrentDictionary _handlers = new(); private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsTxClient(ClientWebSocket ws, Channel channel, RecyclableMemoryStreamManager memoryManager, int maxHeaderBytes) { - _in = channel.Reader; - _tx = new(ws, channel.Writer, memoryManager); + public WsTxConsumer(ChannelReader channel, int maxHeaderBytes) { + _in = channel; MaxHeaderBytes = maxHeaderBytes; } @@ -93,11 +91,9 @@ public void Open() { _cts = new(); _execute = Execute(_cts.Token); } - _tx.Open(); } public async Task Close() { - await _tx.Close().Inv(); Task task; lock (_lock) { ThrowIfDisconnected(); @@ -110,21 +106,18 @@ public async Task Close() { [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] private void ThrowIfDisconnected() { - Debug.Assert(_tx.Connected == Connected); if (!Connected) { throw new InvalidOperationException("The connection is not open."); } } private void ThrowIfConnected() { - Debug.Assert(_tx.Connected == Connected); if (Connected) { throw new InvalidOperationException("The connection is already open"); } } public void Dispose() { - _tx.Dispose(); _cts?.Cancel(); _cts?.Dispose(); _execute?.Dispose(); diff --git a/src/Ws/WsTxReader.cs b/src/Ws/WsTxProducer.cs similarity index 90% rename from src/Ws/WsTxReader.cs rename to src/Ws/WsTxProducer.cs index 094b4d75..dc66bd85 100644 --- a/src/Ws/WsTxReader.cs +++ b/src/Ws/WsTxProducer.cs @@ -11,7 +11,7 @@ namespace SurrealDB.Ws; /// Receives messages from a websocket server and passes them to a channel -public sealed class WsTxReader : IDisposable { +public struct WsTxProducer : IDisposable { private readonly ClientWebSocket _ws; private readonly ChannelWriter _out; private readonly RecyclableMemoryStreamManager _memoryManager; @@ -19,16 +19,19 @@ public sealed class WsTxReader : IDisposable { private CancellationTokenSource? _cts; private Task? _execute; - public WsTxReader(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager) { + private readonly int _blockSize; + + public WsTxProducer(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager, int blockSize) { _ws = ws; _out = @out; _memoryManager = memoryManager; + _blockSize = blockSize; } private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - var buffer = ArrayPool.Shared.Rent(BufferStreamReader.BUFFER_SIZE); + var buffer = ArrayPool.Shared.Rent(_blockSize); try { await ReceiveMessage(ct, buffer).Inv(); } finally { From a821b981c818de5c558dbce41ca8b571ccaafe7a Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:07:22 +0100 Subject: [PATCH 35/87] Fix unterminated recursion for Inv caused by regex replace ^^ --- src/Common/TaskExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/TaskExtensions.cs b/src/Common/TaskExtensions.cs index 998905a6..7d4a6f4c 100644 --- a/src/Common/TaskExtensions.cs +++ b/src/Common/TaskExtensions.cs @@ -6,17 +6,17 @@ namespace SurrealDB.Common; public static class TaskExtensions { /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredTaskAwaitable Inv(this Task t) => t.Inv(); + public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredTaskAwaitable Inv(this Task t) => t.Inv(); + public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.Inv(); + public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); /// The task is invariant. /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.Inv(); + public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); } From bff795b5cd820e9411c3f89259610f73e03fb2b0 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:11:21 +0100 Subject: [PATCH 36/87] Inline base class DatabaseTestBase --- tests/Driver.Tests/DatabaseTests.cs | 133 ++++++++++++---------------- 1 file changed, 59 insertions(+), 74 deletions(-) diff --git a/tests/Driver.Tests/DatabaseTests.cs b/tests/Driver.Tests/DatabaseTests.cs index 4b3e91b4..01a4b831 100644 --- a/tests/Driver.Tests/DatabaseTests.cs +++ b/tests/Driver.Tests/DatabaseTests.cs @@ -10,99 +10,84 @@ public sealed class RestDatabaseTest : DatabaseTestDriver { [Collection("SurrealDBRequired")] public abstract class DatabaseTestDriver - : DriverBase where T : IDatabase, IDisposable, new() { - protected override async Task Run(T db) { - db.GetConfig().Should().BeEquivalentTo(TestHelper.Default); - - var useResp = await db.Use(TestHelper.Database, TestHelper.Namespace); - TestHelper.AssertOk(useResp); - var infoResp = await db.Info(); - TestHelper.AssertOk(infoResp); - - var signInStatus = await db.Signin(new RootAuth(TestHelper.User, TestHelper.Pass)); - - TestHelper.AssertOk(signInStatus); - //AssertOk(await db.Invalidate()); - - (string id1, string id2) = ("id1", "id2"); - var res1 = await db.Create( - "person", - new { - Title = "Founder & CEO", - Name = new { First = "Tobie", Last = "Morgan Hitchcock", }, - Marketing = true, - Identifier = ThreadRng.Shared.Next(), - } - ); - - TestHelper.AssertOk(res1); + [Fact] + public async Task TestSuite() => await DbHandle.WithDatabase( + async db => { + db.GetConfig().Should().BeEquivalentTo(TestHelper.Default); - var res2 = await db.Create( - "person", - new { - Title = "Contributor", - Name = new { First = "Prophet", Last = "Lamb", }, - Marketing = false, - Identifier = ThreadRng.Shared.Next(), - } - ); + var useResp = await db.Use(TestHelper.Database, TestHelper.Namespace); + TestHelper.AssertOk(useResp); + var infoResp = await db.Info(); + TestHelper.AssertOk(infoResp); - TestHelper.AssertOk(res2); + var signInStatus = await db.Signin(new RootAuth(TestHelper.User, TestHelper.Pass)); - Thing thing2 = Thing.From("person", id2); - TestHelper.AssertOk(await db.Update(thing2, new { Marketing = false, })); + TestHelper.AssertOk(signInStatus); + //AssertOk(await db.Invalidate()); - TestHelper.AssertOk(await db.Select(thing2)); + (string id1, string id2) = ("id1", "id2"); + var res1 = await db.Create( + "person", + new { + Title = "Founder & CEO", + Name = new { First = "Tobie", Last = "Morgan Hitchcock", }, + Marketing = true, + Identifier = ThreadRng.Shared.Next(), + } + ); - TestHelper.AssertOk(await db.Delete(thing2)); + TestHelper.AssertOk(res1); - Thing thing1 = Thing.From("person", id1); - TestHelper.AssertOk( - await db.Change( - thing1, + var res2 = await db.Create( + "person", new { - Title = "Founder & CEO", - Name = new { First = "Tobie", Last = "Hitchcock Morgan", }, + Title = "Contributor", + Name = new { First = "Prophet", Last = "Lamb", }, Marketing = false, Identifier = ThreadRng.Shared.Next(), } - ) - ); + ); - string newTitle = "Founder & CEO & Ruler of the known free World"; - var modifyResp = await db.Modify(thing1, new[] { - Patch.Replace("/Title", newTitle), - }); - TestHelper.AssertOk(modifyResp); + TestHelper.AssertOk(res2); - TestHelper.AssertOk(await db.Let("tbl", "person")); + Thing thing2 = Thing.From("person", id2); + TestHelper.AssertOk(await db.Update(thing2, new { Marketing = false, })); - var queryResp = await db.Query( - "SELECT $props FROM $tbl WHERE title = $title", - new Dictionary { ["props"] = "title, identifier", ["tbl"] = "person", ["title"] = newTitle, } - ); + TestHelper.AssertOk(await db.Select(thing2)); - TestHelper.AssertOk(queryResp); + TestHelper.AssertOk(await db.Delete(thing2)); - await db.Close(); - } -} + Thing thing1 = Thing.From("person", id1); + TestHelper.AssertOk( + await db.Change( + thing1, + new { + Title = "Founder & CEO", + Name = new { First = "Tobie", Last = "Hitchcock Morgan", }, + Marketing = false, + Identifier = ThreadRng.Shared.Next(), + } + ) + ); -/// -/// The test driver executes the testsuite on the client. -/// -[Collection("SurrealDBRequired")] -public abstract class DriverBase - where T : IDatabase, IDisposable, new() { + string newTitle = "Founder & CEO & Ruler of the known free World"; + var modifyResp = await db.Modify(thing1, new[] { Patch.Replace("/Title", newTitle), }); + TestHelper.AssertOk(modifyResp); + TestHelper.AssertOk(await db.Let("tbl", "person")); - [Fact] - public async Task TestSuite() { - using var handle = await DbHandle.Create(); - await Run(handle.Database); - } + var queryResp = await db.Query( + "SELECT $props FROM $tbl WHERE title = $title", + new Dictionary { + ["props"] = "title, identifier", ["tbl"] = "person", ["title"] = newTitle, + } + ); + + TestHelper.AssertOk(queryResp); - protected abstract Task Run(T db); + await db.Close(); + } + ); } From abcac4ce805ad910196ce25e4d666536acabfc34 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:11:42 +0100 Subject: [PATCH 37/87] Initialize MemoryManager for default WsClientOptions --- src/Ws/WsClientOptions.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index f2a45fe5..4f0afbff 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -84,5 +84,11 @@ public void ValidateAndMakeReadonly() { } } - internal static WsClientOptions Default => new(); + public static WsClientOptions Default { get; } = CreateDefault(); + + private static WsClientOptions CreateDefault() { + WsClientOptions o = new() { MemoryManager = new(), }; + o.ValidateAndMakeReadonly(); + return o; + } } From 8d762226f10972b23272659a08afb92e85d48e63 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:11:49 +0100 Subject: [PATCH 38/87] Refactoring --- src/Ws/WsRxProducer.cs | 2 +- src/Ws/WsTxConsumer.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Ws/WsRxProducer.cs b/src/Ws/WsRxProducer.cs index 6d32af8c..c535108c 100644 --- a/src/Ws/WsRxProducer.cs +++ b/src/Ws/WsRxProducer.cs @@ -14,7 +14,7 @@ public WsRxProducer(ChannelWriter channel, int bufferSize) { } public async Task SendAsync(Stream stream) { - // reader is disposed by the consumber + // reader is disposed by the consumer BufferStreamReader reader = new(stream, _bufferSize); await _channel.WriteAsync(reader); } diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 20fff9da..80fe4245 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -1,12 +1,8 @@ -using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; using System.Threading.Channels; -using Microsoft.IO; - using SurrealDB.Common; namespace SurrealDB.Ws; From 12bef01f9361bfc74eb543da4fd7a70cb830e711 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:15:19 +0100 Subject: [PATCH 39/87] Prevent premature disposing of input stream --- src/Ws/WsClient.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index ac7bc7ee..e07b09b0 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -89,10 +89,9 @@ public async Task Send(Request req, CancellationToken ct = default) { return default; } // send request - await using (var stream = await SerializeAsync(req, ct).Inv()) { - await _rxProducer.SendAsync(stream); - } - // await response, dispose message when done + var stream = await SerializeAsync(req, ct).Inv(); + await _rxProducer.SendAsync(stream); + // await response, dispose message when done using var response = await handler.Task.Inv(); // validate header var responseHeader = response.Header.Response; From 6dc740d8ffc0123943972b1f387f5f6abe5ea3ea Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:40:03 +0100 Subject: [PATCH 40/87] Extract producer/consumer functionality into separate method --- src/Ws/WsMessageReader.cs | 7 ++--- src/Ws/WsRxConsumer.cs | 30 +++++++++++--------- src/Ws/WsTxConsumer.cs | 59 +++++++++++++++++++++------------------ src/Ws/WsTxProducer.cs | 11 ++++---- 4 files changed, 58 insertions(+), 49 deletions(-) diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsMessageReader.cs index ee0f790c..4657e2d3 100644 --- a/src/Ws/WsMessageReader.cs +++ b/src/Ws/WsMessageReader.cs @@ -25,9 +25,8 @@ protected override void Dispose(bool disposing) { return; } - lock (_stream) { - _stream.Dispose(); - } + Interlocked.MemoryBarrierProcessWide(); + _stream.Dispose(); } private async ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { @@ -54,7 +53,7 @@ internal ValueTask WriteResultAsync(ReadOnlyMemory buffer, WebSocketReceiv return SetReceivedAsync(result, ct); } -#region Stream member +#region Stream members public override bool CanRead => true; public override bool CanSeek => true; diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 3b9d8ecf..91467183 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -26,22 +26,26 @@ public WsRxConsumer(ClientWebSocket ws, ChannelReader @in, i private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - using var reader = await _in.ReadAsync(ct).Inv(); + ThrowIfDisconnected(); + await Consume(ct).Inv(); + ct.ThrowIfCancellationRequested(); + } + } - bool isFinalBlock = false; - while (!isFinalBlock && !ct.IsCancellationRequested) { - var rom = await reader.ReadAsync(_blockSize, ct).Inv(); - isFinalBlock = rom.Length != _blockSize; - await _ws.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); - } + private async Task Consume(CancellationToken ct) { + using var reader = await _in.ReadAsync(ct).Inv(); - if (!isFinalBlock) { - // ensure that the message is always terminated - // no not pass a CancellationToken - await _ws.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); - } + bool isFinalBlock = false; + while (!isFinalBlock && !ct.IsCancellationRequested) { + var rom = await reader.ReadAsync(_blockSize, ct).Inv(); + isFinalBlock = rom.Length != _blockSize; + await _ws.SendAsync(rom, WebSocketMessageType.Text, isFinalBlock, ct).Inv(); + } - ct.ThrowIfCancellationRequested(); + if (!isFinalBlock) { + // ensure that the message is always terminated + // no not pass a CancellationToken + await _ws.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); } } diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 80fe4245..c708b6b4 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -30,37 +30,42 @@ private async Task Execute(CancellationToken ct) { while (!ct.IsCancellationRequested) { ThrowIfDisconnected(); - var message = await _in.ReadAsync(ct).Inv(); - - // receive the first part of the message - var result = await message.ReceiveAsync(ct).Inv(); - // parse the header from the message - WsHeader header = PeekHeader(message, result.Count); - - // find the handler - string? id = header.Id; - if (id is null || !_handlers.TryGetValue(id, out var handler)) { - // invalid format, or no registered -> discard message - await message.DisposeAsync().Inv(); - return; - } - - // dispatch the message to the handler - try { - handler.Dispatch(new(header, message)); - } catch (OperationCanceledException) { - // handler is canceled -> unregister - Unregister(handler.Id); - } - - if (!handler.Persistent) { - // handler is only used once -> unregister - Unregister(handler.Id); - } + await Consume(ct).Inv(); + ct.ThrowIfCancellationRequested(); } } + private async Task Consume(CancellationToken ct) { + var message = await _in.ReadAsync(ct).Inv(); + + // receive the first part of the message + var result = await message.ReceiveAsync(ct).Inv(); + // parse the header from the message + WsHeader header = PeekHeader(message, result.Count); + + // find the handler + string? id = header.Id; + if (id is null || !_handlers.TryGetValue(id, out var handler)) { + // invalid format, or no registered -> discard message + await message.DisposeAsync().Inv(); + return; + } + + // dispatch the message to the handler + try { + handler.Dispatch(new(header, message)); + } catch (OperationCanceledException) { + // handler is canceled -> unregister + Unregister(handler.Id); + } + + if (!handler.Persistent) { + // handler is only used once -> unregister + Unregister(handler.Id); + } + } + private WsHeader PeekHeader(Stream stream, int seekLength) { Span bytes = stackalloc byte[Math.Min(MaxHeaderBytes, seekLength)]; int read = stream.Read(bytes); diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index dc66bd85..f1e95bdc 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -31,9 +31,10 @@ public WsTxProducer(ClientWebSocket ws, ChannelWriter @out, Rec private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { + ThrowIfDisconnected(); var buffer = ArrayPool.Shared.Rent(_blockSize); try { - await ReceiveMessage(ct, buffer).Inv(); + await Produce(ct, buffer).Inv(); } finally { ArrayPool.Shared.Return(buffer); } @@ -41,27 +42,27 @@ private async Task Execute(CancellationToken ct) { } } - private async Task ReceiveMessage(CancellationToken ct, byte[] buffer) { + private async Task Produce(CancellationToken ct, byte[] buffer) { // receive the first part var result = await _ws.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsMessageReader msg = new(new RecyclableMemoryStream(_memoryManager)); // begin adding the message to the output - var writeOutput = _out.WriteAsync(msg, ct); + await _out.WriteAsync(msg, ct).Inv(); await msg.WriteResultAsync(buffer, result, ct).Inv(); while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts result = await _ws.ReceiveAsync(buffer, ct).Inv(); - msg.WriteResultAsync(buffer, result, ct).Inv(); + await msg.WriteResultAsync(buffer, result, ct).Inv(); ct.ThrowIfCancellationRequested(); } // finish adding the message to the output - await writeOutput.Inv(); + //await writeOutput; } From eb49c68338714147b91d9549fdb3ade0c4833723 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 15:47:38 +0100 Subject: [PATCH 41/87] Docs --- src/Common/TaskExtensions.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Common/TaskExtensions.cs b/src/Common/TaskExtensions.cs index 7d4a6f4c..ed93b743 100644 --- a/src/Common/TaskExtensions.cs +++ b/src/Common/TaskExtensions.cs @@ -4,19 +4,19 @@ namespace SurrealDB.Common; /// Extension methods for Tasks public static class TaskExtensions { - /// The task is invariant. + /// The task is invariant. + /// The previous context is not restored upon completion. /// Equivalent to Task.ConfigureAwait(false). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); - /// The task is invariant. - /// Equivalent to Task.ConfigureAwait(false). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ConfiguredTaskAwaitable Inv(this Task t) => t.ConfigureAwait(false); - - /// The task is invariant. - /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); - - /// The task is invariant. - /// Equivalent to Task.ConfigureAwait(false). - public static ConfiguredValueTaskAwaitable Inv(this ValueTask t) => t.ConfigureAwait(false); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ConfiguredValueTaskAwaitable Inv(in this ValueTask t) => t.ConfigureAwait(false); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ConfiguredValueTaskAwaitable Inv(in this ValueTask t) => t.ConfigureAwait(false); } From 9e29cd5eb8471280aeed3a86037a9a674e14d16c Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 16:02:35 +0100 Subject: [PATCH 42/87] Reorder open, close and dispose --- src/Ws/WsClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index e07b09b0..e1d64838 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -51,9 +51,9 @@ public WsClient(WsClientOptions options) { public async Task OpenAsync(Uri url, CancellationToken ct = default) { ThrowIfConnected(); await _ws.ConnectAsync(url, ct).Inv(); - _rxConsumer.Open(); _txConsumer.Open(); _txProducer.Open(); + _rxConsumer.Open(); } /// @@ -61,17 +61,17 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { /// public async Task CloseAsync(CancellationToken ct = default) { await _ws.CloseAsync(WebSocketCloseStatus.Empty, "client connection closed orderly", ct).Inv(); - await _rxConsumer.Close().Inv(); await _txProducer.Close().Inv(); await _txConsumer.Close().Inv(); + await _rxConsumer.Close().Inv(); } /// public void Dispose() { _rxProducer.Dispose(); _rxConsumer.Dispose(); - _txConsumer.Dispose(); _txProducer.Dispose(); + _txConsumer.Dispose(); _ws.Dispose(); } From bbff464f40aa9aa0d9e02069b8d18dbbc4a5624d Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 16:18:09 +0100 Subject: [PATCH 43/87] Fix bug where AppendResultAsync would also read --- src/Ws/WsMessageReader.cs | 14 ++++++++++---- src/Ws/WsRxConsumer.cs | 5 +++-- src/Ws/WsTxConsumer.cs | 5 +++-- src/Ws/WsTxProducer.cs | 9 +++++---- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsMessageReader.cs index 4657e2d3..d30ec971 100644 --- a/src/Ws/WsMessageReader.cs +++ b/src/Ws/WsMessageReader.cs @@ -18,7 +18,7 @@ internal WsMessageReader(MemoryStream stream) { _endOfMessage = 0; } - public bool IsEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; + public bool HasReceivedEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; protected override void Dispose(bool disposing) { if (!disposing) { @@ -41,13 +41,15 @@ public ValueTask ReceiveAsync(CancellationToken ct) { } public WebSocketReceiveResult Receive(CancellationToken ct) { - return ReceiveAsync(ct).Inv().GetAwaiter().GetResult(); + return ReceiveAsync(ct).Result; } - internal ValueTask WriteResultAsync(ReadOnlyMemory buffer, WebSocketReceiveResult result, CancellationToken ct) { + internal ValueTask AppendResultAsync(ReadOnlyMemory buffer, WebSocketReceiveResult result, CancellationToken ct) { ReadOnlySpan span = buffer.Span.Slice(0, result.Count); lock (_stream) { + var pos = _stream.Position; _stream.Write(span); + _stream.Position = pos; } return SetReceivedAsync(result, ct); @@ -96,6 +98,10 @@ private int Read(Span buffer, CancellationToken ct) { read = _stream.Read(buffer); } + if (read == buffer.Length || HasReceivedEndOfMessage) { + return read; + } + while (true) { var result = Receive(ct); int inc; @@ -130,7 +136,7 @@ private ValueTask ReadInternalAsync(Memory buffer, CancellationToken read = _stream.Read(buffer.Span); } - if (read == buffer.Length || IsEndOfMessage) { + if (read == buffer.Length || HasReceivedEndOfMessage) { return new(read); } diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 91467183..15e958ed 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -26,7 +26,6 @@ public WsRxConsumer(ClientWebSocket ws, ChannelReader @in, i private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - ThrowIfDisconnected(); await Consume(ct).Inv(); ct.ThrowIfCancellationRequested(); } @@ -64,8 +63,10 @@ public Task Close() { Task task; lock (_lock) { ThrowIfDisconnected(); - _cts.Cancel(); task = _execute; + _cts.Cancel(); + _cts.Dispose(); // not relly needed here + _cts = null; _execute = null; } return task; diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index c708b6b4..343596fe 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -29,7 +29,6 @@ private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - ThrowIfDisconnected(); await Consume(ct).Inv(); ct.ThrowIfCancellationRequested(); @@ -98,8 +97,10 @@ public async Task Close() { Task task; lock (_lock) { ThrowIfDisconnected(); - _cts.Cancel(); task = _execute; + _cts.Cancel(); + _cts.Dispose(); // not relly needed here + _cts = null; _execute = null; } await task.Inv(); diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index f1e95bdc..d0325383 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -31,7 +31,6 @@ public WsTxProducer(ClientWebSocket ws, ChannelWriter @out, Rec private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { - ThrowIfDisconnected(); var buffer = ArrayPool.Shared.Rent(_blockSize); try { await Produce(ct, buffer).Inv(); @@ -51,12 +50,12 @@ private async Task Produce(CancellationToken ct, byte[] buffer) { // begin adding the message to the output await _out.WriteAsync(msg, ct).Inv(); - await msg.WriteResultAsync(buffer, result, ct).Inv(); + await msg.AppendResultAsync(buffer, result, ct).Inv(); while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts result = await _ws.ReceiveAsync(buffer, ct).Inv(); - await msg.WriteResultAsync(buffer, result, ct).Inv(); + await msg.AppendResultAsync(buffer, result, ct).Inv(); ct.ThrowIfCancellationRequested(); } @@ -81,8 +80,10 @@ public Task Close() { Task task; lock (_lock) { ThrowIfDisconnected(); - _cts.Cancel(); task = _execute; + _cts.Cancel(); + _cts.Dispose(); // not relly needed here + _cts = null; _execute = null; } return task; From 66eeb4601c6ffd53e51b1cdbefe74afe1efbbac5 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 16:23:12 +0100 Subject: [PATCH 44/87] Do not dispose Tasks (that should really be Threads) --- src/Ws/WsRxConsumer.cs | 1 - src/Ws/WsTxConsumer.cs | 1 - src/Ws/WsTxProducer.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 15e958ed..7231b896 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -87,6 +87,5 @@ private void ThrowIfConnected() { public void Dispose() { _cts?.Dispose(); - _execute?.Dispose(); } } diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 343596fe..f074911b 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -122,6 +122,5 @@ private void ThrowIfConnected() { public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); - _execute?.Dispose(); } } diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index d0325383..790cd08d 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -104,7 +104,6 @@ private void ThrowIfConnected() { public void Dispose() { _cts?.Dispose(); - _execute?.Dispose(); _out.Complete(); } } From 89b28d22c25174f761b4ac73f50acdb79324bb11 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 16:32:59 +0100 Subject: [PATCH 45/87] Reorder close --- src/Ws/WsClient.cs | 4 ++-- src/Ws/WsRxConsumer.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index e1d64838..30a98663 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -60,10 +60,10 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { /// Closes the connection to the Surreal server. /// public async Task CloseAsync(CancellationToken ct = default) { - await _ws.CloseAsync(WebSocketCloseStatus.Empty, "client connection closed orderly", ct).Inv(); + await _rxConsumer.Close().Inv(); await _txProducer.Close().Inv(); await _txConsumer.Close().Inv(); - await _rxConsumer.Close().Inv(); + await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); } /// diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 7231b896..cf0210ff 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -69,6 +69,7 @@ public Task Close() { _cts = null; _execute = null; } + return task; } From 11fa24647abcc629526c2376f0141f15a20ad450 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 16:47:37 +0100 Subject: [PATCH 46/87] Handle OperationCanceledException from long running task when Closing --- src/Ws/WsClient.cs | 3 ++- src/Ws/WsRxConsumer.cs | 11 +++++++---- src/Ws/WsTxConsumer.cs | 11 +++++++---- src/Ws/WsTxProducer.cs | 14 +++++++++----- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 30a98663..0909ca39 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -60,10 +60,11 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { /// Closes the connection to the Surreal server. /// public async Task CloseAsync(CancellationToken ct = default) { + ThrowIfDisconnected(); + await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); await _rxConsumer.Close().Inv(); await _txProducer.Close().Inv(); await _txConsumer.Close().Inv(); - await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); } /// diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index cf0210ff..38b7c5fb 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -27,7 +27,6 @@ private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { await Consume(ct).Inv(); - ct.ThrowIfCancellationRequested(); } } @@ -59,10 +58,10 @@ public void Open() { } } - public Task Close() { + public async Task Close() { + ThrowIfDisconnected(); Task task; lock (_lock) { - ThrowIfDisconnected(); task = _execute; _cts.Cancel(); _cts.Dispose(); // not relly needed here @@ -70,7 +69,11 @@ public Task Close() { _execute = null; } - return task; + try { + await task.Inv(); + } catch (OperationCanceledException) { + // expected on close using cts + } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index f074911b..995b6ae5 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -30,8 +30,6 @@ private async Task Execute(CancellationToken ct) { while (!ct.IsCancellationRequested) { await Consume(ct).Inv(); - - ct.ThrowIfCancellationRequested(); } } @@ -94,16 +92,21 @@ public void Open() { } public async Task Close() { + ThrowIfDisconnected(); Task task; lock (_lock) { - ThrowIfDisconnected(); task = _execute; _cts.Cancel(); _cts.Dispose(); // not relly needed here _cts = null; _execute = null; } - await task.Inv(); + + try { + await task.Inv(); + } catch (OperationCanceledException) { + // expected on close using cts + } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index 790cd08d..b8eb4ce0 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -37,7 +37,6 @@ private async Task Execute(CancellationToken ct) { } finally { ArrayPool.Shared.Return(buffer); } - ct.ThrowIfCancellationRequested(); } } @@ -56,8 +55,6 @@ private async Task Produce(CancellationToken ct, byte[] buffer) { // receive more parts result = await _ws.ReceiveAsync(buffer, ct).Inv(); await msg.AppendResultAsync(buffer, result, ct).Inv(); - - ct.ThrowIfCancellationRequested(); } // finish adding the message to the output @@ -76,7 +73,7 @@ public void Open() { } } - public Task Close() { + public async Task Close() { Task task; lock (_lock) { ThrowIfDisconnected(); @@ -86,7 +83,14 @@ public Task Close() { _cts = null; _execute = null; } - return task; + + try { + await task.Inv(); + } catch (OperationCanceledException) { + // expected on close using cts + } catch (WebSocketException) { + // expected when the socket is closed before the receiver + } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] From ecc3dd5e106b0f50018e0dd23f3df1a8d877a5ea Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 17:11:10 +0100 Subject: [PATCH 47/87] Make cons/prod classes, bc of dispose cow --- src/Ws/WsClient.cs | 12 ++++++------ src/Ws/WsRxConsumer.cs | 2 +- src/Ws/WsRxProducer.cs | 2 +- src/Ws/WsTxConsumer.cs | 4 +++- src/Ws/WsTxProducer.cs | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 0909ca39..96d02973 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -19,9 +19,9 @@ public sealed class WsClient : IDisposable { private readonly ClientWebSocket _ws = new(); private readonly RecyclableMemoryStreamManager _memoryManager; private readonly WsRxProducer _rxProducer; - private WsRxConsumer _rxConsumer; - private WsTxConsumer _txConsumer; - private WsTxProducer _txProducer; + private readonly WsRxConsumer _rxConsumer; + private readonly WsTxConsumer _txConsumer; + private readonly WsTxProducer _txProducer; private readonly int _idBytes; @@ -63,16 +63,16 @@ public async Task CloseAsync(CancellationToken ct = default) { ThrowIfDisconnected(); await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); await _rxConsumer.Close().Inv(); - await _txProducer.Close().Inv(); await _txConsumer.Close().Inv(); + await _txProducer.Close().Inv(); } /// public void Dispose() { - _rxProducer.Dispose(); _rxConsumer.Dispose(); - _txProducer.Dispose(); + _rxProducer.Dispose(); _txConsumer.Dispose(); + _txProducer.Dispose(); _ws.Dispose(); } diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 38b7c5fb..49001c08 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -8,7 +8,7 @@ namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -public struct WsRxConsumer : IDisposable { +public sealed class WsRxConsumer : IDisposable { private readonly ClientWebSocket _ws; private readonly ChannelReader _in; private readonly object _lock = new(); diff --git a/src/Ws/WsRxProducer.cs b/src/Ws/WsRxProducer.cs index c535108c..a1fe2860 100644 --- a/src/Ws/WsRxProducer.cs +++ b/src/Ws/WsRxProducer.cs @@ -4,7 +4,7 @@ namespace SurrealDB.Ws; -public readonly struct WsRxProducer : IDisposable { +public sealed class WsRxProducer : IDisposable { private readonly ChannelWriter _channel; private readonly int _bufferSize; diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 995b6ae5..c431ce32 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -8,7 +8,7 @@ namespace SurrealDB.Ws; /// Listens for s and dispatches them by their headers to different s. -internal struct WsTxConsumer : IDisposable { +internal sealed class WsTxConsumer : IDisposable { private readonly ChannelReader _in; private readonly ConcurrentDictionary _handlers = new(); private readonly object _lock = new(); @@ -38,6 +38,8 @@ private async Task Consume(CancellationToken ct) { // receive the first part of the message var result = await message.ReceiveAsync(ct).Inv(); + // throw if the result is a close ack + result.ThrowCancelIfClosed(); // parse the header from the message WsHeader header = PeekHeader(message, result.Count); diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index b8eb4ce0..e4195550 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -11,7 +11,7 @@ namespace SurrealDB.Ws; /// Receives messages from a websocket server and passes them to a channel -public struct WsTxProducer : IDisposable { +public sealed class WsTxProducer : IDisposable { private readonly ClientWebSocket _ws; private readonly ChannelWriter _out; private readonly RecyclableMemoryStreamManager _memoryManager; From 622e037f04ec260e6dd633bf094257bd34e71927 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 17:11:32 +0100 Subject: [PATCH 48/87] Add WebsocketReceiveResult.ThrowCancelIfClosed --- src/Common/WebSocketExtensions.cs | 22 ++++++++++++++++++++++ src/Ws/WsTxConsumer.cs | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/Common/WebSocketExtensions.cs diff --git a/src/Common/WebSocketExtensions.cs b/src/Common/WebSocketExtensions.cs new file mode 100644 index 00000000..e18ca7fa --- /dev/null +++ b/src/Common/WebSocketExtensions.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; + +namespace SurrealDB.Common; + +internal static class WebSocketExtensions { + /// Throws a if the result is a close ack. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfClose(this WebSocketReceiveResult result) { + if (result.CloseStatus is not null) { + ThrowConnectionClosed(); + } + } + + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowConnectionClosed() { + throw new OperationCanceledException("Connection closed"); + } +} diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index c431ce32..e90c759d 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -39,7 +39,7 @@ private async Task Consume(CancellationToken ct) { // receive the first part of the message var result = await message.ReceiveAsync(ct).Inv(); // throw if the result is a close ack - result.ThrowCancelIfClosed(); + result.ThrowIfClose(); // parse the header from the message WsHeader header = PeekHeader(message, result.Count); From 2419a50e088398102f10401c280189b82ed41dcc Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 17:16:00 +0100 Subject: [PATCH 49/87] Mark throw methods correctly as [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] --- src/Common/LatentReadonly.cs | 3 ++- src/Common/ValidateReadonly.cs | 3 ++- src/Common/WsStream.cs | 6 ++++-- src/Driver/Rpc/RpcClientExtensions.cs | 4 +++- src/Json/Time/DateOnlyConv.cs | 12 +++++++----- src/Json/Time/DateTimeConv.cs | 6 ++++-- src/Json/Time/DateTimeOffsetConv.cs | 8 +++++--- src/Json/Time/TimeOnlyConv.cs | 8 +++++--- src/Json/Time/TimeSpanConv.cs | 8 +++++--- src/Models/Result/ResultContentException.cs | 10 ++++++---- src/Models/Result/ResultValue.cs | 4 ++-- src/Ws/WsClient.cs | 6 ++++-- src/Ws/WsMessageReader.cs | 2 +- 13 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/Common/LatentReadonly.cs b/src/Common/LatentReadonly.cs index dc092a67..1405a50e 100644 --- a/src/Common/LatentReadonly.cs +++ b/src/Common/LatentReadonly.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -26,7 +27,7 @@ private void ThrowIfReadonly() { } } - [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowReadonly() { throw new InvalidOperationException("The object is readonly and cannot be mutated."); } diff --git a/src/Common/ValidateReadonly.cs b/src/Common/ValidateReadonly.cs index 9374b86f..9459e2c9 100644 --- a/src/Common/ValidateReadonly.cs +++ b/src/Common/ValidateReadonly.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.Serialization; @@ -67,7 +68,7 @@ public override string ToString() { return sb.ToString(); } - [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] public static void Throw(string? message, (string, string)[]? errors = null, Exception? inner = default) { throw new AggregatePropertyValidationException(message, errors, inner); } diff --git a/src/Common/WsStream.cs b/src/Common/WsStream.cs index ba467496..1ffda8aa 100644 --- a/src/Common/WsStream.cs +++ b/src/Common/WsStream.cs @@ -1,6 +1,8 @@ using System.Buffers; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; +using System.Runtime.CompilerServices; namespace SurrealDB.Common; @@ -134,12 +136,12 @@ public override async ValueTask DisposeAsync() { DisposePrefix(); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowWriteDisallowed() { throw new InvalidOperationException("Cannot write a readonly stream"); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static long ThrowSeekDisallowed() { throw new InvalidOperationException("Cannot seek in the stream"); } diff --git a/src/Driver/Rpc/RpcClientExtensions.cs b/src/Driver/Rpc/RpcClientExtensions.cs index 21e7f892..6bc69691 100644 --- a/src/Driver/Rpc/RpcClientExtensions.cs +++ b/src/Driver/Rpc/RpcClientExtensions.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using SurrealDB.Common; @@ -89,7 +91,7 @@ private static DriverResponse FromNestedStatus(in WsClient.Response rsp) { return DriverResponse.FromOwned(builder.AsSegment()); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowIdMissing() { throw new InvalidOperationException("Response does not have an id."); } diff --git a/src/Json/Time/DateOnlyConv.cs b/src/Json/Time/DateOnlyConv.cs index 394093a6..b9331309 100644 --- a/src/Json/Time/DateOnlyConv.cs +++ b/src/Json/Time/DateOnlyConv.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; @@ -44,14 +46,14 @@ public static bool TryParse(string? s, out DateOnly value) { public static string ToString(in DateOnly value) { return $"{value.Year.ToString("D4")}-{value.Month.ToString("D2")}-{value.Day.ToString("D2")}"; } - - [DoesNotReturn] + + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static DateOnly ThrowParseInvalid(string? s) { throw new ParseException($"Unable to parse DateOnly from `{s}`"); } - [DoesNotReturn] - private DateOnly ThrowJsonTokenTypeInvalid() { + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] + private static DateOnly ThrowJsonTokenTypeInvalid() { throw new JsonException("Cannot deserialize a non string token as a DateOnly."); } -} \ No newline at end of file +} diff --git a/src/Json/Time/DateTimeConv.cs b/src/Json/Time/DateTimeConv.cs index 43a500d5..09bc268b 100644 --- a/src/Json/Time/DateTimeConv.cs +++ b/src/Json/Time/DateTimeConv.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; @@ -46,8 +48,8 @@ public static string ToString(in DateTime value) { return value.ToString("O"); } - - [DoesNotReturn] + + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static DateTime ThrowJsonTokenTypeInvalid() { throw new JsonException("Cannot deserialize a non numeric non string token as a DateTime."); } diff --git a/src/Json/Time/DateTimeOffsetConv.cs b/src/Json/Time/DateTimeOffsetConv.cs index 0b93f4aa..041bf5a1 100644 --- a/src/Json/Time/DateTimeOffsetConv.cs +++ b/src/Json/Time/DateTimeOffsetConv.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; @@ -49,13 +51,13 @@ public static string ToString(in DateTimeOffset value) { return value.ToString("O"); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static DateTimeOffset ThrowParseInvalid(string? s) { throw new ParseException($"Unable to parse DateTimeOffset from `{s}`"); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static DateTimeOffset ThrowJsonTokenInvalid() { throw new JsonException("Cannot deserialize a non numeric non string token as a DateTime."); } -} \ No newline at end of file +} diff --git a/src/Json/Time/TimeOnlyConv.cs b/src/Json/Time/TimeOnlyConv.cs index ff91dfb0..c3219b5c 100644 --- a/src/Json/Time/TimeOnlyConv.cs +++ b/src/Json/Time/TimeOnlyConv.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; @@ -46,13 +48,13 @@ public static string ToString(in TimeOnly value) { return $"{value.Hour.ToString("D2")}:{value.Minute.ToString("D2")}:{value.Second.ToString("D2")}.{value.FractionString()}"; } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static TimeOnly ThrowParseInvalid(string? s) { throw new ParseException($"Unable to parse TimeOnly from `{s}`"); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private TimeOnly ThrowJsonTokenTypeInvalid() { throw new JsonException("Cannot deserialize a non string token as a TimeOnly."); } -} \ No newline at end of file +} diff --git a/src/Json/Time/TimeSpanConv.cs b/src/Json/Time/TimeSpanConv.cs index 238e3d46..0f101d4c 100644 --- a/src/Json/Time/TimeSpanConv.cs +++ b/src/Json/Time/TimeSpanConv.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -57,14 +59,14 @@ public static string ToString(in TimeSpan value) { return $"{value.Days}d{value.Hours}h{value.Minutes}m{value.Seconds}s{value.Milliseconds}ms"; } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static TimeSpan ThrowParseInvalid(string? s) { throw new ParseException($"Unable to parse TimeSpan from `{s}`"); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static TimeSpan ThrowJsonTokenTypeInvalid() { throw new JsonException("Cannot deserialize a non numeric non string token as a TimeSpan."); } -} \ No newline at end of file +} diff --git a/src/Models/Result/ResultContentException.cs b/src/Models/Result/ResultContentException.cs index 590147d3..0b3bb4f5 100644 --- a/src/Models/Result/ResultContentException.cs +++ b/src/Models/Result/ResultContentException.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.Serialization; namespace SurrealDB.Models.Result; @@ -16,15 +18,15 @@ public ResultContentException(string? message) : base(message) { public ResultContentException(string? message, Exception? innerException) : base(message, innerException) { } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] public static ErrorResult ExpectedAnyError() => throw new ResultContentException($"The {nameof(Result.DriverResponse)} does not contain any {nameof(ErrorResult)}"); - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] public static OkResult ExpectedAnyOk() => throw new ResultContentException($"The {nameof(Result.DriverResponse)} does not contain any {nameof(OkResult)}"); - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] public static ErrorResult ExpectedSingleError() => throw new ResultContentException($"The {nameof(Result.DriverResponse)} does not contain exactly one {nameof(ErrorResult)}"); - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] public static OkResult ExpectedSingleOk() => throw new ResultContentException($"The {nameof(Result.DriverResponse)} does not contain exactly one {nameof(OkResult)}"); } diff --git a/src/Models/Result/ResultValue.cs b/src/Models/Result/ResultValue.cs index f6353cab..ec53db0f 100644 --- a/src/Models/Result/ResultValue.cs +++ b/src/Models/Result/ResultValue.cs @@ -198,7 +198,7 @@ public Kind GetKind() { return Kind.None; } - [DoesNotReturn, DebuggerStepThrough,] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static ResultValue ThrowUnknownJsonValueKind(JsonElement json) { throw new ArgumentOutOfRangeException(nameof(json), json.ValueKind, "Unknown value kind."); } @@ -325,7 +325,7 @@ public override string ToString() { return Inner.ToString(); } - [DoesNotReturn, DebuggerStepThrough,] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static int ThrowInvalidCompareTypes() { throw new InvalidOperationException("Cannot compare SurrealResult of different types, if one or more is not numeric.."); } diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 96d02973..ab93c416 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Channels; @@ -139,12 +141,12 @@ private void ThrowIfConnected() { } } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowExpectResponseGotNotify() { throw new InvalidOperationException("Expected a response, got a notification"); } - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowInvalidResponse() { throw new InvalidOperationException("Invalid response"); } diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsMessageReader.cs index d30ec971..68dc56ef 100644 --- a/src/Ws/WsMessageReader.cs +++ b/src/Ws/WsMessageReader.cs @@ -186,7 +186,7 @@ public override void Write(byte[] buffer, int offset, int count) { #endregion - [DoesNotReturn] + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowCantWrite() { throw new NotSupportedException("The stream does not support writing"); } From f062036c98c67c4b7d21e6fc782b06ed0a8d997e Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 17:22:46 +0100 Subject: [PATCH 50/87] Prevent duplicate dispose --- src/Ws/WsRxConsumer.cs | 1 + src/Ws/WsRxProducer.cs | 2 +- src/Ws/WsTxConsumer.cs | 2 +- src/Ws/WsTxProducer.cs | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 49001c08..76384464 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -91,5 +91,6 @@ private void ThrowIfConnected() { public void Dispose() { _cts?.Dispose(); + _cts = null; } } diff --git a/src/Ws/WsRxProducer.cs b/src/Ws/WsRxProducer.cs index a1fe2860..9fd2f3d0 100644 --- a/src/Ws/WsRxProducer.cs +++ b/src/Ws/WsRxProducer.cs @@ -20,6 +20,6 @@ public async Task SendAsync(Stream stream) { } public void Dispose() { - _channel.Complete(); + _channel.TryComplete(); } } diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index e90c759d..05acbb73 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -125,7 +125,7 @@ private void ThrowIfConnected() { } public void Dispose() { - _cts?.Cancel(); _cts?.Dispose(); + _cts = null; } } diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index e4195550..98e6e663 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -108,6 +108,7 @@ private void ThrowIfConnected() { public void Dispose() { _cts?.Dispose(); - _out.Complete(); + _cts = null; + _out.TryComplete(); } } From bd27910cc7568d9985b6d16182204c0298738f66 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 18:28:46 +0100 Subject: [PATCH 51/87] Use DisposingCache instead of ConcurrentDictionary DisposingCache is a sliding-cache that disposes values when evicting --- src/Common/DisposingCache.cs | 101 +++++++++++++++++++++++++++++++++++ src/Ws/WsClient.cs | 4 +- src/Ws/WsClientOptions.cs | 12 +++++ src/Ws/WsTxConsumer.cs | 7 +-- 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/Common/DisposingCache.cs diff --git a/src/Common/DisposingCache.cs b/src/Common/DisposingCache.cs new file mode 100644 index 00000000..85e11c8c --- /dev/null +++ b/src/Common/DisposingCache.cs @@ -0,0 +1,101 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SurrealDB.Common; + +/// Thread-safe sliding cache that disposed values when evicting +internal sealed class DisposingCache + where K : notnull + where V : IDisposable { + private int _evictLock; + private long _lastEvictedTicks; // timestamp of latest eviction operation. + private readonly long _evictionIntervalTicks; // min timespan needed to trigger a new evict operation. + private readonly long _slidingExpirationTicks; // max timespan allowed for cache entries to remain inactive. + private readonly ConcurrentDictionary _cache = new(); + + public DisposingCache(TimeSpan slidingExpiration, TimeSpan evictionInterval) { + _slidingExpirationTicks = slidingExpiration.Ticks; + _evictionIntervalTicks = evictionInterval.Ticks; + _lastEvictedTicks = DateTime.UtcNow.Ticks; + } + + public V GetOrAdd(K key, Func valueFactory) { + CacheEntry entry = _cache.GetOrAdd(key, static (key, valueFactory) => new(valueFactory(key)), valueFactory); + EnsureSlidingEviction(entry); + + return entry.Value; + } + + public bool TryAdd(K key, V value) { + CacheEntry entry = new(value); + bool added = _cache.TryAdd(key, entry); + EnsureSlidingEviction(entry); + + return added; + } + + public bool TryRemove(K key, [MaybeNullWhen(false)] out V value) { + if (_cache.TryRemove(key, out var entry)) { + EnsureSlidingEviction(entry); + value = entry.Value; + return true; + } + + value = default; + return false; + } + + public bool TryGetValue(K key, [MaybeNullWhen(false)] out V value) { + if (_cache.TryRemove(key, out var entry)) { + EnsureSlidingEviction(entry); + value = entry.Value; + return true; + } + + value = default; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureSlidingEviction(CacheEntry entry) { + long utcNowTicks = DateTime.UtcNow.Ticks; + Volatile.Write(ref entry.LastUsedTicks, utcNowTicks); + + if (utcNowTicks - Volatile.Read(ref _lastEvictedTicks) >= _evictionIntervalTicks) { + if (Interlocked.CompareExchange(ref _evictLock, 1, 0) == 0) { + if (utcNowTicks - _lastEvictedTicks >= _evictionIntervalTicks) { + EvictStaleCacheEntries(utcNowTicks); + Volatile.Write(ref _lastEvictedTicks, utcNowTicks); + } + + Volatile.Write(ref _evictLock, 0); + } + } + } + + public void Clear() { + _cache.Clear(); + _lastEvictedTicks = DateTime.UtcNow.Ticks; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void EvictStaleCacheEntries(long utcNowTicks) { + foreach (KeyValuePair kvp in _cache) { + if (utcNowTicks - Volatile.Read(ref kvp.Value.LastUsedTicks) >= _slidingExpirationTicks) { + if (_cache.TryRemove(kvp.Key, out var entry)) { + entry.Value.Dispose(); + } + } + } + } + + private sealed class CacheEntry { + public readonly V Value; + public long LastUsedTicks; + + public CacheEntry(V value) { + Value = value; + } + } +} diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index ab93c416..effc4a8a 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -38,7 +38,7 @@ public WsClient(WsClientOptions options) { _rxProducer = new(rx.Writer, _memoryManager.BlockSize); _rxConsumer = new(_ws, rx.Reader, _memoryManager.BlockSize); var tx = Channel.CreateBounded(options.ChannelTxMessagesMax); - _txConsumer = new(tx.Reader, options.ReceiveHeaderBytesMax); + _txConsumer = new(tx.Reader, options.ReceiveHeaderBytesMax, options.RequestExpiration, TimeSpan.FromSeconds(1)); _txProducer = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize); _idBytes = options.IdBytes; @@ -88,7 +88,7 @@ public async Task Send(Request req, CancellationToken ct = default) { // listen for the response ResponseHandler handler = new(req.id, ct); - if (!_txConsumer.TryRegister(handler)) { + if (!_txConsumer.RegisterOrGet(handler)) { return default; } // send request diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index 4f0afbff..dee0ed41 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -43,11 +43,19 @@ public RecyclableMemoryStreamManager MemoryManager { set => Set(out _memoryManager, in value); } + /// The maximum time a request is awaited, before a is thrown. + /// Limited by the internal cache eviction timeout (1s) & pressure/traffic. + public TimeSpan RequestExpiration { + get => _requestExpiration; + set => Set(out _requestExpiration, value); + } + private RecyclableMemoryStreamManager? _memoryManager; private int _idBytes = 6; private int _receiveHeaderBytesMax = 512; private int _channelTxMessagesMax = 256; private int _channelRxMessagesMax = 256; + private TimeSpan _requestExpiration = TimeSpan.FromSeconds(10); public void ValidateAndMakeReadonly() { if (!IsReadonly()) { @@ -82,6 +90,10 @@ public void ValidateAndMakeReadonly() { if (_memoryManager is null) { yield return (nameof(MemoryManager), "cannot be null"); } + + if (RequestExpiration <= TimeSpan.Zero) { + yield return (nameof(RequestExpiration), "expiration time cannot be less then or equal to zero"); + } } public static WsClientOptions Default { get; } = CreateDefault(); diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 05acbb73..a37a23a0 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -10,14 +10,15 @@ namespace SurrealDB.Ws; /// Listens for s and dispatches them by their headers to different s. internal sealed class WsTxConsumer : IDisposable { private readonly ChannelReader _in; - private readonly ConcurrentDictionary _handlers = new(); + private readonly DisposingCache _handlers; private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsTxConsumer(ChannelReader channel, int maxHeaderBytes) { + public WsTxConsumer(ChannelReader channel, int maxHeaderBytes, TimeSpan cacheSlidingExpiration, TimeSpan cacheEvictionInterval) { _in = channel; MaxHeaderBytes = maxHeaderBytes; + _handlers = new(cacheSlidingExpiration, cacheEvictionInterval); } public int MaxHeaderBytes { get; } @@ -81,7 +82,7 @@ public void Unregister(string id) { } } - public bool TryRegister(IHandler handler) { + public bool RegisterOrGet(IHandler handler) { return _handlers.TryAdd(handler.Id, handler); } From b4dd9aa096b6991549e4ca87bcda3b2899ee1a23 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 19:55:18 +0100 Subject: [PATCH 52/87] Reorder validation out if lock --- src/Ws/WsRxConsumer.cs | 12 ++++++++++-- src/Ws/WsTxConsumer.cs | 12 +++++++++--- src/Ws/WsTxProducer.cs | 12 +++++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index 76384464..cb50eb19 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -51,8 +51,12 @@ private async Task Consume(CancellationToken ct) { public bool Connected => _cts is not null & _execute is not null; public void Open() { + ThrowIfConnected(); lock (_lock) { - ThrowIfConnected(); + if (Connected) { + return; + } + _cts = new(); _execute = Execute(_cts.Token); } @@ -62,6 +66,10 @@ public async Task Close() { ThrowIfDisconnected(); Task task; lock (_lock) { + if (!Connected) { + return; + } + task = _execute; _cts.Cancel(); _cts.Dispose(); // not relly needed here @@ -90,7 +98,7 @@ private void ThrowIfConnected() { } public void Dispose() { + _cts?.Cancel(); _cts?.Dispose(); - _cts = null; } } diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index a37a23a0..ddd6f231 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -87,8 +87,11 @@ public bool RegisterOrGet(IHandler handler) { } public void Open() { + ThrowIfConnected(); lock (_lock) { - ThrowIfConnected(); + if (Connected) { + return; + } _cts = new(); _execute = Execute(_cts.Token); } @@ -98,10 +101,13 @@ public async Task Close() { ThrowIfDisconnected(); Task task; lock (_lock) { - task = _execute; + if (!Connected) { + return; + } _cts.Cancel(); _cts.Dispose(); // not relly needed here _cts = null; + task = _execute; _execute = null; } @@ -126,7 +132,7 @@ private void ThrowIfConnected() { } public void Dispose() { + _cts?.Cancel(); _cts?.Dispose(); - _cts = null; } } diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index 98e6e663..b9437ef7 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -66,8 +66,11 @@ private async Task Produce(CancellationToken ct, byte[] buffer) { public bool Connected => _cts is not null & _execute is not null; public void Open() { + ThrowIfConnected(); lock (_lock) { - ThrowIfConnected(); + if (Connected) { + return; + } _cts = new(); _execute = Execute(_cts.Token); } @@ -75,8 +78,11 @@ public void Open() { public async Task Close() { Task task; + ThrowIfDisconnected(); lock (_lock) { - ThrowIfDisconnected(); + if (!Connected) { + return; + } task = _execute; _cts.Cancel(); _cts.Dispose(); // not relly needed here @@ -107,8 +113,8 @@ private void ThrowIfConnected() { } public void Dispose() { + _cts?.Cancel(); _cts?.Dispose(); - _cts = null; _out.TryComplete(); } } From 6c39727afacc3d311ec1827ceb2df90111bddc0e Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:04:29 +0100 Subject: [PATCH 53/87] Implement IAsyncDisposable for DbHandle --- tests/Shared/DbHandle.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/Shared/DbHandle.cs b/tests/Shared/DbHandle.cs index a6ca36b6..d421a6ee 100644 --- a/tests/Shared/DbHandle.cs +++ b/tests/Shared/DbHandle.cs @@ -1,8 +1,9 @@ using SurrealDB.Abstractions; +using SurrealDB.Common; namespace SurrealDB.Shared.Tests; -public class DbHandle : IDisposable +public class DbHandle : IAsyncDisposable where T: IDatabase, IDisposable, new() { private Process? _process; @@ -20,22 +21,25 @@ public static async Task> Create() { [DebuggerStepThrough] public static async Task WithDatabase(Func action) { - using DbHandle db = await Create(); + await using DbHandle db = await Create(); await action(db.Database); } public T Database { get; } - ~DbHandle() { - Dispose(); - } - - public void Dispose() { + public ValueTask DisposeAsync() { Process? p = _process; _process = null; if (p is not null) { - Database.Dispose(); - p.Kill(); + return new(DisposeActualAsync(p)); } + + return default; + } + + private async Task DisposeActualAsync(Process p) { + await Database.Close().Inv(); + Database.Dispose(); + p.Kill(); } } From de5addf811fad22b839b25a1e51855722ed8e845 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:13:56 +0100 Subject: [PATCH 54/87] Remove need for Rx channel --- src/Ws/WsClient.cs | 13 ++----- src/Ws/WsRxConsumer.cs | 77 ++++-------------------------------------- src/Ws/WsRxProducer.cs | 25 -------------- 3 files changed, 10 insertions(+), 105 deletions(-) delete mode 100644 src/Ws/WsRxProducer.cs diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index effc4a8a..272e43a4 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -20,8 +20,7 @@ public sealed class WsClient : IDisposable { private readonly ClientWebSocket _ws = new(); private readonly RecyclableMemoryStreamManager _memoryManager; - private readonly WsRxProducer _rxProducer; - private readonly WsRxConsumer _rxConsumer; + private readonly WsRx _rx; private readonly WsTxConsumer _txConsumer; private readonly WsTxProducer _txProducer; @@ -34,9 +33,7 @@ public WsClient() public WsClient(WsClientOptions options) { options.ValidateAndMakeReadonly(); _memoryManager = options.MemoryManager; - var rx = Channel.CreateBounded(options.ChannelRxMessagesMax); - _rxProducer = new(rx.Writer, _memoryManager.BlockSize); - _rxConsumer = new(_ws, rx.Reader, _memoryManager.BlockSize); + _rx = new(_ws, _memoryManager.BlockSize); var tx = Channel.CreateBounded(options.ChannelTxMessagesMax); _txConsumer = new(tx.Reader, options.ReceiveHeaderBytesMax, options.RequestExpiration, TimeSpan.FromSeconds(1)); _txProducer = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize); @@ -55,7 +52,6 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { await _ws.ConnectAsync(url, ct).Inv(); _txConsumer.Open(); _txProducer.Open(); - _rxConsumer.Open(); } /// @@ -64,15 +60,12 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { public async Task CloseAsync(CancellationToken ct = default) { ThrowIfDisconnected(); await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); - await _rxConsumer.Close().Inv(); await _txConsumer.Close().Inv(); await _txProducer.Close().Inv(); } /// public void Dispose() { - _rxConsumer.Dispose(); - _rxProducer.Dispose(); _txConsumer.Dispose(); _txProducer.Dispose(); _ws.Dispose(); @@ -93,7 +86,7 @@ public async Task Send(Request req, CancellationToken ct = default) { } // send request var stream = await SerializeAsync(req, ct).Inv(); - await _rxProducer.SendAsync(stream); + await _rx.SendAsync(stream, ct).Inv(); // await response, dispose message when done using var response = await handler.Task.Inv(); // validate header diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRxConsumer.cs index cb50eb19..360c75ca 100644 --- a/src/Ws/WsRxConsumer.cs +++ b/src/Ws/WsRxConsumer.cs @@ -8,31 +8,23 @@ namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -public sealed class WsRxConsumer : IDisposable { +public sealed class WsRx { private readonly ClientWebSocket _ws; - private readonly ChannelReader _in; - private readonly object _lock = new(); - private CancellationTokenSource? _cts; - private Task? _execute; - private readonly int _blockSize; - public WsRxConsumer(ClientWebSocket ws, ChannelReader @in, int blockSize) { + public WsRx(ClientWebSocket ws, int blockSize) { _ws = ws; - _in = @in; _blockSize = blockSize; } - private async Task Execute(CancellationToken ct) { - Debug.Assert(ct.CanBeCanceled); - while (!ct.IsCancellationRequested) { - await Consume(ct).Inv(); - } + public async Task SendAsync(Stream stream, CancellationToken ct) { + // reader is disposed by the consumer + using BufferStreamReader reader = new(stream, _blockSize); + await Consume(reader, ct).Inv(); } - private async Task Consume(CancellationToken ct) { - using var reader = await _in.ReadAsync(ct).Inv(); + private async Task Consume(BufferStreamReader reader, CancellationToken ct) { bool isFinalBlock = false; while (!isFinalBlock && !ct.IsCancellationRequested) { var rom = await reader.ReadAsync(_blockSize, ct).Inv(); @@ -46,59 +38,4 @@ private async Task Consume(CancellationToken ct) { await _ws.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); } } - - [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] - public bool Connected => _cts is not null & _execute is not null; - - public void Open() { - ThrowIfConnected(); - lock (_lock) { - if (Connected) { - return; - } - - _cts = new(); - _execute = Execute(_cts.Token); - } - } - - public async Task Close() { - ThrowIfDisconnected(); - Task task; - lock (_lock) { - if (!Connected) { - return; - } - - task = _execute; - _cts.Cancel(); - _cts.Dispose(); // not relly needed here - _cts = null; - _execute = null; - } - - try { - await task.Inv(); - } catch (OperationCanceledException) { - // expected on close using cts - } - } - - [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] - private void ThrowIfDisconnected() { - if (!Connected) { - throw new InvalidOperationException("The connection is not open."); - } - } - - private void ThrowIfConnected() { - if (Connected) { - throw new InvalidOperationException("The connection is already open"); - } - } - - public void Dispose() { - _cts?.Cancel(); - _cts?.Dispose(); - } } diff --git a/src/Ws/WsRxProducer.cs b/src/Ws/WsRxProducer.cs deleted file mode 100644 index 9fd2f3d0..00000000 --- a/src/Ws/WsRxProducer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Channels; - -using SurrealDB.Common; - -namespace SurrealDB.Ws; - -public sealed class WsRxProducer : IDisposable { - private readonly ChannelWriter _channel; - private readonly int _bufferSize; - - public WsRxProducer(ChannelWriter channel, int bufferSize) { - _channel = channel; - _bufferSize = bufferSize; - } - - public async Task SendAsync(Stream stream) { - // reader is disposed by the consumer - BufferStreamReader reader = new(stream, _bufferSize); - await _channel.WriteAsync(reader); - } - - public void Dispose() { - _channel.TryComplete(); - } -} From 5714fc13a9997d83cf07d928b0a1f3ae2b7d92f6 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:14:18 +0100 Subject: [PATCH 55/87] Rename file to WsRx --- src/Ws/{WsRxConsumer.cs => WsRx.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Ws/{WsRxConsumer.cs => WsRx.cs} (100%) diff --git a/src/Ws/WsRxConsumer.cs b/src/Ws/WsRx.cs similarity index 100% rename from src/Ws/WsRxConsumer.cs rename to src/Ws/WsRx.cs From 500a2497d80f07dc0d2646e941d15c1f15cac998 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:17:53 +0100 Subject: [PATCH 56/87] BufferStreamReader does not dispose underlying stream --- src/Common/BufferStreamReader.cs | 5 ----- src/Ws/WsClient.cs | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Common/BufferStreamReader.cs b/src/Common/BufferStreamReader.cs index 92242d19..3b9dc2b4 100644 --- a/src/Common/BufferStreamReader.cs +++ b/src/Common/BufferStreamReader.cs @@ -93,11 +93,6 @@ public ReadOnlySpan Read(int expectedSize) { public void Dispose() { - _arbitraryStream?.Dispose(); - _arbitraryStream = null; - _memoryStream?.Dispose(); - _memoryStream = null; - var poolArray = _poolArray; _poolArray = null; if (poolArray is not null) { diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 272e43a4..231aa404 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -87,7 +87,7 @@ public async Task Send(Request req, CancellationToken ct = default) { // send request var stream = await SerializeAsync(req, ct).Inv(); await _rx.SendAsync(stream, ct).Inv(); - // await response, dispose message when done + // await response, dispose message when done using var response = await handler.Task.Inv(); // validate header var responseHeader = response.Header.Response; From 697690e28bfa6fd04aaca2e102f5bcbe5f4e3d51 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:19:00 +0100 Subject: [PATCH 57/87] Remove unused classed - ConcurrentBufferReaderWriter - MemoryExtensions - WsStream --- src/Common/ConcurrentBufferReaderWriter.cs | 88 ------------ src/Common/MemoryExtensions.cs | 10 -- src/Common/WsStream.cs | 148 --------------------- 3 files changed, 246 deletions(-) delete mode 100644 src/Common/ConcurrentBufferReaderWriter.cs delete mode 100644 src/Common/MemoryExtensions.cs delete mode 100644 src/Common/WsStream.cs diff --git a/src/Common/ConcurrentBufferReaderWriter.cs b/src/Common/ConcurrentBufferReaderWriter.cs deleted file mode 100644 index ebf7cc4c..00000000 --- a/src/Common/ConcurrentBufferReaderWriter.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Buffers; - -namespace SurrealDB.Common; - -public sealed class ConcurrentBufferReaderWriter { - private readonly ArrayBufferWriter _buf = new(); - private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); - /// State used by multiple threads. Only interact via methods! - private int _readerPosition; - - public WriteHandle Write() => new(this); - - public ReadHandle Read() => new(this); - - /// Allows writing the buffer. Proxy for . - public readonly ref struct WriteHandle { - private readonly ConcurrentBufferReaderWriter _owner; - - internal WriteHandle(ConcurrentBufferReaderWriter owner) { - _owner = owner; - _owner._lock.EnterWriteLock(); - } - - public ReadOnlyMemory WrittenMemory => _owner._buf.WrittenMemory; - public ReadOnlySpan WrittenSpan => _owner._buf.WrittenSpan; - public int WrittenCount => _owner._buf.WrittenCount; - public int Capacity => _owner._buf.Capacity; - public int FreeCapacity => _owner._buf.FreeCapacity; - - public void Clear() => _owner._buf.Clear(); - - public void Advance(int count) => _owner._buf.Advance(count); - - public Memory GetMemory(int sizeHint = 0) => _owner._buf.GetMemory(sizeHint); - - public Span GetSpan(int sizeHint = 0) => _owner._buf.GetSpan(sizeHint); - - public void Dispose() { - _owner._lock.ExitWriteLock(); - } - } - - /// Allows concurrent reading from the buffer. - public readonly ref struct ReadHandle { - private readonly ConcurrentBufferReaderWriter _owner; - - internal ReadHandle(ConcurrentBufferReaderWriter owner) { - _owner = owner; - _owner._lock.EnterReadLock(); - } - - /// Reads a section of memory from the buffer - /// The maximum expected amount of memory read. - public ReadOnlyMemory ReadMemory(int expectedSize) { - // [THEADSAFE] increment the position - int newPosition = Interlocked.Add(ref _owner._readerPosition, expectedSize); - ReadOnlyMemory available = _owner._buf.WrittenMemory; - int start = newPosition - expectedSize; - int end = Math.Min(available.Length, newPosition); - return (nuint)start <= (nuint)available.Length ? available.Slice(start, end - start) : default; - } - - /// - public ReadOnlySpan ReadSpan(int expectedSize) { - // [THEADSAFE] increment the position - int newPosition = Interlocked.Add(ref _owner._readerPosition, expectedSize); - ReadOnlySpan available = _owner._buf.WrittenSpan; - int start = newPosition - expectedSize; - int end = Math.Min(available.Length, newPosition); - return (nuint)start <= (nuint)available.Length ? available.Slice(start, end - start) : default; - } - - /// Reads at most the size of from the buffer, and writes it to the . - /// Where the elements are written to. - /// The number of elements read and also written to the . - public int CopyTo(Span destination) { - var source = ReadSpan(destination.Length); - source.CopyTo(destination); - return source.Length; - } - - public void Dispose() { - _owner._lock.ExitReadLock(); - } - } -} - - diff --git a/src/Common/MemoryExtensions.cs b/src/Common/MemoryExtensions.cs deleted file mode 100644 index 6309a11c..00000000 --- a/src/Common/MemoryExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SurrealDB.Common; - -internal static class MemoryExtensions { - public static Span ClipLength(in this Span span, int length) { - return span.Length <= length ? span : span.Slice(0, length); - } - public static ReadOnlySpan ClipLength(in this ReadOnlySpan span, int length) { - return span.Length <= length ? span : span.Slice(0, length); - } -} diff --git a/src/Common/WsStream.cs b/src/Common/WsStream.cs deleted file mode 100644 index 1ffda8aa..00000000 --- a/src/Common/WsStream.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Buffers; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; -using System.Runtime.CompilerServices; - -namespace SurrealDB.Common; - -public sealed class WsStream : Stream { - private readonly IDisposable _prefixOwner; - /// - /// The prefix is the memory already obtained to be consumed before queries the socket - /// - private readonly ReadOnlyMemory _prefix; - private int _prefixConsumed; - - private readonly WebSocket _ws; - - public override bool CanRead => true; - public override bool CanSeek => false; - public override bool CanWrite => false; - public override long Length => ThrowSeekDisallowed(); - public override long Position { get => ThrowSeekDisallowed(); set => ThrowSeekDisallowed(); } - - public WsStream(IDisposable prefixOwner, ReadOnlyMemory prefix, WebSocket ws) { - _prefixOwner = prefixOwner; - _prefix = prefix; - _ws = ws; - } - - public override void Flush() { - // Readonly - } - - public override int Read(byte[] buffer, int offset, int count) { - return Read(buffer.AsSpan(offset, count)); - } - - /// - /// Use , or if possible. - /// - public override int Read(Span buffer) { - int read = 0; - // consume the prefix - ReadOnlySpan pref = ConsumePrefix(buffer.Length); - if (!pref.IsEmpty) { - pref.CopyTo(buffer); - buffer = buffer.Slice(pref.Length); - read += pref.Length; - } - - if (buffer.IsEmpty) { - return read; - } - - using IMemoryOwner o = MemoryPool.Shared.Rent(buffer.Length); - Memory m = o.Memory.Slice(0, buffer.Length); - buffer.CopyTo(m.Span); - return read + ReadSync(m); - } - - /// - public int Read(Memory buffer) { - int read = 0; - // consume the prefix - ReadOnlySpan pref = ConsumePrefix(buffer.Length); - if (!pref.IsEmpty) { - pref.CopyTo(buffer.Span); - buffer = buffer.Slice(pref.Length); - read += pref.Length; - } - - return read + ReadSync(buffer); - } - - private int ReadSync(Memory buffer) { - // This causes issues if the scheduler is exclusive. - return ReadAsync(buffer).Result; - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - } - - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - int read = 0; - while (!buffer.IsEmpty) { - ValueWebSocketReceiveResult rsp = await _ws.ReceiveAsync(buffer, cancellationToken); - buffer = buffer.Slice(rsp.Count); - read += rsp.Count; - - if (rsp.EndOfMessage) { - break; - } - } - - return read; - } - - private ReadOnlySpan ConsumePrefix(int length) { - int len = _prefix.Length; - int con = _prefixConsumed; - if (con == len) { - return default; - } - int rem = len - con; - int inc = Math.Min(rem, length); - _prefixConsumed = con + inc; - return _prefix.Span.Slice(con, inc); - } - - private void DisposePrefix() { - _prefixConsumed = _prefix.Length; - _prefixOwner.Dispose(); - } - - public override long Seek(long offset, SeekOrigin origin) { - return ThrowSeekDisallowed(); - } - - public override void SetLength(long value) { - ThrowWriteDisallowed(); - } - - public override void Write(byte[] buffer, int offset, int count) { - ThrowWriteDisallowed(); - } - - protected override void Dispose(bool disposing) { - base.Dispose(disposing); - DisposePrefix(); - } - - public override async ValueTask DisposeAsync() { - await base.DisposeAsync(); - DisposePrefix(); - } - - [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowWriteDisallowed() { - throw new InvalidOperationException("Cannot write a readonly stream"); - } - - [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] - private static long ThrowSeekDisallowed() { - throw new InvalidOperationException("Cannot seek in the stream"); - } -} From 0cd5722206a4d1ca112d43520924877a08c7c3a5 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:27:07 +0100 Subject: [PATCH 58/87] Remove duplicate db.Close call --- tests/Driver.Tests/DatabaseTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Driver.Tests/DatabaseTests.cs b/tests/Driver.Tests/DatabaseTests.cs index 01a4b831..f22f1037 100644 --- a/tests/Driver.Tests/DatabaseTests.cs +++ b/tests/Driver.Tests/DatabaseTests.cs @@ -86,8 +86,6 @@ await db.Change( ); TestHelper.AssertOk(queryResp); - - await db.Close(); } ); } From 1d692ee0ce6653e832c1f71dbd82d501790b05a3 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:32:12 +0100 Subject: [PATCH 59/87] Disable orderly closing of database when Disposing DbHandle This slows down tests way to much, bc the current implementation of ClientWebsocket takes over a !!!!SECOND!!!!! to negotiate a orderly closing with surrealdb --- tests/Shared/DbHandle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Shared/DbHandle.cs b/tests/Shared/DbHandle.cs index d421a6ee..5e5f586d 100644 --- a/tests/Shared/DbHandle.cs +++ b/tests/Shared/DbHandle.cs @@ -38,7 +38,7 @@ public ValueTask DisposeAsync() { } private async Task DisposeActualAsync(Process p) { - await Database.Close().Inv(); + //await Database.Close().Inv(); Database.Dispose(); p.Kill(); } From 1e4800203bebe6f4125975efc7f59c0602faa233 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 20:51:09 +0100 Subject: [PATCH 60/87] Remove ChannelRxMessagesMax from WsClientOptions --- src/Ws/WsClientOptions.cs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index dee0ed41..3b2e60c3 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -9,13 +9,8 @@ namespace SurrealDB.Ws; public sealed record WsClientOptions : ValidateReadonly { public const int MaxArraySize = 0X7FFFFFC7; - /// The maximum number of messages in the client outbound channel. - /// A message may consist of multiple blocks. Only the message counts towards this number. - public int ChannelRxMessagesMax { - get => _channelRxMessagesMax; - set => Set(out _channelRxMessagesMax, in value); - } - /// The maximum number of messages in the client inbound channel. + /// The maximum number of messages in the client inbound channel. + /// This does not refer to the number of simultaneous queries, but the number of unread messages, the size of the "inbox" /// A message may consist of multiple blocks. Only the message counts towards this number. public int ChannelTxMessagesMax { get => _channelTxMessagesMax; @@ -54,7 +49,6 @@ public TimeSpan RequestExpiration { private int _idBytes = 6; private int _receiveHeaderBytesMax = 512; private int _channelTxMessagesMax = 256; - private int _channelRxMessagesMax = 256; private TimeSpan _requestExpiration = TimeSpan.FromSeconds(10); public void ValidateAndMakeReadonly() { @@ -65,13 +59,6 @@ public void ValidateAndMakeReadonly() { } protected override IEnumerable<(string PropertyName, string Message)> Validations() { - if (ChannelRxMessagesMax <= 0) { - yield return (nameof(ChannelRxMessagesMax), "cannot be less then or equal to zero"); - } - if (ChannelRxMessagesMax > MaxArraySize) { - yield return (nameof(ChannelRxMessagesMax), "cannot be greater then MaxArraySize"); - } - if (ChannelTxMessagesMax <= 0) { yield return (nameof(ChannelTxMessagesMax), "cannot be less then or equal to zero"); } From 0c39462affbd5c3c13788c41ec3052576adb33bd Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 21:14:26 +0100 Subject: [PATCH 61/87] Remove RecyclableMemoryStream dependency from Common --- src/Common/Common.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index 68bee439..4aeee446 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -17,7 +17,6 @@ - From 8829bc722f44e665e686213c58af1f4cdbcd16e0 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 23:00:35 +0100 Subject: [PATCH 62/87] Implement BoundedChannelPool --- src/Common/BitOperations.cs | 699 +++++++++++++++++++++++++++ src/Common/BufferStreamReader.cs | 7 +- src/Common/GCHelper.cs | 55 +++ src/Common/Gen2GCCallback.cs | 112 +++++ src/Common/MemoryHelper.cs | 41 ++ src/Ws/Helpers/BoundedChannelPool.cs | 475 ++++++++++++++++++ 6 files changed, 1385 insertions(+), 4 deletions(-) create mode 100644 src/Common/BitOperations.cs create mode 100644 src/Common/GCHelper.cs create mode 100644 src/Common/Gen2GCCallback.cs create mode 100644 src/Common/MemoryHelper.cs create mode 100644 src/Ws/Helpers/BoundedChannelPool.cs diff --git a/src/Common/BitOperations.cs b/src/Common/BitOperations.cs new file mode 100644 index 00000000..2289545d --- /dev/null +++ b/src/Common/BitOperations.cs @@ -0,0 +1,699 @@ +// BitOperations without intrinsics for netstandard21 support +#if !NETCOREAPP3_0_OR_GREATER || !NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using SurrealDB.Common; + +// ReSharper disable once CheckNamespace +namespace System.Numerics; + +public static class BitOperations +{ + // C# no-alloc optimization that directly wraps the data section of the dll (similar to string constants) + // https://github.com/dotnet/roslyn/pull/24621 + + private static ReadOnlySpan TrailingZeroCountDeBruijn => new byte[32] + { + 00, 01, 28, 02, 29, 14, 24, 03, + 30, 22, 20, 15, 25, 17, 04, 08, + 31, 27, 13, 23, 21, 19, 16, 07, + 26, 12, 18, 06, 11, 05, 10, 09 + }; + + private static ReadOnlySpan Log2DeBruijn => new byte[32] + { + 00, 09, 01, 10, 13, 21, 02, 29, + 11, 14, 16, 18, 22, 25, 03, 30, + 08, 12, 20, 28, 15, 17, 24, 07, + 19, 27, 23, 06, 26, 05, 04, 31 + }; + + /// + /// Evaluate whether a given integral value is a power of 2. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPow2(int value) => (value & (value - 1)) == 0 && value > 0; + + /// + /// Evaluate whether a given integral value is a power of 2. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static bool IsPow2(uint value) => (value & (value - 1)) == 0 && value != 0; + + /// + /// Evaluate whether a given integral value is a power of 2. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPow2(long value) => (value & (value - 1)) == 0 && value > 0; + + /// + /// Evaluate whether a given integral value is a power of 2. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static bool IsPow2(ulong value) => (value & (value - 1)) == 0 && value != 0; + + /// + /// Evaluate whether a given integral value is a power of 2. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPow2(nint value) => (value & (value - 1)) == 0 && value > 0; + + /// + /// Evaluate whether a given integral value is a power of 2. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static bool IsPow2(nuint value) => (value & (value - 1)) == 0 && value != 0; + + /// Round the given integral value up to a power of 2. + /// The value. + /// + /// The smallest power of 2 which is greater than or equal to . + /// If is 0 or the result overflows, returns 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static uint RoundUpToPowerOf2(uint value) + { + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; + } + + /// + /// Round the given integral value up to a power of 2. + /// + /// The value. + /// + /// The smallest power of 2 which is greater than or equal to . + /// If is 0 or the result overflows, returns 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static ulong RoundUpToPowerOf2(ulong value) + { + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + return value + 1; + } + + /// + /// Round the given integral value up to a power of 2. + /// + /// The value. + /// + /// The smallest power of 2 which is greater than or equal to . + /// If is 0 or the result overflows, returns 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static nuint RoundUpToPowerOf2(nuint value) + { +#if TARGET_64BIT + return (nuint)RoundUpToPowerOf2((ulong)value); +#else + return (nuint)RoundUpToPowerOf2((uint)value); +#endif + } + + /// + /// Count the number of leading zero bits in a mask. + /// Similar in behavior to the x86 instruction LZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int LeadingZeroCount(uint value) + { + // Unguarded fallback contract is 0->31, BSR contract is 0->undefined + if (value == 0) + { + return 32; + } + + return 31 ^ Log2SoftwareFallback(value); + } + + /// + /// Count the number of leading zero bits in a mask. + /// Similar in behavior to the x86 instruction LZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int LeadingZeroCount(ulong value) + { + uint hi = (uint)(value >> 32); + + if (hi == 0) + { + return 32 + LeadingZeroCount((uint)value); + } + + return LeadingZeroCount(hi); + } + + /// + /// Count the number of leading zero bits in a mask. + /// Similar in behavior to the x86 instruction LZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int LeadingZeroCount(nuint value) + { +#if TARGET_64BIT + return LeadingZeroCount((ulong)value); +#else + return LeadingZeroCount((uint)value); +#endif + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since log(0) is undefined. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int Log2(uint value) + { + // The 0->0 contract is fulfilled by setting the LSB to 1. + // Log(1) is 0, and setting the LSB for values > 1 does not change the log2 result. + value |= 1; + + // Fallback contract is 0->0 + return Log2SoftwareFallback(value); + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since log(0) is undefined. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int Log2(ulong value) + { + value |= 1; + + uint hi = (uint)(value >> 32); + + if (hi == 0) + { + return Log2((uint)value); + } + + return 32 + Log2(hi); + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since log(0) is undefined. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int Log2(nuint value) + { +#if TARGET_64BIT + return Log2((ulong)value); +#else + return Log2((uint)value); +#endif + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since Log(0) is undefined. + /// Does not directly use any hardware intrinsics, nor does it incur branching. + /// + /// The value. + private static int Log2SoftwareFallback(uint value) + { + // No AggressiveInlining due to large method size + // Has conventional contract 0->0 (Log(0) is undefined) + + // Fill trailing zeros with ones, eg 00010010 becomes 00011111 + value |= value >> 01; + value |= value >> 02; + value |= value >> 04; + value |= value >> 08; + value |= value >> 16; + + // uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check + return Unsafe.AddByteOffset( + // Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_1100_0100_1010_1100_1101_1101u + ref MemoryMarshal.GetReference(Log2DeBruijn), + // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here + (IntPtr)(int)((value * 0x07C4ACDDu) >> 27)); + } + + /// Returns the integer (ceiling) log of the specified value, base 2. + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int Log2Ceiling(uint value) + { + int result = Log2(value); + if (PopCount(value) != 1) + { + result++; + } + return result; + } + + /// Returns the integer (ceiling) log of the specified value, base 2. + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int Log2Ceiling(ulong value) + { + int result = Log2(value); + if (PopCount(value) != 1) + { + result++; + } + return result; + } + + /// + /// Returns the population count (number of bits set) of a mask. + /// Similar in behavior to the x86 instruction POPCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int PopCount(uint value) + { + const uint c1 = 0x_55555555u; + const uint c2 = 0x_33333333u; + const uint c3 = 0x_0F0F0F0Fu; + const uint c4 = 0x_01010101u; + + value -= (value >> 1) & c1; + value = (value & c2) + ((value >> 2) & c2); + value = (((value + (value >> 4)) & c3) * c4) >> 24; + + return (int)value; + } + + /// + /// Returns the population count (number of bits set) of a mask. + /// Similar in behavior to the x86 instruction POPCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int PopCount(ulong value) + { +#if TARGET_32BIT + return PopCount((uint)value) // lo + + PopCount((uint)(value >> 32)); // hi +#else + const ulong c1 = 0x_55555555_55555555ul; + const ulong c2 = 0x_33333333_33333333ul; + const ulong c3 = 0x_0F0F0F0F_0F0F0F0Ful; + const ulong c4 = 0x_01010101_01010101ul; + + value -= (value >> 1) & c1; + value = (value & c2) + ((value >> 2) & c2); + value = (((value + (value >> 4)) & c3) * c4) >> 56; + + return (int)value; +#endif + } + + /// + /// Returns the population count (number of bits set) of a mask. + /// Similar in behavior to the x86 instruction POPCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int PopCount(nuint value) + { +#if TARGET_64BIT + return PopCount((ulong)value); +#else + return PopCount((uint)value); +#endif + } + + /// + /// Count the number of trailing zero bits in an integer value. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TrailingZeroCount(int value) + => TrailingZeroCount((uint)value); + + /// + /// Count the number of trailing zero bits in an integer value. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TrailingZeroCount(uint value) + { + // Unguarded fallback contract is 0->0, BSF contract is 0->undefined + if (value == 0) + { + return 32; + } + + // uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check + return Unsafe.AddByteOffset( + // Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_0111_1100_1011_0101_0011_0001u + ref MemoryMarshal.GetReference(TrailingZeroCountDeBruijn), + // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here + (IntPtr)(int)(((value & (uint)-(int)value) * 0x077CB531u) >> 27)); // Multi-cast mitigates redundant conv.u8 + } + + /// + /// Count the number of trailing zero bits in a mask. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TrailingZeroCount(long value) + => TrailingZeroCount((ulong)value); + + /// + /// Count the number of trailing zero bits in a mask. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int TrailingZeroCount(ulong value) + { + uint lo = (uint)value; + + if (lo == 0) + { + return 32 + TrailingZeroCount((uint)(value >> 32)); + } + + return TrailingZeroCount(lo); + } + + /// + /// Count the number of trailing zero bits in a mask. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TrailingZeroCount(nint value) + => TrailingZeroCount((nuint)value); + + /// + /// Count the number of trailing zero bits in a mask. + /// Similar in behavior to the x86 instruction TZCNT. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static int TrailingZeroCount(nuint value) + { +#if TARGET_64BIT + return TrailingZeroCount((ulong)value); +#else + return TrailingZeroCount((uint)value); +#endif + } + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..63] is treated as congruent mod 64. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static ulong RotateLeft(ulong value, int offset) + => (value << offset) | (value >> (64 - offset)); + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32 on a 32-bit process, + /// and any value outside the range [0..63] is treated as congruent mod 64 on a 64-bit process. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static nuint RotateLeft(nuint value, int offset) + { +#if TARGET_64BIT + return (nuint)RotateLeft((ulong)value, offset); +#else + return (nuint)RotateLeft((uint)value, offset); +#endif + } + + /// + /// Rotates the specified value right by the specified number of bits. + /// Similar in behavior to the x86 instruction ROR. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static uint RotateRight(uint value, int offset) + => (value >> offset) | (value << (32 - offset)); + + /// + /// Rotates the specified value right by the specified number of bits. + /// Similar in behavior to the x86 instruction ROR. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..63] is treated as congruent mod 64. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static ulong RotateRight(ulong value, int offset) + => (value >> offset) | (value << (64 - offset)); + + /// + /// Rotates the specified value right by the specified number of bits. + /// Similar in behavior to the x86 instruction ROR. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32 on a 32-bit process, + /// and any value outside the range [0..63] is treated as congruent mod 64 on a 64-bit process. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public static nuint RotateRight(nuint value, int offset) + { +#if TARGET_64BIT + return (nuint)RotateRight((ulong)value, offset); +#else + return (nuint)RotateRight((uint)value, offset); +#endif + } + + /// + /// Accumulates the CRC (Cyclic redundancy check) checksum. + /// + /// The base value to calculate checksum on + /// The data for which to compute the checksum + /// The CRC-checksum + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Crc32C(uint crc, byte data) + { + return Crc32Fallback.Crc32C(crc, data); + } + + /// + /// Accumulates the CRC (Cyclic redundancy check) checksum. + /// + /// The base value to calculate checksum on + /// The data for which to compute the checksum + /// The CRC-checksum + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Crc32C(uint crc, ushort data) + { + return Crc32Fallback.Crc32C(crc, data); + } + + /// + /// Accumulates the CRC (Cyclic redundancy check) checksum. + /// + /// The base value to calculate checksum on + /// The data for which to compute the checksum + /// The CRC-checksum + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Crc32C(uint crc, uint data) + { + return Crc32Fallback.Crc32C(crc, data); + } + + /// + /// Accumulates the CRC (Cyclic redundancy check) checksum. + /// + /// The base value to calculate checksum on + /// The data for which to compute the checksum + /// The CRC-checksum + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Crc32C(uint crc, ulong data) + { + return Crc32Fallback.Crc32C(crc, data); + } + + private static class Crc32Fallback + { + // Pre-computed CRC-32 transition table. + // While this implementation is based on the Castagnoli CRC-32 polynomial (CRC-32C), + // x32 + x28 + x27 + x26 + x25 + x23 + x22 + x20 + x19 + x18 + x14 + x13 + x11 + x10 + x9 + x8 + x6 + x0, + // this version uses reflected bit ordering, so 0x1EDC6F41 becomes 0x82F63B78u + private static readonly uint[] s_crcTable = Crc32ReflectedTable.Generate(0x82F63B78u); + + internal static uint Crc32C(uint crc, byte data) + { + ref uint lookupTable = ref MemoryHelper.GetArrayDataReference(s_crcTable); + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ data)) ^ (crc >> 8); + + return crc; + } + + internal static uint Crc32C(uint crc, ushort data) + { + ref uint lookupTable = ref MemoryHelper.GetArrayDataReference(s_crcTable); + + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ (byte)data)) ^ (crc >> 8); + data >>= 8; + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ data)) ^ (crc >> 8); + + return crc; + } + + internal static uint Crc32C(uint crc, uint data) + { + ref uint lookupTable = ref MemoryHelper.GetArrayDataReference(s_crcTable); + return Crc32CCore(ref lookupTable, crc, data); + } + + internal static uint Crc32C(uint crc, ulong data) + { + ref uint lookupTable = ref MemoryHelper.GetArrayDataReference(s_crcTable); + + crc = Crc32CCore(ref lookupTable, crc, (uint)data); + data >>= 32; + crc = Crc32CCore(ref lookupTable, crc, (uint)data); + + return crc; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Crc32CCore(ref uint lookupTable, uint crc, uint data) + { + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ (byte)data)) ^ (crc >> 8); + data >>= 8; + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ (byte)data)) ^ (crc >> 8); + data >>= 8; + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ (byte)data)) ^ (crc >> 8); + data >>= 8; + crc = Unsafe.Add(ref lookupTable, (nint)(byte)(crc ^ data)) ^ (crc >> 8); + + return crc; + } + } + + /// + /// Reset the lowest significant bit in the given value + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint ResetLowestSetBit(uint value) + { + // It's lowered to BLSR on x86 + return value & (value - 1); + } + + /// + /// Reset specific bit in the given value + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint ResetBit(uint value, int bitPos) + { + // TODO: Recognize BTR on x86 and LSL+BIC on ARM + return value & ~(uint)(1 << bitPos); + } +} +#endif + + +internal static class Crc32ReflectedTable +{ + internal static uint[] Generate(uint reflectedPolynomial) + { + uint[] table = new uint[256]; + + for (int i = 0; i < 256; i++) + { + uint val = (uint)i; + + for (int j = 0; j < 8; j++) + { + if ((val & 0b0000_0001) == 0) + { + val >>= 1; + } + else + { + val = (val >> 1) ^ reflectedPolynomial; + } + } + + table[i] = val; + } + + return table; + } +} diff --git a/src/Common/BufferStreamReader.cs b/src/Common/BufferStreamReader.cs index 3b9dc2b4..942c89e2 100644 --- a/src/Common/BufferStreamReader.cs +++ b/src/Common/BufferStreamReader.cs @@ -3,8 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using Microsoft.IO; - namespace SurrealDB.Common; /// Allows reading a stream efficiently @@ -26,8 +24,9 @@ private BufferStreamReader(Stream? arbitraryStream, MemoryStream? memoryStream, public BufferStreamReader(Stream stream, int bufferSize) { ThrowArgIfStreamCantRead(stream); this = stream switch { - RecyclableMemoryStream => new(stream, null, bufferSize), // TryGetBuffer is expensive! - MemoryStream ms => new(null, ms, bufferSize), + // inhering from ms doesnt guarantee a good GetBuffer impl, such as RecyclableMemoryStream, + // therefore we have to check the exact runtime type. + MemoryStream ms when ms.GetType() == typeof(MemoryStream) => new(null, ms, bufferSize), _ => new(stream, null, bufferSize) }; } diff --git a/src/Common/GCHelper.cs b/src/Common/GCHelper.cs new file mode 100644 index 00000000..408117d5 --- /dev/null +++ b/src/Common/GCHelper.cs @@ -0,0 +1,55 @@ +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace System.Buffers; + +internal static class GCHelper { + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int SelectBucketIndex(int bufferSize) + { + // Buffers are bucketed so that a request between 2^(n-1) + 1 and 2^n is given a buffer of 2^n + // Bucket index is log2(bufferSize - 1) with the exception that buffers between 1 and 16 bytes + // are combined, and the index is slid down by 3 to compensate. + // Zero is a valid bufferSize, and it is assigned the highest bucket index so that zero-length + // buffers are not retained by the pool. The pool will return the Array.Empty singleton for these. + return BitOperations.Log2((uint)bufferSize - 1 | 15) - 3; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int GetMaxSizeForBucket(int binIndex) + { + int maxSize = 16 << binIndex; + Debug.Assert(maxSize >= 0); + return maxSize; + } + + internal enum MemoryPressure + { + Low, + Medium, + High + } + + internal static MemoryPressure GetMemoryPressure() + { +#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER + const double HighPressureThreshold = .90; // Percent of GC memory pressure threshold we consider "high" + const double MediumPressureThreshold = .70; // Percent of GC memory pressure threshold we consider "medium" + GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); + + if (memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * HighPressureThreshold) + { + return MemoryPressure.High; + } + + if (memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * MediumPressureThreshold) + { + return MemoryPressure.Medium; + } +#endif + return MemoryPressure.Low; + } +} diff --git a/src/Common/Gen2GCCallback.cs b/src/Common/Gen2GCCallback.cs new file mode 100644 index 00000000..81a4eb5e --- /dev/null +++ b/src/Common/Gen2GCCallback.cs @@ -0,0 +1,112 @@ +using System.Diagnostics; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +// Source: https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs + +// ReSharper disable once CheckNamespace +namespace System; + +/// +/// Schedules a callback roughly every gen 2 GC (you may see a Gen 0 an Gen 1 but only once) +/// (We can fix this by capturing the Gen 2 count at startup and testing, but I mostly don't care) +/// +internal sealed class Gen2GcCallback : CriticalFinalizerObject +{ + private readonly Func? _callback0; + private readonly Func? _callback1; + private GCHandle _weakTargetObj; + + private Gen2GcCallback(Func callback) + { + _callback0 = callback; + } + + private Gen2GcCallback(Func callback, object targetObj) + { + _callback1 = callback; + _weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak); + } + + /// + /// Schedule 'callback' to be called in the next GC. If the callback returns true it is + /// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop. + /// + public static void Register(Func callback) + { + // Create a unreachable object that remembers the callback function and target object. + new Gen2GcCallback(callback); + } + + /// + /// Schedule 'callback' to be called in the next GC. If the callback returns true it is + /// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop. + /// + /// NOTE: This callback will be kept alive until either the callback function returns false, + /// or the target object dies. + /// + public static void Register(Func callback, object targetObj) + { + // Create a unreachable object that remembers the callback function and target object. + new Gen2GcCallback(callback, targetObj); + } + + ~Gen2GcCallback() + { + if (_weakTargetObj.IsAllocated) + { + // Check to see if the target object is still alive. + object? targetObj = _weakTargetObj.Target; + if (targetObj == null) + { + // The target object is dead, so this callback object is no longer needed. + _weakTargetObj.Free(); + return; + } + + // Execute the callback method. + try + { + Debug.Assert(_callback1 != null); + if (!_callback1(targetObj)) + { + // If the callback returns false, this callback object is no longer needed. + _weakTargetObj.Free(); + return; + } + } + catch + { + // Ensure that we still get a chance to resurrect this object, even if the callback throws an exception. +#if DEBUG + // Except in DEBUG, as we really shouldn't be hitting any exceptions here. + throw; +#endif + } + } + else + { + // Execute the callback method. + try + { + Debug.Assert(_callback0 != null); + if (!_callback0()) + { + // If the callback returns false, this callback object is no longer needed. + return; + } + } + catch + { + // Ensure that we still get a chance to resurrect this object, even if the callback throws an exception. +#if DEBUG + // Except in DEBUG, as we really shouldn't be hitting any exceptions here. + throw; +#endif + } + } + + // Resurrect ourselves by re-registering for finalization. + GC.ReRegisterForFinalize(this); + } +} diff --git a/src/Common/MemoryHelper.cs b/src/Common/MemoryHelper.cs new file mode 100644 index 00000000..09407f2e --- /dev/null +++ b/src/Common/MemoryHelper.cs @@ -0,0 +1,41 @@ +using System.Runtime.CompilerServices; + +namespace SurrealDB.Common; + +internal static class MemoryHelper { + /// + /// Returns a reference to the 0th element of . If the array is empty, returns a reference to where the 0th element + /// would have been stored. Such a reference may be used for pinning but must never be dereferenced. + /// + /// is . + /// + /// This method does not perform array variance checks. The caller must manually perform any array variance checks + /// if the caller wishes to write to the returned reference. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetArrayDataReference(T[] array) { +#if !NET6_0_OR_GREATER + return ref Unsafe.As(ref Unsafe.As(array).Data); +#else + return ref System.Runtime.InteropServices.MemoryMarshal.GetArrayDataReference(array); +#endif + } + + + // CLR arrays are laid out in memory as follows (multidimensional array bounds are optional): + // [ sync block || pMethodTable || num components || MD array bounds || array data .. ] + // ^ ^ ^ ^ returned reference + // | | \-- ref Unsafe.As(array).Data + // \-- array \-- ref Unsafe.As(array).Data + // The BaseSize of an array includes all the fields before the array data, + // including the sync block and method table. The reference to RawData.Data + // points at the number of components, skipping over these two pointer-sized fields. + internal sealed class RawArrayData + { + public uint Length; // Array._numComponents padded to IntPtr +#if TARGET_64BIT + public uint Padding; +#endif + public byte Data; + } +} diff --git a/src/Ws/Helpers/BoundedChannelPool.cs b/src/Ws/Helpers/BoundedChannelPool.cs new file mode 100644 index 00000000..010e09e2 --- /dev/null +++ b/src/Ws/Helpers/BoundedChannelPool.cs @@ -0,0 +1,475 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +// ReSharper disable once CheckNamespace +namespace System.Buffers; + +public abstract class BoundedChannelPool { + private static readonly TlsOverPerCoreLockedStacksBoundedChannelPool s_boundedShared = new(); + + public static BoundedChannelPool Shared => s_boundedShared; + + public abstract BoundedChannel Rent(int minimumLength); + + public abstract void Return(BoundedChannel channel); +} + +public sealed class BoundedChannel : Channel, IDisposable { + private BoundedChannelPool? _owner; + + public BoundedChannel(Channel wrapped, int capacity, BoundedChannelPool owner) { + Reader = wrapped.Reader; + Writer = wrapped.Writer; + Capacity = capacity; + _owner = owner; + } + + public int Capacity { get; } + + public void Dispose() { + var owner = _owner; + _owner = null; + if (owner is not null) { + owner.Return(this); + } + } +} + +// Source: https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Buffers/TlsOverPerCoreLockedStacksArrayPool.cs +// modified for use with channels +public sealed class TlsOverPerCoreLockedStacksBoundedChannelPool : BoundedChannelPool { + /// The number of buckets (array sizes) in the pool, one for each array length, starting from length 16. + private const int NumBuckets = 27; // GCHelper.SelectBucketIndex(1024 * 1024 * 1024 + 1) + /// Maximum number of per-core stacks to use per array size. + private const int MaxPerCorePerArraySizeStacks = 64; // selected to avoid needing to worry about processor groups + /// The maximum number of buffers to store in a bucket's global queue. + private const int MaxBuffersPerArraySizePerCore = 8; + + /// A per-thread array of arrays, to cache one array per array size per thread. + [ThreadStatic] + private static ThreadLocalArray[]? t_tlsBuckets; + /// Used to keep track of all thread local buckets for trimming if needed. + private readonly ConditionalWeakTable _allTlsBuckets = new ConditionalWeakTable(); + /// + /// An array of per-core array stacks. The slots are lazily initialized to avoid creating + /// lots of overhead for unused array sizes. + /// + private readonly PerCoreLockedStacks?[] _buckets = new PerCoreLockedStacks[NumBuckets]; + /// Whether the callback to trim arrays in response to memory pressure has been created. + private int _trimCallbackCreated; + + /// Allocate a new PerCoreLockedStacks and try to store it into the array. + private PerCoreLockedStacks CreatePerCoreLockedStacks(int bucketIndex) + { + var inst = new PerCoreLockedStacks(); + return Interlocked.CompareExchange(ref _buckets[bucketIndex], inst, null) ?? inst; + } + + /// Gets an ID for the pool to use with events. + private int Id => GetHashCode(); + + public override BoundedChannel Rent(int minimumLength) + { + BoundedChannel? buffer; + + // Get the bucket number for the array length. The result may be out of range of buckets, + // either for too large a value or for 0 and negative values. + int bucketIndex = GCHelper.SelectBucketIndex(minimumLength); + + // First, try to get an array from TLS if possible. + ThreadLocalArray[]? tlsBuckets = t_tlsBuckets; + if (tlsBuckets is not null && (uint)bucketIndex < (uint)tlsBuckets.Length) + { + buffer = tlsBuckets[bucketIndex].Channel; + if (buffer is not null) + { + tlsBuckets[bucketIndex].Channel = null; + return buffer; + } + } + + // Next, try to get an array from one of the per-core stacks. + PerCoreLockedStacks?[] perCoreBuckets = _buckets; + if ((uint)bucketIndex < (uint)perCoreBuckets.Length) + { + PerCoreLockedStacks? b = perCoreBuckets[bucketIndex]; + if (b is not null) + { + buffer = b.TryPop(); + if (buffer is not null) + { + return buffer; + } + } + + // No buffer available. Ensure the length we'll allocate matches that of a bucket + // so we can later return it. + minimumLength = GCHelper.GetMaxSizeForBucket(bucketIndex); + } + else if (minimumLength <= 0) + { + throw new ArgumentOutOfRangeException(nameof(minimumLength)); + } + + // allocate a new bounded channel, that belongs to this instance + buffer = new(Channel.CreateBounded(minimumLength), minimumLength, this); + + return buffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Return(BoundedChannel? channel) { + if (channel is null) { + ThrowChannelNull(); + } + + if (channel.Reader.Completion.IsCompleted) { + ThrowChannelCompleted(); + } + + // Determine with what bucket this array length is associated + int bucketIndex = GCHelper.SelectBucketIndex(channel.Capacity); + + // Make sure our TLS buckets are initialized. Technically we could avoid doing + // this if the array being returned is erroneous or too large for the pool, but the + // former condition is an error we don't need to optimize for, and the latter is incredibly + // rare, given a max size of 1B elements. + ThreadLocalArray[] tlsBuckets = t_tlsBuckets ?? InitializeTlsBucketsAndTrimming(); + + bool haveBucket = false; + bool returned = true; + if ((uint)bucketIndex < (uint)tlsBuckets.Length) + { + haveBucket = true; + + // Check to see if the buffer is the correct size for this bucket. + if (channel.Capacity != GCHelper.GetMaxSizeForBucket(bucketIndex)) { + ThrowChannelNotOfPool(); + } + + // Store the array into the TLS bucket. If there's already an array in it, + // push that array down into the per-core stacks, preferring to keep the latest + // one in TLS for better locality. + ref ThreadLocalArray tla = ref tlsBuckets[bucketIndex]; + BoundedChannel? prev = tla.Channel; + tla = new ThreadLocalArray(channel); + if (prev is not null) + { + PerCoreLockedStacks stackBucket = _buckets[bucketIndex] ?? CreatePerCoreLockedStacks(bucketIndex); + returned = stackBucket.TryPush(prev); + } + } + } + + public bool Trim() + { + int currentMilliseconds = Environment.TickCount; + GCHelper.MemoryPressure pressure = GCHelper.GetMemoryPressure(); + + // Trim each of the per-core buckets. + PerCoreLockedStacks?[] perCoreBuckets = _buckets; + for (int i = 0; i < perCoreBuckets.Length; i++) + { + perCoreBuckets[i]?.Trim(currentMilliseconds, Id, pressure, GCHelper.GetMaxSizeForBucket(i)); + } + + // Trim each of the TLS buckets. Note that threads may be modifying their TLS slots concurrently with + // this trimming happening. We do not force synchronization with those operations, so we accept the fact + // that we may end up firing a trimming event even if an array wasn't trimmed, and potentially + // trim an array we didn't need to. Both of these should be rare occurrences. + + // Under high pressure, release all thread locals. + if (pressure == GCHelper.MemoryPressure.High) { + foreach (KeyValuePair tlsBuckets in _allTlsBuckets) + { +#if NET6_0_OR_GREATER + Array.Clear(tlsBuckets.Key); +#else + tlsBuckets.Key.AsSpan().Clear(); +#endif + } + } + else + { + // Otherwise, release thread locals based on how long we've observed them to be stored. This time is + // approximate, with the time set not when the array is stored but when we see it during a Trim, so it + // takes at least two Trim calls (and thus two gen2 GCs) to drop an array, unless we're in high memory + // pressure. These values have been set arbitrarily; we could tune them in the future. + uint millisecondsThreshold = pressure switch + { + GCHelper.MemoryPressure.Medium => 15_000, + _ => 30_000, + }; + + foreach (KeyValuePair tlsBuckets in _allTlsBuckets) + { + ThreadLocalArray[] buckets = tlsBuckets.Key; + for (int i = 0; i < buckets.Length; i++) + { + if (buckets[i].Channel is null) + { + continue; + } + + // We treat 0 to mean it hasn't yet been seen in a Trim call. In the very rare case where Trim records 0, + // it'll take an extra Trim call to remove the array. + int lastSeen = buckets[i].MillisecondsTimeStamp; + if (lastSeen == 0) + { + buckets[i].MillisecondsTimeStamp = currentMilliseconds; + } + else if ((currentMilliseconds - lastSeen) >= millisecondsThreshold) + { + // Time noticeably wrapped, or we've surpassed the threshold. + // Clear out the array, and log its being trimmed if desired. + Interlocked.Exchange(ref buckets[i].Channel, null); + } + } + } + } + + return true; + } + + private ThreadLocalArray[] InitializeTlsBucketsAndTrimming() + { + Debug.Assert(t_tlsBuckets is null, $"Non-null {nameof(t_tlsBuckets)}"); + + var tlsBuckets = new ThreadLocalArray[NumBuckets]; + t_tlsBuckets = tlsBuckets; + + _allTlsBuckets.Add(tlsBuckets, null); + if (Interlocked.Exchange(ref _trimCallbackCreated, 1) == 0) + { + Gen2GcCallback.Register(s => ((TlsOverPerCoreLockedStacksBoundedChannelPool)s).Trim(), this); + } + + return tlsBuckets; + } + + /// Stores a set of stacks of arrays, with one stack per core. + private sealed class PerCoreLockedStacks + { + /// Number of locked stacks to employ. + private static readonly int s_lockedStackCount = Math.Min(Environment.ProcessorCount, MaxPerCorePerArraySizeStacks); + /// The stacks. + private readonly LockedStack[] _perCoreStacks; + + /// Initializes the stacks. + public PerCoreLockedStacks() + { + // Create the stacks. We create as many as there are processors, limited by our max. + var stacks = new LockedStack[s_lockedStackCount]; + for (int i = 0; i < stacks.Length; i++) + { + stacks[i] = new LockedStack(); + } + _perCoreStacks = stacks; + } + + /// Try to push the array into the stacks. If each is full when it's tested, the array will be dropped. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryPush(BoundedChannel array) + { + // Try to push on to the associated stack first. If that fails, + // round-robin through the other stacks. + LockedStack[] stacks = _perCoreStacks; + int index = (int)((uint)Thread.GetCurrentProcessorId() % (uint)s_lockedStackCount); // mod by constant in tier 1 + for (int i = 0; i < stacks.Length; i++) + { + if (stacks[index].TryPush(array)) return true; + if (++index == stacks.Length) index = 0; + } + + return false; + } + + /// Try to get an array from the stacks. If each is empty when it's tested, null will be returned. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundedChannel? TryPop() + { + // Try to pop from the associated stack first. If that fails, round-robin through the other stacks. + BoundedChannel? arr; + LockedStack[] stacks = _perCoreStacks; + int index = (int)((uint)Thread.GetCurrentProcessorId() % (uint)s_lockedStackCount); // mod by constant in tier 1 + for (int i = 0; i < stacks.Length; i++) + { + if ((arr = stacks[index].TryPop()) is not null) return arr; + if (++index == stacks.Length) index = 0; + } + return null; + } + + public void Trim(int currentMilliseconds, int id, GCHelper.MemoryPressure pressure, int bucketSize) + { + LockedStack[] stacks = _perCoreStacks; + for (int i = 0; i < stacks.Length; i++) + { + stacks[i].Trim(currentMilliseconds, id, pressure, bucketSize); + } + } + } + + /// Provides a simple, bounded stack of arrays, protected by a lock. + private sealed class LockedStack + { + /// The arrays in the stack. + private readonly BoundedChannel?[] _arrays = new BoundedChannel[MaxBuffersPerArraySizePerCore]; + /// Number of arrays stored in . + private int _count; + /// Timestamp set by Trim when it sees this as 0. + private int _millisecondsTimestamp; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryPush(BoundedChannel array) + { + bool enqueued = false; + Monitor.Enter(this); + BoundedChannel?[] arrays = _arrays; + int count = _count; + if ((uint)count < (uint)arrays.Length) + { + if (count == 0) + { + // Reset the time stamp now that we're transitioning from empty to non-empty. + // Trim will see this as 0 and initialize it to the current time when Trim is called. + _millisecondsTimestamp = 0; + } + + arrays[count] = array; + _count = count + 1; + enqueued = true; + } + Monitor.Exit(this); + return enqueued; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundedChannel? TryPop() + { + BoundedChannel? arr = null; + Monitor.Enter(this); + BoundedChannel?[] arrays = _arrays; + int count = _count - 1; + if ((uint)count < (uint)arrays.Length) + { + arr = arrays[count]; + arrays[count] = null; + _count = count; + } + Monitor.Exit(this); + return arr; + } + + public void Trim(int currentMilliseconds, int id, GCHelper.MemoryPressure pressure, int bucketSize) + { + const int StackTrimAfterMS = 60 * 1000; // Trim after 60 seconds for low/moderate pressure + const int StackHighTrimAfterMS = 10 * 1000; // Trim after 10 seconds for high pressure + const int StackLowTrimCount = 1; // Trim one item when pressure is low + const int StackMediumTrimCount = 2; // Trim two items when pressure is moderate + const int StackHighTrimCount = MaxBuffersPerArraySizePerCore; // Trim all items when pressure is high + const int StackLargeBucket = 16384; // If the bucket is larger than this we'll trim an extra when under high pressure + const int StackModerateTypeSize = 16; // If T is larger than this we'll trim an extra when under high pressure + const int StackLargeTypeSize = 32; // If T is larger than this we'll trim an extra (additional) when under high pressure + + if (_count == 0) + { + return; + } + + int trimMilliseconds = pressure == GCHelper.MemoryPressure.High ? StackHighTrimAfterMS : StackTrimAfterMS; + + lock (this) + { + if (_count == 0) + { + return; + } + + if (_millisecondsTimestamp == 0) + { + _millisecondsTimestamp = currentMilliseconds; + return; + } + + if ((currentMilliseconds - _millisecondsTimestamp) <= trimMilliseconds) + { + return; + } + + // We've elapsed enough time since the first item went into the stack. + // Drop the top item so it can be collected and make the stack look a little newer. + + int trimCount = StackLowTrimCount; + switch (pressure) + { + case GCHelper.MemoryPressure.High: + trimCount = StackHighTrimCount; + + // When pressure is high, aggressively trim larger arrays. + if (bucketSize > StackLargeBucket) + { + trimCount++; + } + if (Unsafe.SizeOf() > StackModerateTypeSize) + { + trimCount++; + } + if (Unsafe.SizeOf() > StackLargeTypeSize) + { + trimCount++; + } + break; + + case GCHelper.MemoryPressure.Medium: + trimCount = StackMediumTrimCount; + break; + } + + while (_count > 0 && trimCount-- > 0) + { + BoundedChannel? array = _arrays[--_count]; + Debug.Assert(array is not null, "No nulls should have been present in slots < _count."); + _arrays[_count] = null; + } + + _millisecondsTimestamp = _count > 0 ? + _millisecondsTimestamp + (trimMilliseconds / 4) : // Give the remaining items a bit more time + 0; + } + } + } + + /// Wrapper for arrays stored in ThreadStatic buckets. + private struct ThreadLocalArray + { + /// The stored array. + public BoundedChannel? Channel; + /// Environment.TickCount timestamp for when this array was observed by Trim. + public int MillisecondsTimeStamp; + + public ThreadLocalArray(BoundedChannel channel) + { + Channel = channel; + MillisecondsTimeStamp = 0; + } + } + + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowChannelNull() { + throw new ArgumentNullException("channel"); + } + + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowChannelNotOfPool() { + throw new ArgumentException("The channel does not belong to the bool", "channel"); + } + + [DoesNotReturn, DebuggerStepThrough, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowChannelCompleted() { + throw new ArgumentException("Cannot add a completed channel to the pool", "channel"); + } +} + From 8128e189b4fc11caa228a25c2f208b442ff2c122 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 23:03:10 +0100 Subject: [PATCH 63/87] Simplify preprocessor directives --- src/Common/BitOperations.cs | 2 +- src/Common/CallerArgumentExpressionAttribute.cs | 2 +- src/Common/GCHelper.cs | 2 +- src/Common/IsExternalInit.cs | 2 +- src/Common/NullableAttributes.cs | 2 +- src/Common/StringHelper.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Common/BitOperations.cs b/src/Common/BitOperations.cs index 2289545d..3ffd9620 100644 --- a/src/Common/BitOperations.cs +++ b/src/Common/BitOperations.cs @@ -1,5 +1,5 @@ // BitOperations without intrinsics for netstandard21 support -#if !NETCOREAPP3_0_OR_GREATER || !NET5_0_OR_GREATER +#if !NETCOREAPP3_0_OR_GREATER using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/src/Common/CallerArgumentExpressionAttribute.cs b/src/Common/CallerArgumentExpressionAttribute.cs index 6f7f11b7..e853763a 100644 --- a/src/Common/CallerArgumentExpressionAttribute.cs +++ b/src/Common/CallerArgumentExpressionAttribute.cs @@ -1,5 +1,5 @@ // ReSharper disable CheckNamespace -#if !(NET6_0 || NET_5_0 || NET5_0_OR_GREATER || NETCOREAPP3_0_OR_GREATER) +#if !NETCOREAPP3_0_OR_GREATER #pragma warning disable IDE0130 namespace System.Runtime.CompilerServices; diff --git a/src/Common/GCHelper.cs b/src/Common/GCHelper.cs index 408117d5..93d130d9 100644 --- a/src/Common/GCHelper.cs +++ b/src/Common/GCHelper.cs @@ -35,7 +35,7 @@ internal enum MemoryPressure internal static MemoryPressure GetMemoryPressure() { -#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER +#if NETCOREAPP3_0_OR_GREATER const double HighPressureThreshold = .90; // Percent of GC memory pressure threshold we consider "high" const double MediumPressureThreshold = .70; // Percent of GC memory pressure threshold we consider "medium" GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); diff --git a/src/Common/IsExternalInit.cs b/src/Common/IsExternalInit.cs index 4129a311..7f1adb48 100644 --- a/src/Common/IsExternalInit.cs +++ b/src/Common/IsExternalInit.cs @@ -1,5 +1,5 @@ // ReSharper disable CheckNamespace -#if !(NET6_0 || NET_5_0 || NET5_0_OR_GREATER) +#if !NET5_0_OR_GREATER #pragma warning disable IDE0130 namespace System.Runtime.CompilerServices; diff --git a/src/Common/NullableAttributes.cs b/src/Common/NullableAttributes.cs index 673fab35..bf53cdf6 100644 --- a/src/Common/NullableAttributes.cs +++ b/src/Common/NullableAttributes.cs @@ -1,5 +1,5 @@ // ReSharper disable CheckNamespace -#if !(NET6_0 || NET_5_0 || NET5_0_OR_GREATER) +#if !NET5_0_OR_GREATER #pragma warning disable IDE0130 namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Common/StringHelper.cs b/src/Common/StringHelper.cs index 7952c0c0..4ec80848 100644 --- a/src/Common/StringHelper.cs +++ b/src/Common/StringHelper.cs @@ -16,7 +16,7 @@ public static bool IsEmpty([NotNullWhen(false)] this string? str) { } public static string Concat(ReadOnlySpan p0, ReadOnlySpan p1, ReadOnlySpan p2 = default, ReadOnlySpan p3 = default) { -#if NET5_0_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP3_0_OR_GREATER return String.Concat(p0, p1, p2, p3); #else int cap = p0.Length + p1.Length + p2.Length + p3.Length; From b8d2a71e28455c9e546c5c8ac73dddc778a4b7fa Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 23:03:46 +0100 Subject: [PATCH 64/87] Mark Common as CLSCompliant --- src/Common/AssemblyInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Common/AssemblyInfo.cs b/src/Common/AssemblyInfo.cs index 8cfad412..4b93d270 100644 --- a/src/Common/AssemblyInfo.cs +++ b/src/Common/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; +[assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("SurrealDB.Abstractions")] [assembly: InternalsVisibleTo("SurrealDB.Configuration")] [assembly: InternalsVisibleTo("SurrealDB.Driver.Rest")] From 21c03f42b8bba1a337bf5efcf8ca9ae4267f9fec Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 23:19:36 +0100 Subject: [PATCH 65/87] Use BoundChannelPool in WsMessageReader --- src/Ws/WsMessageReader.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsMessageReader.cs index 68dc56ef..0a39a746 100644 --- a/src/Ws/WsMessageReader.cs +++ b/src/Ws/WsMessageReader.cs @@ -1,20 +1,24 @@ +using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Threading.Channels; +using Microsoft.IO; + using SurrealDB.Common; namespace SurrealDB.Ws; public sealed class WsMessageReader : Stream { - private readonly Channel _channel = Channel.CreateUnbounded(); - private readonly MemoryStream _stream; + private readonly BoundedChannel _channel; + private readonly RecyclableMemoryStream _stream; private int _endOfMessage; - internal WsMessageReader(MemoryStream stream) { - _stream = stream; + internal WsMessageReader(RecyclableMemoryStreamManager memoryManager, int channelCapacity) { + _stream = new(memoryManager); + _channel = BoundedChannelPool.Shared.Rent(channelCapacity); _endOfMessage = 0; } @@ -27,6 +31,7 @@ protected override void Dispose(bool disposing) { Interlocked.MemoryBarrierProcessWide(); _stream.Dispose(); + _channel.Dispose(); } private async ValueTask SetReceivedAsync(WebSocketReceiveResult result, CancellationToken ct) { From bf844943ab04479b106dc06d528743d014320711 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Tue, 1 Nov 2022 23:20:05 +0100 Subject: [PATCH 66/87] Add MessageChannelCapacity to options --- src/Ws/WsClient.cs | 4 ++-- src/Ws/WsClientOptions.cs | 48 ++++++++++++++++++++++++++------------- src/Ws/WsTxProducer.cs | 6 +++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 231aa404..e629dd43 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -34,9 +34,9 @@ public WsClient(WsClientOptions options) { options.ValidateAndMakeReadonly(); _memoryManager = options.MemoryManager; _rx = new(_ws, _memoryManager.BlockSize); - var tx = Channel.CreateBounded(options.ChannelTxMessagesMax); + var tx = Channel.CreateBounded(options.TxChannelCapacity); _txConsumer = new(tx.Reader, options.ReceiveHeaderBytesMax, options.RequestExpiration, TimeSpan.FromSeconds(1)); - _txProducer = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize); + _txProducer = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize, options.MessageChannelCapacity); _idBytes = options.IdBytes; } diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index 3b2e60c3..8fccede5 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -1,5 +1,3 @@ -using System.Text; - using Microsoft.IO; using SurrealDB.Common; @@ -9,18 +7,28 @@ namespace SurrealDB.Ws; public sealed record WsClientOptions : ValidateReadonly { public const int MaxArraySize = 0X7FFFFFC7; - /// The maximum number of messages in the client inbound channel. - /// This does not refer to the number of simultaneous queries, but the number of unread messages, the size of the "inbox" - /// A message may consist of multiple blocks. Only the message counts towards this number. - public int ChannelTxMessagesMax { - get => _channelTxMessagesMax; - set => Set(out _channelTxMessagesMax, in value); + /// The maximum number of pending messages in the client inbound channel. Default 1024 + /// This does not refer to the number of simultaneous queries, but the number of unread messages, the size of the "inbox". + /// A message may consist of multiple blocks. Only the message counts towards this number. + public int TxChannelCapacity { + get => _txChannelCapacity; + set => Set(out _txChannelCapacity, in value); + } + + /// The maximum number of pending blocks in a single message channel. Default 64 + /// The number of blocks of a message that can be received by the client, before they are consumed, + /// by reading from the . + /// A block have up to bytes. + public int MessageChannelCapacity { + get => _messageChannelCapacity; + set => Set(out _messageChannelCapacity, in value); } - /// The maximum number of bytes a received header can consist of. + + /// The maximum number of bytes a received header can consist of. Default 4 * 1024bytes /// The client receives a message with a and the message. /// This is the length the socket "peeks" at the beginning of the network stream, in oder to fully deserialize the or . /// The entire header must be contained within the peeked memory. - /// The length is bound to . + /// The length is bound to . /// Longer lengths introduce additional overhead. public int ReceiveHeaderBytesMax { get => _receiveHeaderBytesMax; @@ -47,8 +55,9 @@ public TimeSpan RequestExpiration { private RecyclableMemoryStreamManager? _memoryManager; private int _idBytes = 6; - private int _receiveHeaderBytesMax = 512; - private int _channelTxMessagesMax = 256; + private int _receiveHeaderBytesMax = 4 * 1024; + private int _txChannelCapacity = 1024; + private int _messageChannelCapacity = 64; private TimeSpan _requestExpiration = TimeSpan.FromSeconds(10); public void ValidateAndMakeReadonly() { @@ -59,11 +68,18 @@ public void ValidateAndMakeReadonly() { } protected override IEnumerable<(string PropertyName, string Message)> Validations() { - if (ChannelTxMessagesMax <= 0) { - yield return (nameof(ChannelTxMessagesMax), "cannot be less then or equal to zero"); + if (TxChannelCapacity <= 0) { + yield return (nameof(TxChannelCapacity), "cannot be less then or equal to zero"); + } + if (TxChannelCapacity > MaxArraySize) { + yield return (nameof(TxChannelCapacity), "cannot be greater then MaxArraySize"); + } + + if (MessageChannelCapacity <= 0) { + yield return (nameof(MessageChannelCapacity), "cannot be less then or equal to zero"); } - if (ChannelTxMessagesMax > MaxArraySize) { - yield return (nameof(ChannelTxMessagesMax), "cannot be greater then MaxArraySize"); + if (MessageChannelCapacity > MaxArraySize) { + yield return (nameof(MessageChannelCapacity), "cannot be greater then MaxArraySize"); } if (ReceiveHeaderBytesMax <= 0) { diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index b9437ef7..b4a20c64 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -20,12 +20,14 @@ public sealed class WsTxProducer : IDisposable { private Task? _execute; private readonly int _blockSize; + private readonly int _messageSize; - public WsTxProducer(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager, int blockSize) { + public WsTxProducer(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager, int blockSize, int messageSize) { _ws = ws; _out = @out; _memoryManager = memoryManager; _blockSize = blockSize; + _messageSize = messageSize; } private async Task Execute(CancellationToken ct) { @@ -45,7 +47,7 @@ private async Task Produce(CancellationToken ct, byte[] buffer) { var result = await _ws.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking - WsMessageReader msg = new(new RecyclableMemoryStream(_memoryManager)); + WsMessageReader msg = new(_memoryManager, _messageSize); // begin adding the message to the output await _out.WriteAsync(msg, ct).Inv(); From be90e1a3018efd763c4b3be7e290739b1ed41c85 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 14:12:41 +0100 Subject: [PATCH 67/87] Make retrieve private --- src/Ws/WsMessageReader.cs | 7 ++++--- src/Ws/WsTxConsumer.cs | 28 +++++++++++----------------- src/Ws/WsTxProducer.cs | 4 ++-- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsMessageReader.cs index 0a39a746..ea9646d9 100644 --- a/src/Ws/WsMessageReader.cs +++ b/src/Ws/WsMessageReader.cs @@ -41,12 +41,13 @@ private async ValueTask SetReceivedAsync(WebSocketReceiveResult result, Cancella } } - public ValueTask ReceiveAsync(CancellationToken ct) { + private ValueTask ReceiveAsync(CancellationToken ct) { return _channel.Reader.ReadAsync(ct); } - public WebSocketReceiveResult Receive(CancellationToken ct) { - return ReceiveAsync(ct).Result; + private WebSocketReceiveResult Receive(CancellationToken ct) { + var t = ReceiveAsync(ct); + return t.IsCompleted ? t.Result : t.AsTask().Result; } internal ValueTask AppendResultAsync(ReadOnlyMemory buffer, WebSocketReceiveResult result, CancellationToken ct) { diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index ddd6f231..43386076 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -38,11 +39,13 @@ private async Task Consume(CancellationToken ct) { var message = await _in.ReadAsync(ct).Inv(); // receive the first part of the message - var result = await message.ReceiveAsync(ct).Inv(); - // throw if the result is a close ack - result.ThrowIfClose(); - // parse the header from the message - WsHeader header = PeekHeader(message, result.Count); + var bytes = ArrayPool.Shared.Rent(MaxHeaderBytes); + int read = await message.ReadAsync(bytes, ct).Inv(); + // peek instead of reading + message.Position = 0; + Debug.Assert(read == bytes.Length); + var header = HeaderHelper.Parse(bytes); + ArrayPool.Shared.Return(bytes); // find the handler string? id = header.Id; @@ -53,28 +56,19 @@ private async Task Consume(CancellationToken ct) { } // dispatch the message to the handler + bool persist = handler.Persistent; try { handler.Dispatch(new(header, message)); } catch (OperationCanceledException) { // handler is canceled -> unregister - Unregister(handler.Id); + persist = false; } - if (!handler.Persistent) { - // handler is only used once -> unregister + if (!persist) { Unregister(handler.Id); } } - private WsHeader PeekHeader(Stream stream, int seekLength) { - Span bytes = stackalloc byte[Math.Min(MaxHeaderBytes, seekLength)]; - int read = stream.Read(bytes); - // peek instead of reading - stream.Position = 0; - Debug.Assert(read == bytes.Length); - return HeaderHelper.Parse(bytes); - } - public void Unregister(string id) { if (_handlers.TryRemove(id, out var handler)) { diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index b4a20c64..ca865c64 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -35,14 +35,14 @@ private async Task Execute(CancellationToken ct) { while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(_blockSize); try { - await Produce(ct, buffer).Inv(); + await Produce(buffer, ct).Inv(); } finally { ArrayPool.Shared.Return(buffer); } } } - private async Task Produce(CancellationToken ct, byte[] buffer) { + private async Task Produce(byte[] buffer, CancellationToken ct) { // receive the first part var result = await _ws.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream From 4b3422269fb4f03b3b6d4f6a9898c0043dfca80f Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 14:25:26 +0100 Subject: [PATCH 68/87] Remove redundant assertion --- src/Ws/WsTxConsumer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 43386076..541ecda3 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -43,7 +43,8 @@ private async Task Consume(CancellationToken ct) { int read = await message.ReadAsync(bytes, ct).Inv(); // peek instead of reading message.Position = 0; - Debug.Assert(read == bytes.Length); + // parse the header portion of the stream, without reading the `result` property. + // the header is a sum-type of all possible headers. var header = HeaderHelper.Parse(bytes); ArrayPool.Shared.Return(bytes); From 69902423470d743b39fb29f16e9bdc49243e1927 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 14:43:50 +0100 Subject: [PATCH 69/87] Pass cancellation token to final block --- src/Ws/WsRx.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Ws/WsRx.cs b/src/Ws/WsRx.cs index 360c75ca..2b2ddc19 100644 --- a/src/Ws/WsRx.cs +++ b/src/Ws/WsRx.cs @@ -34,8 +34,7 @@ private async Task Consume(BufferStreamReader reader, CancellationToken ct) { if (!isFinalBlock) { // ensure that the message is always terminated - // no not pass a CancellationToken - await _ws.SendAsync(default, WebSocketMessageType.Text, true, default).Inv(); + await _ws.SendAsync(default, WebSocketMessageType.Text, true, ct).Inv(); } } } From 26cdb4936c937bca2a4fcac4a8e2f673195826a7 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 14:55:28 +0100 Subject: [PATCH 70/87] Delay waiting for channel to end of message --- src/Ws/WsTxProducer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsTxProducer.cs index ca865c64..4220eb05 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsTxProducer.cs @@ -49,7 +49,7 @@ private async Task Produce(byte[] buffer, CancellationToken ct) { // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsMessageReader msg = new(_memoryManager, _messageSize); // begin adding the message to the output - await _out.WriteAsync(msg, ct).Inv(); + var writeOutput = _out.WriteAsync(msg, ct); await msg.AppendResultAsync(buffer, result, ct).Inv(); @@ -60,7 +60,7 @@ private async Task Produce(byte[] buffer, CancellationToken ct) { } // finish adding the message to the output - //await writeOutput; + await writeOutput.Inv(); } From 48fc6351164755270d1a35b184dc452d43a4b3e5 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 14:56:45 +0100 Subject: [PATCH 71/87] Slice buffer before parsing json --- src/Ws/WsTxConsumer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsTxConsumer.cs index 541ecda3..298b152d 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsTxConsumer.cs @@ -45,7 +45,7 @@ private async Task Consume(CancellationToken ct) { message.Position = 0; // parse the header portion of the stream, without reading the `result` property. // the header is a sum-type of all possible headers. - var header = HeaderHelper.Parse(bytes); + var header = HeaderHelper.Parse(bytes.AsSpan(0, read)); ArrayPool.Shared.Return(bytes); // find the handler From d060cdf5d57fd1bdec379f5dad1d3dae7bbe93b6 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 15:03:03 +0100 Subject: [PATCH 72/87] Return obvious invalid state instead of throwing when failing to parse header --- src/Ws/HeaderHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index 4e7ae8a6..b04e2f26 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -25,7 +25,7 @@ public static WsHeader Parse(ReadOnlySpan utf8) { return new(default, nty, (int)ntyOff); } - throw new JsonException($"Failed to parse RspHeader or NotifyHeader: {rspErr} \n--AND--\n {ntyErr}", null, 0, Math.Max(rspOff, ntyOff)); + return default; } } From be8e40e0a6f6bee5873745a96b730e08b366766b Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:02:30 +0100 Subject: [PATCH 73/87] Rename classes --- src/Common/TaskExtensions.cs | 37 +++++++++++++++++++ src/Ws/Handler.cs | 4 +- src/Ws/HeaderHelper.cs | 2 +- src/Ws/WsClient.cs | 30 +++++++-------- src/Ws/WsClientOptions.cs | 2 +- ...{WsTxConsumer.cs => WsReceiverDeflater.cs} | 10 +++-- ...{WsTxProducer.cs => WsReceiverInflater.cs} | 20 ++++------ ...geReader.cs => WsReceiverMessageReader.cs} | 10 ++++- src/Ws/{WsRx.cs => WsTransmitter.cs} | 4 +- 9 files changed, 80 insertions(+), 39 deletions(-) rename src/Ws/{WsTxConsumer.cs => WsReceiverDeflater.cs} (87%) rename src/Ws/{WsTxProducer.cs => WsReceiverInflater.cs} (84%) rename src/Ws/{WsMessageReader.cs => WsReceiverMessageReader.cs} (93%) rename src/Ws/{WsRx.cs => WsTransmitter.cs} (92%) diff --git a/src/Common/TaskExtensions.cs b/src/Common/TaskExtensions.cs index ed93b743..9661c73e 100644 --- a/src/Common/TaskExtensions.cs +++ b/src/Common/TaskExtensions.cs @@ -19,4 +19,41 @@ public static class TaskExtensions { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ConfiguredValueTaskAwaitable Inv(in this ValueTask t) => t.ConfigureAwait(false); + + /// Creates a task awaiting the . + /// the handle is null + public static Task ToTask(this WaitHandle handle) + { + if (handle == null) { + throw new ArgumentNullException(nameof(handle)); + } + + TaskCompletionSource tcs = new(); + RegisteredWaitHandle? shared = null; + RegisteredWaitHandle produced = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => + { + tcs.SetResult(null); + + while (true) + { + RegisteredWaitHandle? consumed = Interlocked.CompareExchange(ref shared, null, null); + if (consumed is not null) + { + consumed.Unregister(null); + break; + } + } + }, + state: null, + millisecondsTimeOutInterval: Timeout.Infinite, + executeOnlyOnce: true); + + // Publish the RegisteredWaitHandle so that the callback can see it. + Interlocked.CompareExchange(ref shared, produced, null); + + return tcs.Task; + } + } diff --git a/src/Ws/Handler.cs b/src/Ws/Handler.cs index 80807bf4..c869ad1f 100644 --- a/src/Ws/Handler.cs +++ b/src/Ws/Handler.cs @@ -37,10 +37,10 @@ public void Dispose() { } internal class NotificationHandler : IHandler, IAsyncEnumerable { - private WsTxConsumer _mediator; + private WsReceiverDeflater _mediator; private readonly CancellationToken _ct; private TaskCompletionSource _tcs = new(); - public NotificationHandler(WsTxConsumer mediator, string id, CancellationToken ct) { + public NotificationHandler(WsReceiverDeflater mediator, string id, CancellationToken ct) { _mediator = mediator; Id = id; _ct = ct; diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index b04e2f26..62c136ad 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -37,7 +37,7 @@ public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int }; } -public readonly record struct WsHeaderWithMessage(WsHeader Header, WsMessageReader Message) : IDisposable { +public readonly record struct WsHeaderWithMessage(WsHeader Header, WsReceiverMessageReader Message) : IDisposable { public void Dispose() { Message.Dispose(); } diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index e629dd43..16841631 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -20,9 +20,9 @@ public sealed class WsClient : IDisposable { private readonly ClientWebSocket _ws = new(); private readonly RecyclableMemoryStreamManager _memoryManager; - private readonly WsRx _rx; - private readonly WsTxConsumer _txConsumer; - private readonly WsTxProducer _txProducer; + private readonly WsTransmitter _transmitter; + private readonly WsReceiverDeflater _deflater; + private readonly WsReceiverInflater _inflater; private readonly int _idBytes; @@ -33,10 +33,10 @@ public WsClient() public WsClient(WsClientOptions options) { options.ValidateAndMakeReadonly(); _memoryManager = options.MemoryManager; - _rx = new(_ws, _memoryManager.BlockSize); - var tx = Channel.CreateBounded(options.TxChannelCapacity); - _txConsumer = new(tx.Reader, options.ReceiveHeaderBytesMax, options.RequestExpiration, TimeSpan.FromSeconds(1)); - _txProducer = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize, options.MessageChannelCapacity); + _transmitter = new(_ws, _memoryManager.BlockSize); + var tx = Channel.CreateBounded(options.TxChannelCapacity); + _deflater = new(tx.Reader, options.ReceiveHeaderBytesMax, options.RequestExpiration, TimeSpan.FromSeconds(1)); + _inflater = new(_ws, tx.Writer, _memoryManager, _memoryManager.BlockSize, options.MessageChannelCapacity); _idBytes = options.IdBytes; } @@ -50,8 +50,8 @@ public WsClient(WsClientOptions options) { public async Task OpenAsync(Uri url, CancellationToken ct = default) { ThrowIfConnected(); await _ws.ConnectAsync(url, ct).Inv(); - _txConsumer.Open(); - _txProducer.Open(); + _deflater.Open(); + _inflater.Open(); } /// @@ -60,14 +60,14 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { public async Task CloseAsync(CancellationToken ct = default) { ThrowIfDisconnected(); await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); - await _txConsumer.Close().Inv(); - await _txProducer.Close().Inv(); + await _deflater.Close().Inv(); + await _inflater.Close().Inv(); } /// public void Dispose() { - _txConsumer.Dispose(); - _txProducer.Dispose(); + _deflater.Dispose(); + _inflater.Dispose(); _ws.Dispose(); } @@ -81,12 +81,12 @@ public async Task Send(Request req, CancellationToken ct = default) { // listen for the response ResponseHandler handler = new(req.id, ct); - if (!_txConsumer.RegisterOrGet(handler)) { + if (!_deflater.RegisterOrGet(handler)) { return default; } // send request var stream = await SerializeAsync(req, ct).Inv(); - await _rx.SendAsync(stream, ct).Inv(); + await _transmitter.SendAsync(stream, ct).Inv(); // await response, dispose message when done using var response = await handler.Task.Inv(); // validate header diff --git a/src/Ws/WsClientOptions.cs b/src/Ws/WsClientOptions.cs index 8fccede5..8f63f56c 100644 --- a/src/Ws/WsClientOptions.cs +++ b/src/Ws/WsClientOptions.cs @@ -17,7 +17,7 @@ public int TxChannelCapacity { /// The maximum number of pending blocks in a single message channel. Default 64 /// The number of blocks of a message that can be received by the client, before they are consumed, - /// by reading from the . + /// by reading from the . /// A block have up to bytes. public int MessageChannelCapacity { get => _messageChannelCapacity; diff --git a/src/Ws/WsTxConsumer.cs b/src/Ws/WsReceiverDeflater.cs similarity index 87% rename from src/Ws/WsTxConsumer.cs rename to src/Ws/WsReceiverDeflater.cs index 298b152d..e06f99fb 100644 --- a/src/Ws/WsTxConsumer.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -8,15 +8,15 @@ namespace SurrealDB.Ws; -/// Listens for s and dispatches them by their headers to different s. -internal sealed class WsTxConsumer : IDisposable { - private readonly ChannelReader _in; +/// Listens for s and dispatches them by their headers to different s. +internal sealed class WsReceiverDeflater : IDisposable { + private readonly ChannelReader _in; private readonly DisposingCache _handlers; private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; - public WsTxConsumer(ChannelReader channel, int maxHeaderBytes, TimeSpan cacheSlidingExpiration, TimeSpan cacheEvictionInterval) { + public WsReceiverDeflater(ChannelReader channel, int maxHeaderBytes, TimeSpan cacheSlidingExpiration, TimeSpan cacheEvictionInterval) { _in = channel; MaxHeaderBytes = maxHeaderBytes; _handlers = new(cacheSlidingExpiration, cacheEvictionInterval); @@ -32,6 +32,7 @@ private async Task Execute(CancellationToken ct) { while (!ct.IsCancellationRequested) { await Consume(ct).Inv(); + ct.ThrowIfCancellationRequested(); } } @@ -47,6 +48,7 @@ private async Task Consume(CancellationToken ct) { // the header is a sum-type of all possible headers. var header = HeaderHelper.Parse(bytes.AsSpan(0, read)); ArrayPool.Shared.Return(bytes); + ct.ThrowIfCancellationRequested(); // find the handler string? id = header.Id; diff --git a/src/Ws/WsTxProducer.cs b/src/Ws/WsReceiverInflater.cs similarity index 84% rename from src/Ws/WsTxProducer.cs rename to src/Ws/WsReceiverInflater.cs index 4220eb05..a6fe79e5 100644 --- a/src/Ws/WsTxProducer.cs +++ b/src/Ws/WsReceiverInflater.cs @@ -11,9 +11,9 @@ namespace SurrealDB.Ws; /// Receives messages from a websocket server and passes them to a channel -public sealed class WsTxProducer : IDisposable { +public sealed class WsReceiverInflater : IDisposable { private readonly ClientWebSocket _ws; - private readonly ChannelWriter _out; + private readonly ChannelWriter _out; private readonly RecyclableMemoryStreamManager _memoryManager; private readonly object _lock = new(); private CancellationTokenSource? _cts; @@ -22,7 +22,7 @@ public sealed class WsTxProducer : IDisposable { private readonly int _blockSize; private readonly int _messageSize; - public WsTxProducer(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager, int blockSize, int messageSize) { + public WsReceiverInflater(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager, int blockSize, int messageSize) { _ws = ws; _out = @out; _memoryManager = memoryManager; @@ -34,11 +34,8 @@ private async Task Execute(CancellationToken ct) { Debug.Assert(ct.CanBeCanceled); while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(_blockSize); - try { - await Produce(buffer, ct).Inv(); - } finally { - ArrayPool.Shared.Return(buffer); - } + await Produce(buffer, ct).Inv(); + ct.ThrowIfCancellationRequested(); } } @@ -47,7 +44,7 @@ private async Task Produce(byte[] buffer, CancellationToken ct) { var result = await _ws.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking - WsMessageReader msg = new(_memoryManager, _messageSize); + WsReceiverMessageReader msg = new(_memoryManager, _messageSize); // begin adding the message to the output var writeOutput = _out.WriteAsync(msg, ct); @@ -79,13 +76,12 @@ public void Open() { } public async Task Close() { - Task task; ThrowIfDisconnected(); + Task task = _execute; lock (_lock) { if (!Connected) { return; } - task = _execute; _cts.Cancel(); _cts.Dispose(); // not relly needed here _cts = null; @@ -97,7 +93,7 @@ public async Task Close() { } catch (OperationCanceledException) { // expected on close using cts } catch (WebSocketException) { - // expected when the socket is closed before the receiver + // expected on abort } } diff --git a/src/Ws/WsMessageReader.cs b/src/Ws/WsReceiverMessageReader.cs similarity index 93% rename from src/Ws/WsMessageReader.cs rename to src/Ws/WsReceiverMessageReader.cs index ea9646d9..df9d7bd1 100644 --- a/src/Ws/WsMessageReader.cs +++ b/src/Ws/WsReceiverMessageReader.cs @@ -11,12 +11,12 @@ namespace SurrealDB.Ws; -public sealed class WsMessageReader : Stream { +public sealed class WsReceiverMessageReader : Stream { private readonly BoundedChannel _channel; private readonly RecyclableMemoryStream _stream; private int _endOfMessage; - internal WsMessageReader(RecyclableMemoryStreamManager memoryManager, int channelCapacity) { + internal WsReceiverMessageReader(RecyclableMemoryStreamManager memoryManager, int channelCapacity) { _stream = new(memoryManager); _channel = BoundedChannelPool.Shared.Rent(channelCapacity); _endOfMessage = 0; @@ -57,6 +57,7 @@ internal ValueTask AppendResultAsync(ReadOnlyMemory buffer, WebSocketRecei _stream.Write(span); _stream.Position = pos; } + ct.ThrowIfCancellationRequested(); return SetReceivedAsync(result, ct); } @@ -103,6 +104,7 @@ private int Read(Span buffer, CancellationToken ct) { // attempt to read from present buffer read = _stream.Read(buffer); } + ct.ThrowIfCancellationRequested(); if (read == buffer.Length || HasReceivedEndOfMessage) { return read; @@ -114,6 +116,7 @@ private int Read(Span buffer, CancellationToken ct) { lock (_stream) { inc = _stream.Read(buffer.Slice(read)); } + ct.ThrowIfCancellationRequested(); Debug.Assert(inc == result.Count); read += inc; @@ -141,6 +144,7 @@ private ValueTask ReadInternalAsync(Memory buffer, CancellationToken // attempt to read from present buffer read = _stream.Read(buffer.Span); } + ct.ThrowIfCancellationRequested(); if (read == buffer.Length || HasReceivedEndOfMessage) { return new(read); @@ -156,6 +160,8 @@ private async Task ReadFromChannelAsync(Memory buffer, int read, Canc lock (_stream) { inc = _stream.Read(buffer.Span.Slice(read)); } + ct.ThrowIfCancellationRequested(); + Debug.Assert(inc == result.Count); read += inc; diff --git a/src/Ws/WsRx.cs b/src/Ws/WsTransmitter.cs similarity index 92% rename from src/Ws/WsRx.cs rename to src/Ws/WsTransmitter.cs index 2b2ddc19..e2b06ccb 100644 --- a/src/Ws/WsRx.cs +++ b/src/Ws/WsTransmitter.cs @@ -8,11 +8,11 @@ namespace SurrealDB.Ws; /// Sends messages from a channel to a websocket server. -public sealed class WsRx { +public sealed class WsTransmitter { private readonly ClientWebSocket _ws; private readonly int _blockSize; - public WsRx(ClientWebSocket ws, int blockSize) { + public WsTransmitter(ClientWebSocket ws, int blockSize) { _ws = ws; _blockSize = blockSize; } From da6fc4d20d94819f0ad86bfb64bc5d3c0ed9a264 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:17:20 +0100 Subject: [PATCH 74/87] Separate read method --- src/Ws/HeaderHelper.cs | 4 ++-- src/Ws/WsClient.cs | 4 ++-- src/Ws/WsReceiverDeflater.cs | 38 ++++++++++++++++++++---------------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index 62c136ad..6b0b3566 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -37,9 +37,9 @@ public readonly record struct WsHeader(RspHeader Response, NtyHeader Notify, int }; } -public readonly record struct WsHeaderWithMessage(WsHeader Header, WsReceiverMessageReader Message) : IDisposable { +public readonly record struct WsHeaderWithMessage(WsHeader Header, WsReceiverMessageReader Reader) : IDisposable { public void Dispose() { - Message.Dispose(); + Reader.Dispose(); } } diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 16841631..0823db8b 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -99,9 +99,9 @@ public async Task Send(Request req, CancellationToken ct = default) { } // position stream beyond header and deserialize message body - response.Message.Position = response.Header.BytesLength; + response.Reader.Position = response.Header.BytesLength; // deserialize body - var body = await JsonSerializer.DeserializeAsync(response.Message, SerializerOptions.Shared, ct).Inv(); + var body = await JsonSerializer.DeserializeAsync(response.Reader, SerializerOptions.Shared, ct).Inv(); if (body is null) { ThrowInvalidResponse(); } diff --git a/src/Ws/WsReceiverDeflater.cs b/src/Ws/WsReceiverDeflater.cs index e06f99fb..5d079cb4 100644 --- a/src/Ws/WsReceiverDeflater.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -15,15 +15,14 @@ internal sealed class WsReceiverDeflater : IDisposable { private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; + private readonly int _maxHeaderBytes; public WsReceiverDeflater(ChannelReader channel, int maxHeaderBytes, TimeSpan cacheSlidingExpiration, TimeSpan cacheEvictionInterval) { _in = channel; - MaxHeaderBytes = maxHeaderBytes; + _maxHeaderBytes = maxHeaderBytes; _handlers = new(cacheSlidingExpiration, cacheEvictionInterval); } - public int MaxHeaderBytes { get; } - [MemberNotNullWhen(true, nameof(_cts)), MemberNotNullWhen(true, nameof(_execute))] public bool Connected => _cts is not null & _execute is not null; @@ -37,31 +36,21 @@ private async Task Execute(CancellationToken ct) { } private async Task Consume(CancellationToken ct) { - var message = await _in.ReadAsync(ct).Inv(); - - // receive the first part of the message - var bytes = ArrayPool.Shared.Rent(MaxHeaderBytes); - int read = await message.ReadAsync(bytes, ct).Inv(); - // peek instead of reading - message.Position = 0; - // parse the header portion of the stream, without reading the `result` property. - // the header is a sum-type of all possible headers. - var header = HeaderHelper.Parse(bytes.AsSpan(0, read)); - ArrayPool.Shared.Return(bytes); + var message = await ReadAsync(ct).Inv(); ct.ThrowIfCancellationRequested(); // find the handler - string? id = header.Id; + string? id = message.Header.Id; if (id is null || !_handlers.TryGetValue(id, out var handler)) { // invalid format, or no registered -> discard message - await message.DisposeAsync().Inv(); + await message.Reader.DisposeAsync().Inv(); return; } // dispatch the message to the handler bool persist = handler.Persistent; try { - handler.Dispatch(new(header, message)); + handler.Dispatch(message); } catch (OperationCanceledException) { // handler is canceled -> unregister persist = false; @@ -83,6 +72,21 @@ public bool RegisterOrGet(IHandler handler) { return _handlers.TryAdd(handler.Id, handler); } + private async Task ReadAsync(CancellationToken ct) { + var message = await _in.ReadAsync(ct).Inv(); + + // receive the first part of the message + var bytes = ArrayPool.Shared.Rent(_maxHeaderBytes); + int read = await message.ReadAsync(bytes, ct).Inv(); + // peek instead of reading + message.Position = 0; + // parse the header portion of the stream, without reading the `result` property. + // the header is a sum-type of all possible headers. + var header = HeaderHelper.Parse(bytes.AsSpan(0, read)); + ArrayPool.Shared.Return(bytes); + return new(header, message); + } + public void Open() { ThrowIfConnected(); lock (_lock) { From 4ac56d9988130daaf0fd3ead6522f8351e76e38d Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:19:49 +0100 Subject: [PATCH 75/87] Refactoring --- src/Ws/WsReceiverDeflater.cs | 6 +++--- src/Ws/WsReceiverInflater.cs | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Ws/WsReceiverDeflater.cs b/src/Ws/WsReceiverDeflater.cs index 5d079cb4..404742df 100644 --- a/src/Ws/WsReceiverDeflater.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -10,7 +10,7 @@ namespace SurrealDB.Ws; /// Listens for s and dispatches them by their headers to different s. internal sealed class WsReceiverDeflater : IDisposable { - private readonly ChannelReader _in; + private readonly ChannelReader _channel; private readonly DisposingCache _handlers; private readonly object _lock = new(); private CancellationTokenSource? _cts; @@ -18,7 +18,7 @@ internal sealed class WsReceiverDeflater : IDisposable { private readonly int _maxHeaderBytes; public WsReceiverDeflater(ChannelReader channel, int maxHeaderBytes, TimeSpan cacheSlidingExpiration, TimeSpan cacheEvictionInterval) { - _in = channel; + _channel = channel; _maxHeaderBytes = maxHeaderBytes; _handlers = new(cacheSlidingExpiration, cacheEvictionInterval); } @@ -73,7 +73,7 @@ public bool RegisterOrGet(IHandler handler) { } private async Task ReadAsync(CancellationToken ct) { - var message = await _in.ReadAsync(ct).Inv(); + var message = await _channel.ReadAsync(ct).Inv(); // receive the first part of the message var bytes = ArrayPool.Shared.Rent(_maxHeaderBytes); diff --git a/src/Ws/WsReceiverInflater.cs b/src/Ws/WsReceiverInflater.cs index a6fe79e5..8cd995f6 100644 --- a/src/Ws/WsReceiverInflater.cs +++ b/src/Ws/WsReceiverInflater.cs @@ -12,8 +12,8 @@ namespace SurrealDB.Ws; /// Receives messages from a websocket server and passes them to a channel public sealed class WsReceiverInflater : IDisposable { - private readonly ClientWebSocket _ws; - private readonly ChannelWriter _out; + private readonly ClientWebSocket _socket; + private readonly ChannelWriter _channel; private readonly RecyclableMemoryStreamManager _memoryManager; private readonly object _lock = new(); private CancellationTokenSource? _cts; @@ -22,9 +22,9 @@ public sealed class WsReceiverInflater : IDisposable { private readonly int _blockSize; private readonly int _messageSize; - public WsReceiverInflater(ClientWebSocket ws, ChannelWriter @out, RecyclableMemoryStreamManager memoryManager, int blockSize, int messageSize) { - _ws = ws; - _out = @out; + public WsReceiverInflater(ClientWebSocket socket, ChannelWriter channel, RecyclableMemoryStreamManager memoryManager, int blockSize, int messageSize) { + _socket = socket; + _channel = channel; _memoryManager = memoryManager; _blockSize = blockSize; _messageSize = messageSize; @@ -41,18 +41,18 @@ private async Task Execute(CancellationToken ct) { private async Task Produce(byte[] buffer, CancellationToken ct) { // receive the first part - var result = await _ws.ReceiveAsync(buffer, ct).Inv(); + var result = await _socket.ReceiveAsync(buffer, ct).Inv(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsReceiverMessageReader msg = new(_memoryManager, _messageSize); // begin adding the message to the output - var writeOutput = _out.WriteAsync(msg, ct); + var writeOutput = _channel.WriteAsync(msg, ct); await msg.AppendResultAsync(buffer, result, ct).Inv(); while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts - result = await _ws.ReceiveAsync(buffer, ct).Inv(); + result = await _socket.ReceiveAsync(buffer, ct).Inv(); await msg.AppendResultAsync(buffer, result, ct).Inv(); } @@ -113,6 +113,6 @@ private void ThrowIfConnected() { public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); - _out.TryComplete(); + _channel.TryComplete(); } } From 79ba821f20dd8cdb7abe5752e7b6a47513e3bb3f Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:25:38 +0100 Subject: [PATCH 76/87] Remove Open and Close locks --- src/Ws/WsClient.cs | 4 ++-- src/Ws/WsReceiverDeflater.cs | 39 ++++++++++++--------------------- src/Ws/WsReceiverInflater.cs | 42 +++++++++++++----------------------- 3 files changed, 31 insertions(+), 54 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 0823db8b..3112913b 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -60,8 +60,8 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { public async Task CloseAsync(CancellationToken ct = default) { ThrowIfDisconnected(); await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); - await _deflater.Close().Inv(); - await _inflater.Close().Inv(); + _deflater.Close(); + _inflater.Close(); } /// diff --git a/src/Ws/WsReceiverDeflater.cs b/src/Ws/WsReceiverDeflater.cs index 404742df..df51e946 100644 --- a/src/Ws/WsReceiverDeflater.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -12,7 +12,6 @@ namespace SurrealDB.Ws; internal sealed class WsReceiverDeflater : IDisposable { private readonly ChannelReader _channel; private readonly DisposingCache _handlers; - private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; private readonly int _maxHeaderBytes; @@ -89,34 +88,24 @@ private async Task ReadAsync(CancellationToken ct) { public void Open() { ThrowIfConnected(); - lock (_lock) { - if (Connected) { - return; - } - _cts = new(); - _execute = Execute(_cts.Token); - } + _cts = new(); + _execute = Execute(_cts.Token); } - public async Task Close() { + public void Close() { ThrowIfDisconnected(); Task task; - lock (_lock) { - if (!Connected) { - return; - } - _cts.Cancel(); - _cts.Dispose(); // not relly needed here - _cts = null; - task = _execute; - _execute = null; - } - - try { - await task.Inv(); - } catch (OperationCanceledException) { - // expected on close using cts - } + _cts.Cancel(); + _cts.Dispose(); // not relly needed here + _cts = null; + task = _execute; + _execute = null; + + // try { + // await task.Inv(); + // } catch (OperationCanceledException) { + // // expected on close using cts + // } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] diff --git a/src/Ws/WsReceiverInflater.cs b/src/Ws/WsReceiverInflater.cs index 8cd995f6..6a0cb289 100644 --- a/src/Ws/WsReceiverInflater.cs +++ b/src/Ws/WsReceiverInflater.cs @@ -15,7 +15,6 @@ public sealed class WsReceiverInflater : IDisposable { private readonly ClientWebSocket _socket; private readonly ChannelWriter _channel; private readonly RecyclableMemoryStreamManager _memoryManager; - private readonly object _lock = new(); private CancellationTokenSource? _cts; private Task? _execute; @@ -66,35 +65,24 @@ private async Task Produce(byte[] buffer, CancellationToken ct) { public void Open() { ThrowIfConnected(); - lock (_lock) { - if (Connected) { - return; - } - _cts = new(); - _execute = Execute(_cts.Token); - } + _cts = new(); + _execute = Execute(_cts.Token); } - public async Task Close() { + public void Close() { ThrowIfDisconnected(); - Task task = _execute; - lock (_lock) { - if (!Connected) { - return; - } - _cts.Cancel(); - _cts.Dispose(); // not relly needed here - _cts = null; - _execute = null; - } - - try { - await task.Inv(); - } catch (OperationCanceledException) { - // expected on close using cts - } catch (WebSocketException) { - // expected on abort - } + _cts.Cancel(); + _cts.Dispose(); // not relly needed here + _cts = null; + _execute = null; + // + // try { + // await task.Inv(); + // } catch (OperationCanceledException) { + // // expected on close using cts + // } catch (WebSocketException) { + // // expected on abort + // } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] From ec09821f83ecb994e58fe888af3b85366db514a0 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:36:50 +0100 Subject: [PATCH 77/87] Enable async close --- src/Ws/WsClient.cs | 4 ++-- src/Ws/WsReceiverDeflater.cs | 12 ++++++------ src/Ws/WsReceiverInflater.cs | 19 ++++++++++--------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Ws/WsClient.cs b/src/Ws/WsClient.cs index 3112913b..d37b9edc 100644 --- a/src/Ws/WsClient.cs +++ b/src/Ws/WsClient.cs @@ -60,8 +60,8 @@ public async Task OpenAsync(Uri url, CancellationToken ct = default) { public async Task CloseAsync(CancellationToken ct = default) { ThrowIfDisconnected(); await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "client connection closed orderly", ct).Inv(); - _deflater.Close(); - _inflater.Close(); + await _deflater.CloseAsync().Inv(); + await _inflater.CloseAsync().Inv(); } /// diff --git a/src/Ws/WsReceiverDeflater.cs b/src/Ws/WsReceiverDeflater.cs index df51e946..66cb4189 100644 --- a/src/Ws/WsReceiverDeflater.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -92,7 +92,7 @@ public void Open() { _execute = Execute(_cts.Token); } - public void Close() { + public async Task CloseAsync() { ThrowIfDisconnected(); Task task; _cts.Cancel(); @@ -101,11 +101,11 @@ public void Close() { task = _execute; _execute = null; - // try { - // await task.Inv(); - // } catch (OperationCanceledException) { - // // expected on close using cts - // } + try { + await task.Inv(); + } catch (OperationCanceledException) { + // expected on close using cts + } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] diff --git a/src/Ws/WsReceiverInflater.cs b/src/Ws/WsReceiverInflater.cs index 6a0cb289..1b5ba65b 100644 --- a/src/Ws/WsReceiverInflater.cs +++ b/src/Ws/WsReceiverInflater.cs @@ -69,20 +69,21 @@ public void Open() { _execute = Execute(_cts.Token); } - public void Close() { + public async Task CloseAsync() { ThrowIfDisconnected(); + var task = _execute; _cts.Cancel(); _cts.Dispose(); // not relly needed here _cts = null; _execute = null; - // - // try { - // await task.Inv(); - // } catch (OperationCanceledException) { - // // expected on close using cts - // } catch (WebSocketException) { - // // expected on abort - // } + + try { + await task.Inv(); + } catch (OperationCanceledException) { + // expected on close using cts + } catch (WebSocketException) { + // expected on abort + } } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] From 34ad688e55828cd215935d0d7796b391058e9bb2 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:39:36 +0100 Subject: [PATCH 78/87] Implement IDisposable for DbHandle --- tests/Shared/DbHandle.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/Shared/DbHandle.cs b/tests/Shared/DbHandle.cs index 5e5f586d..95c43e8d 100644 --- a/tests/Shared/DbHandle.cs +++ b/tests/Shared/DbHandle.cs @@ -3,7 +3,7 @@ namespace SurrealDB.Shared.Tests; -public class DbHandle : IAsyncDisposable +public class DbHandle : IDisposable where T: IDatabase, IDisposable, new() { private Process? _process; @@ -21,24 +21,21 @@ public static async Task> Create() { [DebuggerStepThrough] public static async Task WithDatabase(Func action) { - await using DbHandle db = await Create(); + using DbHandle db = await Create(); await action(db.Database); } public T Database { get; } - public ValueTask DisposeAsync() { + public void Dispose() { Process? p = _process; _process = null; if (p is not null) { - return new(DisposeActualAsync(p)); + DisposeActual(p); } - - return default; } - private async Task DisposeActualAsync(Process p) { - //await Database.Close().Inv(); + private void DisposeActual(Process p) { Database.Dispose(); p.Kill(); } From 2d4c970855298fcd7110c6c2f4a916b4ad34bd19 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:45:43 +0100 Subject: [PATCH 79/87] Remove duplicate Close --- tests/Driver.Tests/DatabaseTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Driver.Tests/DatabaseTests.cs b/tests/Driver.Tests/DatabaseTests.cs index df43db87..439d530d 100644 --- a/tests/Driver.Tests/DatabaseTests.cs +++ b/tests/Driver.Tests/DatabaseTests.cs @@ -85,8 +85,6 @@ await db.Change( ); TestHelper.AssertOk(queryResp); - - await db.Close(); } ); } From 4fe09142b8d5ae998ffdfe6874f92568d7dcd966 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 16:48:16 +0100 Subject: [PATCH 80/87] Remove unused using directives --- src/Common/StreamExtensions.cs | 2 -- src/Driver/Rest/RestClientExtensions.cs | 1 - src/Driver/Rpc/RpcClientExtensions.cs | 1 - src/Models/Thing.cs | 4 ---- src/Ws/HeaderHelper.cs | 2 -- src/Ws/WsReceiverDeflater.cs | 1 - src/Ws/WsReceiverMessageReader.cs | 1 - src/Ws/WsTransmitter.cs | 3 --- tests/Driver.Tests/Roundtrip/RoundTripTests.cs | 2 -- tests/Shared/DbHandle.cs | 1 - 10 files changed, 18 deletions(-) diff --git a/src/Common/StreamExtensions.cs b/src/Common/StreamExtensions.cs index 43a12663..9e299425 100644 --- a/src/Common/StreamExtensions.cs +++ b/src/Common/StreamExtensions.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace SurrealDB.Common; internal static class StreamExtensions { diff --git a/src/Driver/Rest/RestClientExtensions.cs b/src/Driver/Rest/RestClientExtensions.cs index 350cdae1..45d9190e 100644 --- a/src/Driver/Rest/RestClientExtensions.cs +++ b/src/Driver/Rest/RestClientExtensions.cs @@ -5,7 +5,6 @@ using SurrealDB.Common; using SurrealDB.Json; -using SurrealDB.Models; using SurrealDB.Models.Result; using DriverResponse = SurrealDB.Models.Result.DriverResponse; diff --git a/src/Driver/Rpc/RpcClientExtensions.cs b/src/Driver/Rpc/RpcClientExtensions.cs index 6bc69691..1788182b 100644 --- a/src/Driver/Rpc/RpcClientExtensions.cs +++ b/src/Driver/Rpc/RpcClientExtensions.cs @@ -5,7 +5,6 @@ using SurrealDB.Common; using SurrealDB.Json; -using SurrealDB.Models; using SurrealDB.Models.Result; using SurrealDB.Ws; diff --git a/src/Models/Thing.cs b/src/Models/Thing.cs index 813c51ef..9b2ec3af 100644 --- a/src/Models/Thing.cs +++ b/src/Models/Thing.cs @@ -1,5 +1,3 @@ -using SurrealDB.Json; - using System.Diagnostics; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; @@ -7,8 +5,6 @@ using System.Text.Json; using System.Text.Json.Serialization; -using SurrealDB.Common; - namespace SurrealDB.Models; /// diff --git a/src/Ws/HeaderHelper.cs b/src/Ws/HeaderHelper.cs index 6b0b3566..8c9fc8f0 100644 --- a/src/Ws/HeaderHelper.cs +++ b/src/Ws/HeaderHelper.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.Net.WebSockets; using System.Text.Json; using SurrealDB.Common; diff --git a/src/Ws/WsReceiverDeflater.cs b/src/Ws/WsReceiverDeflater.cs index 66cb4189..35d88d44 100644 --- a/src/Ws/WsReceiverDeflater.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading.Channels; diff --git a/src/Ws/WsReceiverMessageReader.cs b/src/Ws/WsReceiverMessageReader.cs index df9d7bd1..1a08cbdd 100644 --- a/src/Ws/WsReceiverMessageReader.cs +++ b/src/Ws/WsReceiverMessageReader.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; using System.Runtime.CompilerServices; -using System.Threading.Channels; using Microsoft.IO; diff --git a/src/Ws/WsTransmitter.cs b/src/Ws/WsTransmitter.cs index e2b06ccb..a083c5d3 100644 --- a/src/Ws/WsTransmitter.cs +++ b/src/Ws/WsTransmitter.cs @@ -1,7 +1,4 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; -using System.Threading.Channels; using SurrealDB.Common; diff --git a/tests/Driver.Tests/Roundtrip/RoundTripTests.cs b/tests/Driver.Tests/Roundtrip/RoundTripTests.cs index 0d827678..51a2d74a 100644 --- a/tests/Driver.Tests/Roundtrip/RoundTripTests.cs +++ b/tests/Driver.Tests/Roundtrip/RoundTripTests.cs @@ -1,5 +1,3 @@ -using System.Collections; - using SurrealDB.Json; using SurrealDB.Models.Result; diff --git a/tests/Shared/DbHandle.cs b/tests/Shared/DbHandle.cs index 95c43e8d..0b6a7019 100644 --- a/tests/Shared/DbHandle.cs +++ b/tests/Shared/DbHandle.cs @@ -1,5 +1,4 @@ using SurrealDB.Abstractions; -using SurrealDB.Common; namespace SurrealDB.Shared.Tests; From 393890252f5a6fdc735bf1138654983a485c244e Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 20:37:42 +0100 Subject: [PATCH 81/87] Short-circuit reads --- src/Ws/WsReceiverMessageReader.cs | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Ws/WsReceiverMessageReader.cs b/src/Ws/WsReceiverMessageReader.cs index 1a08cbdd..f59b3ab5 100644 --- a/src/Ws/WsReceiverMessageReader.cs +++ b/src/Ws/WsReceiverMessageReader.cs @@ -21,14 +21,13 @@ internal WsReceiverMessageReader(RecyclableMemoryStreamManager memoryManager, in _endOfMessage = 0; } - public bool HasReceivedEndOfMessage => Interlocked.Add(ref _endOfMessage, 0) == 1; + public bool HasReceivedEndOfMessage => Interlocked.CompareExchange(ref _endOfMessage, 0, 0) != 0; protected override void Dispose(bool disposing) { if (!disposing) { return; } - Interlocked.MemoryBarrierProcessWide(); _stream.Dispose(); _channel.Dispose(); } @@ -68,15 +67,17 @@ internal ValueTask AppendResultAsync(ReadOnlyMemory buffer, WebSocketRecei public override bool CanWrite => false; public override long Length { get { - Interlocked.MemoryBarrierProcessWide(); - return _stream.Length; + lock (_stream) { + return _stream.Length; + } } } public override long Position { get { - Interlocked.MemoryBarrierProcessWide(); - return _stream.Position; + lock (_stream) { + return _stream.Position; + } } set { lock (_stream) { @@ -97,7 +98,11 @@ public override int Read(Span buffer) { return Read(buffer, default); } - private int Read(Span buffer, CancellationToken ct) { + public int Read(Span buffer, CancellationToken ct) { + if (0 >= (uint)buffer.Length || ct.IsCancellationRequested) { + return 0; + } + int read; lock (_stream) { // attempt to read from present buffer @@ -109,21 +114,18 @@ private int Read(Span buffer, CancellationToken ct) { return read; } - while (true) { - var result = Receive(ct); + WebSocketReceiveResult result; + do { + result = Receive(ct); int inc; lock (_stream) { inc = _stream.Read(buffer.Slice(read)); } - ct.ThrowIfCancellationRequested(); + ct.ThrowIfCancellationRequested(); Debug.Assert(inc == result.Count); read += inc; - - if (result.EndOfMessage) { - break; - } - } + } while (!result.EndOfMessage); return read; } @@ -134,10 +136,10 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel [MethodImpl(MethodImplOptions.AggressiveInlining)] public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) { - return ct.IsCancellationRequested ? new(0) : ReadInternalAsync(buffer, ct); - } + if (0 >= (uint)buffer.Length || ct.IsCancellationRequested) { + return new(0); + } - private ValueTask ReadInternalAsync(Memory buffer, CancellationToken ct) { int read; lock (_stream) { // attempt to read from present buffer @@ -153,21 +155,19 @@ private ValueTask ReadInternalAsync(Memory buffer, CancellationToken } private async Task ReadFromChannelAsync(Memory buffer, int read, CancellationToken ct) { - while (true) { - var result = await ReceiveAsync(ct).Inv(); + WebSocketReceiveResult? result; + do { + result = await ReceiveAsync(ct).Inv(); int inc; lock (_stream) { inc = _stream.Read(buffer.Span.Slice(read)); } + ct.ThrowIfCancellationRequested(); Debug.Assert(inc == result.Count); read += inc; - - if (result.EndOfMessage) { - break; - } - } + } while (!result.EndOfMessage); return read; } From f599319c5c07b8c55f4639d2024e08eeda9a0dac Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 21:08:33 +0100 Subject: [PATCH 82/87] Execute return array to pool --- src/Ws/WsReceiverInflater.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ws/WsReceiverInflater.cs b/src/Ws/WsReceiverInflater.cs index 1b5ba65b..8fe16731 100644 --- a/src/Ws/WsReceiverInflater.cs +++ b/src/Ws/WsReceiverInflater.cs @@ -34,6 +34,7 @@ private async Task Execute(CancellationToken ct) { while (!ct.IsCancellationRequested) { var buffer = ArrayPool.Shared.Rent(_blockSize); await Produce(buffer, ct).Inv(); + ArrayPool.Shared.Return(buffer); ct.ThrowIfCancellationRequested(); } } @@ -41,6 +42,7 @@ private async Task Execute(CancellationToken ct) { private async Task Produce(byte[] buffer, CancellationToken ct) { // receive the first part var result = await _socket.ReceiveAsync(buffer, ct).Inv(); + ct.ThrowIfCancellationRequested(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsReceiverMessageReader msg = new(_memoryManager, _messageSize); From 80111465a29be7525c48bfa8f353e821ef6d3c81 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 23:10:50 +0100 Subject: [PATCH 83/87] Add Diagnostics EventSource to WsReceiverDeflater --- src/Ws/Ws.csproj | 2 +- src/Ws/WsReceiverDeflater.cs | 52 ++++++++--- src/Ws/WsReceiverDeflaterEventSource.cs | 114 ++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 src/Ws/WsReceiverDeflaterEventSource.cs diff --git a/src/Ws/Ws.csproj b/src/Ws/Ws.csproj index 51ef5f4f..ba51b4d5 100644 --- a/src/Ws/Ws.csproj +++ b/src/Ws/Ws.csproj @@ -2,6 +2,7 @@ SurrealDB.Ws + true @@ -22,5 +23,4 @@ - diff --git a/src/Ws/WsReceiverDeflater.cs b/src/Ws/WsReceiverDeflater.cs index 35d88d44..96ba78aa 100644 --- a/src/Ws/WsReceiverDeflater.cs +++ b/src/Ws/WsReceiverDeflater.cs @@ -34,28 +34,42 @@ private async Task Execute(CancellationToken ct) { } private async Task Consume(CancellationToken ct) { - var message = await ReadAsync(ct).Inv(); + var log = WsReceiverDeflaterEventSource.Log; + ct.ThrowIfCancellationRequested(); + // log that we are waiting for a message from the channel + log.MessageAwaiting(); + + var message = await ReadAsync(ct).Inv(); + // log that a message has been retrieved from the channel + log.MessageReceived(message.Header.Id); + // find the handler string? id = message.Header.Id; if (id is null || !_handlers.TryGetValue(id, out var handler)) { // invalid format, or no registered -> discard message - await message.Reader.DisposeAsync().Inv(); + message.Dispose(); + // log that the message has been discarded + log.MessageDiscarded(id); return; } + Debug.Assert(id == handler.Id); // dispatch the message to the handler - bool persist = handler.Persistent; try { handler.Dispatch(message); - } catch (OperationCanceledException) { + } catch (Exception ex) { // handler is canceled -> unregister - persist = false; + Unregister(id); + // log that the dispatch has resulted in a exception + log.HandlerUnregisteredAfterException(id, ex); } - if (!persist) { - Unregister(handler.Id); + if (!handler.Persistent) { + Unregister(id); + // log that the handler has been unregistered + log.HandlerUnregisterdFleeting(id); } } @@ -86,12 +100,18 @@ private async Task ReadAsync(CancellationToken ct) { } public void Open() { + var log = WsReceiverDeflaterEventSource.Log; + ThrowIfConnected(); _cts = new(); _execute = Execute(_cts.Token); + + log.Opened(); } public async Task CloseAsync() { + var log = WsReceiverDeflaterEventSource.Log; + ThrowIfDisconnected(); Task task; _cts.Cancel(); @@ -100,11 +120,24 @@ public async Task CloseAsync() { task = _execute; _execute = null; + log.CloseBegin(); + try { await task.Inv(); } catch (OperationCanceledException) { // expected on close using cts } + + log.CloseFinish(); + } + + public void Dispose() { + var log = WsReceiverDeflaterEventSource.Log; + + _cts?.Cancel(); + _cts?.Dispose(); + + log.Disposed(); } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] @@ -119,9 +152,4 @@ private void ThrowIfConnected() { throw new InvalidOperationException("The connection is already open"); } } - - public void Dispose() { - _cts?.Cancel(); - _cts?.Dispose(); - } } diff --git a/src/Ws/WsReceiverDeflaterEventSource.cs b/src/Ws/WsReceiverDeflaterEventSource.cs new file mode 100644 index 00000000..2082d8e0 --- /dev/null +++ b/src/Ws/WsReceiverDeflaterEventSource.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; + +namespace SurrealDB.Ws; + +[EventSource(Guid = "03c50b03-e245-46e5-a99a-6eaa28990a41", Name = "SurrealDB.Ws.WsReceiverDeflaterEventSource")] +public sealed class WsReceiverDeflaterEventSource : EventSource +{ + private WsReceiverDeflaterEventSource() { } + + public static WsReceiverDeflaterEventSource Log { get; } = new(); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MessageReceived(string? messageId) { + if (IsEnabled()) { + MessageReceivedCore(messageId); + } + } + + [Event(1, Level = EventLevel.Verbose, Message = "Message (Id = {0}) pulled from channel")] + private void MessageReceivedCore(string? messageId) => WriteEvent(1, messageId); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MessageDiscarded(string? messageId) { + if (IsEnabled()) { + MessageDiscardedCore(messageId); + } + } + + [Event(2, Level = EventLevel.Warning, Message = "No handler registered for the message (Id = {0})")] + private void MessageDiscardedCore(string? messageId) => WriteEvent(2, messageId); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void HandlerUnregisteredAfterException(string handlerId, Exception ex) { + if (IsEnabled()) { + HandlerUnregisteredAfterExceptionCore(handlerId, ex.ToString()); + } + } + + [Event(3, Level = EventLevel.Error, Message = "The handler (Id = {0}) threw an exception during dispatch, and was unregistered")] + private unsafe void HandlerUnregisteredAfterExceptionCore(string handlerId, string ex) { + EventData* payload = stackalloc EventData[2]; + fixed (char* handlerIdPtr = handlerId) { + payload[0].DataPointer = (IntPtr)handlerIdPtr; + payload[0].Size = (handlerId.Length + 1) * 2; + } + + fixed (char* exPtr = ex) { + payload[1].DataPointer = (IntPtr)exPtr; + payload[1].Size = (ex.Length + 1) * 2; + } + WriteEventCore(3, 2, payload); + } + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void HandlerUnregisterdFleeting(string handlerId) { + if (IsEnabled()) { + HandlerUnregisterdFleetingCore(handlerId); + } + } + + [Event(4, Level = EventLevel.Verbose, Message = "The handler (Id = {0}) is fleeting and was unregistered after dispatch")] + private void HandlerUnregisterdFleetingCore(string handlerId) => WriteEvent(4, handlerId); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MessageAwaiting() { + if (IsEnabled()) { + MessageAwaitingCore(); + } + } + + [Event(5, Level = EventLevel.Verbose, Message = "Waiting for message to pull from channel")] + private void MessageAwaitingCore() => WriteEvent(5); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Opened() { + if (IsEnabled()) { + OpenedCore(); + } + } + + [Event(6, Level = EventLevel.Informational, Message = "Deflater opened and is now pulling from the channel")] + private void OpenedCore() => WriteEvent(6); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CloseBegin() { + if (IsEnabled()) { + CloseBeginCore(); + } + } + + [Event(7, Level = EventLevel.Informational, Message = "Deflater closed and stopped pulling from the channel")] + private void CloseBeginCore() => WriteEvent(7); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CloseFinish() { + if (IsEnabled()) { + CloseFinishCore(); + } + } + + [Event(8, Level = EventLevel.Informational, Message = "Deflater closing has finished")] + private void CloseFinishCore() => WriteEvent(8); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Disposed() { + if (IsEnabled()) { + DisposedCore(); + } + } + + [Event(9, Level = EventLevel.Informational, Message = "Deflater disposed")] + private void DisposedCore() => WriteEvent(9); +} From c574de41169ac4958104748d45fd7ef7239d47cd Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 23:37:24 +0100 Subject: [PATCH 84/87] Add Diagnostics EventSource to WsReceiverInflater --- src/Ws/WsReceiverInflater.cs | 31 ++++++- src/Ws/WsReceiverInflaterEventSource.cs | 102 ++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/Ws/WsReceiverInflaterEventSource.cs diff --git a/src/Ws/WsReceiverInflater.cs b/src/Ws/WsReceiverInflater.cs index 8fe16731..ba54e5f9 100644 --- a/src/Ws/WsReceiverInflater.cs +++ b/src/Ws/WsReceiverInflater.cs @@ -40,25 +40,38 @@ private async Task Execute(CancellationToken ct) { } private async Task Produce(byte[] buffer, CancellationToken ct) { + var log = WsReceiverInflaterEventSource.Log; + + // log that we are waiting for the socket + log.SocketWaiting(); // receive the first part var result = await _socket.ReceiveAsync(buffer, ct).Inv(); + // log that we have received a message from the socket + log.SockedReceived(result); + ct.ThrowIfCancellationRequested(); // create a new message with a RecyclableMemoryStream // use buffer instead of the build the builtin IBufferWriter, bc of thread safely issues related to locking WsReceiverMessageReader msg = new(_memoryManager, _messageSize); // begin adding the message to the output - var writeOutput = _channel.WriteAsync(msg, ct); + var push = _channel.WriteAsync(msg, ct); await msg.AppendResultAsync(buffer, result, ct).Inv(); while (!result.EndOfMessage && !ct.IsCancellationRequested) { // receive more parts result = await _socket.ReceiveAsync(buffer, ct).Inv(); + // log that we have received a message from the socket + log.SockedReceived(result); await msg.AppendResultAsync(buffer, result, ct).Inv(); } + // log that we have completely received the message + log.MessageReceiveFinished(); // finish adding the message to the output - await writeOutput.Inv(); + await push.Inv(); + // log that the message has been pushed to the channel + log.MessagePushed(); } @@ -66,12 +79,18 @@ private async Task Produce(byte[] buffer, CancellationToken ct) { public bool Connected => _cts is not null & _execute is not null; public void Open() { + var log = WsReceiverInflaterEventSource.Log; + ThrowIfConnected(); _cts = new(); _execute = Execute(_cts.Token); + + log.Opened(); } public async Task CloseAsync() { + var log = WsReceiverInflaterEventSource.Log; + ThrowIfDisconnected(); var task = _execute; _cts.Cancel(); @@ -79,6 +98,8 @@ public async Task CloseAsync() { _cts = null; _execute = null; + log.CloseBegin(); + try { await task.Inv(); } catch (OperationCanceledException) { @@ -86,6 +107,8 @@ public async Task CloseAsync() { } catch (WebSocketException) { // expected on abort } + + log.CloseFinish(); } [MemberNotNull(nameof(_cts)), MemberNotNull(nameof(_execute))] @@ -102,8 +125,12 @@ private void ThrowIfConnected() { } public void Dispose() { + var log = WsReceiverInflaterEventSource.Log; + _cts?.Cancel(); _cts?.Dispose(); _channel.TryComplete(); + + log.Disposed(); } } diff --git a/src/Ws/WsReceiverInflaterEventSource.cs b/src/Ws/WsReceiverInflaterEventSource.cs new file mode 100644 index 00000000..50f5f15b --- /dev/null +++ b/src/Ws/WsReceiverInflaterEventSource.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.Tracing; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; + +namespace SurrealDB.Ws; + +[EventSource(Guid = "03c50b03-e245-46e5-a99a-6eaa28990a41", Name = "SurrealDB.Ws.WsReceiverDeflaterEventSource")] +public sealed class WsReceiverInflaterEventSource : EventSource +{ + private WsReceiverInflaterEventSource() { } + + public static WsReceiverInflaterEventSource Log { get; } = new(); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SocketWaiting() { + if (IsEnabled()) { + SocketWaitingCore(); + } + } + + [Event(1, Level = EventLevel.Verbose, Message = "Waiting to receive a block from the socket")] + private void SocketWaitingCore() => WriteEvent(1); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SockedReceived(WebSocketReceiveResult result) { + if (IsEnabled()) { + SockedReceivedCore(result.Count, result.EndOfMessage, result.CloseStatus is not null); + } + } + + [Event(2, Level = EventLevel.Verbose, Message = "Received a block from the socket (Count = {0], EndOfMessage = {1}, Closed = {2})")] + private unsafe void SockedReceivedCore(int count, bool endOfMessage, bool closed) { + EventData* payload = stackalloc EventData[3]; + payload[0].Size = sizeof(int); + payload[0].DataPointer = (IntPtr)(&count); + payload[1].Size = sizeof(bool); + payload[1].DataPointer = (IntPtr)(&endOfMessage); + payload[2].Size = sizeof(bool); + payload[2].DataPointer = (IntPtr)(&closed); + WriteEventCore(2, 3, payload); + } + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MessageReceiveFinished() { + if (IsEnabled()) { + MessageReceiveFinishedCore(); + } + } + + [Event(3, Level = EventLevel.Verbose, Message = "Finished receiving the message from the socket")] + private void MessageReceiveFinishedCore() => WriteEvent(3); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MessagePushed() { + if (IsEnabled()) { + MessagePushedCore(); + } + } + + [Event(4, Level = EventLevel.Informational, Message = "Pushed the message to the channel")] + private void MessagePushedCore() => WriteEvent(4); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Opened() { + if (IsEnabled()) { + OpenedCore(); + } + } + + [Event(5, Level = EventLevel.Informational, Message = "Inflater opened and is now pushing to the channel")] + private void OpenedCore() => WriteEvent(5); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CloseBegin() { + if (IsEnabled()) { + CloseBeginCore(); + } + } + + [Event(6, Level = EventLevel.Informational, Message = "Inflater closed and stopped pushing to the channel")] + private void CloseBeginCore() => WriteEvent(6); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CloseFinish() { + if (IsEnabled()) { + CloseFinishCore(); + } + } + + [Event(7, Level = EventLevel.Informational, Message = "Inflater closing has finished")] + private void CloseFinishCore() => WriteEvent(7); + + [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Disposed() { + if (IsEnabled()) { + DisposedCore(); + } + } + + [Event(8, Level = EventLevel.Informational, Message = "Inflater disposed")] + private void DisposedCore() => WriteEvent(8); +} From 57587ce42b5fe5cf0a8ad4247973cb589e588e5b Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Wed, 2 Nov 2022 23:59:56 +0100 Subject: [PATCH 85/87] Fix duped GUID --- src/Ws/WsReceiverInflaterEventSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ws/WsReceiverInflaterEventSource.cs b/src/Ws/WsReceiverInflaterEventSource.cs index 50f5f15b..68e9762f 100644 --- a/src/Ws/WsReceiverInflaterEventSource.cs +++ b/src/Ws/WsReceiverInflaterEventSource.cs @@ -4,7 +4,7 @@ namespace SurrealDB.Ws; -[EventSource(Guid = "03c50b03-e245-46e5-a99a-6eaa28990a41", Name = "SurrealDB.Ws.WsReceiverDeflaterEventSource")] +[EventSource(Guid = "91a1c84b-f0aa-43c8-ad21-6ff518a8fa01", Name = "SurrealDB.Ws.WsReceiverDeflaterEventSource")] public sealed class WsReceiverInflaterEventSource : EventSource { private WsReceiverInflaterEventSource() { } From bffe2e03693fe5f944540fec6cda8c85cab0c64a Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Thu, 3 Nov 2022 00:00:17 +0100 Subject: [PATCH 86/87] Add ConsoleOutEventListener when using DbHandle --- tests/Shared/ConsoleOutEventListener.cs | 16 ++++++++++++++++ tests/Shared/DbHandle.cs | 9 +++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/Shared/ConsoleOutEventListener.cs diff --git a/tests/Shared/ConsoleOutEventListener.cs b/tests/Shared/ConsoleOutEventListener.cs new file mode 100644 index 00000000..117cee60 --- /dev/null +++ b/tests/Shared/ConsoleOutEventListener.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.Tracing; + +namespace SurrealDB.Shared.Tests; + +public class ConsoleOutEventListener : EventListener { + + protected override void OnEventWritten(EventWrittenEventArgs eventData) { + string message = $"{eventData.TimeStamp:T} - {eventData.EventSource.Name} - {eventData.EventName} - {eventData.OSThreadId}: {eventData.Message}"; + if (eventData.Payload is null) { + Console.WriteLine(message); + } else { + Console.WriteLine(message, eventData.Payload.ToArray()); + } + base.OnEventWritten(eventData); + } +} diff --git a/tests/Shared/DbHandle.cs b/tests/Shared/DbHandle.cs index 0b6a7019..111a7439 100644 --- a/tests/Shared/DbHandle.cs +++ b/tests/Shared/DbHandle.cs @@ -1,4 +1,7 @@ +using System.Diagnostics.Tracing; + using SurrealDB.Abstractions; +using SurrealDB.Ws; namespace SurrealDB.Shared.Tests; @@ -20,7 +23,13 @@ public static async Task> Create() { [DebuggerStepThrough] public static async Task WithDatabase(Func action) { + // enable console logging for events + using ConsoleOutEventListener l = new(); + l.EnableEvents(WsReceiverInflaterEventSource.Log, EventLevel.LogAlways); + l.EnableEvents(WsReceiverDeflaterEventSource.Log, EventLevel.LogAlways); + // connect to the database using DbHandle db = await Create(); + // execute test methods await action(db.Database); } From 6e4586c44119266ea56f3596f5c5e13912569ed6 Mon Sep 17 00:00:00 2001 From: Prophet Lamb Date: Thu, 3 Nov 2022 00:00:17 +0100 Subject: [PATCH 87/87] Add ConsoleOutEventListener when using DbHandle --- src/Ws/WsReceiverDeflaterEventSource.cs | 24 ++++-------- src/Ws/WsReceiverInflaterEventSource.cs | 30 +++++++-------- tests/Shared/DbHandle.cs | 2 +- tests/Shared/TestEventListener.cs | 51 +++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 tests/Shared/TestEventListener.cs diff --git a/src/Ws/WsReceiverDeflaterEventSource.cs b/src/Ws/WsReceiverDeflaterEventSource.cs index 2082d8e0..7b2d04d5 100644 --- a/src/Ws/WsReceiverDeflaterEventSource.cs +++ b/src/Ws/WsReceiverDeflaterEventSource.cs @@ -3,7 +3,7 @@ namespace SurrealDB.Ws; -[EventSource(Guid = "03c50b03-e245-46e5-a99a-6eaa28990a41", Name = "SurrealDB.Ws.WsReceiverDeflaterEventSource")] +[EventSource(Guid = "03c50b03-e245-46e5-a99a-6eaa28990a41", Name = "WsReceiverDeflaterEventSource")] public sealed class WsReceiverDeflaterEventSource : EventSource { private WsReceiverDeflaterEventSource() { } @@ -37,19 +37,9 @@ public void HandlerUnregisteredAfterException(string handlerId, Exception ex) { } } - [Event(3, Level = EventLevel.Error, Message = "The handler (Id = {0}) threw an exception during dispatch, and was unregistered")] + [Event(3, Level = EventLevel.Error, Message = "The handler (Id = {0}) threw an exception during dispatch, and was unregistered. ERROR: {1}")] private unsafe void HandlerUnregisteredAfterExceptionCore(string handlerId, string ex) { - EventData* payload = stackalloc EventData[2]; - fixed (char* handlerIdPtr = handlerId) { - payload[0].DataPointer = (IntPtr)handlerIdPtr; - payload[0].Size = (handlerId.Length + 1) * 2; - } - - fixed (char* exPtr = ex) { - payload[1].DataPointer = (IntPtr)exPtr; - payload[1].Size = (ex.Length + 1) * 2; - } - WriteEventCore(3, 2, payload); + WriteEvent(3, handlerId, ex); } [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -79,7 +69,7 @@ public void Opened() { } } - [Event(6, Level = EventLevel.Informational, Message = "Deflater opened and is now pulling from the channel")] + [Event(6, Level = EventLevel.Informational, Message = "Opened and is now pulling from the channel")] private void OpenedCore() => WriteEvent(6); [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -89,7 +79,7 @@ public void CloseBegin() { } } - [Event(7, Level = EventLevel.Informational, Message = "Deflater closed and stopped pulling from the channel")] + [Event(7, Level = EventLevel.Informational, Message = "Closed and stopped pulling from the channel")] private void CloseBeginCore() => WriteEvent(7); [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -99,7 +89,7 @@ public void CloseFinish() { } } - [Event(8, Level = EventLevel.Informational, Message = "Deflater closing has finished")] + [Event(8, Level = EventLevel.Informational, Message = "Closing has finished")] private void CloseFinishCore() => WriteEvent(8); [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -109,6 +99,6 @@ public void Disposed() { } } - [Event(9, Level = EventLevel.Informational, Message = "Deflater disposed")] + [Event(9, Level = EventLevel.Informational, Message = "Disposed")] private void DisposedCore() => WriteEvent(9); } diff --git a/src/Ws/WsReceiverInflaterEventSource.cs b/src/Ws/WsReceiverInflaterEventSource.cs index 68e9762f..4415dc16 100644 --- a/src/Ws/WsReceiverInflaterEventSource.cs +++ b/src/Ws/WsReceiverInflaterEventSource.cs @@ -4,9 +4,8 @@ namespace SurrealDB.Ws; -[EventSource(Guid = "91a1c84b-f0aa-43c8-ad21-6ff518a8fa01", Name = "SurrealDB.Ws.WsReceiverDeflaterEventSource")] -public sealed class WsReceiverInflaterEventSource : EventSource -{ +[EventSource(Guid = "91a1c84b-f0aa-43c8-ad21-6ff518a8fa01", Name = "WsReceiverInflaterEventSource")] +public sealed class WsReceiverInflaterEventSource : EventSource { private WsReceiverInflaterEventSource() { } public static WsReceiverInflaterEventSource Log { get; } = new(); @@ -28,16 +27,15 @@ public void SockedReceived(WebSocketReceiveResult result) { } } - [Event(2, Level = EventLevel.Verbose, Message = "Received a block from the socket (Count = {0], EndOfMessage = {1}, Closed = {2})")] + [ThreadStatic] + private static object[]? _socketReceivedArgs; + [Event(2, Level = EventLevel.Verbose, Message = "Received a block from the socket (Count = {0}, EndOfMessage = {1}, Closed = {2})")] private unsafe void SockedReceivedCore(int count, bool endOfMessage, bool closed) { - EventData* payload = stackalloc EventData[3]; - payload[0].Size = sizeof(int); - payload[0].DataPointer = (IntPtr)(&count); - payload[1].Size = sizeof(bool); - payload[1].DataPointer = (IntPtr)(&endOfMessage); - payload[2].Size = sizeof(bool); - payload[2].DataPointer = (IntPtr)(&closed); - WriteEventCore(2, 3, payload); + _socketReceivedArgs ??= new object[3]; + _socketReceivedArgs[0] = count; + _socketReceivedArgs[1] = endOfMessage; + _socketReceivedArgs[2] = closed; + WriteEvent(2, _socketReceivedArgs); } [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -67,7 +65,7 @@ public void Opened() { } } - [Event(5, Level = EventLevel.Informational, Message = "Inflater opened and is now pushing to the channel")] + [Event(5, Level = EventLevel.Informational, Message = "Opened and is now pushing to the channel")] private void OpenedCore() => WriteEvent(5); [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -77,7 +75,7 @@ public void CloseBegin() { } } - [Event(6, Level = EventLevel.Informational, Message = "Inflater closed and stopped pushing to the channel")] + [Event(6, Level = EventLevel.Informational, Message = "Closed and stopped pushing to the channel")] private void CloseBeginCore() => WriteEvent(6); [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -87,7 +85,7 @@ public void CloseFinish() { } } - [Event(7, Level = EventLevel.Informational, Message = "Inflater closing has finished")] + [Event(7, Level = EventLevel.Informational, Message = "Closing has finished")] private void CloseFinishCore() => WriteEvent(7); [NonEvent, MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -97,6 +95,6 @@ public void Disposed() { } } - [Event(8, Level = EventLevel.Informational, Message = "Inflater disposed")] + [Event(8, Level = EventLevel.Informational, Message = "Disposed")] private void DisposedCore() => WriteEvent(8); } diff --git a/tests/Shared/DbHandle.cs b/tests/Shared/DbHandle.cs index 111a7439..0581ca00 100644 --- a/tests/Shared/DbHandle.cs +++ b/tests/Shared/DbHandle.cs @@ -24,7 +24,7 @@ public static async Task> Create() { [DebuggerStepThrough] public static async Task WithDatabase(Func action) { // enable console logging for events - using ConsoleOutEventListener l = new(); + using TestEventListener l = new(); l.EnableEvents(WsReceiverInflaterEventSource.Log, EventLevel.LogAlways); l.EnableEvents(WsReceiverDeflaterEventSource.Log, EventLevel.LogAlways); // connect to the database diff --git a/tests/Shared/TestEventListener.cs b/tests/Shared/TestEventListener.cs new file mode 100644 index 00000000..eaf5ab89 --- /dev/null +++ b/tests/Shared/TestEventListener.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.Tracing; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace SurrealDB.Shared.Tests; + +public class TestEventListener : EventListener { + private StreamWriter? _writer = new(File.OpenWrite($"./{nameof(TestEventListener)}_{DateTimeOffset.UtcNow:s}.log")); + + public TestEventListener() { + AppDomain.CurrentDomain.FirstChanceException += FirstChanceException; + } + + private void FirstChanceException(object? sender, FirstChanceExceptionEventArgs e) { + ValueStringBuilder sb = new(stackalloc char[512]); + sb.Append(DateTime.UtcNow.ToString("T")); + sb.Append(" Error: "); + sb.Append(e.Exception.ToString()); + + WriteLine(sb.ToString()); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) { + string message = $"{eventData.TimeStamp:T} {eventData.EventSource.Name}: {eventData.Message} (E:{eventData.EventName}, T:{eventData.OSThreadId})"; + if (eventData.Payload is null) { + WriteLine(message); + } else { + object?[] parameters = eventData.Payload.ToArray(); + WriteLine(message, parameters); + } + base.OnEventWritten(eventData); + } + + private void WriteLine(string message) { + _writer?.WriteLine(message); + } + + private void WriteLine(string format, object?[] args) { + _writer?.WriteLine(format, args); + } + + public override void Dispose() { + var writer = Interlocked.Exchange(ref _writer, null); + if (writer is not null) { + writer.Dispose(); + AppDomain.CurrentDomain.FirstChanceException -= FirstChanceException; + } + base.Dispose(); + } +}