308 lines
12 KiB
C#
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;
|
|
}
|
|
}
|