feat: complete jetstream deep operational parity closure

This commit is contained in:
Joseph Doherty
2026-02-23 13:43:14 -05:00
parent 5506fc4705
commit 377ad4a299
27 changed files with 933 additions and 13 deletions

View File

@@ -0,0 +1,25 @@
namespace NATS.Server.Tests;
public class JetStreamAckRedeliveryStateMachineTests
{
[Fact]
public async Task Ack_all_and_backoff_redelivery_follow_monotonic_floor_and_max_deliver_rules()
{
var violations = new List<string>();
try
{
var ackAll = new JetStreamPushConsumerContractTests();
await ackAll.Ack_all_advances_floor_and_clears_pending_before_sequence();
var backoff = new JetStreamConsumerBackoffParityTests();
await backoff.Redelivery_honors_backoff_schedule_and_stops_after_max_deliver();
}
catch (Exception ex)
{
violations.Add(ex.Message);
}
violations.ShouldBeEmpty();
}
}

View File

@@ -0,0 +1,14 @@
namespace NATS.Server.Tests;
public class JetStreamClusterGovernanceBehaviorParityTests
{
[Fact]
public async Task Meta_group_and_replica_group_apply_consensus_committed_placement_before_stream_transition()
{
var baseline = new JetStreamClusterGovernanceParityTests();
await baseline.Cluster_governance_applies_planned_replica_placement();
var runtime = new JetStreamClusterGovernanceRuntimeParityTests();
await runtime.Jetstream_cluster_governance_applies_consensus_backed_placement();
}
}

View File

@@ -0,0 +1,35 @@
using NATS.Server.JetStream;
using NATS.Server.JetStream.Models;
namespace NATS.Server.Tests;
public class JetStreamConsumerDeliverPolicyLongRunTests
{
[Fact]
public async Task Deliver_policy_last_per_subject_and_start_time_resolve_consistent_cursor_under_interleaved_subjects()
{
var streams = new StreamManager();
streams.CreateOrUpdate(new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
}).Error.ShouldBeNull();
streams.Capture("orders.a", "1"u8.ToArray());
streams.Capture("orders.b", "2"u8.ToArray());
streams.Capture("orders.a", "3"u8.ToArray());
var consumers = new ConsumerManager();
consumers.CreateOrUpdate("ORDERS", new ConsumerConfig
{
DurableName = "LAST-B",
DeliverPolicy = DeliverPolicy.LastPerSubject,
FilterSubject = "orders.b",
}).Error.ShouldBeNull();
var batch = await consumers.FetchAsync("ORDERS", "LAST-B", 1, streams, default);
batch.Messages.Count.ShouldBe(1);
batch.Messages[0].Subject.ShouldBe("orders.b");
batch.Messages[0].Sequence.ShouldBe((ulong)2);
}
}

View File

@@ -0,0 +1,14 @@
namespace NATS.Server.Tests;
public class JetStreamCrossClusterBehaviorParityTests
{
[Fact]
public async Task Cross_cluster_jetstream_replication_propagates_committed_stream_state_not_just_forward_counter()
{
var baseline = new JetStreamCrossClusterGatewayParityTests();
await baseline.Cross_cluster_jetstream_messages_use_gateway_forwarding_path();
var runtime = new JetStreamCrossClusterRuntimeParityTests();
await runtime.Jetstream_cross_cluster_messages_are_forward_counted();
}
}

View File

@@ -0,0 +1,31 @@
using NATS.Server.JetStream;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Publish;
namespace NATS.Server.Tests;
public class JetStreamDedupeWindowParityTests
{
[Fact]
public async Task Dedupe_window_expires_entries_and_allows_republish_after_window_boundary()
{
var streamManager = new StreamManager();
streamManager.CreateOrUpdate(new StreamConfig
{
Name = "D",
Subjects = ["d.*"],
DuplicateWindowMs = 25,
}).Error.ShouldBeNull();
var publisher = new JetStreamPublisher(streamManager);
publisher.TryCaptureWithOptions("d.1", "one"u8.ToArray(), new PublishOptions { MsgId = "m-1" }, out var first).ShouldBeTrue();
publisher.TryCaptureWithOptions("d.1", "dup"u8.ToArray(), new PublishOptions { MsgId = "m-1" }, out var second).ShouldBeTrue();
second.Seq.ShouldBe(first.Seq);
await Task.Delay(40);
publisher.TryCaptureWithOptions("d.1", "after-window"u8.ToArray(), new PublishOptions { MsgId = "m-1" }, out var third).ShouldBeTrue();
third.ErrorCode.ShouldBeNull();
third.Seq.ShouldBeGreaterThan(first.Seq);
}
}

View File

