From bbb0e4908c10e6f2755a3dada33adbe093b52ea0 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 19:54:47 +0200 Subject: [PATCH 01/10] Update connection manager behaviour if saea pool is empty --- .../ConnectionManager/ConnectionManager.cs | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs index 8d56043..caabd7d 100644 --- a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs +++ b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs @@ -18,38 +18,45 @@ public sealed class ConnectionManager : IHeartbeatHandler, IDisposable { private const int MinimalReceiveSpeed = 1024; // 1 KB/s private static readonly TimeSpan Second = TimeSpan.FromSeconds(1); - + private readonly DefaultObjectPool _saeaPool = new(); private readonly ConcurrentDictionary _connections; private readonly ConnectionContextFactory _connectionContextFactory = new(); - + public ConnectionManager() { const int initObjectsCount = 50000; _connections = new ConcurrentDictionary(-1, initObjectsCount); - + ObjectPoolInitializationHelper.Initialize(initObjectsCount, _saeaPool, () => { - const int bufferSize = 4 * 1024; // 4 KB - var saea = new SocketAsyncEventArgs(); - - saea.Completed += IoCompleted; - saea.SetBuffer(new byte[bufferSize], 0, bufferSize); + var saea = InitSaea(); return saea; }); } - + + private SocketAsyncEventArgs InitSaea() + { + const int bufferSize = 4 * 1024; // 4 KB + + var saea = new SocketAsyncEventArgs(); + + saea.Completed += IoCompleted; + saea.SetBuffer(new byte[bufferSize], 0, bufferSize); + return saea; + } + public void OnHeartbeat() { var now = DateTime.UtcNow; - + foreach (var kvp in _connections) { var connection = kvp.Value; - - var lifetime = now - connection.CreatedAtUtc; + + var lifetime = now - connection.CreatedAtUtc; if (lifetime < Second) continue; var speed = connection.BytesReceived / lifetime.TotalSeconds; @@ -57,13 +64,13 @@ public void OnHeartbeat() CloseConnection(connection.SocketEventArgs); } } - + public void HandleAccept(SocketAsyncEventArgs acceptEventArg) { - if (!_saeaPool.TryGet(out var saea)) return; - + if (!_saeaPool.TryGet(out var saea)) saea = InitSaea(); + saea.AcceptSocket = acceptEventArg.AcceptSocket; - + ThreadPool.UnsafeQueueUserWorkItem(InitializeConnection, saea, false); } @@ -72,11 +79,11 @@ private void InitializeConnection(SocketAsyncEventArgs saea) var connectionContext = _connectionContextFactory.Create(saea); saea.UserToken = connectionContext; - + // REVIEW: not thread safe. Should be refactored to support multiple accept loops if (!_connections.TryAdd(connectionContext.Id, connectionContext)) throw new InvalidOperationException($"Cannot add task {connectionContext.Id}"); - + Receive(saea); } @@ -93,14 +100,14 @@ public void SendResponse(ConnectionContext connectionContext) { var socket = connectionContext.SocketEventArgs.AcceptSocket; bool willRaiseEvent = socket.SendAsync(connectionContext.SocketEventArgs); - + if (!willRaiseEvent) ProcessSend(connectionContext.SocketEventArgs); } - + private void IoCompleted(object? sender, SocketAsyncEventArgs saea) { - switch(saea.LastOperation) + switch (saea.LastOperation) { case SocketAsyncOperation.Receive: ProcessReceive(saea); @@ -117,17 +124,17 @@ private void ProcessSend(SocketAsyncEventArgs saea) saea.AcceptSocket = null; saea.UserToken = null; saea.SetBuffer(0, saea.Buffer.Length); - + _saeaPool.TryReturn(saea); } private void CloseConnection(SocketAsyncEventArgs saea) { var connectionContext = (ConnectionContext)saea.UserToken; - + if (!_connections.TryRemove(connectionContext.Id, out _)) throw new InvalidOperationException($"Cannot remove connection {connectionContext.Id}"); - + saea.AcceptSocket.Shutdown(SocketShutdown.Both); saea.AcceptSocket.Close(); } @@ -135,9 +142,9 @@ private void CloseConnection(SocketAsyncEventArgs saea) private void ProcessReceive(SocketAsyncEventArgs saea) { var connectionContext = (ConnectionContext)saea.UserToken; - + connectionContext.IncrementBytesReceived(saea.BytesTransferred); - + saea.SetBuffer(saea.Offset, saea.Offset + saea.BytesTransferred); OnDataReceived(connectionContext); } From f34ce6fefad7b952d034b7c47d7491987fa6d29f Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:11:24 +0200 Subject: [PATCH 02/10] Extract ConnectionStore from ConnectionManager --- .../LiteHttp/Abstractions/IConnectionStore.cs | 7 +++ .../ConnectionManager/ConnectionManager.cs | 50 ++++++++----------- .../ConnectionManager/ConnectionStore.cs | 35 +++++++++++++ 3 files changed, 62 insertions(+), 30 deletions(-) create mode 100644 src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/Abstractions/IConnectionStore.cs create mode 100644 src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionStore.cs diff --git a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/Abstractions/IConnectionStore.cs b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/Abstractions/IConnectionStore.cs new file mode 100644 index 0000000..3c85a17 --- /dev/null +++ b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/Abstractions/IConnectionStore.cs @@ -0,0 +1,7 @@ +namespace LiteHttp.ConnectionManager.Abstractions; + +public interface IConnectionStore +{ + public bool TryCloseConnection(ConnectionContext connection); + public ConnectionContext InitializeConnection(SocketAsyncEventArgs saea); +} diff --git a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs index caabd7d..8d03439 100644 --- a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs +++ b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs @@ -5,29 +5,22 @@ // // The rest of the code is written without any inspiration, any similarities are purely coincidental. -using System.Collections.Concurrent; - -using LiteHttp.Heartbeat; -using LiteHttp.Helpers; +using LiteHttp.ConnectionManager.Abstractions; namespace LiteHttp.ConnectionManager; #nullable disable #pragma warning disable CS8632 -public sealed class ConnectionManager : IHeartbeatHandler, IDisposable +public sealed class ConnectionManager : IDisposable { - private const int MinimalReceiveSpeed = 1024; // 1 KB/s - private static readonly TimeSpan Second = TimeSpan.FromSeconds(1); - private readonly DefaultObjectPool _saeaPool = new(); - private readonly ConcurrentDictionary _connections; - private readonly ConnectionContextFactory _connectionContextFactory = new(); + private readonly IConnectionStore _connectionStore; public ConnectionManager() { const int initObjectsCount = 50000; - _connections = new ConcurrentDictionary(-1, initObjectsCount); + _connectionStore = new ConnectionStore(); ObjectPoolInitializationHelper.Initialize(initObjectsCount, _saeaPool, () => { @@ -48,22 +41,23 @@ private SocketAsyncEventArgs InitSaea() return saea; } - public void OnHeartbeat() - { - var now = DateTime.UtcNow; + // TODO: implement "walker" service with similar logic. + //public void OnHeartbeat() + //{ + // var now = DateTime.UtcNow; - foreach (var kvp in _connections) - { - var connection = kvp.Value; + // foreach (var kvp in _connections) + // { + // var connection = kvp.Value; - var lifetime = now - connection.CreatedAtUtc; - if (lifetime < Second) continue; + // var lifetime = now - connection.CreatedAtUtc; + // if (lifetime < Second) continue; - var speed = connection.BytesReceived / lifetime.TotalSeconds; - if (speed < MinimalReceiveSpeed) - CloseConnection(connection.SocketEventArgs); - } - } + // var speed = connection.BytesReceived / lifetime.TotalSeconds; + // if (speed < MinimalReceiveSpeed) + // CloseConnection(connection.SocketEventArgs); + // } + //} public void HandleAccept(SocketAsyncEventArgs acceptEventArg) { @@ -76,14 +70,10 @@ public void HandleAccept(SocketAsyncEventArgs acceptEventArg) private void InitializeConnection(SocketAsyncEventArgs saea) { - var connectionContext = _connectionContextFactory.Create(saea); + var connectionContext = _connectionStore.InitConnection(saea); saea.UserToken = connectionContext; - // REVIEW: not thread safe. Should be refactored to support multiple accept loops - if (!_connections.TryAdd(connectionContext.Id, connectionContext)) - throw new InvalidOperationException($"Cannot add task {connectionContext.Id}"); - Receive(saea); } @@ -132,7 +122,7 @@ private void CloseConnection(SocketAsyncEventArgs saea) { var connectionContext = (ConnectionContext)saea.UserToken; - if (!_connections.TryRemove(connectionContext.Id, out _)) + if (!_connectionStore.TryCloseConnection(connectionContext)) throw new InvalidOperationException($"Cannot remove connection {connectionContext.Id}"); saea.AcceptSocket.Shutdown(SocketShutdown.Both); diff --git a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionStore.cs b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionStore.cs new file mode 100644 index 0000000..400b333 --- /dev/null +++ b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionStore.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; + +using LiteHttp.ConnectionManager.Abstractions; +using LiteHttp.Helpers; + +namespace LiteHttp.ConnectionManager; + +// Used to store active connections and manage them (close, timeout, etc.). +// The motivation to extract Walker service to avoid iterating over connections in ConnectionManager itself, +// and instead delegate this responsibility to a separate service, which will be called in Heartbeat. +// This way we can keep ConnectionManager focused on connection management (creation, closing, etc.) and have a +// separate service that will be responsible for "walking" through connections and performing necessary checks +// (like timeouts, etc.). This separation of concerns can lead to cleaner code and better maintainability. +internal sealed class ConnectionStore : IConnectionStore +{ + private readonly ConcurrentDictionary _connections; + private readonly ConnectionContextFactory _connectionContextFactory = new(); + + public ConnectionStore() + { + const int initialCapacity = 10000; + + _connections = new(-1, initialCapacity); + } + + public bool TryCloseConnection(ConnectionContext connection) => _connections.TryRemove(connection.Id, out _); + + public ConnectionContext InitializeConnection(SocketAsyncEventArgs saea) + { + var connectionContext = _connectionContextFactory.Create(saea); + if (!_connections.TryAdd(connectionContext.Id, connectionContext)) + ; + return connectionContext; + } +} From 79504e251401f8ccb328ec4dc1acf68bb2e750d0 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:13:44 +0200 Subject: [PATCH 03/10] Fix build errors --- .../LiteHttp/ConnectionManager/ConnectionManager.cs | 2 +- .../src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs | 8 ++++++++ .../Server/src/LiteHttp/Server/InternalServer.cs | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs diff --git a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs index 8d03439..9918521 100644 --- a/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs +++ b/src/Server/Infrastructure/ConnectionManager/src/LiteHttp.ConnectionManager/LiteHttp/ConnectionManager/ConnectionManager.cs @@ -70,7 +70,7 @@ public void HandleAccept(SocketAsyncEventArgs acceptEventArg) private void InitializeConnection(SocketAsyncEventArgs saea) { - var connectionContext = _connectionStore.InitConnection(saea); + var connectionContext = _connectionStore.InitializeConnection(saea); saea.UserToken = connectionContext; diff --git a/src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs b/src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs new file mode 100644 index 0000000..6714248 --- /dev/null +++ b/src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs @@ -0,0 +1,8 @@ +using LiteHttp.Heartbeat; + +namespace LiteHttp.Server.LiteHttp.Server; + +internal class HeartbeatHandlerPlaceholder : IHeartbeatHandler +{ + public void OnHeartbeat() { } +} diff --git a/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs b/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs index 2078489..c1586da 100644 --- a/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs +++ b/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs @@ -8,6 +8,7 @@ using LiteHttp.Pipeline; using LiteHttp.RequestProcessors; using LiteHttp.Routing; +using LiteHttp.Server.LiteHttp.Server; namespace LiteHttp.Server; @@ -30,7 +31,7 @@ public InternalServer(ILogger? logger, IPAddress address, int port) Listener = new(address, logger.ForContext(), port); ConnectionManager = new(); - heartbeatHandlers.Add(ConnectionManager); + heartbeatHandlers.Add(new HeartbeatHandlerPlaceholder()); _endpointProviderConfiguration = new EndpointProviderConfiguration(); From 5410906155330bf9ad45ed059c08a8e2e09ea956 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:22:17 +0200 Subject: [PATCH 04/10] Update listener --- .../Listener/src/LiteHttp/Listener/Listener.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Server/Infrastructure/Listener/src/LiteHttp/Listener/Listener.cs b/src/Server/Infrastructure/Listener/src/LiteHttp/Listener/Listener.cs index 3d2b71c..c30bef5 100644 --- a/src/Server/Infrastructure/Listener/src/LiteHttp/Listener/Listener.cs +++ b/src/Server/Infrastructure/Listener/src/LiteHttp/Listener/Listener.cs @@ -62,13 +62,10 @@ public void Dispose() _isListening = false; } - public bool StartListen(CancellationToken cancellationToken) + public bool StartListen(CancellationToken cancellationToken = default) { _cancellationToken = cancellationToken; - if (_endPoint is null) - throw new InvalidOperationException("Listener endpoint cannot be null"); - if (!Socket.IsBound) { Socket.Bind(_endPoint); @@ -84,7 +81,7 @@ public bool StartListen(CancellationToken cancellationToken) var acceptEventArg = new SocketAsyncEventArgs(); acceptEventArg.Completed += AcceptEventArg_Completed; - return ThreadPool.UnsafeQueueUserWorkItem(StartAccept, acceptEventArg, false); + return ThreadPool.QueueUserWorkItem(StartAccept, acceptEventArg, false); } catch (Exception ex) { @@ -93,7 +90,6 @@ public bool StartListen(CancellationToken cancellationToken) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AcceptEventArg_Completed(object? sender, SocketAsyncEventArgs saea) { ProcessAccept(saea); From e68f3e1920acc2c644945557718a0e50ffbdf663 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:24:12 +0200 Subject: [PATCH 05/10] Rename Executor to ActionInvoker --- src/Server/Infrastructure/Pipeline/src/Pipeline.cs | 6 +++--- src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs | 4 ++-- .../RequestProcessors/{Executor.cs => ActionInvoker.cs} | 2 +- .../Server/src/LiteHttp/Server/InternalServer.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/{Executor.cs => ActionInvoker.cs} (77%) diff --git a/src/Server/Infrastructure/Pipeline/src/Pipeline.cs b/src/Server/Infrastructure/Pipeline/src/Pipeline.cs index 70146f5..aeaeab4 100644 --- a/src/Server/Infrastructure/Pipeline/src/Pipeline.cs +++ b/src/Server/Infrastructure/Pipeline/src/Pipeline.cs @@ -6,14 +6,14 @@ public sealed class Pipeline private readonly IRouter _router; private readonly Parser _parser; private readonly ResponseBuilder _responseBuilder; - private readonly Executor _executor; + private readonly ActionInvoker _actionInvoker; internal Pipeline(PipelineFactory factory) { _router = factory.RouterFactory(); _parser = factory.ParserFactory(); _responseBuilder = factory.ResponseBuilderFactory(); - _executor = factory.ExecutorFactory(); + _actionInvoker = factory.ActionInvokerFactory(); } [SkipLocalsInit] @@ -41,7 +41,7 @@ public void ProcessRequest(ConnectionContext connectionContext) return; } - var executionResult = _executor.Execute(action); + var executionResult = _actionInvoker.Execute(action); responseLength = _responseBuilder.Build(executionResult, buffer); connectionContext.SocketEventArgs.SetBuffer(0, responseLength); diff --git a/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs b/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs index 87316a1..c048cee 100644 --- a/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs +++ b/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs @@ -6,7 +6,7 @@ public sealed class PipelineFactory public Func RouterFactory { get; set; } public Func ParserFactory { get; set; } public Func ResponseBuilderFactory { get; set; } - public Func ExecutorFactory { get; set; } + public Func ActionInvokerFactory { get; set; } public PipelineFactory(Action factoryDelegate) { @@ -20,7 +20,7 @@ private void ThrowIfAnyFactoryIsNull() if (RouterFactory is null) throw new ArgumentNullException(nameof(RouterFactory)); if (ParserFactory is null) throw new ArgumentNullException(nameof(ParserFactory)); if (ResponseBuilderFactory is null) throw new ArgumentNullException(nameof(ResponseBuilderFactory)); - if (ExecutorFactory is null) throw new ArgumentNullException(nameof(ExecutorFactory)); + if (ActionInvokerFactory is null) throw new ArgumentNullException(nameof(ActionInvokerFactory)); } public Pipeline Create() => new(this); diff --git a/src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/Executor.cs b/src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/ActionInvoker.cs similarity index 77% rename from src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/Executor.cs rename to src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/ActionInvoker.cs index 65cca94..2aa83dc 100644 --- a/src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/Executor.cs +++ b/src/Server/Infrastructure/RequestProcessors/src/LiteHttp/RequestProcessors/ActionInvoker.cs @@ -1,6 +1,6 @@ namespace LiteHttp.RequestProcessors; -public sealed class Executor +public sealed class ActionInvoker { public IActionResult Execute(Func action) => action(); } diff --git a/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs b/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs index c1586da..3f88904 100644 --- a/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs +++ b/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs @@ -40,7 +40,7 @@ public InternalServer(ILogger? logger, IPAddress address, int port) factory.ParserFactory = () => Parser.Instance; factory.RouterFactory = () => RouterFactory.Build(_endpointProviderConfiguration.EndpointContext); factory.ResponseBuilderFactory = () => new(); - factory.ExecutorFactory = () => new(); + factory.ActionInvokerFactory = () => new(); }); Heartbeat = new (CollectionsMarshal.AsSpan(heartbeatHandlers), From b6a2b6a63203ae2e12df3b88aa2bdd224f474951 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:28:09 +0200 Subject: [PATCH 06/10] Refactor request processing in pipeline --- .../Infrastructure/Pipeline/src/Pipeline.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Server/Infrastructure/Pipeline/src/Pipeline.cs b/src/Server/Infrastructure/Pipeline/src/Pipeline.cs index aeaeab4..fad318b 100644 --- a/src/Server/Infrastructure/Pipeline/src/Pipeline.cs +++ b/src/Server/Infrastructure/Pipeline/src/Pipeline.cs @@ -7,7 +7,7 @@ public sealed class Pipeline private readonly Parser _parser; private readonly ResponseBuilder _responseBuilder; private readonly ActionInvoker _actionInvoker; - + internal Pipeline(PipelineFactory factory) { _router = factory.RouterFactory(); @@ -15,19 +15,16 @@ internal Pipeline(PipelineFactory factory) _responseBuilder = factory.ResponseBuilderFactory(); _actionInvoker = factory.ActionInvokerFactory(); } - + [SkipLocalsInit] public void ProcessRequest(ConnectionContext connectionContext) { Memory buffer = connectionContext.SocketEventArgs.Buffer; var parsingResult = _parser.Parse(buffer); - int responseLength; - + if (!parsingResult.Success) { - responseLength = _responseBuilder.Build(InternalActionResults.BadRequest(), buffer); - connectionContext.SocketEventArgs.SetBuffer(0, responseLength); - ThreadPool.UnsafeQueueUserWorkItem(OnExecuted, connectionContext, false); + SendResponse(connectionContext, buffer, InternalActionResults.BadRequest()); return; } @@ -35,19 +32,23 @@ public void ProcessRequest(ConnectionContext connectionContext) if (action is null) { - responseLength = _responseBuilder.Build(InternalActionResults.NotFound(), buffer); - connectionContext.SocketEventArgs.SetBuffer(0, responseLength); - ThreadPool.UnsafeQueueUserWorkItem(OnExecuted, connectionContext, false); + SendResponse(connectionContext, buffer, InternalActionResults.NotFound()); return; } var executionResult = _actionInvoker.Execute(action); - responseLength = _responseBuilder.Build(executionResult, buffer); + SendResponse(connectionContext, buffer, executionResult); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SendResponse(ConnectionContext connectionContext, Memory buffer, IActionResult actionResult) + { + int responseLength = _responseBuilder.Build(actionResult, buffer); connectionContext.SocketEventArgs.SetBuffer(0, responseLength); ThreadPool.UnsafeQueueUserWorkItem(OnExecuted, connectionContext, false); } - + private Action _executed; private void OnExecuted(ConnectionContext response) => _executed?.Invoke(response); From 4283f4afe7f1cf860285b7bd0082346d99a43749 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:29:11 +0200 Subject: [PATCH 07/10] Replace nullable disable with pragma warning disable --- src/Server/Infrastructure/Pipeline/src/Pipeline.cs | 3 ++- src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Server/Infrastructure/Pipeline/src/Pipeline.cs b/src/Server/Infrastructure/Pipeline/src/Pipeline.cs index fad318b..97a507b 100644 --- a/src/Server/Infrastructure/Pipeline/src/Pipeline.cs +++ b/src/Server/Infrastructure/Pipeline/src/Pipeline.cs @@ -1,6 +1,5 @@ namespace LiteHttp.Pipeline; -#nullable disable public sealed class Pipeline { private readonly IRouter _router; @@ -8,6 +7,7 @@ public sealed class Pipeline private readonly ResponseBuilder _responseBuilder; private readonly ActionInvoker _actionInvoker; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. internal Pipeline(PipelineFactory factory) { _router = factory.RouterFactory(); @@ -15,6 +15,7 @@ internal Pipeline(PipelineFactory factory) _responseBuilder = factory.ResponseBuilderFactory(); _actionInvoker = factory.ActionInvokerFactory(); } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. [SkipLocalsInit] public void ProcessRequest(ConnectionContext connectionContext) diff --git a/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs b/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs index c048cee..e1e69fe 100644 --- a/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs +++ b/src/Server/Infrastructure/Pipeline/src/PipelineFactory.cs @@ -1,6 +1,5 @@ namespace LiteHttp.Pipeline; -#nullable disable public sealed class PipelineFactory { public Func RouterFactory { get; set; } @@ -8,12 +7,14 @@ public sealed class PipelineFactory public Func ResponseBuilderFactory { get; set; } public Func ActionInvokerFactory { get; set; } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public PipelineFactory(Action factoryDelegate) { factoryDelegate(this); ThrowIfAnyFactoryIsNull(); } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. private void ThrowIfAnyFactoryIsNull() { From 68910c5161895bbf807ae21db1521acd6ddd9095 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:34:15 +0200 Subject: [PATCH 08/10] Refactor heartbeat service --- .../Infrastructure/Heartbeat/src/Heartbeat.cs | 19 +++++++++++++------ .../Heartbeat/src/IHeartbeatHandler.cs | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs b/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs index d323c5a..4935551 100644 --- a/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs +++ b/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs @@ -12,18 +12,24 @@ namespace LiteHttp.Heartbeat; public sealed class Heartbeat : IDisposable { private static readonly TimeSpan Interval = TimeSpan.FromSeconds(1); - private const string NoHandlersExceptionString = "Heartbeat not needed to be initialized if here is no handlers in app"; + private const string NoHandlersString = "Heartbeat handlers were not initialized"; private readonly Action[] _callbacks; private readonly ManualResetEventSlim _timer = new ManualResetEventSlim(false, 0); private readonly Thread _heartbeatThread; private readonly ILogger _logger; - - public Heartbeat(Span heartbeatHandlers, ILogger logger) + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + public Heartbeat(IHeartbeatHandler[] heartbeatHandlers, ILogger logger) { - Debug.Assert(heartbeatHandlers.Length > 0, NoHandlersExceptionString); + Debug.Assert(heartbeatHandlers.Length > 0, NoHandlersString); - ArgumentException.ThrowIfNullOrEmpty(NoHandlersExceptionString); + if (heartbeatHandlers.Length == 0) + { + logger.LogWarning($"{NoHandlersString}"); + _logger = logger; + return; // we don't have any handlers, so we won't start the heartbeat thread + } _logger = logger; @@ -40,6 +46,7 @@ public Heartbeat(Span heartbeatHandlers, ILogger l _heartbeatThread.Start(); } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. private void OnHeartbeat() { @@ -52,7 +59,7 @@ private void OnHeartbeat() } catch (Exception ex) { - _logger.LogTrace($"Exception thrown in heartbeat handler"); + _logger.LogError(ex, $"Exception thrown in heartbeat handler"); } } } diff --git a/src/Server/Infrastructure/Heartbeat/src/IHeartbeatHandler.cs b/src/Server/Infrastructure/Heartbeat/src/IHeartbeatHandler.cs index c64d805..5ea5a28 100644 --- a/src/Server/Infrastructure/Heartbeat/src/IHeartbeatHandler.cs +++ b/src/Server/Infrastructure/Heartbeat/src/IHeartbeatHandler.cs @@ -2,5 +2,5 @@ public interface IHeartbeatHandler { - void OnHeartbeat(); + public void OnHeartbeat(); } \ No newline at end of file From 1db1014d7902be780c2bcc987c72ac704d921253 Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:36:29 +0200 Subject: [PATCH 09/10] Remove heartbeat placeholder & debug assertion --- src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs | 2 -- .../src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs | 8 -------- .../Server/src/LiteHttp/Server/InternalServer.cs | 10 ++-------- 3 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs diff --git a/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs b/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs index 4935551..fad46cc 100644 --- a/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs +++ b/src/Server/Infrastructure/Heartbeat/src/Heartbeat.cs @@ -22,8 +22,6 @@ public sealed class Heartbeat : IDisposable #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public Heartbeat(IHeartbeatHandler[] heartbeatHandlers, ILogger logger) { - Debug.Assert(heartbeatHandlers.Length > 0, NoHandlersString); - if (heartbeatHandlers.Length == 0) { logger.LogWarning($"{NoHandlersString}"); diff --git a/src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs b/src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs deleted file mode 100644 index 6714248..0000000 --- a/src/Server/Infrastructure/Server/src/LiteHttp/Server/HeartbeatHandlerPlaceholder.cs +++ /dev/null @@ -1,8 +0,0 @@ -using LiteHttp.Heartbeat; - -namespace LiteHttp.Server.LiteHttp.Server; - -internal class HeartbeatHandlerPlaceholder : IHeartbeatHandler -{ - public void OnHeartbeat() { } -} diff --git a/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs b/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs index 3f88904..89deda1 100644 --- a/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs +++ b/src/Server/Infrastructure/Server/src/LiteHttp/Server/InternalServer.cs @@ -1,6 +1,4 @@ -using System.Runtime.InteropServices; - -using LiteHttp.Constants; +using LiteHttp.Constants; using LiteHttp.Heartbeat; using LiteHttp.Logging; using LiteHttp.Logging.Abstractions; @@ -8,7 +6,6 @@ using LiteHttp.Pipeline; using LiteHttp.RequestProcessors; using LiteHttp.Routing; -using LiteHttp.Server.LiteHttp.Server; namespace LiteHttp.Server; @@ -30,8 +27,6 @@ public InternalServer(ILogger? logger, IPAddress address, int port) Listener = new(address, logger.ForContext(), port); ConnectionManager = new(); - - heartbeatHandlers.Add(new HeartbeatHandlerPlaceholder()); _endpointProviderConfiguration = new EndpointProviderConfiguration(); @@ -43,8 +38,7 @@ public InternalServer(ILogger? logger, IPAddress address, int port) factory.ActionInvokerFactory = () => new(); }); - Heartbeat = new (CollectionsMarshal.AsSpan(heartbeatHandlers), - _logger.ForContext()); + Heartbeat = new (heartbeatHandlers.ToArray(), _logger.ForContext()); Binder.Bind(this); } From 682ce0041ddb9a68d700c685a8f3aec4834fdb6c Mon Sep 17 00:00:00 2001 From: Archivelit Date: Tue, 21 Apr 2026 20:37:34 +0200 Subject: [PATCH 10/10] Update Router.SetContext visibility --- .../Infrastructure/Routing/src/LiteHttp/Routing/Router.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server/Infrastructure/Routing/src/LiteHttp/Routing/Router.cs b/src/Server/Infrastructure/Routing/src/LiteHttp/Routing/Router.cs index dc26e29..8ea4ff6 100644 --- a/src/Server/Infrastructure/Routing/src/LiteHttp/Routing/Router.cs +++ b/src/Server/Infrastructure/Routing/src/LiteHttp/Routing/Router.cs @@ -7,6 +7,6 @@ internal sealed class Router : IRouter public Func? GetAction(HttpContext context) => _endpointContext?.EndpointProvider.GetEndpoint(context.Route, context.Method); - public void SetContext(IEndpointContext endpointContext) => + internal void SetContext(IEndpointContext endpointContext) => _endpointContext = endpointContext; } \ No newline at end of file