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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -183,12 +183,19 @@ public sealed class CompressionInfo
|
||||
|
||||
/// <summary>
|
||||
/// Serialises compression metadata as a compact binary prefix.
|
||||
/// Format: 'c' 'm' 'p' <algorithmByte> <uvarint originalSize>
|
||||
/// Format: 'c' 'm' 'p' <algorithmByte> <uvarint originalSize> <uvarint compressedSize>
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user