// Go ref: filestore.go:10599 (_writeFullState) // AtomicFileWriter wraps the write-to-temp-then-rename pattern used by // Go's fileStore._writeFullState to guarantee crash-safe state persistence. using NATS.Server.JetStream.Storage; namespace NATS.Server.Tests.JetStream.Storage; public sealed class AtomicFileWriterTests : IDisposable { private readonly DirectoryInfo _dir; public AtomicFileWriterTests() { _dir = Directory.CreateTempSubdirectory("atomic_writer_tests_"); } public void Dispose() => _dir.Delete(recursive: true); private string TempPath(string name) => Path.Combine(_dir.FullName, name); // ------------------------------------------------------------------------- // byte[] overload // ------------------------------------------------------------------------- [Fact] public async Task WriteAtomicallyAsync_creates_file() { var path = TempPath("state.json"); var data = "{ \"seq\": 1 }"u8.ToArray(); await AtomicFileWriter.WriteAtomicallyAsync(path, data); File.Exists(path).ShouldBeTrue(); var written = await File.ReadAllBytesAsync(path); written.ShouldBe(data); } [Fact] public async Task WriteAtomicallyAsync_no_temp_file_remains() { var path = TempPath("state.json"); var data = "hello world"u8.ToArray(); await AtomicFileWriter.WriteAtomicallyAsync(path, data); // No .tmp file should remain after a successful write. // The temp file uses a random component ({path}.{random}.tmp) so check by extension. _dir.EnumerateFiles("*.tmp").ShouldBeEmpty(); } [Fact] public async Task WriteAtomicallyAsync_overwrites_existing() { var path = TempPath("state.json"); await AtomicFileWriter.WriteAtomicallyAsync(path, "first"u8.ToArray()); await AtomicFileWriter.WriteAtomicallyAsync(path, "second"u8.ToArray()); var written = await File.ReadAllTextAsync(path); written.ShouldBe("second"); } [Fact] public async Task WriteAtomicallyAsync_concurrent_writes_are_safe() { // Multiple concurrent writes to the same file must not corrupt it. // Each write uses a unique payload; after all writes complete the // file must contain exactly one of the payloads (no partial data). var path = TempPath("concurrent.json"); const int concurrency = 20; var tasks = Enumerable.Range(0, concurrency).Select(i => AtomicFileWriter.WriteAtomicallyAsync(path, System.Text.Encoding.UTF8.GetBytes($"payload-{i:D3}"))); await Task.WhenAll(tasks); // File must exist and contain exactly one complete payload. File.Exists(path).ShouldBeTrue(); var content = await File.ReadAllTextAsync(path); content.ShouldMatch(@"^payload-\d{3}$"); } // ------------------------------------------------------------------------- // ReadOnlyMemory overload // ------------------------------------------------------------------------- [Fact] public async Task WriteAtomicallyAsync_memory_overload_creates_file() { var path = TempPath("state_mem.json"); ReadOnlyMemory data = "{ \"seq\": 42 }"u8.ToArray(); await AtomicFileWriter.WriteAtomicallyAsync(path, data); File.Exists(path).ShouldBeTrue(); var written = await File.ReadAllBytesAsync(path); written.ShouldBe(data.ToArray()); } [Fact] public async Task WriteAtomicallyAsync_memory_overload_no_temp_file_remains() { var path = TempPath("state_mem.json"); ReadOnlyMemory data = "memory data"u8.ToArray(); await AtomicFileWriter.WriteAtomicallyAsync(path, data); // The temp file uses a random component ({path}.{random}.tmp) so check by extension. _dir.EnumerateFiles("*.tmp").ShouldBeEmpty(); } [Fact] public async Task WriteAtomicallyAsync_memory_overload_overwrites_existing() { var path = TempPath("state_mem.json"); await AtomicFileWriter.WriteAtomicallyAsync(path, (ReadOnlyMemory)"first"u8.ToArray()); await AtomicFileWriter.WriteAtomicallyAsync(path, (ReadOnlyMemory)"second"u8.ToArray()); var written = await File.ReadAllTextAsync(path); written.ShouldBe("second"); } [Fact] public async Task WriteAtomicallyAsync_writes_empty_data() { var path = TempPath("empty.json"); await AtomicFileWriter.WriteAtomicallyAsync(path, Array.Empty()); File.Exists(path).ShouldBeTrue(); var written = await File.ReadAllBytesAsync(path); written.ShouldBeEmpty(); } [Fact] public async Task WriteAtomicallyAsync_writes_large_payload() { var path = TempPath("large.bin"); var data = new byte[256 * 1024]; // 256 KB Random.Shared.NextBytes(data); await AtomicFileWriter.WriteAtomicallyAsync(path, data); var written = await File.ReadAllBytesAsync(path); written.ShouldBe(data); } }