Files
natsdotnet/src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs
2026-02-23 12:11:19 -05:00

308 lines
12 KiB
C#

using System.Text;
using System.Text.Json;
using NATS.Server.JetStream.Models;
namespace NATS.Server.JetStream.Api.Handlers;
public static class ConsumerApiHandlers
{
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)
{
var parsed = ParseSubject(subject, CreatePrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
var config = ParseConfig(payload);
if (string.IsNullOrWhiteSpace(config.DurableName))
config.DurableName = durableName;
return consumerManager.CreateOrUpdate(stream, config);
}
public static JetStreamApiResponse HandleInfo(string subject, ConsumerManager consumerManager)
{
var parsed = ParseSubject(subject, InfoPrefix);
if (parsed == null)
return JetStreamApiResponse.NotFound(subject);
var (stream, durableName) = parsed.Value;
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))
return null;
var remainder = subject[prefix.Length..];
var split = remainder.Split('.', 2, StringSplitOptions.RemoveEmptyEntries);
if (split.Length != 2)
return null;
return (split[0], split[1]);
}
private static ConsumerConfig ParseConfig(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
return new ConsumerConfig();
try
{
using var doc = JsonDocument.Parse(payload.ToArray());
var root = doc.RootElement;
var config = new ConsumerConfig();
if (root.TryGetProperty("durable_name", out var durableEl))
config.DurableName = durableEl.GetString() ?? string.Empty;
if (root.TryGetProperty("filter_subject", out var filterEl))
config.FilterSubject = filterEl.GetString();
if (root.TryGetProperty("filter_subjects", out var filterSubjectsEl) && filterSubjectsEl.ValueKind == JsonValueKind.Array)
{
foreach (var item in filterSubjectsEl.EnumerateArray())
{
var filter = item.GetString();
if (!string.IsNullOrWhiteSpace(filter))
config.FilterSubjects.Add(filter);
}
}
if (root.TryGetProperty("ephemeral", out var ephemeralEl) && ephemeralEl.ValueKind == JsonValueKind.True)
config.Ephemeral = true;
if (root.TryGetProperty("push", out var pushEl) && pushEl.ValueKind == JsonValueKind.True)
config.Push = true;
if (root.TryGetProperty("heartbeat_ms", out var hbEl) && hbEl.TryGetInt32(out var hbMs))
config.HeartbeatMs = hbMs;
if (root.TryGetProperty("ack_wait_ms", out var ackWaitEl) && ackWaitEl.TryGetInt32(out var ackWait))
config.AckWaitMs = ackWait;
if (root.TryGetProperty("max_deliver", out var maxDeliverEl) && maxDeliverEl.TryGetInt32(out var maxDeliver))
config.MaxDeliver = Math.Max(maxDeliver, 0);
if (root.TryGetProperty("max_ack_pending", out var maxAckPendingEl) && maxAckPendingEl.TryGetInt32(out var maxAckPending))
config.MaxAckPending = Math.Max(maxAckPending, 0);
if (root.TryGetProperty("flow_control", out var flowControlEl) && flowControlEl.ValueKind is JsonValueKind.True or JsonValueKind.False)
config.FlowControl = flowControlEl.GetBoolean();
if (root.TryGetProperty("rate_limit_bps", out var rateLimitEl) && rateLimitEl.TryGetInt64(out var rateLimit))
config.RateLimitBps = Math.Max(rateLimit, 0);
if (root.TryGetProperty("opt_start_seq", out var optStartSeqEl) && optStartSeqEl.TryGetUInt64(out var optStartSeq))
config.OptStartSeq = optStartSeq;
if (root.TryGetProperty("opt_start_time_utc", out var optStartTimeEl)
&& optStartTimeEl.ValueKind == JsonValueKind.String
&& DateTime.TryParse(optStartTimeEl.GetString(), out var optStartTime))
{
config.OptStartTimeUtc = optStartTime.ToUniversalTime();
}
if (root.TryGetProperty("backoff_ms", out var backoffEl) && backoffEl.ValueKind == JsonValueKind.Array)
{
foreach (var item in backoffEl.EnumerateArray())
{
if (item.TryGetInt32(out var backoffValue))
config.BackOffMs.Add(Math.Max(backoffValue, 0));
}
}
if (root.TryGetProperty("ack_policy", out var ackPolicyEl))
{
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;
}
if (root.TryGetProperty("deliver_policy", out var deliverPolicyEl))
{
var deliver = deliverPolicyEl.GetString();
if (string.Equals(deliver, "last", StringComparison.OrdinalIgnoreCase))
config.DeliverPolicy = DeliverPolicy.Last;
else if (string.Equals(deliver, "new", StringComparison.OrdinalIgnoreCase))
config.DeliverPolicy = DeliverPolicy.New;
else if (string.Equals(deliver, "by_start_sequence", StringComparison.OrdinalIgnoreCase))
config.DeliverPolicy = DeliverPolicy.ByStartSequence;
else if (string.Equals(deliver, "by_start_time", StringComparison.OrdinalIgnoreCase))
config.DeliverPolicy = DeliverPolicy.ByStartTime;
else if (string.Equals(deliver, "last_per_subject", StringComparison.OrdinalIgnoreCase))
config.DeliverPolicy = DeliverPolicy.LastPerSubject;
}
if (root.TryGetProperty("replay_policy", out var replayPolicyEl))
{
var replay = replayPolicyEl.GetString();
if (string.Equals(replay, "original", StringComparison.OrdinalIgnoreCase))
config.ReplayPolicy = ReplayPolicy.Original;
}
return config;
}
catch (JsonException)
{
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;
}
}