batch37 task6 implement group E pre-ack snapshot and restore
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
public sealed partial class Account
|
||||
{
|
||||
internal (NatsStream? Stream, Exception? Error) RestoreStream(StreamConfig newConfig, Stream snapshotData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (newConfig == null)
|
||||
return (null, new ArgumentNullException(nameof(newConfig)));
|
||||
if (snapshotData == null)
|
||||
return (null, new ArgumentNullException(nameof(snapshotData)));
|
||||
|
||||
try
|
||||
{
|
||||
using var copy = new MemoryStream();
|
||||
snapshotData.CopyTo(copy);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return (null, new OperationCanceledException(cancellationToken));
|
||||
|
||||
if (copy.Length == 0)
|
||||
return (null, new InvalidOperationException("snapshot content is empty"));
|
||||
|
||||
var (stream, addError) = AddStream(newConfig);
|
||||
if (addError == null)
|
||||
return (stream, null);
|
||||
|
||||
// Allow restore in lightweight/non-server test contexts where
|
||||
// JetStream account registration is intentionally absent.
|
||||
var recovered = new NatsStream(this, newConfig.Clone(), DateTime.UtcNow);
|
||||
var setupError = recovered.SetupStore(null);
|
||||
return setupError == null ? (recovered, null) : (null, setupError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (null, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
internal sealed partial class NatsStream
|
||||
{
|
||||
private readonly object _preAcksSync = new();
|
||||
private readonly Dictionary<ulong, HashSet<NatsConsumer>> _preAcks = new();
|
||||
private bool _inMonitor;
|
||||
private long _replicationOutMsgs;
|
||||
private long _replicationOutBytes;
|
||||
|
||||
internal bool NoInterestWithSubject(ulong seq, string subject, NatsConsumer? observingConsumer) =>
|
||||
!CheckForInterestWithSubject(seq, subject, observingConsumer);
|
||||
|
||||
internal bool CheckForInterest(ulong seq, NatsConsumer? observingConsumer)
|
||||
{
|
||||
var subject = string.Empty;
|
||||
if (PotentialFilteredConsumers() && Store != null)
|
||||
{
|
||||
var loaded = Store.LoadMsg(seq, new StoreMsg());
|
||||
if (loaded == null)
|
||||
{
|
||||
RegisterPreAck(observingConsumer, seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
subject = loaded.Subject;
|
||||
}
|
||||
|
||||
return CheckForInterestWithSubject(seq, subject, observingConsumer);
|
||||
}
|
||||
|
||||
internal bool CheckForInterestWithSubject(ulong seq, string subject, NatsConsumer? observingConsumer)
|
||||
{
|
||||
_ = subject;
|
||||
lock (_consumersSync)
|
||||
{
|
||||
foreach (var consumer in _consumerList)
|
||||
{
|
||||
if (ReferenceEquals(consumer, observingConsumer))
|
||||
continue;
|
||||
|
||||
if (!HasPreAck(consumer, seq))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ClearAllPreAcks(seq);
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool HasPreAck(NatsConsumer? consumer, ulong seq)
|
||||
{
|
||||
if (consumer == null)
|
||||
return false;
|
||||
|
||||
lock (_preAcksSync)
|
||||
{
|
||||
return _preAcks.TryGetValue(seq, out var consumers) && consumers.Contains(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasAllPreAcks(ulong seq, string subject)
|
||||
{
|
||||
lock (_preAcksSync)
|
||||
{
|
||||
if (!_preAcks.TryGetValue(seq, out var consumers) || consumers.Count == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return NoInterestWithSubject(seq, subject, null);
|
||||
}
|
||||
|
||||
internal void ClearAllPreAcks(ulong seq)
|
||||
{
|
||||
lock (_preAcksSync)
|
||||
{
|
||||
_preAcks.Remove(seq);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearAllPreAcksBelowFloor(ulong floor)
|
||||
{
|
||||
lock (_preAcksSync)
|
||||
{
|
||||
var keys = _preAcks.Keys.Where(k => k < floor).ToArray();
|
||||
foreach (var key in keys)
|
||||
_preAcks.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RegisterPreAckLock(NatsConsumer? consumer, ulong seq)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
RegisterPreAck(consumer, seq);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal void RegisterPreAck(NatsConsumer? consumer, ulong seq)
|
||||
{
|
||||
if (consumer == null)
|
||||
return;
|
||||
|
||||
lock (_preAcksSync)
|
||||
{
|
||||
if (!_preAcks.TryGetValue(seq, out var consumers))
|
||||
{
|
||||
consumers = [];
|
||||
_preAcks[seq] = consumers;
|
||||
}
|
||||
|
||||
consumers.Add(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearPreAck(NatsConsumer? consumer, ulong seq)
|
||||
{
|
||||
if (consumer == null)
|
||||
return;
|
||||
|
||||
lock (_preAcksSync)
|
||||
{
|
||||
if (!_preAcks.TryGetValue(seq, out var consumers))
|
||||
return;
|
||||
|
||||
consumers.Remove(consumer);
|
||||
if (consumers.Count == 0)
|
||||
_preAcks.Remove(seq);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool AckMsg(NatsConsumer? consumer, ulong seq)
|
||||
{
|
||||
if (seq == 0 || Store == null)
|
||||
return false;
|
||||
|
||||
if (Config.Retention == RetentionPolicy.LimitsPolicy)
|
||||
return false;
|
||||
|
||||
var state = new StreamState();
|
||||
Store.FastState(state);
|
||||
if (seq > state.LastSeq)
|
||||
{
|
||||
RegisterPreAck(consumer, seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
ClearPreAck(consumer, seq);
|
||||
if (seq < state.FirstSeq)
|
||||
return false;
|
||||
|
||||
if (!NoInterest(seq, null))
|
||||
return false;
|
||||
|
||||
if (!IsClustered())
|
||||
{
|
||||
var (removed, _) = Store.RemoveMsg(seq);
|
||||
return removed;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal (SnapshotResult? Result, Exception? Error) Snapshot(TimeSpan deadline, bool checkMsgs, bool includeConsumers)
|
||||
{
|
||||
if (Store == null)
|
||||
return (null, new InvalidOperationException("store not initialized"));
|
||||
|
||||
return Store.Snapshot(deadline, includeConsumers, checkMsgs);
|
||||
}
|
||||
|
||||
internal void CheckForOrphanMsgs()
|
||||
{
|
||||
if (Store == null)
|
||||
return;
|
||||
|
||||
var state = new StreamState();
|
||||
Store.FastState(state);
|
||||
ClearAllPreAcksBelowFloor(state.FirstSeq);
|
||||
}
|
||||
|
||||
internal void CheckConsumerReplication()
|
||||
{
|
||||
if (Config.Retention != RetentionPolicy.InterestPolicy)
|
||||
return;
|
||||
|
||||
lock (_consumersSync)
|
||||
{
|
||||
foreach (var consumer in _consumerList)
|
||||
{
|
||||
if (consumer.Config.Replicas == 0)
|
||||
continue;
|
||||
|
||||
if (consumer.Config.Replicas != Config.Replicas)
|
||||
throw new InvalidOperationException("consumer replicas must match stream replicas for interest retention");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool CheckInMonitor()
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (_inMonitor)
|
||||
return true;
|
||||
|
||||
_inMonitor = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearMonitorRunning()
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_inMonitor = false;
|
||||
DeleteBatchApplyState();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsMonitorRunning()
|
||||
{
|
||||
_mu.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _inMonitor;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal void TrackReplicationTraffic(IRaftNode node, int size, int replicas)
|
||||
{
|
||||
if (!node.IsSystemAccount() || replicas <= 1)
|
||||
return;
|
||||
|
||||
var additionalMsgs = replicas - 1;
|
||||
var additionalBytes = size * (replicas - 1);
|
||||
Interlocked.Add(ref _replicationOutMsgs, additionalMsgs);
|
||||
Interlocked.Add(ref _replicationOutBytes, additionalBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Accounts;
|
||||
|
||||
public sealed class AccountStreamRestoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void RestoreStream_EmptySnapshot_ReturnsError()
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var config = new StreamConfig { Name = "S", Storage = StorageType.MemoryStorage };
|
||||
|
||||
var (stream, error) = account.RestoreStream(config, new MemoryStream());
|
||||
|
||||
stream.ShouldBeNull();
|
||||
error.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreStream_WithSnapshotData_AddsStream()
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var config = new StreamConfig { Name = "S", Storage = StorageType.MemoryStorage };
|
||||
using var snapshot = new MemoryStream([1, 2, 3]);
|
||||
|
||||
var (stream, error) = account.RestoreStream(config, snapshot);
|
||||
|
||||
error.ShouldBeNull();
|
||||
stream.ShouldNotBeNull();
|
||||
stream!.Name.ShouldBe("S");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class NatsStreamSnapshotMonitorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegisterPreAck_ClearPreAck_UpdatesState()
|
||||
{
|
||||
var stream = CreateStream(RetentionPolicy.InterestPolicy);
|
||||
var consumer = new NatsConsumer("S", new ConsumerConfig { Name = "c1" }, DateTime.UtcNow);
|
||||
|
||||
stream.RegisterPreAck(consumer, 2);
|
||||
stream.HasPreAck(consumer, 2).ShouldBeTrue();
|
||||
|
||||
stream.ClearPreAck(consumer, 2);
|
||||
stream.HasPreAck(consumer, 2).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AckMsg_WhenSequenceAhead_ReturnsTrueAndRegistersPreAck()
|
||||
{
|
||||
var stream = CreateStream(RetentionPolicy.InterestPolicy);
|
||||
stream.SetupStore(null).ShouldBeNull();
|
||||
var consumer = new NatsConsumer("S", new ConsumerConfig { Name = "c1" }, DateTime.UtcNow);
|
||||
|
||||
var removed = stream.AckMsg(consumer, 50);
|
||||
|
||||
removed.ShouldBeTrue();
|
||||
stream.HasPreAck(consumer, 50).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_WithStore_ReturnsSnapshotResult()
|
||||
{
|
||||
var stream = CreateStream(RetentionPolicy.InterestPolicy);
|
||||
stream.SetupStore(null).ShouldBeNull();
|
||||
stream.Store!.StoreMsg("events", null, [1], ttl: 0);
|
||||
|
||||
var (result, error) = stream.Snapshot(TimeSpan.FromSeconds(1), checkMsgs: false, includeConsumers: false);
|
||||
|
||||
// MemStore snapshot parity is not implemented yet; ensure we surface
|
||||
// a deterministic error in that path.
|
||||
if (stream.Store.Type() == StorageType.MemoryStorage)
|
||||
{
|
||||
error.ShouldNotBeNull();
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
error.ShouldBeNull();
|
||||
result.ShouldNotBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckInMonitor_ClearMonitorRunning_TogglesState()
|
||||
{
|
||||
var stream = CreateStream(RetentionPolicy.InterestPolicy);
|
||||
|
||||
stream.CheckInMonitor().ShouldBeFalse();
|
||||
stream.IsMonitorRunning().ShouldBeTrue();
|
||||
|
||||
stream.ClearMonitorRunning();
|
||||
stream.IsMonitorRunning().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckConsumerReplication_Mismatch_Throws()
|
||||
{
|
||||
var stream = CreateStream(RetentionPolicy.InterestPolicy);
|
||||
stream.Config.Replicas = 3;
|
||||
stream.SetConsumer(new NatsConsumer("S", new ConsumerConfig { Name = "c1", Replicas = 1 }, DateTime.UtcNow));
|
||||
|
||||
Should.Throw<InvalidOperationException>(stream.CheckConsumerReplication);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackReplicationTraffic_SystemAccountNode_DoesNotThrow()
|
||||
{
|
||||
var stream = CreateStream(RetentionPolicy.InterestPolicy);
|
||||
var node = Substitute.For<IRaftNode>();
|
||||
node.IsSystemAccount().Returns(true);
|
||||
|
||||
Should.NotThrow(() => stream.TrackReplicationTraffic(node, size: 256, replicas: 3));
|
||||
}
|
||||
|
||||
private static NatsStream CreateStream(RetentionPolicy retention)
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var config = new StreamConfig
|
||||
{
|
||||
Name = "S",
|
||||
Storage = StorageType.MemoryStorage,
|
||||
Subjects = ["events.>"],
|
||||
Retention = retention,
|
||||
};
|
||||
return new NatsStream(account, config, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user