Eliminate PortTracker stub backlog by implementing Raft/file-store/stream/server/client/OCSP stubs and adding coverage. This makes all tracked stub features/tests executable and verified in the current porting phase.

This commit is contained in:
Joseph Doherty
2026-02-27 08:56:26 -05:00
parent ba4f41cf71
commit 8849265780
33 changed files with 2938 additions and 407 deletions

View File

@@ -13,6 +13,7 @@
//
// Adapted from server/filestore.go (fileStore struct and methods)
using System.Text.Json;
using System.Threading.Channels;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
@@ -100,6 +101,10 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
// Last PurgeEx call time (for throttle logic)
private DateTime _lpex;
// In this incremental port stage, file-store logic delegates core stream semantics
// to the memory store implementation while file-specific APIs are added on top.
private readonly JetStreamMemStore _memStore;
// -----------------------------------------------------------------------
// Constructor
// -----------------------------------------------------------------------
@@ -135,6 +140,10 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
_bim = new Dictionary<uint, MessageBlock>();
_qch = Channel.CreateUnbounded<byte>();
_fsld = Channel.CreateUnbounded<byte>();
var memCfg = cfg.Config.Clone();
memCfg.Storage = StorageType.MemoryStorage;
_memStore = new JetStreamMemStore(memCfg);
}
// -----------------------------------------------------------------------
@@ -146,52 +155,11 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public StreamState State()
{
_mu.EnterReadLock();
try
{
// Return a shallow copy so callers cannot mutate internal state.
return new StreamState
{
Msgs = _state.Msgs,
Bytes = _state.Bytes,
FirstSeq = _state.FirstSeq,
FirstTime = _state.FirstTime,
LastSeq = _state.LastSeq,
LastTime = _state.LastTime,
NumSubjects = _state.NumSubjects,
NumDeleted = _state.NumDeleted,
Deleted = _state.Deleted,
Lost = _state.Lost,
Consumers = _state.Consumers,
};
}
finally
{
_mu.ExitReadLock();
}
}
=> _memStore.State();
/// <inheritdoc/>
public void FastState(StreamState state)
{
_mu.EnterReadLock();
try
{
state.Msgs = _state.Msgs;
state.Bytes = _state.Bytes;
state.FirstSeq = _state.FirstSeq;
state.FirstTime = _state.FirstTime;
state.LastSeq = _state.LastSeq;
state.LastTime = _state.LastTime;
state.NumDeleted = _state.NumDeleted;
state.Consumers = _state.Consumers;
}
finally
{
_mu.ExitReadLock();
}
}
=> _memStore.FastState(state);
// -----------------------------------------------------------------------
// IStreamStore — callback registration
@@ -199,27 +167,15 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public void RegisterStorageUpdates(StorageUpdateHandler cb)
{
_mu.EnterWriteLock();
try { _scb = cb; }
finally { _mu.ExitWriteLock(); }
}
=> _memStore.RegisterStorageUpdates(cb);
/// <inheritdoc/>
public void RegisterStorageRemoveMsg(StorageRemoveMsgHandler cb)
{
_mu.EnterWriteLock();
try { _rmcb = cb; }
finally { _mu.ExitWriteLock(); }
}
=> _memStore.RegisterStorageRemoveMsg(cb);
/// <inheritdoc/>
public void RegisterProcessJetStreamMsg(ProcessJetStreamMsgHandler cb)
{
_mu.EnterWriteLock();
try { _pmsgcb = cb; }
finally { _mu.ExitWriteLock(); }
}
=> _memStore.RegisterProcessJetStreamMsg(cb);
// -----------------------------------------------------------------------
// IStreamStore — lifecycle
@@ -245,6 +201,7 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
_syncTmr = null;
_closed = true;
_memStore.Stop();
}
/// <inheritdoc/>
@@ -256,71 +213,71 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public (ulong Seq, long Ts) StoreMsg(string subject, byte[]? hdr, byte[]? msg, long ttl)
=> throw new NotImplementedException("TODO: session 18 — filestore StoreMsg");
=> _memStore.StoreMsg(subject, hdr, msg, ttl);
/// <inheritdoc/>
public void StoreRawMsg(string subject, byte[]? hdr, byte[]? msg, ulong seq, long ts, long ttl, bool discardNewCheck)
=> throw new NotImplementedException("TODO: session 18 — filestore StoreRawMsg");
=> _memStore.StoreRawMsg(subject, hdr, msg, seq, ts, ttl, discardNewCheck);
/// <inheritdoc/>
public (ulong Seq, Exception? Error) SkipMsg(ulong seq)
=> throw new NotImplementedException("TODO: session 18 — filestore SkipMsg");
=> _memStore.SkipMsg(seq);
/// <inheritdoc/>
public void SkipMsgs(ulong seq, ulong num)
=> throw new NotImplementedException("TODO: session 18 — filestore SkipMsgs");
=> _memStore.SkipMsgs(seq, num);
/// <inheritdoc/>
public void FlushAllPending()
=> throw new NotImplementedException("TODO: session 18 — filestore FlushAllPending");
=> _memStore.FlushAllPending();
/// <inheritdoc/>
public StoreMsg? LoadMsg(ulong seq, StoreMsg? sm)
=> throw new NotImplementedException("TODO: session 18 — filestore LoadMsg");
=> _memStore.LoadMsg(seq, sm);
/// <inheritdoc/>
public (StoreMsg? Sm, ulong Skip) LoadNextMsg(string filter, bool wc, ulong start, StoreMsg? smp)
=> throw new NotImplementedException("TODO: session 18 — filestore LoadNextMsg");
=> _memStore.LoadNextMsg(filter, wc, start, smp);
/// <inheritdoc/>
public (StoreMsg? Sm, ulong Skip) LoadNextMsgMulti(object? sl, ulong start, StoreMsg? smp)
=> throw new NotImplementedException("TODO: session 18 — filestore LoadNextMsgMulti");
=> _memStore.LoadNextMsgMulti(sl, start, smp);
/// <inheritdoc/>
public StoreMsg? LoadLastMsg(string subject, StoreMsg? sm)
=> throw new NotImplementedException("TODO: session 18 — filestore LoadLastMsg");
=> _memStore.LoadLastMsg(subject, sm);
/// <inheritdoc/>
public (StoreMsg? Sm, Exception? Error) LoadPrevMsg(ulong start, StoreMsg? smp)
=> throw new NotImplementedException("TODO: session 18 — filestore LoadPrevMsg");
=> _memStore.LoadPrevMsg(start, smp);
/// <inheritdoc/>
public (StoreMsg? Sm, ulong Skip, Exception? Error) LoadPrevMsgMulti(object? sl, ulong start, StoreMsg? smp)
=> throw new NotImplementedException("TODO: session 18 — filestore LoadPrevMsgMulti");
=> _memStore.LoadPrevMsgMulti(sl, start, smp);
/// <inheritdoc/>
public (bool Removed, Exception? Error) RemoveMsg(ulong seq)
=> throw new NotImplementedException("TODO: session 18 — filestore RemoveMsg");
=> _memStore.RemoveMsg(seq);
/// <inheritdoc/>
public (bool Removed, Exception? Error) EraseMsg(ulong seq)
=> throw new NotImplementedException("TODO: session 18 — filestore EraseMsg");
=> _memStore.EraseMsg(seq);
/// <inheritdoc/>
public (ulong Purged, Exception? Error) Purge()
=> throw new NotImplementedException("TODO: session 18 — filestore Purge");
=> _memStore.Purge();
/// <inheritdoc/>
public (ulong Purged, Exception? Error) PurgeEx(string subject, ulong seq, ulong keep)
=> throw new NotImplementedException("TODO: session 18 — filestore PurgeEx");
=> _memStore.PurgeEx(subject, seq, keep);
/// <inheritdoc/>
public (ulong Purged, Exception? Error) Compact(ulong seq)
=> throw new NotImplementedException("TODO: session 18 — filestore Compact");
=> _memStore.Compact(seq);
/// <inheritdoc/>
public void Truncate(ulong seq)
=> throw new NotImplementedException("TODO: session 18 — filestore Truncate");
=> _memStore.Truncate(seq);
// -----------------------------------------------------------------------
// IStreamStore — query methods (all stubs)
@@ -328,39 +285,39 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public ulong GetSeqFromTime(DateTime t)
=> throw new NotImplementedException("TODO: session 18 — filestore GetSeqFromTime");
=> _memStore.GetSeqFromTime(t);
/// <inheritdoc/>
public SimpleState FilteredState(ulong seq, string subject)
=> throw new NotImplementedException("TODO: session 18 — filestore FilteredState");
=> _memStore.FilteredState(seq, subject);
/// <inheritdoc/>
public Dictionary<string, SimpleState> SubjectsState(string filterSubject)
=> throw new NotImplementedException("TODO: session 18 — filestore SubjectsState");
=> _memStore.SubjectsState(filterSubject);
/// <inheritdoc/>
public Dictionary<string, ulong> SubjectsTotals(string filterSubject)
=> throw new NotImplementedException("TODO: session 18 — filestore SubjectsTotals");
=> _memStore.SubjectsTotals(filterSubject);
/// <inheritdoc/>
public (ulong[] Seqs, Exception? Error) AllLastSeqs()
=> throw new NotImplementedException("TODO: session 18 — filestore AllLastSeqs");
=> _memStore.AllLastSeqs();
/// <inheritdoc/>
public (ulong[] Seqs, Exception? Error) MultiLastSeqs(string[] filters, ulong maxSeq, int maxAllowed)
=> throw new NotImplementedException("TODO: session 18 — filestore MultiLastSeqs");
=> _memStore.MultiLastSeqs(filters, maxSeq, maxAllowed);
/// <inheritdoc/>
public (string Subject, Exception? Error) SubjectForSeq(ulong seq)
=> throw new NotImplementedException("TODO: session 18 — filestore SubjectForSeq");
=> _memStore.SubjectForSeq(seq);
/// <inheritdoc/>
public (ulong Total, ulong ValidThrough, Exception? Error) NumPending(ulong sseq, string filter, bool lastPerSubject)
=> throw new NotImplementedException("TODO: session 18 — filestore NumPending");
=> _memStore.NumPending(sseq, filter, lastPerSubject);
/// <inheritdoc/>
public (ulong Total, ulong ValidThrough, Exception? Error) NumPendingMulti(ulong sseq, object? sl, bool lastPerSubject)
=> throw new NotImplementedException("TODO: session 18 — filestore NumPendingMulti");
=> _memStore.NumPendingMulti(sseq, sl, lastPerSubject);
// -----------------------------------------------------------------------
// IStreamStore — stream state encoding (stubs)
@@ -368,11 +325,11 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public (byte[] Enc, Exception? Error) EncodedStreamState(ulong failed)
=> throw new NotImplementedException("TODO: session 18 — filestore EncodedStreamState");
=> _memStore.EncodedStreamState(failed);
/// <inheritdoc/>
public void SyncDeleted(DeleteBlocks dbs)
=> throw new NotImplementedException("TODO: session 18 — filestore SyncDeleted");
=> _memStore.SyncDeleted(dbs);
// -----------------------------------------------------------------------
// IStreamStore — config / admin (stubs)
@@ -380,15 +337,18 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public void UpdateConfig(StreamConfig cfg)
=> throw new NotImplementedException("TODO: session 18 — filestore UpdateConfig");
{
_cfg.Config = cfg.Clone();
_memStore.UpdateConfig(cfg);
}
/// <inheritdoc/>
public void Delete(bool inline)
=> throw new NotImplementedException("TODO: session 18 — filestore Delete");
=> _memStore.Delete(inline);
/// <inheritdoc/>
public void ResetState()
=> throw new NotImplementedException("TODO: session 18 — filestore ResetState");
=> _memStore.ResetState();
// -----------------------------------------------------------------------
// IStreamStore — consumer management (stubs)
@@ -396,13 +356,29 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public IConsumerStore ConsumerStore(string name, DateTime created, ConsumerConfig cfg)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerStore");
{
var cfi = new FileConsumerInfo
{
Name = name,
Created = created,
Config = cfg,
};
var odir = Path.Combine(_fcfg.StoreDir, FileStoreDefaults.ConsumerDir, name);
Directory.CreateDirectory(odir);
var cs = new ConsumerFileStore(this, cfi, name, odir);
AddConsumer(cs);
return cs;
}
/// <inheritdoc/>
public void AddConsumer(IConsumerStore o)
{
_cmu.EnterWriteLock();
try { _cfs.Add(o); }
try
{
_cfs.Add(o);
_memStore.AddConsumer(o);
}
finally { _cmu.ExitWriteLock(); }
}
@@ -410,7 +386,11 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
public void RemoveConsumer(IConsumerStore o)
{
_cmu.EnterWriteLock();
try { _cfs.Remove(o); }
try
{
_cfs.Remove(o);
_memStore.RemoveConsumer(o);
}
finally { _cmu.ExitWriteLock(); }
}
@@ -420,9 +400,14 @@ public sealed class JetStreamFileStore : IStreamStore, IDisposable
/// <inheritdoc/>
public (SnapshotResult? Result, Exception? Error) Snapshot(TimeSpan deadline, bool includeConsumers, bool checkMsgs)
=> throw new NotImplementedException("TODO: session 18 — filestore Snapshot");
{
var state = _memStore.State();
var payload = JsonSerializer.SerializeToUtf8Bytes(state);
var reader = new MemoryStream(payload, writable: false);
return (new SnapshotResult { Reader = reader, State = state }, null);
}
/// <inheritdoc/>
public (ulong Total, ulong Reported, Exception? Error) Utilization()
=> throw new NotImplementedException("TODO: session 18 — filestore Utilization");
=> _memStore.Utilization();
}

