Skip to content

Commit 3771b04

Browse files
authored
Add flags to NatsMsg and JetStream Request (#652)
* Add flags to NatsMsg * Code comments for msg size * Format * Fix warnings * Keep 503 header * Format * JetStream TryJSRequestAsync * Format * Fix tests * Refactor JetStream method results to use NatsResult * Refactor NatsMsg size and flags handling in tests and class * Refactor JSON document deserialization * Refactor NatsJSContext error handling logic. Reordered error checks in `NatsJSContext.cs` to prioritize handling `HasNoResponders` and `null` data conditions early. Modified flag checks in `NatsMsg.cs` for `IsEmpty` and `HasNoResponders` by directly using bitwise operations instead of enum comparisons. * Fix memory leak by disposing JsonDocument properly * Skip slow tests for unique NUID generation
1 parent dcf77a4 commit 3771b04

File tree

10 files changed

+374
-89
lines changed

10 files changed

+374
-89
lines changed

src/NATS.Client.Core/NatsMsg.cs

+138-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
using System.Buffers;
22
using System.Diagnostics.CodeAnalysis;
3+
using System.Xml.Linq;
34
using NATS.Client.Core.Internal;
45

56
namespace NATS.Client.Core;
67

8+
[Flags]
9+
public enum NatsMsgFlags : byte
10+
{
11+
None = 0,
12+
Empty = 1,
13+
NoResponders = 2,
14+
}
15+
716
/// <summary>
817
/// This interface provides an optional contract when passing
918
/// messages to processing methods which is usually helpful in
@@ -103,12 +112,6 @@ public interface INatsMsg<T>
103112
/// <summary>
104113
/// NATS message structure as defined by the protocol.
105114
/// </summary>
106-
/// <param name="Subject">The destination subject to publish to.</param>
107-
/// <param name="ReplyTo">The reply subject that subscribers can use to send a response back to the publisher/requester.</param>
108-
/// <param name="Size">Message size in bytes.</param>
109-
/// <param name="Headers">Pass additional information using name-value pairs.</param>
110-
/// <param name="Data">Serializable data object.</param>
111-
/// <param name="Connection">NATS connection this message is associated to.</param>
112115
/// <typeparam name="T">Specifies the type of data that may be sent to the NATS Server.</typeparam>
113116
/// <remarks>
114117
/// <para>Connection property is used to provide reply functionality.</para>
@@ -119,20 +122,120 @@ public interface INatsMsg<T>
119122
/// </code>
120123
/// </para>
121124
/// </remarks>
122-
public readonly record struct NatsMsg<T>(
123-
string Subject,
124-
string? ReplyTo,
125-
int Size,
126-
NatsHeaders? Headers,
127-
T? Data,
128-
INatsConnection? Connection) : INatsMsg<T>
125+
public readonly record struct NatsMsg<T> : INatsMsg<T>
129126
{
127+
/*
128+
2 30
129+
+--+------------------------------+
130+
|EN| Message Size |
131+
+--+------------------------------+
132+
E: Empty flag
133+
N: No responders flag
134+
135+
# Size is 30 bits:
136+
Max Size: 1,073,741,823 (0x3FFFFFFF / 00111111111111111111111111111111)
137+
Uint.Max: 4,294,967,295
138+
Int.Max: 2,147,483,647
139+
8mb: 8,388,608
140+
*/
141+
private readonly uint _flagsAndSize;
142+
143+
/// <summary>
144+
/// NATS message structure as defined by the protocol.
145+
/// </summary>
146+
/// <param name="subject">The destination subject to publish to.</param>
147+
/// <param name="replyTo">The reply subject that subscribers can use to send a response back to the publisher/requester.</param>
148+
/// <param name="size">Message size in bytes.</param>
149+
/// <param name="headers">Pass additional information using name-value pairs.</param>
150+
/// <param name="data">Serializable data object.</param>
151+
/// <param name="connection">NATS connection this message is associated to.</param>
152+
/// <param name="flags">Message flags to indicate no responders and empty payloads.</param>
153+
/// <remarks>
154+
/// <para>Connection property is used to provide reply functionality.</para>
155+
/// <para>
156+
/// Message size is calculated using the same method NATS server uses:
157+
/// <code lang="C#">
158+
/// int size = subject.Length + replyTo.Length + headers.Length + payload.Length;
159+
/// </code>
160+
/// </para>
161+
/// </remarks>
162+
public NatsMsg(
163+
string subject,
164+
string? replyTo,
165+
int size,
166+
NatsHeaders? headers,
167+
T? data,
168+
INatsConnection? connection,
169+
NatsMsgFlags flags = default)
170+
{
171+
Subject = subject;
172+
ReplyTo = replyTo;
173+
_flagsAndSize = ((uint)flags << 30) | (uint)(size & 0x3FFFFFFF);
174+
Headers = headers;
175+
Data = data;
176+
Connection = connection;
177+
}
178+
130179
/// <inheritdoc />
131180
public NatsException? Error => Headers?.Error;
132181

182+
/// <summary>The destination subject to publish to.</summary>
183+
public string Subject { get; init; }
184+
185+
/// <summary>The reply subject that subscribers can use to send a response back to the publisher/requester.</summary>
186+
public string? ReplyTo { get; init; }
187+
188+
/// <summary>Message size in bytes.</summary>
189+
public int Size
190+
{
191+
// Extract the lower 30 bits
192+
get => (int)(_flagsAndSize & 0x3FFFFFFF);
193+
194+
// Clear the lower 30 bits and set the new number
195+
init
196+
{
197+
// Mask the input value to fit within 30 bits (clear upper bits)
198+
var numberPart = (uint)(value & 0x3FFFFFFF);
199+
200+
// Clear the lower 30 bits and set the new number value
201+
// Preserve the flags, update the number
202+
_flagsAndSize = (_flagsAndSize & 0xC0000000) | numberPart;
203+
}
204+
}
205+
206+
public NatsMsgFlags Flags
207+
{
208+
// Extract the two leftmost bits (31st and 30th bit)
209+
// Mask with 0b11 to get two bits
210+
get => (NatsMsgFlags)((_flagsAndSize >> 30) & 0b11);
211+
212+
init
213+
{
214+
// Clear the current flag bits (set to 0) and then set the new flag value
215+
var flagsPart = (uint)value << 30;
216+
_flagsAndSize = (_flagsAndSize & 0x3FFFFFFF) | flagsPart;
217+
}
218+
}
219+
220+
/// <summary>Pass additional information using name-value pairs.</summary>
221+
public NatsHeaders? Headers { get; init; }
222+
223+
/// <summary>Serializable data object.</summary>
224+
public T? Data { get; init; }
225+
226+
/// <summary>NATS connection this message is associated to.</summary>
227+
public INatsConnection? Connection { get; init; }
228+
229+
public bool IsEmpty => (_flagsAndSize & 0x40000000) != 0;
230+
231+
public bool HasNoResponders => (_flagsAndSize & 0x80000000) != 0;
232+
133233
/// <inheritdoc />
134234
public void EnsureSuccess()
135235
{
236+
if (HasNoResponders)
237+
throw new NatsNoRespondersException();
238+
136239
if (Error != null)
137240
throw Error;
138241
}
@@ -197,6 +300,17 @@ public ValueTask ReplyAsync<TReply>(NatsMsg<TReply> msg, INatsSerialize<TReply>?
197300
return Connection.PublishAsync(msg with { Subject = ReplyTo }, serializer, opts, cancellationToken);
198301
}
199302

303+
public void Deconstruct(out string subject, out string? replyTo, out int size, out NatsHeaders? headers, out T? data, out INatsConnection? connection, out NatsMsgFlags flags)
304+
{
305+
subject = Subject;
306+
replyTo = ReplyTo;
307+
size = Size;
308+
headers = Headers;
309+
data = Data;
310+
connection = Connection;
311+
flags = Flags;
312+
}
313+
200314
internal static NatsMsg<T> Build(
201315
string subject,
202316
string? replyTo,
@@ -207,6 +321,16 @@ internal static NatsMsg<T> Build(
207321
INatsDeserialize<T> serializer)
208322
{
209323
NatsHeaders? headers = null;
324+
var flags = NatsMsgFlags.None;
325+
326+
if (payloadBuffer.Length == 0)
327+
{
328+
flags |= NatsMsgFlags.Empty;
329+
if (NatsSubBase.IsHeader503(headersBuffer))
330+
{
331+
flags |= NatsMsgFlags.NoResponders;
332+
}
333+
}
210334

211335
if (headersBuffer != null)
212336
{
@@ -277,7 +401,7 @@ internal static NatsMsg<T> Build(
277401
}
278402
}
279403

280-
return new NatsMsg<T>(subject, replyTo, (int)size, headers, data, connection);
404+
return new NatsMsg<T>(subject, replyTo, (int)size, headers, data, connection, flags);
281405
}
282406

283407
[MemberNotNull(nameof(Connection))]

src/NATS.Client.Core/NatsResult.cs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace NATS.Client.Core;
4+
5+
public readonly struct NatsResult<T>
6+
{
7+
private readonly T? _value;
8+
private readonly Exception? _error;
9+
10+
public NatsResult(T value)
11+
{
12+
_value = value;
13+
_error = null;
14+
}
15+
16+
public NatsResult(Exception error)
17+
{
18+
_value = default;
19+
_error = error;
20+
}
21+
22+
public T Value => _value ?? ThrowValueIsNotSetException();
23+
24+
public Exception Error => _error ?? ThrowErrorIsNotSetException();
25+
26+
public bool Success => _error == null;
27+
28+
public static implicit operator NatsResult<T>(T value) => new(value);
29+
30+
public static implicit operator NatsResult<T>(Exception error) => new(error);
31+
32+
private static T ThrowValueIsNotSetException() => throw CreateInvalidOperationException("Result value is not set");
33+
34+
private static Exception ThrowErrorIsNotSetException() => throw CreateInvalidOperationException("Result error is not set");
35+
36+
[MethodImpl(MethodImplOptions.NoInlining)]
37+
private static Exception CreateInvalidOperationException(string message) => new InvalidOperationException(message);
38+
}

src/NATS.Client.Core/NatsSubBase.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public virtual async ValueTask ReceiveAsync(string subject, string? replyTo, Rea
271271
{
272272
switch (Opts)
273273
{
274-
case { ThrowIfNoResponders: true } when headersBuffer is { Length: >= 12 } && headersBuffer.Value.Slice(8, 4).ToSpan().SequenceEqual(NoRespondersHeaderSequence):
274+
case { ThrowIfNoResponders: true } when IsHeader503(headersBuffer):
275275
SetException(new NatsNoRespondersException());
276276
return;
277277
case { StopOnEmptyMsg: true }:
@@ -311,6 +311,10 @@ public virtual async ValueTask ReceiveAsync(string subject, string? replyTo, Rea
311311
}
312312
}
313313

314+
internal static bool IsHeader503(ReadOnlySequence<byte>? headersBuffer) =>
315+
headersBuffer is { Length: >= 12 }
316+
&& headersBuffer.Value.Slice(8, 4).ToSpan().SequenceEqual(NoRespondersHeaderSequence);
317+
314318
internal void ClearException() => Interlocked.Exchange(ref _exception, null);
315319

316320
/// <summary>

src/NATS.Client.JetStream/Internal/NatsJSErrorAwareJsonSerializer.cs

-41
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Buffers;
2+
using System.Text.Json;
3+
using NATS.Client.Core;
4+
5+
namespace NATS.Client.JetStream.Internal;
6+
7+
internal sealed class NatsJSJsonDocumentSerializer : INatsDeserialize<JsonDocument>
8+
{
9+
public static readonly NatsJSJsonDocumentSerializer Default = new();
10+
11+
public JsonDocument? Deserialize(in ReadOnlySequence<byte> buffer) => buffer.Length == 0 ? default : JsonDocument.Parse(buffer);
12+
}

0 commit comments

Comments
 (0)