feat: complete remaining jetstream parity implementation plan

This commit is contained in:
Joseph Doherty
2026-02-23 10:16:16 -05:00
parent c7bbf45c8f
commit f46b331921
59 changed files with 1734 additions and 54 deletions

View File

@@ -1,3 +1,4 @@
using System.Text;
using System.Text.Json;
using NATS.Server.JetStream.Models;
@@ -5,8 +6,15 @@ namespace NATS.Server.JetStream.Api.Handlers;
public static class ConsumerApiHandlers
{
private const string CreatePrefix = "$JS.API.CONSUMER.CREATE.";
private const string InfoPrefix = "$JS.API.CONSUMER.INFO.";
private const string CreatePrefix = JetStreamApiSubjects.ConsumerCreate;
private const string InfoPrefix = JetStreamApiSubjects.ConsumerInfo;
private const string NamesPrefix = JetStreamApiSubjects.ConsumerNames;
private const string ListPrefix = JetStreamApiSubjects.ConsumerList;
private const string DeletePrefix = JetStreamApiSubjects.ConsumerDelete;
private const string PausePrefix = JetStreamApiSubjects.ConsumerPause;
private const string ResetPrefix = JetStreamApiSubjects.ConsumerReset;
private const string UnpinPrefix = JetStreamApiSubjects.ConsumerUnpin;
private const string NextPrefix = JetStreamApiSubjects.ConsumerNext;
public static JetStreamApiResponse HandleCreate(string subject, ReadOnlySpan<byte> payload, ConsumerManager consumerManager)
{
@@ -32,6 +40,104 @@ public static class ConsumerApiHandlers
return consumerManager.GetInfo(stream, durableName);
}
public static JetStreamApiResponse HandleDelete(string subject, ConsumerManager consumerManager)
{
var parsed = ParseSubject(subject, DeletePrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
return consumerManager.Delete(stream, durableName)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandleNames(string subject, ConsumerManager consumerManager)
{
var stream = ParseStreamSubject(subject, NamesPrefix);
if (stream == null)
return JetStreamApiResponse.NotFound(subject);
return new JetStreamApiResponse
{
ConsumerNames = consumerManager.ListNames(stream),
};
}
public static JetStreamApiResponse HandleList(string subject, ConsumerManager consumerManager)
{
var stream = ParseStreamSubject(subject, ListPrefix);
if (stream == null)
return JetStreamApiResponse.NotFound(subject);
return new JetStreamApiResponse
{
ConsumerNames = consumerManager.ListNames(stream),
};
}
public static JetStreamApiResponse HandlePause(string subject, ReadOnlySpan<byte> payload, ConsumerManager consumerManager)
{
var parsed = ParseSubject(subject, PausePrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
var paused = ParsePause(payload);
return consumerManager.Pause(stream, durableName, paused)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandleReset(string subject, ConsumerManager consumerManager)
{
var parsed = ParseSubject(subject, ResetPrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
return consumerManager.Reset(stream, durableName)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandleUnpin(string subject, ConsumerManager consumerManager)
{
var parsed = ParseSubject(subject, UnpinPrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
return consumerManager.Unpin(stream, durableName)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandleNext(string subject, ReadOnlySpan<byte> payload, ConsumerManager consumerManager, StreamManager streamManager)
{
var parsed = ParseSubject(subject, NextPrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
var batch = ParseBatch(payload);
var pullBatch = consumerManager.FetchAsync(stream, durableName, batch, streamManager, default).GetAwaiter().GetResult();
return new JetStreamApiResponse
{
PullBatch = new JetStreamPullBatch
{
Messages = pullBatch.Messages
.Select(m => new JetStreamDirectMessage
{
Sequence = m.Sequence,
Subject = m.Subject,
Payload = Encoding.UTF8.GetString(m.Payload.Span),
})
.ToArray(),
},
};
}
private static (string Stream, string Durable)? ParseSubject(string subject, string prefix)
{
if (!subject.StartsWith(prefix, StringComparison.Ordinal))
@@ -76,6 +182,8 @@ public static class ConsumerApiHandlers
var ackPolicy = ackPolicyEl.GetString();
if (string.Equals(ackPolicy, "explicit", StringComparison.OrdinalIgnoreCase))
config.AckPolicy = AckPolicy.Explicit;
else if (string.Equals(ackPolicy, "all", StringComparison.OrdinalIgnoreCase))
config.AckPolicy = AckPolicy.All;
}
return config;
@@ -85,4 +193,49 @@ public static class ConsumerApiHandlers
return new ConsumerConfig();
}
}
private static int ParseBatch(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
return 1;
try
{
using var doc = JsonDocument.Parse(payload.ToArray());
if (doc.RootElement.TryGetProperty("batch", out var batchEl) && batchEl.TryGetInt32(out var batch))
return Math.Max(batch, 1);
}
catch (JsonException)
{
}
return 1;
}
private static bool ParsePause(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
return false;
try
{
using var doc = JsonDocument.Parse(payload.ToArray());
if (doc.RootElement.TryGetProperty("pause", out var pauseEl))
return pauseEl.ValueKind == JsonValueKind.True;
}
catch (JsonException)
{
}
return false;
}
private static string? ParseStreamSubject(string subject, string prefix)
{
if (!subject.StartsWith(prefix, StringComparison.Ordinal))
return null;
var stream = subject[prefix.Length..].Trim();
return stream.Length == 0 ? null : stream;
}
}