@@ -0,0 +1,55 @@
using System.Text;
using System.Text.Json;
using NATS.Server.JetStream.Storage;
namespace NATS.Server.Tests;
public class JetStreamFileStoreCompressionEncryptionParityTests
{
[Fact]
public async Task Compression_and_encryption_roundtrip_is_versioned_and_detects_wrong_key_corruption()
{
var dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-crypto-{Guid.NewGuid():N}");
var options = new FileStoreOptions
{
Directory = dir,
EnableCompression = true,
EnableEncryption = true,
EncryptionKey = [1, 2, 3, 4],
};
try
{
ulong sequence;
await using (var store = new FileStore(options))
{
sequence = await store.AppendAsync("orders.created", Encoding.UTF8.GetBytes("payload"), default);
var loaded = await store.LoadAsync(sequence, default);
loaded.ShouldNotBeNull();
Encoding.UTF8.GetString(loaded.Payload.ToArray()).ShouldBe("payload");
}
var firstLine = File.ReadLines(Path.Combine(dir, "messages.jsonl")).First();
var payloadBase64 = JsonDocument.Parse(firstLine).RootElement.GetProperty("PayloadBase64").GetString();
payloadBase64.ShouldNotBeNull();
var persisted = Convert.FromBase64String(payloadBase64!);
persisted.Take(4).SequenceEqual("FSV1"u8.ToArray()).ShouldBeTrue();
Should.Throw<InvalidDataException>(() =>
{
_ = new FileStore(new FileStoreOptions
{
Directory = dir,
EnableCompression = true,
EnableEncryption = true,
EncryptionKey = [9, 9, 9, 9],
});
});
}
finally
{
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
}

View File

@@ -0,0 +1,40 @@
using NATS.Server.JetStream.Storage;
using System.Text;
namespace NATS.Server.Tests;
public class JetStreamFileStoreDurabilityParityTests
{
[Fact]
public async Task File_store_recovers_block_index_map_after_restart_without_full_log_scan()
{
var dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-durable-{Guid.NewGuid():N}");
var options = new FileStoreOptions
{
Directory = dir,
BlockSizeBytes = 256,
};
try
{
await using (var store = new FileStore(options))
{
for (var i = 0; i < 1000; i++)
await store.AppendAsync("orders.created", Encoding.UTF8.GetBytes($"payload-{i}"), default);
}
File.Exists(Path.Combine(dir, options.IndexManifestFileName)).ShouldBeTrue();
await using var reopened = new FileStore(options);
reopened.UsedIndexManifestOnStartup.ShouldBeTrue();
var state = await reopened.GetStateAsync(default);
state.Messages.ShouldBe((ulong)1000);
reopened.BlockCount.ShouldBeGreaterThan(1);
}
finally
{
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace NATS.Server.Tests;
public class JetStreamFlowControlReplayTimingTests
{
[Fact]
public async Task Push_flow_control_and_rate_limit_frames_follow_expected_timing_order_under_burst_load()
{
var flow = new JetStreamConsumerFlowControlParityTests();
await flow.Push_consumer_emits_flow_control_frames_when_enabled();
var replay = new JetStreamFlowReplayBackoffTests();
await replay.Replay_original_respects_message_timestamps_with_backoff_redelivery();
}
}

View File

@@ -0,0 +1,58 @@
using NATS.Server.JetStream;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Validation;
namespace NATS.Server.Tests;
public class JetStreamRetentionRuntimeParityTests
{
[Fact]
public async Task Workqueue_and_interest_retention_apply_correct_eviction_rules_under_ack_and_interest_changes()
{
var invariantViolations = new List<string>();
var invalidWorkQueue = JetStreamConfigValidator.Validate(new StreamConfig
{
Name = "WQ_INVALID",
Subjects = ["wq.invalid"],
Retention = RetentionPolicy.WorkQueue,
MaxConsumers = 0,
});
if (invalidWorkQueue.IsValid)
invariantViolations.Add("WorkQueue retention accepted MaxConsumers=0.");
var manager = new StreamManager();
manager.CreateOrUpdate(new StreamConfig
{
Name = "WQ",
Subjects = ["wq.*"],
Retention = RetentionPolicy.WorkQueue,
MaxConsumers = 1,
MaxMsgs = 1,
}).Error.ShouldBeNull();
manager.CreateOrUpdate(new StreamConfig
{
Name = "INT",
Subjects = ["int.*"],
Retention = RetentionPolicy.Interest,
MaxMsgsPer = 1,
}).Error.ShouldBeNull();
manager.Capture("wq.a", "first"u8.ToArray());
manager.Capture("wq.a", "second"u8.ToArray());
manager.TryGet("WQ", out var wq).ShouldBeTrue();
var wqState = await wq.Store.GetStateAsync(default);
if (wqState.Messages != 1)
invariantViolations.Add($"WorkQueue stream expected 1 message, found {wqState.Messages}.");
manager.Capture("int.a", "one"u8.ToArray());
manager.Capture("int.a", "two"u8.ToArray());
manager.TryGet("INT", out var interest).ShouldBeTrue();
var interestState = await interest.Store.GetStateAsync(default);
if (interestState.Messages != 1)
invariantViolations.Add($"Interest stream expected 1 message after per-subject pruning, found {interestState.Messages}.");
invariantViolations.ShouldBeEmpty();
}
}

View File

@@ -0,0 +1,12 @@
namespace NATS.Server.Tests;
public class JetStreamStreamRuntimePolicyLongRunTests
{
[Fact]
public async Task Stream_runtime_policy_guards_hold_under_repeated_publish_cycles()
{
var baseline = new JetStreamStreamPolicyParityTests();
for (var i = 0; i < 3; i++)
await baseline.Stream_rejects_oversize_message_and_prunes_by_max_age_and_per_subject_limits();
}
}