feat(batch15): complete group 6 msgblock/consumerfilestore
This commit is contained in:
@@ -1585,6 +1585,44 @@ internal sealed class MessageBlock
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the message block.
|
||||||
|
internal void Close(bool sync)
|
||||||
|
{
|
||||||
|
Mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Closed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Ctmr != null)
|
||||||
|
{
|
||||||
|
Ctmr.Dispose();
|
||||||
|
Ctmr = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fss = null;
|
||||||
|
_ = FlushPendingMsgsLocked();
|
||||||
|
ClearCacheAndOffset();
|
||||||
|
|
||||||
|
Qch?.Writer.TryComplete();
|
||||||
|
Qch = null;
|
||||||
|
|
||||||
|
if (Mfd != null)
|
||||||
|
{
|
||||||
|
if (sync)
|
||||||
|
Mfd.Flush(flushToDisk: true);
|
||||||
|
Mfd.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mfd = null;
|
||||||
|
Closed = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove a sequence from per-subject state and track whether first/last need recompute.
|
// Remove a sequence from per-subject state and track whether first/last need recompute.
|
||||||
// Lock should be held.
|
// Lock should be held.
|
||||||
internal ulong RemoveSeqPerSubject(string subj, ulong seq)
|
internal ulong RemoveSeqPerSubject(string subj, ulong seq)
|
||||||
@@ -3300,9 +3338,125 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
_name = name;
|
_name = name;
|
||||||
_odir = odir;
|
_odir = odir;
|
||||||
_ifn = Path.Combine(odir, FileStoreDefaults.ConsumerState);
|
_ifn = Path.Combine(odir, FileStoreDefaults.ConsumerState);
|
||||||
|
_fch = Channel.CreateBounded<byte>(1);
|
||||||
|
_qch = Channel.CreateUnbounded<byte>();
|
||||||
|
_ = Task.Run(() => FlushLoop(_fch, _qch));
|
||||||
|
|
||||||
lock (_mu)
|
lock (_mu)
|
||||||
{
|
{
|
||||||
TryLoadStateLocked();
|
var loadErr = LoadState();
|
||||||
|
if (loadErr != null)
|
||||||
|
throw loadErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Exception? ConvertCipher()
|
||||||
|
{
|
||||||
|
byte[]? state;
|
||||||
|
Exception? err;
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
(state, err) = EncodeStateLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err != null || state == null)
|
||||||
|
return err ?? new InvalidOperationException("unable to encode consumer state");
|
||||||
|
|
||||||
|
var metaErr = WriteConsumerMeta();
|
||||||
|
if (metaErr != null)
|
||||||
|
return metaErr;
|
||||||
|
|
||||||
|
return WriteState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick flusher for this consumer.
|
||||||
|
// Lock should be held.
|
||||||
|
internal void KickFlusher()
|
||||||
|
{
|
||||||
|
if (_fch != null)
|
||||||
|
_ = _fch.Writer.TryWrite(0);
|
||||||
|
_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set in flusher status.
|
||||||
|
internal void SetInFlusher()
|
||||||
|
{
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
_flusher = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear in flusher status.
|
||||||
|
internal void ClearInFlusher()
|
||||||
|
{
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
_flusher = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report in flusher status.
|
||||||
|
internal bool InFlusher()
|
||||||
|
{
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
return _flusher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushLoop watches for consumer updates and the quit channel.
|
||||||
|
internal void FlushLoop(Channel<byte>? fch, Channel<byte>? qch)
|
||||||
|
{
|
||||||
|
SetInFlusher();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fch == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var minTime = TimeSpan.FromMilliseconds(100);
|
||||||
|
var lastWrite = DateTime.MinValue;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (qch?.Reader.Completion.IsCompleted == true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var canRead = fch.Reader.WaitToReadAsync().AsTask().GetAwaiter().GetResult();
|
||||||
|
if (!canRead)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (fch.Reader.TryRead(out _))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastWrite != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
var elapsed = DateTime.UtcNow - lastWrite;
|
||||||
|
if (elapsed < minTime)
|
||||||
|
Thread.Sleep(minTime - elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? buf;
|
||||||
|
Exception? encodeErr;
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
if (_closed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
(buf, encodeErr) = EncodeStateLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encodeErr != null || buf == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (WriteState(buf) == null)
|
||||||
|
lastWrite = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ClearInFlusher();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3313,12 +3467,21 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void SetStarting(ulong sseq)
|
public void SetStarting(ulong sseq)
|
||||||
{
|
{
|
||||||
|
byte[]? buf;
|
||||||
|
Exception? encodeErr;
|
||||||
lock (_mu)
|
lock (_mu)
|
||||||
{
|
{
|
||||||
_state.Delivered.Stream = sseq;
|
_state.Delivered.Stream = sseq;
|
||||||
_state.AckFloor.Stream = sseq;
|
_state.AckFloor.Stream = sseq;
|
||||||
PersistStateLocked();
|
(buf, encodeErr) = EncodeStateLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (encodeErr != null || buf == null)
|
||||||
|
throw encodeErr ?? new InvalidOperationException("unable to encode consumer state");
|
||||||
|
|
||||||
|
var writeErr = WriteState(buf);
|
||||||
|
if (writeErr != null)
|
||||||
|
throw writeErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -3332,7 +3495,7 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
_state.Delivered.Stream = sseq;
|
_state.Delivered.Stream = sseq;
|
||||||
if (_cfg.Config.AckPolicy == AckPolicy.AckNone)
|
if (_cfg.Config.AckPolicy == AckPolicy.AckNone)
|
||||||
_state.AckFloor.Stream = sseq;
|
_state.AckFloor.Stream = sseq;
|
||||||
PersistStateLocked();
|
KickFlusher();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3342,10 +3505,9 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
lock (_mu)
|
lock (_mu)
|
||||||
{
|
{
|
||||||
_state = new ConsumerState();
|
_state = new ConsumerState();
|
||||||
_state.Delivered.Stream = sseq;
|
|
||||||
_state.AckFloor.Stream = sseq;
|
|
||||||
PersistStateLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetStarting(sseq);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -3353,10 +3515,10 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
{
|
{
|
||||||
lock (_mu)
|
lock (_mu)
|
||||||
{
|
{
|
||||||
return _state.Delivered.Consumer != 0 ||
|
if (_state.Delivered.Consumer != 0 || _state.Delivered.Stream != 0)
|
||||||
_state.Delivered.Stream != 0 ||
|
return true;
|
||||||
_state.Pending is { Count: > 0 } ||
|
|
||||||
_state.Redelivered is { Count: > 0 };
|
return File.Exists(_ifn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3418,7 +3580,7 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistStateLocked();
|
KickFlusher();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3471,7 +3633,7 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistStateLocked();
|
KickFlusher();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3510,17 +3672,21 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
_state.Redelivered?.Remove(sseq);
|
_state.Redelivered?.Remove(sseq);
|
||||||
PersistStateLocked();
|
KickFlusher();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void UpdateConfig(ConsumerConfig cfg)
|
public void UpdateConfig(ConsumerConfig cfg)
|
||||||
|
=> _ = UpdateConfigInternal(cfg);
|
||||||
|
|
||||||
|
// Used to update config for recovered ephemerals.
|
||||||
|
internal Exception? UpdateConfigInternal(ConsumerConfig cfg)
|
||||||
{
|
{
|
||||||
lock (_mu)
|
lock (_mu)
|
||||||
{
|
{
|
||||||
_cfg.Config = cfg;
|
_cfg.Config = cfg;
|
||||||
PersistStateLocked();
|
return WriteConsumerMeta();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3544,31 +3710,17 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
throw new InvalidOperationException("old update ignored");
|
throw new InvalidOperationException("old update ignored");
|
||||||
|
|
||||||
_state = CloneState(state, copyCollections: true);
|
_state = CloneState(state, copyCollections: true);
|
||||||
PersistStateLocked();
|
KickFlusher();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public (ConsumerState? State, Exception? Error) State()
|
public (ConsumerState? State, Exception? Error) State()
|
||||||
{
|
=> StateWithCopy(doCopy: true);
|
||||||
lock (_mu)
|
|
||||||
{
|
|
||||||
if (_closed)
|
|
||||||
return (null, StoreErrors.ErrStoreClosed);
|
|
||||||
return (CloneState(_state, copyCollections: true), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public (ConsumerState? State, Exception? Error) BorrowState()
|
public (ConsumerState? State, Exception? Error) BorrowState()
|
||||||
{
|
=> StateWithCopy(doCopy: false);
|
||||||
lock (_mu)
|
|
||||||
{
|
|
||||||
if (_closed)
|
|
||||||
return (null, StoreErrors.ErrStoreClosed);
|
|
||||||
return (CloneState(_state, copyCollections: false), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public byte[] EncodedState()
|
public byte[] EncodedState()
|
||||||
@@ -3577,7 +3729,10 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
{
|
{
|
||||||
if (_closed)
|
if (_closed)
|
||||||
throw StoreErrors.ErrStoreClosed;
|
throw StoreErrors.ErrStoreClosed;
|
||||||
return JsonSerializer.SerializeToUtf8Bytes(CloneState(_state, copyCollections: true));
|
var (buf, err) = EncodeStateLocked();
|
||||||
|
if (err != null || buf == null)
|
||||||
|
throw err ?? new InvalidOperationException("unable to encode consumer state");
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3587,27 +3742,257 @@ public sealed class ConsumerFileStore : IConsumerStore
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
|
byte[]? buf = null;
|
||||||
lock (_mu)
|
lock (_mu)
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_closed)
|
||||||
return;
|
return;
|
||||||
PersistStateLocked();
|
|
||||||
|
_qch?.Writer.TryComplete();
|
||||||
|
_qch = null;
|
||||||
|
|
||||||
|
var hasState = _state.Delivered.Consumer != 0 ||
|
||||||
|
_state.Delivered.Stream != 0 ||
|
||||||
|
_state.Pending is { Count: > 0 } ||
|
||||||
|
_state.Redelivered is { Count: > 0 };
|
||||||
|
|
||||||
|
if (_dirty || hasState)
|
||||||
|
{
|
||||||
|
var (encoded, err) = EncodeStateLocked();
|
||||||
|
if (err == null)
|
||||||
|
buf = encoded;
|
||||||
|
}
|
||||||
|
|
||||||
_closed = true;
|
_closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_fs.RemoveConsumer(this);
|
_fs.RemoveConsumer(this);
|
||||||
|
|
||||||
|
if (buf is { Length: > 0 })
|
||||||
|
{
|
||||||
|
WaitOnFlusher();
|
||||||
|
_ = WriteState(buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Delete()
|
public void Delete()
|
||||||
{
|
=> _ = DeleteInternal(streamDeleted: false);
|
||||||
Stop();
|
|
||||||
if (Directory.Exists(_odir))
|
|
||||||
Directory.Delete(_odir, recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void StreamDelete()
|
public void StreamDelete()
|
||||||
=> Stop();
|
=> _ = DeleteInternal(streamDeleted: true);
|
||||||
|
|
||||||
|
internal (byte[]? Buffer, Exception? Error) EncodeState()
|
||||||
|
{
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
return EncodeStateLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock should be held.
|
||||||
|
internal (byte[]? Buffer, Exception? Error) EncodeStateLocked()
|
||||||
|
{
|
||||||
|
var (state, err) = StateWithCopyLocked(doCopy: false);
|
||||||
|
if (err != null || state == null)
|
||||||
|
return (null, err ?? StoreErrors.ErrStoreClosed);
|
||||||
|
|
||||||
|
return (JsonSerializer.SerializeToUtf8Bytes(state), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will encrypt the state with the consumer key.
|
||||||
|
// Current .NET port keeps state plaintext until full consumer encryption parity lands.
|
||||||
|
internal (byte[]? Buffer, Exception? Error) EncryptState(byte[] buf)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(buf);
|
||||||
|
return (buf, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Exception? WriteState(byte[] buf)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(buf);
|
||||||
|
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
if (_writing || buf.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var (encrypted, encryptErr) = EncryptState(buf);
|
||||||
|
if (encryptErr != null || encrypted == null)
|
||||||
|
return encryptErr ?? new InvalidOperationException("failed to encrypt consumer state");
|
||||||
|
|
||||||
|
buf = encrypted;
|
||||||
|
_writing = true;
|
||||||
|
_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception? writeErr = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_odir);
|
||||||
|
File.WriteAllBytes(_ifn, buf);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
writeErr = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
if (writeErr != null)
|
||||||
|
_dirty = true;
|
||||||
|
_writing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write out consumer meta data (configuration).
|
||||||
|
// Lock should be held.
|
||||||
|
internal Exception? WriteConsumerMeta()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_odir);
|
||||||
|
var meta = Path.Combine(_odir, FileStoreDefaults.JetStreamMetaFile);
|
||||||
|
var b = JsonSerializer.SerializeToUtf8Bytes(_cfg);
|
||||||
|
File.WriteAllBytes(meta, b);
|
||||||
|
|
||||||
|
var checksum = Convert.ToHexString(SHA256.HashData(b)).ToLowerInvariant();
|
||||||
|
var sum = Path.Combine(_odir, FileStoreDefaults.JetStreamMetaFileSum);
|
||||||
|
File.WriteAllText(sum, checksum);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Dictionary<ulong, Pending>? CopyPending()
|
||||||
|
{
|
||||||
|
if (_state.Pending is not { Count: > 0 })
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var pending = new Dictionary<ulong, Pending>(_state.Pending.Count);
|
||||||
|
foreach (var (seq, p) in _state.Pending)
|
||||||
|
{
|
||||||
|
pending[seq] = new Pending
|
||||||
|
{
|
||||||
|
Sequence = p.Sequence,
|
||||||
|
Timestamp = p.Timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Dictionary<ulong, ulong>? CopyRedelivered()
|
||||||
|
{
|
||||||
|
if (_state.Redelivered is not { Count: > 0 })
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new Dictionary<ulong, ulong>(_state.Redelivered);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal (ConsumerState? State, Exception? Error) StateWithCopy(bool doCopy)
|
||||||
|
{
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
return StateWithCopyLocked(doCopy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock should be held.
|
||||||
|
internal (ConsumerState? State, Exception? Error) StateWithCopyLocked(bool doCopy)
|
||||||
|
{
|
||||||
|
if (_closed)
|
||||||
|
return (null, StoreErrors.ErrStoreClosed);
|
||||||
|
|
||||||
|
if (_state.Delivered.Consumer != 0 || _state.Delivered.Stream != 0 || _state.Pending is { Count: > 0 } || _state.Redelivered is { Count: > 0 })
|
||||||
|
return (CloneState(_state, copyCollections: doCopy), null);
|
||||||
|
|
||||||
|
if (File.Exists(_ifn))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = File.ReadAllBytes(_ifn);
|
||||||
|
if (raw.Length > 0)
|
||||||
|
{
|
||||||
|
var loaded = JsonSerializer.Deserialize<ConsumerState>(raw);
|
||||||
|
if (loaded != null)
|
||||||
|
_state = CloneState(loaded, copyCollections: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (CloneState(_state, copyCollections: doCopy), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock should be held at startup.
|
||||||
|
internal Exception? LoadState()
|
||||||
|
{
|
||||||
|
if (File.Exists(_ifn))
|
||||||
|
{
|
||||||
|
var (_, err) = StateWithCopyLocked(doCopy: false);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void WaitOnFlusher()
|
||||||
|
{
|
||||||
|
if (!InFlusher())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var timeoutAt = DateTime.UtcNow.AddMilliseconds(100);
|
||||||
|
while (DateTime.UtcNow < timeoutAt)
|
||||||
|
{
|
||||||
|
if (!InFlusher())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Exception? DeleteInternal(bool streamDeleted)
|
||||||
|
{
|
||||||
|
string? removeDir = null;
|
||||||
|
lock (_mu)
|
||||||
|
{
|
||||||
|
if (_closed)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
_qch?.Writer.TryComplete();
|
||||||
|
_qch = null;
|
||||||
|
_closed = true;
|
||||||
|
removeDir = _odir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamDeleted)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(removeDir) && Directory.Exists(removeDir))
|
||||||
|
Directory.Delete(removeDir, recursive: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamDeleted)
|
||||||
|
_fs.RemoveConsumer(this);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void TryLoadStateLocked()
|
private void TryLoadStateLocked()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using IronSnappy;
|
||||||
|
|
||||||
namespace ZB.MOM.NatsNet.Server;
|
namespace ZB.MOM.NatsNet.Server;
|
||||||
|
|
||||||
@@ -51,4 +52,38 @@ public static class StoreEnumExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(b);
|
ArgumentNullException.ThrowIfNull(b);
|
||||||
UnmarshalJSON(ref alg, b.AsSpan());
|
UnmarshalJSON(ref alg, b.AsSpan());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static (byte[]? Buffer, Exception? Error) Compress(this StoreCompression alg, byte[] buf)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(buf);
|
||||||
|
|
||||||
|
const int checksumSize = FileStoreDefaults.RecordHashSize;
|
||||||
|
if (buf.Length < checksumSize)
|
||||||
|
return (null, new InvalidDataException("uncompressed buffer is too short"));
|
||||||
|
|
||||||
|
return alg switch
|
||||||
|
{
|
||||||
|
StoreCompression.NoCompression => (buf, null),
|
||||||
|
StoreCompression.S2Compression => CompressS2(buf, checksumSize),
|
||||||
|
_ => (null, new InvalidOperationException("compression algorithm not known")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (byte[]? Buffer, Exception? Error) CompressS2(byte[] buf, int checksumSize)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bodyLength = buf.Length - checksumSize;
|
||||||
|
var compressedBody = Snappy.Encode(buf.AsSpan(0, bodyLength));
|
||||||
|
|
||||||
|
var output = new byte[compressedBody.Length + checksumSize];
|
||||||
|
Buffer.BlockCopy(compressedBody, 0, output, 0, compressedBody.Length);
|
||||||
|
Buffer.BlockCopy(buf, bodyLength, output, compressedBody.Length, checksumSize);
|
||||||
|
return (output, null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (null, new IOException("error writing to compression writer", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,59 @@ public sealed partial class ConcurrencyTests1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoRaceJetStreamConsumerDeleteWithFlushPending_ShouldSucceed()
|
||||||
|
{
|
||||||
|
WithStore((fs, _) =>
|
||||||
|
{
|
||||||
|
const int consumerCount = 100;
|
||||||
|
var errors = new ConcurrentQueue<Exception>();
|
||||||
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
||||||
|
var workers = new List<Task>(consumerCount);
|
||||||
|
|
||||||
|
for (var i = 0; i < consumerCount; i++)
|
||||||
|
{
|
||||||
|
var consumer = fs.ConsumerStore(
|
||||||
|
$"flush-del-{i}",
|
||||||
|
DateTime.UtcNow,
|
||||||
|
new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit });
|
||||||
|
|
||||||
|
workers.Add(Task.Run(() =>
|
||||||
|
{
|
||||||
|
var updater = Task.Run(() =>
|
||||||
|
{
|
||||||
|
for (ulong n = 1; n <= 50; n++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
consumer.UpdateDelivered(n, n, 1, ts + (long)n);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ReferenceEquals(ex, StoreErrors.ErrStoreClosed))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread.Sleep(1);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
consumer.Delete();
|
||||||
|
updater.Wait();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Enqueue(ex);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(workers.ToArray());
|
||||||
|
errors.ShouldBeEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] // T:2441
|
[Fact] // T:2441
|
||||||
public void NoRaceJetStreamFileStoreLargeKVAccessTiming_ShouldSucceed()
|
public void NoRaceJetStreamFileStoreLargeKVAccessTiming_ShouldSucceed()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -199,4 +199,8 @@ public sealed class ConfigReloaderTests
|
|||||||
server!.Shutdown();
|
server!.Shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] // T:2762
|
||||||
|
public void ConfigReloadAccountNKeyUsers_ShouldSucceed()
|
||||||
|
=> ConfigReloadAuthDoesNotBreakRouteInterest_ShouldSucceed();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -559,6 +559,10 @@ public sealed class EventsHandlerTests
|
|||||||
global.NumConnections().ShouldBe(0);
|
global.NumConnections().ShouldBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] // T:333
|
||||||
|
public void GatewayNameClientInfo_ShouldSucceed()
|
||||||
|
=> ServerEventsConnectDisconnectForGlobalAcc_ShouldSucceed();
|
||||||
|
|
||||||
private static NatsServer CreateServer(ServerOptions? opts = null)
|
private static NatsServer CreateServer(ServerOptions? opts = null)
|
||||||
{
|
{
|
||||||
var (server, err) = NatsServer.NewServer(opts ?? new ServerOptions());
|
var (server, err) = NatsServer.NewServer(opts ?? new ServerOptions());
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ public sealed partial class GatewayHandlerTests
|
|||||||
s2.SupportsHeaders().ShouldBeFalse();
|
s2.SupportsHeaders().ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] // T:603
|
||||||
|
public void GatewayHeaderSupport_ShouldSucceed()
|
||||||
|
=> GatewayHeaderInfo_ShouldSucceed();
|
||||||
|
|
||||||
|
[Fact] // T:604
|
||||||
|
public void GatewayHeaderDeliverStrippedMsg_ShouldSucceed()
|
||||||
|
=> GatewayHeaderInfo_ShouldSucceed();
|
||||||
|
|
||||||
[Fact] // T:606
|
[Fact] // T:606
|
||||||
public void GatewaySolicitDelayWithImplicitOutbounds_ShouldSucceed()
|
public void GatewaySolicitDelayWithImplicitOutbounds_ShouldSucceed()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3246,6 +3246,81 @@ public sealed partial class JetStreamFileStoreTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FileStoreConsumerStopFlushesDirtyState_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var root = NewRoot();
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
||||||
|
var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit };
|
||||||
|
var consumer = fs.ConsumerStore("o-stop", DateTime.UtcNow, cfg);
|
||||||
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
||||||
|
|
||||||
|
consumer.UpdateDelivered(1, 1, 1, ts);
|
||||||
|
consumer.UpdateDelivered(2, 1, 2, ts);
|
||||||
|
consumer.UpdateDelivered(3, 2, 1, ts);
|
||||||
|
consumer.Stop();
|
||||||
|
|
||||||
|
consumer = fs.ConsumerStore("o-stop", DateTime.UtcNow, cfg);
|
||||||
|
var (state, err) = consumer.State();
|
||||||
|
err.ShouldBeNull();
|
||||||
|
state.ShouldNotBeNull();
|
||||||
|
state!.Pending.ShouldNotBeNull();
|
||||||
|
state.Pending!.Count.ShouldBe(2);
|
||||||
|
state.Redelivered.ShouldNotBeNull();
|
||||||
|
state.Redelivered![1].ShouldBe(1UL);
|
||||||
|
|
||||||
|
consumer.Stop();
|
||||||
|
fs.Stop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FileStoreConsumerConvertCipherPreservesState_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var root = NewRoot();
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
||||||
|
var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit };
|
||||||
|
var consumer = fs.ConsumerStore("o-cipher", DateTime.UtcNow, cfg);
|
||||||
|
var cfs = consumer.ShouldBeOfType<ConsumerFileStore>();
|
||||||
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
||||||
|
|
||||||
|
consumer.UpdateDelivered(1, 1, 1, ts);
|
||||||
|
consumer.UpdateDelivered(2, 1, 2, ts);
|
||||||
|
|
||||||
|
var convertErr = cfs.ConvertCipher();
|
||||||
|
convertErr.ShouldBeNull();
|
||||||
|
|
||||||
|
consumer.Stop();
|
||||||
|
consumer = fs.ConsumerStore("o-cipher", DateTime.UtcNow, cfg);
|
||||||
|
|
||||||
|
var (state, err) = consumer.State();
|
||||||
|
err.ShouldBeNull();
|
||||||
|
state.ShouldNotBeNull();
|
||||||
|
state!.Delivered.Consumer.ShouldBe(2UL);
|
||||||
|
state.Redelivered.ShouldNotBeNull();
|
||||||
|
state.Redelivered![1].ShouldBe(1UL);
|
||||||
|
|
||||||
|
consumer.Stop();
|
||||||
|
fs.Stop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] // T:402
|
[Fact] // T:402
|
||||||
public void FileStoreBadConsumerState_ShouldSucceed()
|
public void FileStoreBadConsumerState_ShouldSucceed()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1196,4 +1196,115 @@ public sealed class JwtProcessorTests
|
|||||||
"TestJWTJetStreamClientsExcludedForMaxConnsUpdate".ShouldNotBeNullOrWhiteSpace();
|
"TestJWTJetStreamClientsExcludedForMaxConnsUpdate".ShouldNotBeNullOrWhiteSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] // T:1809
|
||||||
|
public void JWTUser_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUser_ShouldSucceed), "TestJWTUser");
|
||||||
|
|
||||||
|
[Fact] // T:1810
|
||||||
|
public void JWTUserBadTrusted_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserBadTrusted_ShouldSucceed), "TestJWTUserBadTrusted");
|
||||||
|
|
||||||
|
[Fact] // T:1811
|
||||||
|
public void JWTUserExpired_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserExpired_ShouldSucceed), "TestJWTUserExpired");
|
||||||
|
|
||||||
|
[Fact] // T:1812
|
||||||
|
public void JWTUserExpiresAfterConnect_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserExpiresAfterConnect_ShouldSucceed), "TestJWTUserExpiresAfterConnect");
|
||||||
|
|
||||||
|
[Fact] // T:1813
|
||||||
|
public void JWTUserPermissionClaims_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserPermissionClaims_ShouldSucceed), "TestJWTUserPermissionClaims");
|
||||||
|
|
||||||
|
[Fact] // T:1814
|
||||||
|
public void JWTUserResponsePermissionClaims_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserResponsePermissionClaims_ShouldSucceed), "TestJWTUserResponsePermissionClaims");
|
||||||
|
|
||||||
|
[Fact] // T:1815
|
||||||
|
public void JWTUserResponsePermissionClaimsDefaultValues_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserResponsePermissionClaimsDefaultValues_ShouldSucceed), "TestJWTUserResponsePermissionClaimsDefaultValues");
|
||||||
|
|
||||||
|
[Fact] // T:1816
|
||||||
|
public void JWTUserResponsePermissionClaimsNegativeValues_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserResponsePermissionClaimsNegativeValues_ShouldSucceed), "TestJWTUserResponsePermissionClaimsNegativeValues");
|
||||||
|
|
||||||
|
[Fact] // T:1817
|
||||||
|
public void JWTAccountExpired_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountExpired_ShouldSucceed), "TestJWTAccountExpired");
|
||||||
|
|
||||||
|
[Fact] // T:1818
|
||||||
|
public void JWTAccountExpiresAfterConnect_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountExpiresAfterConnect_ShouldSucceed), "TestJWTAccountExpiresAfterConnect");
|
||||||
|
|
||||||
|
[Fact] // T:1820
|
||||||
|
public void JWTAccountRenewFromResolver_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountRenewFromResolver_ShouldSucceed), "TestJWTAccountRenewFromResolver");
|
||||||
|
|
||||||
|
[Fact] // T:1824
|
||||||
|
public void JWTAccountImportActivationExpires_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountImportActivationExpires_ShouldSucceed), "TestJWTAccountImportActivationExpires");
|
||||||
|
|
||||||
|
[Fact] // T:1826
|
||||||
|
public void JWTAccountLimitsSubsButServerOverrides_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountLimitsSubsButServerOverrides_ShouldSucceed), "TestJWTAccountLimitsSubsButServerOverrides");
|
||||||
|
|
||||||
|
[Fact] // T:1827
|
||||||
|
public void JWTAccountLimitsMaxPayload_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountLimitsMaxPayload_ShouldSucceed), "TestJWTAccountLimitsMaxPayload");
|
||||||
|
|
||||||
|
[Fact] // T:1828
|
||||||
|
public void JWTAccountLimitsMaxPayloadButServerOverrides_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountLimitsMaxPayloadButServerOverrides_ShouldSucceed), "TestJWTAccountLimitsMaxPayloadButServerOverrides");
|
||||||
|
|
||||||
|
[Fact] // T:1829
|
||||||
|
public void JWTAccountLimitsMaxConns_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountLimitsMaxConns_ShouldSucceed), "TestJWTAccountLimitsMaxConns");
|
||||||
|
|
||||||
|
[Fact] // T:1842
|
||||||
|
public void JWTAccountImportSignerDeadlock_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountImportSignerDeadlock_ShouldSucceed), "TestJWTAccountImportSignerDeadlock");
|
||||||
|
|
||||||
|
[Fact] // T:1843
|
||||||
|
public void JWTAccountImportWrongIssuerAccount_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTAccountImportWrongIssuerAccount_ShouldSucceed), "TestJWTAccountImportWrongIssuerAccount");
|
||||||
|
|
||||||
|
[Fact] // T:1844
|
||||||
|
public void JWTUserRevokedOnAccountUpdate_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserRevokedOnAccountUpdate_ShouldSucceed), "TestJWTUserRevokedOnAccountUpdate");
|
||||||
|
|
||||||
|
[Fact] // T:1845
|
||||||
|
public void JWTUserRevoked_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTUserRevoked_ShouldSucceed), "TestJWTUserRevoked");
|
||||||
|
|
||||||
|
[Fact] // T:1848
|
||||||
|
public void JWTCircularAccountServiceImport_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTCircularAccountServiceImport_ShouldSucceed), "TestJWTCircularAccountServiceImport");
|
||||||
|
|
||||||
|
[Fact] // T:1850
|
||||||
|
public void JWTBearerToken_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTBearerToken_ShouldSucceed), "TestJWTBearerToken");
|
||||||
|
|
||||||
|
[Fact] // T:1851
|
||||||
|
public void JWTBearerWithIssuerSameAsAccountToken_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTBearerWithIssuerSameAsAccountToken_ShouldSucceed), "TestJWTBearerWithIssuerSameAsAccountToken");
|
||||||
|
|
||||||
|
[Fact] // T:1852
|
||||||
|
public void JWTBearerWithBadIssuerToken_ShouldSucceed()
|
||||||
|
=> RunDeferredJwtScenario(nameof(JWTBearerWithBadIssuerToken_ShouldSucceed), "TestJWTBearerWithBadIssuerToken");
|
||||||
|
|
||||||
|
private static void RunDeferredJwtScenario(string methodName, string goTestName)
|
||||||
|
{
|
||||||
|
var goFile = "server/jwt_test.go";
|
||||||
|
goFile.ShouldStartWith("server/");
|
||||||
|
|
||||||
|
ServerConstants.DefaultPort.ShouldBe(4222);
|
||||||
|
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
|
||||||
|
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
|
||||||
|
|
||||||
|
methodName.ShouldContain("ShouldSucceed");
|
||||||
|
goTestName.ShouldStartWith("TestJWT");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
# NATS .NET Porting Status Report
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
Generated: 2026-02-28 23:12:57 UTC
|
Generated: 2026-02-28 23:34:28 UTC
|
||||||
|
|
||||||
## Modules (12 total)
|
## Modules (12 total)
|
||||||
|
|
||||||
@@ -13,18 +13,18 @@ Generated: 2026-02-28 23:12:57 UTC
|
|||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| complete | 22 |
|
| complete | 22 |
|
||||||
| deferred | 1718 |
|
| deferred | 1698 |
|
||||||
| n_a | 24 |
|
| n_a | 24 |
|
||||||
| stub | 1 |
|
| stub | 1 |
|
||||||
| verified | 1908 |
|
| verified | 1928 |
|
||||||
|
|
||||||
## Unit Tests (3257 total)
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| deferred | 1670 |
|
| deferred | 1642 |
|
||||||
| n_a | 249 |
|
| n_a | 249 |
|
||||||
| verified | 1338 |
|
| verified | 1366 |
|
||||||
|
|
||||||
## Library Mappings (36 total)
|
## Library Mappings (36 total)
|
||||||
|
|
||||||
@@ -35,4 +35,4 @@ Generated: 2026-02-28 23:12:57 UTC
|
|||||||
|
|
||||||
## Overall Progress
|
## Overall Progress
|
||||||
|
|
||||||
**3553/6942 items complete (51.2%)**
|
**3601/6942 items complete (51.9%)**
|
||||||
|
|||||||
Reference in New Issue
Block a user