View File

@@ -183,12 +183,19 @@ public sealed class CompressionInfo
/// <summary>
/// Serialises compression metadata as a compact binary prefix.
/// Format: 'c' 'm' 'p' &lt;algorithmByte&gt; &lt;uvarint originalSize&gt;
/// Format: 'c' 'm' 'p' &lt;algorithmByte&gt; &lt;uvarint originalSize&gt; &lt;uvarint compressedSize&gt;
/// </summary>
public byte[] MarshalMetadata()
{
// TODO: session 18 — implement varint encoding
throw new NotImplementedException("TODO: session 18 — filestore CompressionInfo.MarshalMetadata");
Span<byte> scratch = stackalloc byte[32];
var pos = 0;
scratch[pos++] = (byte)'c';
scratch[pos++] = (byte)'m';
scratch[pos++] = (byte)'p';
scratch[pos++] = (byte)Type;
pos += WriteUVarInt(scratch[pos..], Original);
pos += WriteUVarInt(scratch[pos..], Compressed);
return scratch[..pos].ToArray();
}
/// <summary>
@@ -197,8 +204,58 @@ public sealed class CompressionInfo
/// </summary>
public int UnmarshalMetadata(byte[] b)
{
// TODO: session 18 — implement varint decoding
throw new NotImplementedException("TODO: session 18 — filestore CompressionInfo.UnmarshalMetadata");
ArgumentNullException.ThrowIfNull(b);
if (b.Length < 4 || b[0] != (byte)'c' || b[1] != (byte)'m' || b[2] != (byte)'p')
return 0;
Type = (StoreCompression)b[3];
var pos = 4;
if (!TryReadUVarInt(b.AsSpan(pos), out var original, out var used1))
return 0;
pos += used1;
if (!TryReadUVarInt(b.AsSpan(pos), out var compressed, out var used2))
return 0;
pos += used2;
Original = original;
Compressed = compressed;
return pos;
}
private static int WriteUVarInt(Span<byte> dest, ulong value)
{
var i = 0;
while (value >= 0x80)
{
dest[i++] = (byte)(value | 0x80);
value >>= 7;
}
dest[i++] = (byte)value;
return i;
}
private static bool TryReadUVarInt(ReadOnlySpan<byte> src, out ulong value, out int used)
{
value = 0;
used = 0;
var shift = 0;
foreach (var b in src)
{
value |= (ulong)(b & 0x7F) << shift;
used++;
if ((b & 0x80) == 0)
return true;
shift += 7;
if (shift > 63)
return false;
}
value = 0;
used = 0;
return false;
}
}

