Files
natsdotnet/tests/NATS.Server.Tests/JetStream/Storage/FileStoreLimitsTests.cs
Joseph Doherty 3ff801865a feat: Waves 3-5 — FileStore, RAFT, JetStream clustering, and concurrency tests
Add comprehensive Go-parity test coverage across 3 subsystems:
- FileStore: basic CRUD, limits, purge, recovery, subjects, encryption,
  compression, MemStore (161 tests, 24 skipped for not-yet-implemented)
- RAFT: core types, wire format, election, log replication, snapshots
  (95 tests)
- JetStream Clustering: meta controller, stream/consumer replica groups,
  concurrency stress tests (90 tests)

Total: ~346 new test annotations across 17 files (+7,557 lines)
Full suite: 2,606 passing, 0 failures, 27 skipped
2026-02-23 22:55:41 -05:00

363 lines
12 KiB
C#

// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStoreMsgLimit, TestFileStoreMsgLimitBug,
// TestFileStoreBytesLimit, TestFileStoreBytesLimitWithDiscardNew,
// TestFileStoreAgeLimit, TestFileStoreMaxMsgsPerSubject,
// TestFileStoreMaxMsgsAndMaxMsgsPerSubject,
// TestFileStoreUpdateMaxMsgsPerSubject
using System.Text;
using NATS.Server.JetStream.Storage;
namespace NATS.Server.Tests.JetStream.Storage;
public sealed class FileStoreLimitsTests : IDisposable
{
private readonly string _dir;
public FileStoreLimitsTests()
{
_dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-limits-{Guid.NewGuid():N}");
Directory.CreateDirectory(_dir);
}
public void Dispose()
{
if (Directory.Exists(_dir))
Directory.Delete(_dir, recursive: true);
}
private FileStore CreateStore(string subdirectory, FileStoreOptions? options = null)
{
var dir = Path.Combine(_dir, subdirectory);
var opts = options ?? new FileStoreOptions();
opts.Directory = dir;
return new FileStore(opts);
}
// Go: TestFileStoreMsgLimit server/filestore_test.go:484
[Fact]
public async Task TrimToMaxMessages_maintains_limit()
{
await using var store = CreateStore("msg-limit");
for (var i = 0; i < 10; i++)
await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)10);
// Store one more, then trim.
await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
store.TrimToMaxMessages(10);
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)10);
state.LastSeq.ShouldBe((ulong)11);
state.FirstSeq.ShouldBe((ulong)2);
// Seq 1 should be evicted.
(await store.LoadAsync(1, default)).ShouldBeNull();
}
// Go: TestFileStoreMsgLimitBug server/filestore_test.go:518
[Fact]
public async Task TrimToMaxMessages_one_across_restart()
{
var subDir = "msg-limit-bug";
await using (var store = CreateStore(subDir))
{
await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
store.TrimToMaxMessages(1);
}
// Reopen and store one more.
await using (var store = CreateStore(subDir))
{
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)1);
await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
store.TrimToMaxMessages(1);
state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)1);
}
}
// Go: TestFileStoreMsgLimit server/filestore_test.go:484
[Fact]
public async Task TrimToMaxMessages_repeated_trims()
{
await using var store = CreateStore("repeated-trim");
for (var i = 0; i < 20; i++)
await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);
store.TrimToMaxMessages(10);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)10);
(await store.GetStateAsync(default)).FirstSeq.ShouldBe((ulong)11);
store.TrimToMaxMessages(5);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)5);
(await store.GetStateAsync(default)).FirstSeq.ShouldBe((ulong)16);
store.TrimToMaxMessages(1);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)1);
(await store.GetStateAsync(default)).FirstSeq.ShouldBe((ulong)20);
}
// Go: TestFileStoreBytesLimit server/filestore_test.go:537
[Fact]
public async Task Bytes_accumulate_correctly()
{
await using var store = CreateStore("bytes-accum");
var payload = new byte[512];
const int count = 10;
for (var i = 0; i < count; i++)
await store.AppendAsync("foo", payload, default);
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)count);
state.Bytes.ShouldBe((ulong)(count * 512));
}
// Go: TestFileStoreBytesLimit server/filestore_test.go:537
[Fact]
public async Task TrimToMaxMessages_reduces_bytes()
{
await using var store = CreateStore("bytes-trim");
var payload = new byte[100];
for (var i = 0; i < 10; i++)
await store.AppendAsync("foo", payload, default);
var beforeState = await store.GetStateAsync(default);
beforeState.Bytes.ShouldBe((ulong)1000);
store.TrimToMaxMessages(5);
var afterState = await store.GetStateAsync(default);
afterState.Messages.ShouldBe((ulong)5);
afterState.Bytes.ShouldBe((ulong)500);
}
// Go: TestFileStoreAgeLimit server/filestore_test.go:616
[Fact]
public async Task MaxAge_expires_old_messages()
{
// MaxAgeMs = 200ms
await using var store = CreateStore("age-limit", new FileStoreOptions { MaxAgeMs = 200 });
for (var i = 0; i < 5; i++)
await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)5);
// Wait for messages to expire.
await Task.Delay(300);
// Trigger pruning by appending a new message.
await store.AppendAsync("foo", "trigger"u8.ToArray(), default);
var state = await store.GetStateAsync(default);
// Only the freshly-appended trigger message should remain.
state.Messages.ShouldBe((ulong)1);
}
// Go: TestFileStoreAgeLimit server/filestore_test.go:660
[Fact]
public async Task MaxAge_timer_fires_again_for_second_batch()
{
await using var store = CreateStore("age-second-batch", new FileStoreOptions { MaxAgeMs = 200 });
for (var i = 0; i < 3; i++)
await store.AppendAsync("foo", "batch1"u8.ToArray(), default);
await Task.Delay(300);
// Trigger pruning.
await store.AppendAsync("foo", "trigger1"u8.ToArray(), default);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)1);
// Second batch.
for (var i = 0; i < 3; i++)
await store.AppendAsync("foo", "batch2"u8.ToArray(), default);
await Task.Delay(300);
await store.AppendAsync("foo", "trigger2"u8.ToArray(), default);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)1);
}
// Go: TestFileStoreAgeLimit server/filestore_test.go:616
[Fact]
public async Task MaxAge_zero_means_no_expiration()
{
await using var store = CreateStore("age-zero", new FileStoreOptions { MaxAgeMs = 0 });
for (var i = 0; i < 5; i++)
await store.AppendAsync("foo", "Hello"u8.ToArray(), default);
await Task.Delay(100);
// Trigger append to check pruning.
await store.AppendAsync("foo", "trigger"u8.ToArray(), default);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)6);
}
// Go: TestFileStoreMsgLimit server/filestore_test.go:484
[Fact]
public async Task TrimToMaxMessages_zero_removes_all()
{
await using var store = CreateStore("trim-zero");
for (var i = 0; i < 5; i++)
await store.AppendAsync("foo", "data"u8.ToArray(), default);
store.TrimToMaxMessages(0);
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)0);
}
// Go: TestFileStoreMsgLimit server/filestore_test.go:484
[Fact]
public async Task TrimToMaxMessages_larger_than_count_is_noop()
{
await using var store = CreateStore("trim-noop");
for (var i = 0; i < 5; i++)
await store.AppendAsync("foo", "data"u8.ToArray(), default);
store.TrimToMaxMessages(100);
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)5);
state.FirstSeq.ShouldBe((ulong)1);
}
// Go: TestFileStoreBytesLimit server/filestore_test.go:537
[Fact]
public async Task Bytes_decrease_after_remove()
{
await using var store = CreateStore("bytes-rm");
var payload = new byte[100];
for (var i = 0; i < 5; i++)
await store.AppendAsync("foo", payload, default);
var before = await store.GetStateAsync(default);
before.Bytes.ShouldBe((ulong)500);
await store.RemoveAsync(1, default);
await store.RemoveAsync(3, default);
var after = await store.GetStateAsync(default);
after.Bytes.ShouldBe((ulong)300);
}
// Go: TestFileStoreBytesLimitWithDiscardNew server/filestore_test.go:583
[Fact(Skip = "DiscardNew policy not yet implemented in .NET FileStore")]
public async Task Bytes_limit_with_discard_new_rejects_over_limit()
{
await Task.CompletedTask;
}
// Go: TestFileStoreMaxMsgsPerSubject server/filestore_test.go:4065
[Fact(Skip = "MaxMsgsPerSubject not yet implemented in .NET FileStore")]
public async Task MaxMsgsPerSubject_enforces_per_subject_limit()
{
await Task.CompletedTask;
}
// Go: TestFileStoreMaxMsgsAndMaxMsgsPerSubject server/filestore_test.go:4098
[Fact(Skip = "MaxMsgsPerSubject not yet implemented in .NET FileStore")]
public async Task MaxMsgs_and_MaxMsgsPerSubject_combined()
{
await Task.CompletedTask;
}
// Go: TestFileStoreUpdateMaxMsgsPerSubject server/filestore_test.go:4563
[Fact(Skip = "UpdateConfig not yet implemented in .NET FileStore")]
public async Task UpdateConfig_changes_MaxMsgsPerSubject()
{
await Task.CompletedTask;
}
// Go: TestFileStoreMsgLimit server/filestore_test.go:484
[Fact]
public async Task TrimToMaxMessages_persists_across_restart()
{
var subDir = "trim-persist";
await using (var store = CreateStore(subDir))
{
for (var i = 0; i < 20; i++)
await store.AppendAsync("foo", "data"u8.ToArray(), default);
store.TrimToMaxMessages(5);
}
await using (var store = CreateStore(subDir))
{
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)5);
state.FirstSeq.ShouldBe((ulong)16);
state.LastSeq.ShouldBe((ulong)20);
}
}
// Go: TestFileStoreAgeLimit server/filestore_test.go:616
[Fact]
public async Task MaxAge_with_interior_deletes()
{
await using var store = CreateStore("age-interior", new FileStoreOptions { MaxAgeMs = 200 });
for (var i = 0; i < 10; i++)
await store.AppendAsync("foo", "Hello"u8.ToArray(), default);
// Remove some interior messages.
await store.RemoveAsync(3, default);
await store.RemoveAsync(5, default);
await store.RemoveAsync(7, default);
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)7);
await Task.Delay(300);
// Trigger pruning.
await store.AppendAsync("foo", "trigger"u8.ToArray(), default);
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)1);
}
// Go: TestFileStoreMsgLimit server/filestore_test.go:484
[Fact]
public async Task Sequence_numbers_monotonically_increase_through_trimming()
{
await using var store = CreateStore("seq-mono");
for (var i = 1; i <= 15; i++)
await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);
store.TrimToMaxMessages(5);
var state = await store.GetStateAsync(default);
state.LastSeq.ShouldBe((ulong)15);
state.FirstSeq.ShouldBe((ulong)11);
// Append more.
var nextSeq = await store.AppendAsync("foo", "after-trim"u8.ToArray(), default);
nextSeq.ShouldBe((ulong)16);
state = await store.GetStateAsync(default);
state.LastSeq.ShouldBe((ulong)16);
state.Messages.ShouldBe((ulong)6);
}
}