From 1a916a3f3643ef7bdf2bc7947ce6f0da845c7c9c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 23:33:21 -0500 Subject: [PATCH] feat: add ClientFlags bitfield with thread-safe holder --- src/NATS.Server/ClientFlags.cs | 41 ++++++++++++++++ tests/NATS.Server.Tests/ClientFlagsTests.cs | 53 +++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/NATS.Server/ClientFlags.cs create mode 100644 tests/NATS.Server.Tests/ClientFlagsTests.cs diff --git a/src/NATS.Server/ClientFlags.cs b/src/NATS.Server/ClientFlags.cs new file mode 100644 index 0000000..8f9e915 --- /dev/null +++ b/src/NATS.Server/ClientFlags.cs @@ -0,0 +1,41 @@ +namespace NATS.Server; + +/// +/// Connection state flags tracked per client. +/// Corresponds to Go server/client.go clientFlag bitfield. +/// Thread-safe via Interlocked operations on the backing int. +/// +[Flags] +public enum ClientFlags +{ + ConnectReceived = 1 << 0, + FirstPongSent = 1 << 1, + HandshakeComplete = 1 << 2, + CloseConnection = 1 << 3, + WriteLoopStarted = 1 << 4, + IsSlowConsumer = 1 << 5, + ConnectProcessFinished = 1 << 6, +} + +/// +/// Thread-safe holder for client flags using Interlocked operations. +/// +public sealed class ClientFlagHolder +{ + private int _flags; + + public void SetFlag(ClientFlags flag) + { + Interlocked.Or(ref _flags, (int)flag); + } + + public void ClearFlag(ClientFlags flag) + { + Interlocked.And(ref _flags, ~(int)flag); + } + + public bool HasFlag(ClientFlags flag) + { + return (Volatile.Read(ref _flags) & (int)flag) != 0; + } +} diff --git a/tests/NATS.Server.Tests/ClientFlagsTests.cs b/tests/NATS.Server.Tests/ClientFlagsTests.cs new file mode 100644 index 0000000..9958c52 --- /dev/null +++ b/tests/NATS.Server.Tests/ClientFlagsTests.cs @@ -0,0 +1,53 @@ +namespace NATS.Server.Tests; + +public class ClientFlagsTests +{ + [Fact] + public void SetFlag_and_HasFlag_work() + { + var holder = new ClientFlagHolder(); + holder.HasFlag(ClientFlags.ConnectReceived).ShouldBeFalse(); + + holder.SetFlag(ClientFlags.ConnectReceived); + holder.HasFlag(ClientFlags.ConnectReceived).ShouldBeTrue(); + } + + [Fact] + public void ClearFlag_removes_flag() + { + var holder = new ClientFlagHolder(); + holder.SetFlag(ClientFlags.ConnectReceived); + holder.SetFlag(ClientFlags.IsSlowConsumer); + + holder.ClearFlag(ClientFlags.ConnectReceived); + + holder.HasFlag(ClientFlags.ConnectReceived).ShouldBeFalse(); + holder.HasFlag(ClientFlags.IsSlowConsumer).ShouldBeTrue(); + } + + [Fact] + public void Multiple_flags_can_be_set_independently() + { + var holder = new ClientFlagHolder(); + holder.SetFlag(ClientFlags.ConnectReceived); + holder.SetFlag(ClientFlags.WriteLoopStarted); + holder.SetFlag(ClientFlags.FirstPongSent); + + holder.HasFlag(ClientFlags.ConnectReceived).ShouldBeTrue(); + holder.HasFlag(ClientFlags.WriteLoopStarted).ShouldBeTrue(); + holder.HasFlag(ClientFlags.FirstPongSent).ShouldBeTrue(); + holder.HasFlag(ClientFlags.IsSlowConsumer).ShouldBeFalse(); + } + + [Fact] + public void SetFlag_is_thread_safe() + { + var holder = new ClientFlagHolder(); + var flags = Enum.GetValues(); + + Parallel.ForEach(flags, flag => holder.SetFlag(flag)); + + foreach (var flag in flags) + holder.HasFlag(flag).ShouldBeTrue(); + } +}