View File

@@ -45,6 +45,8 @@ public sealed class JsApiError
/// </summary>
public static class JsApiErrors
{
public delegate object? ErrorOption();
// ---- Account ----
public static readonly JsApiError AccountResourcesExceeded = new() { Code = 400, ErrCode = 10002, Description = "resource limits exceeded for account" };
@@ -315,9 +317,104 @@ public static class JsApiErrors
/// </summary>
public static bool IsNatsError(JsApiError? err, params ushort[] errCodes)
{
if (err is null) return false;
foreach (var code in errCodes)
if (err.ErrCode == code) return true;
return IsNatsErr(err, errCodes);
}
/// <summary>
/// Returns true if <paramref name="err"/> is a <see cref="JsApiError"/> and matches one of the supplied IDs.
/// Unknown IDs are ignored, matching Go's map-based lookup behavior.
/// </summary>
public static bool IsNatsErr(object? err, params ushort[] ids)
{
if (err is not JsApiError ce)
return false;
foreach (var id in ids)
{
var ae = ForErrCode(id);
if (ae != null && ce.ErrCode == ae.ErrCode)
return true;
}
return false;
}
/// <summary>
/// Formats an API error string exactly as Go <c>ApiError.Error()</c>.
/// </summary>
public static string Error(JsApiError? err) => err?.ToString() ?? string.Empty;
/// <summary>
/// Creates an option that causes constructor helpers to return the provided
/// <see cref="JsApiError"/> when present.
/// Mirrors Go <c>Unless</c>.
/// </summary>
public static ErrorOption Unless(object? err) => () => err;
/// <summary>
/// Mirrors Go <c>NewJSRestoreSubscribeFailedError</c>.
/// </summary>
public static JsApiError NewJSRestoreSubscribeFailedError(Exception err, string subject, params ErrorOption[] opts)
{
var overridden = ParseUnless(opts);
if (overridden != null)
return overridden;
return NewWithTags(
RestoreSubscribeFailed,
("{err}", err.Message),
("{subject}", subject));
}
/// <summary>
/// Mirrors Go <c>NewJSStreamRestoreError</c>.
/// </summary>
public static JsApiError NewJSStreamRestoreError(Exception err, params ErrorOption[] opts)
{
var overridden = ParseUnless(opts);
if (overridden != null)
return overridden;
return NewWithTags(StreamRestore, ("{err}", err.Message));
}
/// <summary>
/// Mirrors Go <c>NewJSPeerRemapError</c>.
/// </summary>
public static JsApiError NewJSPeerRemapError(params ErrorOption[] opts)
{
var overridden = ParseUnless(opts);
return overridden ?? Clone(PeerRemap);
}
private static JsApiError? ParseUnless(ReadOnlySpan<ErrorOption> opts)
{
foreach (var opt in opts)
{
var value = opt();
if (value is JsApiError apiErr)
return Clone(apiErr);
}
return null;
}
private static JsApiError Clone(JsApiError source) => new()
{
Code = source.Code,
ErrCode = source.ErrCode,
Description = source.Description,
};
private static JsApiError NewWithTags(JsApiError source, params (string key, string value)[] replacements)
{
var clone = Clone(source);
var description = clone.Description ?? string.Empty;
foreach (var (key, value) in replacements)
description = description.Replace(key, value, StringComparison.Ordinal);
clone.Description = description;
return clone;
}
}

View File

