Files
natsnet/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs
Joseph Doherty ba4f41cf71 feat: implement SubscriptionIndex + JetStreamMemStore cluster — 39 features verified
Add SubscriptionIndex factory methods, notification wrappers, and
ValidateMapping. Implement 24 MemStore methods (TTL, scheduling, SDM,
age-check, purge/compact/reset) with JetStream header helpers and
constants. Verified features: 987 → 1026.
2026-02-27 06:19:47 -05:00

137 lines
5.1 KiB
C#

// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/sdm.go in the NATS server Go source.
namespace ZB.MOM.NatsNet.Server.Internal.DataStructures;
/// <summary>
/// Per-sequence data tracked by <see cref="StreamDeletionMeta"/>.
/// Mirrors <c>SDMBySeq</c> in server/sdm.go.
/// </summary>
public readonly struct SdmBySeq
{
/// <summary>Whether this sequence was the last message for its subject.</summary>
public bool Last { get; init; }
/// <summary>Timestamp (nanoseconds UTC) when the removal/SDM was last proposed.</summary>
public long Ts { get; init; }
}
/// <summary>
/// Tracks pending subject delete markers (SDMs) and message removals for a stream.
/// Used by JetStream cluster consensus to avoid redundant proposals.
/// Mirrors <c>SDMMeta</c> in server/sdm.go.
/// </summary>
public sealed class StreamDeletionMeta
{
// Per-subject pending-count totals.
private readonly Dictionary<string, ulong> _totals = new(1);
// Per-sequence data keyed by sequence number.
private readonly Dictionary<ulong, SdmBySeq> _pending = new(1);
// -------------------------------------------------------------------------
// Header constants (forward-declared; populated in session 19 — JetStream).
// isSubjectDeleteMarker checks these header keys.
// -------------------------------------------------------------------------
// Mirrors JSMarkerReason header key (defined in jetstream.go).
internal const string HeaderJsMarkerReason = "Nats-Marker-Reason";
// Mirrors KVOperation header key (defined in jetstream.go).
internal const string HeaderKvOperation = "KV-Operation";
// Mirrors KVOperationValuePurge (defined in jetstream.go).
internal const string KvOperationValuePurge = "PURGE";
/// <summary>
/// Returns true when the given header block contains a subject delete marker
/// (either a JetStream marker or a KV purge operation).
/// Mirrors <c>isSubjectDeleteMarker</c> in server/sdm.go.
/// </summary>
public static bool IsSubjectDeleteMarker(ReadOnlySpan<byte> hdr)
{
// Simplified header scan: checks whether JSMarkerReason key is present
// or whether KV-Operation equals "PURGE".
// Full implementation depends on SliceHeader from session 08 (client.go).
// Until then this provides the correct contract.
var text = System.Text.Encoding.UTF8.GetString(hdr);
if (text.Contains(HeaderJsMarkerReason))
return true;
if (text.Contains($"{HeaderKvOperation}: {KvOperationValuePurge}"))
return true;
return false;
}
/// <summary>
/// Tries to get the pending entry for <paramref name="seq"/>.
/// </summary>
public bool TryGetPending(ulong seq, out SdmBySeq entry) => _pending.TryGetValue(seq, out entry);
/// <summary>
/// Sets the pending entry for <paramref name="seq"/>.
/// </summary>
public void SetPending(ulong seq, SdmBySeq entry) => _pending[seq] = entry;
/// <summary>
/// Returns the pending count for <paramref name="subj"/>, or 0 if not tracked.
/// </summary>
public ulong GetSubjectTotal(string subj) => _totals.TryGetValue(subj, out var cnt) ? cnt : 0;
/// <summary>
/// Clears all tracked data.
/// Mirrors <c>SDMMeta.empty</c>.
/// </summary>
public void Empty()
{
_totals.Clear();
_pending.Clear();
}
/// <summary>
/// Tracks <paramref name="seq"/> as pending and returns whether it was
/// the last message for its subject. If the sequence is already tracked
/// the existing <c>Last</c> value is returned without modification.
/// Mirrors <c>SDMMeta.trackPending</c>.
/// </summary>
public bool TrackPending(ulong seq, string subj, bool last)
{
if (_pending.TryGetValue(seq, out var p))
return p.Last;
_pending[seq] = new SdmBySeq { Last = last, Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L };
_totals[subj] = _totals.TryGetValue(subj, out var cnt) ? cnt + 1 : 1;
return last;
}
/// <summary>
/// Removes <paramref name="seq"/> and decrements the pending count for
/// <paramref name="subj"/>, deleting the subject entry when it reaches zero.
/// Mirrors <c>SDMMeta.removeSeqAndSubject</c>.
/// </summary>
public void RemoveSeqAndSubject(ulong seq, string subj)
{
if (!_pending.Remove(seq))
return;
if (_totals.TryGetValue(subj, out var msgs))
{
if (msgs <= 1)
_totals.Remove(subj);
else
_totals[subj] = msgs - 1;
}
}
}