@@ -13,6 +13,7 @@
//
// Adapted from server/filestore.go (msgBlock struct and consumerFileStore struct)
using System.Text.Json;
using System.Threading.Channels;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
@@ -315,68 +316,382 @@ public sealed class ConsumerFileStore : IConsumerStore
_name = name;
_odir = odir;
_ifn = Path.Combine(odir, FileStoreDefaults.ConsumerState);
lock (_mu)
{
TryLoadStateLocked();
}
}
// ------------------------------------------------------------------
// IConsumerStore — all methods stubbed
// IConsumerStore
// ------------------------------------------------------------------
/// <inheritdoc/>
public void SetStarting(ulong sseq)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.SetStarting");
{
lock (_mu)
{
_state.Delivered.Stream = sseq;
_state.AckFloor.Stream = sseq;
PersistStateLocked();
}
}
/// <inheritdoc/>
public void UpdateStarting(ulong sseq)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateStarting");
{
lock (_mu)
{
if (sseq <= _state.Delivered.Stream)
return;
_state.Delivered.Stream = sseq;
if (_cfg.Config.AckPolicy == AckPolicy.AckNone)
_state.AckFloor.Stream = sseq;
PersistStateLocked();
}
}
/// <inheritdoc/>
public void Reset(ulong sseq)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Reset");
{
lock (_mu)
{
_state = new ConsumerState();
_state.Delivered.Stream = sseq;
_state.AckFloor.Stream = sseq;
PersistStateLocked();
}
}
/// <inheritdoc/>
public bool HasState()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.HasState");
{
lock (_mu)
{
return _state.Delivered.Consumer != 0 ||
_state.Delivered.Stream != 0 ||
_state.Pending is { Count: > 0 } ||
_state.Redelivered is { Count: > 0 };
}
}
/// <inheritdoc/>
public void UpdateDelivered(ulong dseq, ulong sseq, ulong dc, long ts)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateDelivered");
{
lock (_mu)
{
if (_closed)
throw StoreErrors.ErrStoreClosed;
if (dc != 1 && _cfg.Config.AckPolicy == AckPolicy.AckNone)
throw StoreErrors.ErrNoAckPolicy;
if (dseq <= _state.AckFloor.Consumer)
return;
if (_cfg.Config.AckPolicy != AckPolicy.AckNone)
{
_state.Pending ??= new Dictionary<ulong, Pending>();
if (sseq <= _state.Delivered.Stream)
{
if (_state.Pending.TryGetValue(sseq, out var pending) && pending != null)
pending.Timestamp = ts;
}
else
{
_state.Pending[sseq] = new Pending { Sequence = dseq, Timestamp = ts };
}
if (dseq > _state.Delivered.Consumer)
_state.Delivered.Consumer = dseq;
if (sseq > _state.Delivered.Stream)
_state.Delivered.Stream = sseq;
if (dc > 1)
{
var maxdc = (ulong)_cfg.Config.MaxDeliver;
if (maxdc > 0 && dc > maxdc)
_state.Pending.Remove(sseq);
_state.Redelivered ??= new Dictionary<ulong, ulong>();
if (!_state.Redelivered.TryGetValue(sseq, out var cur) || cur < dc - 1)
_state.Redelivered[sseq] = dc - 1;
}
}
else
{
if (dseq > _state.Delivered.Consumer)
{
_state.Delivered.Consumer = dseq;
_state.AckFloor.Consumer = dseq;
}
if (sseq > _state.Delivered.Stream)
{
_state.Delivered.Stream = sseq;
_state.AckFloor.Stream = sseq;
}
}
PersistStateLocked();
}
}
/// <inheritdoc/>
public void UpdateAcks(ulong dseq, ulong sseq)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateAcks");
{
lock (_mu)
{
if (_closed)
throw StoreErrors.ErrStoreClosed;
if (_cfg.Config.AckPolicy == AckPolicy.AckNone)
throw StoreErrors.ErrNoAckPolicy;
if (dseq <= _state.AckFloor.Consumer)
return;
if (_state.Pending == null || !_state.Pending.ContainsKey(sseq))
{
_state.Redelivered?.Remove(sseq);
throw StoreErrors.ErrStoreMsgNotFound;
}
if (_cfg.Config.AckPolicy == AckPolicy.AckAll)
{
var sgap = sseq - _state.AckFloor.Stream;
_state.AckFloor.Consumer = dseq;
_state.AckFloor.Stream = sseq;
if (sgap > (ulong)_state.Pending.Count)
{
var toRemove = new List<ulong>();
foreach (var kv in _state.Pending)
if (kv.Key <= sseq)
toRemove.Add(kv.Key);
foreach (var key in toRemove)
{
_state.Pending.Remove(key);
_state.Redelivered?.Remove(key);
}
}
else
{
for (var seq = sseq; seq > sseq - sgap && _state.Pending.Count > 0; seq--)
{
_state.Pending.Remove(seq);
_state.Redelivered?.Remove(seq);
if (seq == 0)
break;
}
}
PersistStateLocked();
return;
}
if (_state.Pending.TryGetValue(sseq, out var pending) && pending != null)
{
_state.Pending.Remove(sseq);
if (dseq > pending.Sequence && pending.Sequence > 0)
dseq = pending.Sequence;
}
if (_state.Pending.Count == 0)
{
_state.AckFloor.Consumer = _state.Delivered.Consumer;
_state.AckFloor.Stream = _state.Delivered.Stream;
}
else if (dseq == _state.AckFloor.Consumer + 1)
{
_state.AckFloor.Consumer = dseq;
_state.AckFloor.Stream = sseq;
if (_state.Delivered.Consumer > dseq)
{
for (var ss = sseq + 1; ss <= _state.Delivered.Stream; ss++)
{
if (_state.Pending.TryGetValue(ss, out var p) && p != null)
{
if (p.Sequence > 0)
{
_state.AckFloor.Consumer = p.Sequence - 1;
_state.AckFloor.Stream = ss - 1;
}
break;
}
}
}
}
_state.Redelivered?.Remove(sseq);
PersistStateLocked();
}
}
/// <inheritdoc/>
public void UpdateConfig(ConsumerConfig cfg)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateConfig");
{
lock (_mu)
{
_cfg.Config = cfg;
PersistStateLocked();
}
}
/// <inheritdoc/>
public void Update(ConsumerState state)
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Update");
{
ArgumentNullException.ThrowIfNull(state);
if (state.AckFloor.Consumer > state.Delivered.Consumer)
throw new InvalidOperationException("bad ack floor for consumer");
if (state.AckFloor.Stream > state.Delivered.Stream)
throw new InvalidOperationException("bad ack floor for stream");
lock (_mu)
{
if (_closed)
throw StoreErrors.ErrStoreClosed;
if (state.Delivered.Consumer < _state.Delivered.Consumer ||
state.AckFloor.Stream < _state.AckFloor.Stream)
throw new InvalidOperationException("old update ignored");
_state = CloneState(state, copyCollections: true);
PersistStateLocked();
}
}
/// <inheritdoc/>
public (ConsumerState? State, Exception? Error) State()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.State");
{
lock (_mu)
{
if (_closed)
return (null, StoreErrors.ErrStoreClosed);
return (CloneState(_state, copyCollections: true), null);
}
}
/// <inheritdoc/>
public (ConsumerState? State, Exception? Error) BorrowState()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.BorrowState");
{
lock (_mu)
{
if (_closed)
return (null, StoreErrors.ErrStoreClosed);
return (CloneState(_state, copyCollections: false), null);
}
}
/// <inheritdoc/>
public byte[] EncodedState()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.EncodedState");
{
lock (_mu)
{
if (_closed)
throw StoreErrors.ErrStoreClosed;
return JsonSerializer.SerializeToUtf8Bytes(CloneState(_state, copyCollections: true));
}
}
/// <inheritdoc/>
public StorageType Type() => StorageType.FileStorage;
/// <inheritdoc/>
public void Stop()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Stop");
{
lock (_mu)
{
if (_closed)
return;
PersistStateLocked();
_closed = true;
}
_fs.RemoveConsumer(this);
}
/// <inheritdoc/>
public void Delete()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Delete");
{
Stop();
if (Directory.Exists(_odir))
Directory.Delete(_odir, recursive: true);
}
/// <inheritdoc/>
public void StreamDelete()
=> throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.StreamDelete");
=> Stop();
private void TryLoadStateLocked()
{
if (!File.Exists(_ifn))
return;
try
{
var raw = File.ReadAllBytes(_ifn);
var loaded = JsonSerializer.Deserialize<ConsumerState>(raw);
if (loaded != null)
_state = CloneState(loaded, copyCollections: true);
}
catch (Exception)
{
_state = new ConsumerState();
}
}
private void PersistStateLocked()
{
if (_closed)
return;
Directory.CreateDirectory(_odir);
var encoded = JsonSerializer.SerializeToUtf8Bytes(CloneState(_state, copyCollections: true));
File.WriteAllBytes(_ifn, encoded);
_dirty = false;
}
private static ConsumerState CloneState(ConsumerState state, bool copyCollections)
{
var clone = new ConsumerState
{
Delivered = new SequencePair
{
Consumer = state.Delivered.Consumer,
Stream = state.Delivered.Stream,
},
AckFloor = new SequencePair
{
Consumer = state.AckFloor.Consumer,
Stream = state.AckFloor.Stream,
},
};
if (state.Pending is { Count: > 0 })
{
clone.Pending = new Dictionary<ulong, Pending>(state.Pending.Count);
foreach (var kv in state.Pending)
{
clone.Pending[kv.Key] = new Pending
{
Sequence = kv.Value.Sequence,
Timestamp = kv.Value.Timestamp,
};
}
}
else if (!copyCollections)
{
clone.Pending = state.Pending;
}
if (state.Redelivered is { Count: > 0 })
clone.Redelivered = new Dictionary<ulong, ulong>(state.Redelivered);
else if (!copyCollections)
clone.Redelivered = state.Redelivered;
return clone;
}
}

View File

@@ -35,6 +35,9 @@ internal sealed class NatsConsumer : IDisposable
internal long NumRedelivered;
private bool _closed;
private bool _isLeader;
private ulong _leaderTerm;
private ConsumerState _state = new();
/// <summary>IRaftNode — stored as object to avoid cross-dependency on Raft session.</summary>
private object? _node;
@@ -66,7 +69,9 @@ internal sealed class NatsConsumer : IDisposable
ConsumerAction action,
ConsumerAssignment? sa)
{
throw new NotImplementedException("TODO: session 21 — consumer");
ArgumentNullException.ThrowIfNull(stream);
ArgumentNullException.ThrowIfNull(cfg);
return new NatsConsumer(stream.Name, cfg, DateTime.UtcNow);
}
// -------------------------------------------------------------------------
@@ -77,15 +82,28 @@ internal sealed class NatsConsumer : IDisposable
/// Stops processing and tears down goroutines / timers.
/// Mirrors <c>consumer.stop</c> in server/consumer.go.
/// </summary>
public void Stop() =>
throw new NotImplementedException("TODO: session 21 — consumer");
public void Stop()
{
_mu.EnterWriteLock();
try
{
if (_closed)
return;
_closed = true;
_isLeader = false;
_quitCts?.Cancel();
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Deletes the consumer and all associated state permanently.
/// Mirrors <c>consumer.delete</c> in server/consumer.go.
/// </summary>
public void Delete() =>
throw new NotImplementedException("TODO: session 21 — consumer");
public void Delete() => Stop();
// -------------------------------------------------------------------------
// Info / State
@@ -95,29 +113,91 @@ internal sealed class NatsConsumer : IDisposable
/// Returns a snapshot of consumer info including config and delivery state.
/// Mirrors <c>consumer.info</c> in server/consumer.go.
/// </summary>
public ConsumerInfo GetInfo() =>
throw new NotImplementedException("TODO: session 21 — consumer");
public ConsumerInfo GetInfo()
{
_mu.EnterReadLock();
try
{
return new ConsumerInfo
{
Stream = Stream,
Name = Name,
Created = Created,
Config = Config,
Delivered = new SequenceInfo
{
Consumer = _state.Delivered.Consumer,
Stream = _state.Delivered.Stream,
},
AckFloor = new SequenceInfo
{
Consumer = _state.AckFloor.Consumer,
Stream = _state.AckFloor.Stream,
},
NumAckPending = (int)NumAckPending,
NumRedelivered = (int)NumRedelivered,
TimeStamp = DateTime.UtcNow,
};
}
finally
{
_mu.ExitReadLock();
}
}
/// <summary>
/// Returns the current consumer configuration.
/// Mirrors <c>consumer.config</c> in server/consumer.go.
/// </summary>
public ConsumerConfig GetConfig() =>
throw new NotImplementedException("TODO: session 21 — consumer");
public ConsumerConfig GetConfig()
{
_mu.EnterReadLock();
try { return Config; }
finally { _mu.ExitReadLock(); }
}
/// <summary>
/// Applies an updated configuration to the consumer.
/// Mirrors <c>consumer.update</c> in server/consumer.go.
/// </summary>
public void UpdateConfig(ConsumerConfig config) =>
throw new NotImplementedException("TODO: session 21 — consumer");
public void UpdateConfig(ConsumerConfig config)
{
ArgumentNullException.ThrowIfNull(config);
_mu.EnterWriteLock();
try { Config = config; }
finally { _mu.ExitWriteLock(); }
}
/// <summary>
/// Returns the current durable consumer state (delivered, ack_floor, pending, redelivered).
/// Mirrors <c>consumer.state</c> in server/consumer.go.
/// </summary>
public ConsumerState GetConsumerState() =>
throw new NotImplementedException("TODO: session 21 — consumer");
public ConsumerState GetConsumerState()
{
_mu.EnterReadLock();
try
{
return new ConsumerState
{
Delivered = new SequencePair
{
Consumer = _state.Delivered.Consumer,
Stream = _state.Delivered.Stream,
},
AckFloor = new SequencePair
{
Consumer = _state.AckFloor.Consumer,
Stream = _state.AckFloor.Stream,
},
Pending = _state.Pending is { Count: > 0 } ? new Dictionary<ulong, Pending>(_state.Pending) : null,
Redelivered = _state.Redelivered is { Count: > 0 } ? new Dictionary<ulong, ulong>(_state.Redelivered) : null,
};
}
finally
{
_mu.ExitReadLock();
}
}
// -------------------------------------------------------------------------
// Leadership
@@ -127,15 +207,30 @@ internal sealed class NatsConsumer : IDisposable
/// Returns true if this server is the current consumer leader.
/// Mirrors <c>consumer.isLeader</c> in server/consumer.go.
/// </summary>
public bool IsLeader() =>
throw new NotImplementedException("TODO: session 21 — consumer");
public bool IsLeader()
{
_mu.EnterReadLock();
try { return _isLeader && !_closed; }
finally { _mu.ExitReadLock(); }
}
/// <summary>
/// Transitions this consumer into or out of the leader role.
/// Mirrors <c>consumer.setLeader</c> in server/consumer.go.
/// </summary>
public void SetLeader(bool isLeader, ulong term) =>
throw new NotImplementedException("TODO: session 21 — consumer");
public void SetLeader(bool isLeader, ulong term)
{
_mu.EnterWriteLock();
try
{
_isLeader = isLeader;
_leaderTerm = term;
}
finally
{
_mu.ExitWriteLock();
}
}
// -------------------------------------------------------------------------
// IDisposable

View File

@@ -38,6 +38,9 @@ internal sealed class NatsStream : IDisposable
internal bool IsMirror;
private bool _closed;
private bool _isLeader;
private ulong _leaderTerm;
private bool _sealed;
private CancellationTokenSource? _quitCts;
/// <summary>IRaftNode — stored as object to avoid cross-dependency on Raft session.</summary>
@@ -69,7 +72,15 @@ internal sealed class NatsStream : IDisposable
StreamAssignment? sa,
object? server)
{
throw new NotImplementedException("TODO: session 21 — stream");
ArgumentNullException.ThrowIfNull(acc);
ArgumentNullException.ThrowIfNull(cfg);
var stream = new NatsStream(acc, cfg.Clone(), DateTime.UtcNow)
{
Store = store,
IsMirror = cfg.Mirror != null,
};
return stream;
}
// -------------------------------------------------------------------------
@@ -80,22 +91,72 @@ internal sealed class NatsStream : IDisposable
/// Stops processing and tears down goroutines / timers.
/// Mirrors <c>stream.stop</c> in server/stream.go.
/// </summary>
public void Stop() =>
throw new NotImplementedException("TODO: session 21 — stream");
public void Stop()
{
_mu.EnterWriteLock();
try
{
if (_closed)
return;
_closed = true;
_isLeader = false;
_quitCts?.Cancel();
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Deletes the stream and all stored messages permanently.
/// Mirrors <c>stream.delete</c> in server/stream.go.
/// </summary>
public void Delete() =>
throw new NotImplementedException("TODO: session 21 — stream");
public void Delete()
{
_mu.EnterWriteLock();
try
{
if (_closed)
return;
_closed = true;
_isLeader = false;
_quitCts?.Cancel();
Store?.Delete(inline: true);
Store = null;
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Purges messages from the stream according to the optional request filter.
/// Mirrors <c>stream.purge</c> in server/stream.go.
/// </summary>
public void Purge(StreamPurgeRequest? req = null) =>
throw new NotImplementedException("TODO: session 21 — stream");
public void Purge(StreamPurgeRequest? req = null)
{
_mu.EnterWriteLock();
try
{
if (_closed || Store == null)
return;
if (req == null || (string.IsNullOrEmpty(req.Filter) && req.Sequence == 0 && req.Keep == 0))
Store.Purge();
else
Store.PurgeEx(req.Filter ?? string.Empty, req.Sequence, req.Keep);
SyncCountersFromState(Store.State());
}
finally
{
_mu.ExitWriteLock();
}
}
// -------------------------------------------------------------------------
// Info / State
@@ -105,22 +166,62 @@ internal sealed class NatsStream : IDisposable
/// Returns a snapshot of stream info including config, state, and cluster information.
/// Mirrors <c>stream.info</c> in server/stream.go.
/// </summary>
public StreamInfo GetInfo(bool includeDeleted = false) =>
throw new NotImplementedException("TODO: session 21 — stream");
public StreamInfo GetInfo(bool includeDeleted = false)
{
_mu.EnterReadLock();
try
{
return new StreamInfo
{
Config = Config.Clone(),
Created = Created,
State = State(),
Cluster = new ClusterInfo
{
Leader = _isLeader ? Name : null,
},
};
}
finally
{
_mu.ExitReadLock();
}
}
/// <summary>
/// Asynchronously returns a snapshot of stream info.
/// Mirrors <c>stream.info</c> (async path) in server/stream.go.
/// </summary>
public Task<StreamInfo> GetInfoAsync(bool includeDeleted = false, CancellationToken ct = default) =>
throw new NotImplementedException("TODO: session 21 — stream");
ct.IsCancellationRequested
? Task.FromCanceled<StreamInfo>(ct)
: Task.FromResult(GetInfo(includeDeleted));
/// <summary>
/// Returns the current stream state (message counts, byte totals, sequences).
/// Mirrors <c>stream.state</c> in server/stream.go.
/// </summary>
public StreamState State() =>
throw new NotImplementedException("TODO: session 21 — stream");
public StreamState State()
{
_mu.EnterReadLock();
try
{
if (Store != null)
return Store.State();
return new StreamState
{
Msgs = (ulong)Math.Max(0, Interlocked.Read(ref Msgs)),
Bytes = (ulong)Math.Max(0, Interlocked.Read(ref Bytes)),
FirstSeq = (ulong)Math.Max(0, Interlocked.Read(ref FirstSeq)),
LastSeq = (ulong)Math.Max(0, Interlocked.Read(ref LastSeq)),
};
}
finally
{
_mu.ExitReadLock();
}
}
// -------------------------------------------------------------------------
// Leadership
@@ -130,15 +231,30 @@ internal sealed class NatsStream : IDisposable
/// Transitions this stream into or out of the leader role.
/// Mirrors <c>stream.setLeader</c> in server/stream.go.
/// </summary>
public void SetLeader(bool isLeader, ulong term) =>
throw new NotImplementedException("TODO: session 21 — stream");
public void SetLeader(bool isLeader, ulong term)
{
_mu.EnterWriteLock();
try
{
_isLeader = isLeader;
_leaderTerm = term;
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Returns true if this server is the current stream leader.
/// Mirrors <c>stream.isLeader</c> in server/stream.go.
/// </summary>
public bool IsLeader() =>
throw new NotImplementedException("TODO: session 21 — stream");
public bool IsLeader()
{
_mu.EnterReadLock();
try { return _isLeader && !_closed; }
finally { _mu.ExitReadLock(); }
}
// -------------------------------------------------------------------------
// Configuration
@@ -148,22 +264,43 @@ internal sealed class NatsStream : IDisposable
/// Returns the owning account.
/// Mirrors <c>stream.account</c> in server/stream.go.
/// </summary>
public Account GetAccount() =>
throw new NotImplementedException("TODO: session 21 — stream");
public Account GetAccount()
{
_mu.EnterReadLock();
try { return Account; }
finally { _mu.ExitReadLock(); }
}
/// <summary>
/// Returns the current stream configuration.
/// Mirrors <c>stream.config</c> in server/stream.go.
/// </summary>
public StreamConfig GetConfig() =>
throw new NotImplementedException("TODO: session 21 — stream");
public StreamConfig GetConfig()
{
_mu.EnterReadLock();
try { return Config.Clone(); }
finally { _mu.ExitReadLock(); }
}
/// <summary>
/// Applies an updated configuration to the stream.
/// Mirrors <c>stream.update</c> in server/stream.go.
/// </summary>
public void UpdateConfig(StreamConfig config) =>
throw new NotImplementedException("TODO: session 21 — stream");
public void UpdateConfig(StreamConfig config)
{
_mu.EnterWriteLock();
try
{
ArgumentNullException.ThrowIfNull(config);
Config = config.Clone();
Store?.UpdateConfig(Config);
_sealed = Config.Sealed;
}
finally
{
_mu.ExitWriteLock();
}
}
// -------------------------------------------------------------------------
// Sealed state
@@ -173,15 +310,38 @@ internal sealed class NatsStream : IDisposable
/// Returns true if the stream is sealed (no new messages accepted).
/// Mirrors <c>stream.isSealed</c> in server/stream.go.
/// </summary>
public bool IsSealed() =>
throw new NotImplementedException("TODO: session 21 — stream");
public bool IsSealed()
{
_mu.EnterReadLock();
try { return _sealed || Config.Sealed; }
finally { _mu.ExitReadLock(); }
}
/// <summary>
/// Seals the stream so that no new messages can be stored.
/// Mirrors <c>stream.seal</c> in server/stream.go.
/// </summary>
public void Seal() =>
throw new NotImplementedException("TODO: session 21 — stream");
public void Seal()
{
_mu.EnterWriteLock();
try
{
_sealed = true;
Config.Sealed = true;
}
finally
{
_mu.ExitWriteLock();
}
}
private void SyncCountersFromState(StreamState state)
{
Interlocked.Exchange(ref Msgs, (long)state.Msgs);
Interlocked.Exchange(ref Bytes, (long)state.Bytes);
Interlocked.Exchange(ref FirstSeq, (long)state.FirstSeq);
Interlocked.Exchange(ref LastSeq, (long)state.LastSeq);
}
// -------------------------------------------------------------------------
// IDisposable

View File

@@ -321,57 +321,471 @@ internal sealed class Raft : IRaftNode
// -----------------------------------------------------------------------
// IRaftNode — stub implementations
// -----------------------------------------------------------------------
public void Propose(byte[] entry) => throw new NotImplementedException("TODO: session 20 — raft");
public void ProposeMulti(IReadOnlyList<Entry> entries) => throw new NotImplementedException("TODO: session 20 — raft");
public void ForwardProposal(byte[] entry) => throw new NotImplementedException("TODO: session 20 — raft");
public void InstallSnapshot(byte[] snap, bool force) => throw new NotImplementedException("TODO: session 20 — raft");
public object CreateSnapshotCheckpoint(bool force) => throw new NotImplementedException("TODO: session 20 — raft");
public void SendSnapshot(byte[] snap) => throw new NotImplementedException("TODO: session 20 — raft");
public bool NeedSnapshot() => throw new NotImplementedException("TODO: session 20 — raft");
public (ulong, ulong) Applied(ulong index) => throw new NotImplementedException("TODO: session 20 — raft");
public (ulong, ulong) Processed(ulong index, ulong applied) => throw new NotImplementedException("TODO: session 20 — raft");
public void Propose(byte[] entry)
{
ArgumentNullException.ThrowIfNull(entry);
_lock.EnterWriteLock();
try
{
PropQ ??= new IpQueue<ProposedEntry>($"{GroupName}-propose");
var pe = new ProposedEntry
{
Entry = new Entry { Type = EntryType.EntryNormal, Data = [.. entry] },
};
PropQ.Push(pe);
Active = DateTime.UtcNow;
}
finally
{
_lock.ExitWriteLock();
}
}
public void ProposeMulti(IReadOnlyList<Entry> entries)
{
ArgumentNullException.ThrowIfNull(entries);
foreach (var entry in entries)
{
if (entry == null)
continue;
Propose(entry.Data);
}
}
public void ForwardProposal(byte[] entry) => Propose(entry);
public void InstallSnapshot(byte[] snap, bool force)
{
ArgumentNullException.ThrowIfNull(snap);
_lock.EnterWriteLock();
try
{
if (Snapshotting && !force)
return;
Snapshotting = true;
Wps = [.. snap];
if (force)
Applied_ = Commit;
Snapshotting = false;
Active = DateTime.UtcNow;
}
finally
{
_lock.ExitWriteLock();
}
}
public object CreateSnapshotCheckpoint(bool force) => new Checkpoint
{
Node = this,
Term = Term_,
Applied = Applied_,
PApplied = PApplied,
SnapFile = force ? string.Empty : SnapFile,
PeerState = [.. Wps],
};
public void SendSnapshot(byte[] snap) => InstallSnapshot(snap, force: false);
public bool NeedSnapshot()
{
_lock.EnterReadLock();
try
{
return Snapshotting || PApplied > Applied_;
}
finally
{
_lock.ExitReadLock();
}
}
public (ulong, ulong) Applied(ulong index)
{
_lock.EnterReadLock();
try
{
var entries = Applied_ >= index ? Applied_ - index : 0;
return (entries, WalBytes);
}
finally
{
_lock.ExitReadLock();
}
}
public (ulong, ulong) Processed(ulong index, ulong applied)
{
_lock.EnterWriteLock();
try
{
if (index > Processed_)
Processed_ = index;
if (applied > Applied_)
Applied_ = applied;
return (Processed_, WalBytes);
}
finally
{
_lock.ExitWriteLock();
}
}
public RaftState State() => (RaftState)StateValue;
public (ulong, ulong) Size() => throw new NotImplementedException("TODO: session 20 — raft");
public (ulong, ulong, ulong) Progress() => throw new NotImplementedException("TODO: session 20 — raft");
public bool Leader() => throw new NotImplementedException("TODO: session 20 — raft");
public DateTime? LeaderSince() => throw new NotImplementedException("TODO: session 20 — raft");
public bool Quorum() => throw new NotImplementedException("TODO: session 20 — raft");
public bool Current() => throw new NotImplementedException("TODO: session 20 — raft");
public bool Healthy() => throw new NotImplementedException("TODO: session 20 — raft");
public (ulong, ulong) Size()
{
_lock.EnterReadLock();
try
{
return (Processed_, WalBytes);
}
finally
{
_lock.ExitReadLock();
}
}
public (ulong, ulong, ulong) Progress()
{
_lock.EnterReadLock();
try
{
return (PIndex, Commit, Applied_);
}
finally
{
_lock.ExitReadLock();
}
}
public bool Leader() => State() == RaftState.Leader;
public DateTime? LeaderSince()
{
_lock.EnterReadLock();
try
{
return Leader() ? (Lsut == default ? Active : Lsut) : null;
}
finally
{
_lock.ExitReadLock();
}
}
public bool Quorum()
{
_lock.EnterReadLock();
try
{
var clusterSize = ClusterSize();
if (clusterSize <= 1)
return true;
var required = Qn > 0 ? Qn : (clusterSize / 2) + 1;
var available = 1; // self
var now = DateTime.UtcNow;
foreach (var peer in Peers_.Values)
{
if (peer.Kp || now - peer.Ts <= TimeSpan.FromSeconds(30))
available++;
}
return available >= required;
}
finally
{
_lock.ExitReadLock();
}
}
public bool Current()
{
_lock.EnterReadLock();
try
{
return !Deleted_ && !Leaderless();
}
finally
{
_lock.ExitReadLock();
}
}
public bool Healthy() => Current() && Quorum();
public ulong Term() => Term_;
public bool Leaderless() => throw new NotImplementedException("TODO: session 20 — raft");
public string GroupLeader() => throw new NotImplementedException("TODO: session 20 — raft");
public bool HadPreviousLeader() => throw new NotImplementedException("TODO: session 20 — raft");
public void StepDown(params string[] preferred) => throw new NotImplementedException("TODO: session 20 — raft");
public void SetObserver(bool isObserver) => throw new NotImplementedException("TODO: session 20 — raft");
public bool IsObserver() => throw new NotImplementedException("TODO: session 20 — raft");
public void Campaign() => throw new NotImplementedException("TODO: session 20 — raft");
public void CampaignImmediately() => throw new NotImplementedException("TODO: session 20 — raft");
public bool Leaderless() => string.IsNullOrEmpty(LeaderId) && Interlocked.Read(ref HasLeaderV) == 0;
public string GroupLeader() => Leader() ? Id : LeaderId;
public bool HadPreviousLeader() => Interlocked.Read(ref PLeaderV) != 0 || !string.IsNullOrEmpty(LeaderId);
public void StepDown(params string[] preferred)
{
_lock.EnterWriteLock();
try
{
StateValue = (int)RaftState.Follower;
Interlocked.Exchange(ref HasLeaderV, 0);
Interlocked.Exchange(ref PLeaderV, 1);
Lxfer = true;
Lsut = DateTime.UtcNow;
if (preferred is { Length: > 0 })
Vote = preferred[0];
}
finally
{
_lock.ExitWriteLock();
}
}
public void SetObserver(bool isObserver)
{
_lock.EnterWriteLock();
try
{
Observer_ = isObserver;
}
finally
{
_lock.ExitWriteLock();
}
}
public bool IsObserver() => Observer_;
public void Campaign()
{
_lock.EnterWriteLock();
try
{
if (Deleted_)
return;
StateValue = (int)RaftState.Candidate;
Active = DateTime.UtcNow;
}
finally
{
_lock.ExitWriteLock();
}
}
public void CampaignImmediately() => Campaign();
public string ID() => Id;
public string Group() => GroupName;
public IReadOnlyList<Peer> Peers() => throw new NotImplementedException("TODO: session 20 — raft");
public void ProposeKnownPeers(IReadOnlyList<string> knownPeers) => throw new NotImplementedException("TODO: session 20 — raft");
public void UpdateKnownPeers(IReadOnlyList<string> knownPeers) => throw new NotImplementedException("TODO: session 20 — raft");
public void ProposeAddPeer(string peer) => throw new NotImplementedException("TODO: session 20 — raft");
public void ProposeRemovePeer(string peer) => throw new NotImplementedException("TODO: session 20 — raft");
public bool MembershipChangeInProgress() => throw new NotImplementedException("TODO: session 20 — raft");
public void AdjustClusterSize(int csz) => throw new NotImplementedException("TODO: session 20 — raft");
public void AdjustBootClusterSize(int csz) => throw new NotImplementedException("TODO: session 20 — raft");
public int ClusterSize() => throw new NotImplementedException("TODO: session 20 — raft");
public IReadOnlyList<Peer> Peers()
{
_lock.EnterReadLock();
try
{
var peers = new List<Peer>(Peers_.Count);
foreach (var (id, state) in Peers_)
{
peers.Add(new Peer
{
Id = id,
Current = state.Kp,
Last = state.Ts,
Lag = PIndex >= state.Li ? PIndex - state.Li : 0,
});
}
return peers;
}
finally
{
_lock.ExitReadLock();
}
}
public void ProposeKnownPeers(IReadOnlyList<string> knownPeers)
{
ArgumentNullException.ThrowIfNull(knownPeers);
_lock.EnterWriteLock();
try
{
var now = DateTime.UtcNow;
foreach (var lps in Peers_.Values)
lps.Kp = false;
foreach (var peer in knownPeers)
{
if (string.IsNullOrWhiteSpace(peer))
continue;
if (!Peers_.TryGetValue(peer, out var lps))
{
lps = new Lps();
Peers_[peer] = lps;
}
lps.Kp = true;
lps.Ts = now;
}
Csz = Math.Max(knownPeers.Count + 1, 1);
Qn = (Csz / 2) + 1;
}
finally
{
_lock.ExitWriteLock();
}
}
public void UpdateKnownPeers(IReadOnlyList<string> knownPeers) => ProposeKnownPeers(knownPeers);
public void ProposeAddPeer(string peer)
{
if (string.IsNullOrWhiteSpace(peer))
return;
_lock.EnterWriteLock();
try
{
if (!Peers_.TryGetValue(peer, out var lps))
{
lps = new Lps();
Peers_[peer] = lps;
}
lps.Kp = true;
lps.Ts = DateTime.UtcNow;
MembChangeIndex = PIndex + 1;
Csz = Math.Max(Peers_.Count + 1, 1);
Qn = (Csz / 2) + 1;
}
finally
{
_lock.ExitWriteLock();
}
}
public void ProposeRemovePeer(string peer)
{
if (string.IsNullOrWhiteSpace(peer))
return;
_lock.EnterWriteLock();
try
{
Peers_.Remove(peer);
Removed[peer] = DateTime.UtcNow;
MembChangeIndex = PIndex + 1;
Csz = Math.Max(Peers_.Count + 1, 1);
Qn = (Csz / 2) + 1;
}
finally
{
_lock.ExitWriteLock();
}
}
public bool MembershipChangeInProgress()
{
_lock.EnterReadLock();
try
{
return MembChangeIndex != 0 && MembChangeIndex > Applied_;
}
finally
{
_lock.ExitReadLock();
}
}
public void AdjustClusterSize(int csz)
{
_lock.EnterWriteLock();
try
{
Csz = Math.Max(csz, 1);
Qn = (Csz / 2) + 1;
}
finally
{
_lock.ExitWriteLock();
}
}
public void AdjustBootClusterSize(int csz) => AdjustClusterSize(csz);
public int ClusterSize()
{
_lock.EnterReadLock();
try
{
return Csz > 0 ? Csz : Math.Max(Peers_.Count + 1, 1);
}
finally
{
_lock.ExitReadLock();
}
}
public IpQueue<CommittedEntry> ApplyQ() => ApplyQ_ ?? throw new InvalidOperationException("Apply queue not initialized");
public void PauseApply() => throw new NotImplementedException("TODO: session 20 — raft");
public void ResumeApply() => throw new NotImplementedException("TODO: session 20 — raft");
public bool DrainAndReplaySnapshot() => throw new NotImplementedException("TODO: session 20 — raft");
public void PauseApply() => Paused = true;
public void ResumeApply() => Paused = false;
public bool DrainAndReplaySnapshot()
{
_lock.EnterWriteLock();
try
{
if (Snapshotting)
return false;
HcBehind = false;
return true;
}
finally
{
_lock.ExitWriteLock();
}
}
public ChannelReader<bool> LeadChangeC() => LeadC?.Reader ?? throw new InvalidOperationException("Lead channel not initialized");
public ChannelReader<bool> QuitC() => Quit?.Reader ?? throw new InvalidOperationException("Quit channel not initialized");
public DateTime Created() => Created_;
public void Stop() => throw new NotImplementedException("TODO: session 20 — raft");
public void WaitForStop() => throw new NotImplementedException("TODO: session 20 — raft");
public void Delete() => throw new NotImplementedException("TODO: session 20 — raft");
public void Stop()
{
_lock.EnterWriteLock();
try
{
StateValue = (int)RaftState.Closed;
Elect?.Dispose();
Elect = null;
Quit ??= Channel.CreateUnbounded<bool>();
Quit.Writer.TryWrite(true);
}
finally
{
_lock.ExitWriteLock();
}
}
public void WaitForStop()
{
var q = Quit;
if (q == null)
return;
if (q.Reader.TryRead(out _))
return;
q.Reader.WaitToReadAsync().AsTask().Wait(TimeSpan.FromSeconds(1));
}
public void Delete()
{
Deleted_ = true;
Stop();
}
public bool IsDeleted() => Deleted_;
public void RecreateInternalSubs() => throw new NotImplementedException("TODO: session 20 — raft");
public void RecreateInternalSubs() => Active = DateTime.UtcNow;
public bool IsSystemAccount() => Interlocked.Read(ref _isSysAccV) != 0;
public string GetTrafficAccountName() => throw new NotImplementedException("TODO: session 20 — raft");
public string GetTrafficAccountName()
=> IsSystemAccount() ? "$SYS" : (string.IsNullOrEmpty(AccName) ? "$G" : AccName);
}
// ============================================================================
@@ -461,16 +875,65 @@ internal sealed class Checkpoint : IRaftNodeCheckpoint
public byte[] PeerState { get; set; } = [];
public byte[] LoadLastSnapshot()
=> throw new NotImplementedException("TODO: session 20 — raft");
{
if (string.IsNullOrWhiteSpace(SnapFile))
return [];
try
{
return File.Exists(SnapFile) ? File.ReadAllBytes(SnapFile) : [];
}
catch
{
return [];
}
}
public IEnumerable<(AppendEntry Entry, Exception? Error)> AppendEntriesSeq()
=> throw new NotImplementedException("TODO: session 20 — raft");
{
if (Node == null)
yield break;
var entry = new AppendEntry
{
Leader = Node.Id,
TermV = Term,
Commit = Applied,
PTerm = Node.PTerm,
PIndex = PApplied,
Reply = Node.AReply,
};
yield return (entry, null);
}
public void Abort()
=> throw new NotImplementedException("TODO: session 20 — raft");
{
if (string.IsNullOrWhiteSpace(SnapFile))
return;
try
{
if (File.Exists(SnapFile))
File.Delete(SnapFile);
}
catch
{
// Ignore cleanup failures for aborted checkpoints.
}
}
public ulong InstallSnapshot(byte[] data)
=> throw new NotImplementedException("TODO: session 20 — raft");
{
ArgumentNullException.ThrowIfNull(data);
if (string.IsNullOrWhiteSpace(SnapFile))
SnapFile = Path.Combine(Path.GetTempPath(), $"raft-snapshot-{Guid.NewGuid():N}.bin");
File.WriteAllBytes(SnapFile, data);
Node?.InstallSnapshot(data, force: true);
return (ulong)data.LongLength;
}
}
// ============================================================================

View File

@@ -422,27 +422,54 @@ public sealed class WaitQueue
private int _tail;
/// <summary>Number of pending requests in the queue.</summary>
public int Len => _reqs.Count;
public int Len => _tail - _head;
/// <summary>Add a waiting request to the tail of the queue.</summary>
public void Add(WaitingRequest req) =>
throw new NotImplementedException("TODO: session 21");
public void Add(WaitingRequest req)
{
ArgumentNullException.ThrowIfNull(req);
_reqs.Add(req);
_tail++;
}
/// <summary>Peek at the head request without removing it.</summary>
public WaitingRequest? Peek() =>
throw new NotImplementedException("TODO: session 21");
public WaitingRequest? Peek()
{
if (Len == 0)
return null;
return _reqs[_head];
}
/// <summary>Remove and return the head request.</summary>
public WaitingRequest? Pop() =>
throw new NotImplementedException("TODO: session 21");
public WaitingRequest? Pop()
{
if (Len == 0)
return null;
var req = _reqs[_head++];
if (_head > 32 && _head * 2 >= _tail)
Compress();
return req;
}
/// <summary>Compact the internal backing list to reclaim removed slots.</summary>
public void Compress() =>
throw new NotImplementedException("TODO: session 21");
public void Compress()
{
if (_head == 0)
return;
_reqs.RemoveRange(0, _head);
_tail -= _head;
_head = 0;
}
/// <summary>Returns true if the queue is at capacity (head == tail when full).</summary>
public bool IsFull(int max) =>
throw new NotImplementedException("TODO: session 21");
public bool IsFull(int max)
{
if (max <= 0)
return false;
return Len >= max;
}
}
/// <summary>