diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 00:42:16 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 00:42:25 +0900 |
| commit | 8870ca5de9dd091830a6c3b80a5fcc4f8f035360 (patch) | |
| tree | ae164b30d72ddf3f7a7b17990396080a74406f84 /platform-paper/src/test | |
| parent | eb5cb6960b4f914e9c7b819974c91408ad259f9f (diff) | |
| download | LunaticChat-8870ca5de9dd091830a6c3b80a5fcc4f8f035360.tar.gz LunaticChat-8870ca5de9dd091830a6c3b80a5fcc4f8f035360.tar.bz2 LunaticChat-8870ca5de9dd091830a6c3b80a5fcc4f8f035360.zip | |
fix: give every data file the same atomic write
Writing to a fixed sibling only moved the interleaving from the destination
to the temporary file: two saves racing there published mixed content, and
the losing move then failed with the temporary file already gone. Each write
now gets a unique temporary file, and falls back to a non-atomic replace on
the network mounts that refuse an atomic rename.
settings.yml and the conversion cache were still written in place. Both are
discarded wholesale when they do not parse, so a torn file silently costs
every player's settings or the whole accumulated cache.
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'platform-paper/src/test')
| -rw-r--r-- | platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/AtomicWriteTest.kt | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/AtomicWriteTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/AtomicWriteTest.kt new file mode 100644 index 0000000..ea0c35f --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/AtomicWriteTest.kt @@ -0,0 +1,76 @@ +package dev.m1sk9.lunaticChat.paper + +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CyclicBarrier +import kotlin.io.path.exists +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AtomicWriteTest { + private fun withTemporaryDirectory(block: (Path) -> Unit) { + val directory = Files.createTempDirectory("atomic-write-test") + try { + block(directory) + } finally { + directory.toFile().deleteRecursively() + } + } + + @Test + fun `writes the content and leaves no temporary file behind`() = + withTemporaryDirectory { directory -> + val target = directory.resolve("channels.json") + + target.writeTextAtomically("""{"channels":[]}""") + + assertEquals("""{"channels":[]}""", target.readText()) + assertEquals(listOf(target), directory.listDirectoryEntries()) + } + + @Test + fun `replaces existing content`() = + withTemporaryDirectory { directory -> + val target = directory.resolve("settings.yml") + target.writeText("version: 1") + + target.writeTextAtomically("version: 2") + + assertEquals("version: 2", target.readText()) + } + + @Test + fun `concurrent writers each publish a whole file rather than colliding`() = + withTemporaryDirectory { directory -> + val target = directory.resolve("channels.json") + val writerCount = 8 + val contents = (1..writerCount).map { "content-$it".repeat(4_000) } + val failures = ConcurrentLinkedQueue<Throwable>() + val barrier = CyclicBarrier(writerCount) + + // A shutdown save and a still-pending debounced save can reach the same file at once. + // With a shared temporary path they interleave there instead, and the losing move fails + // with the temporary file already gone. + val writers = + contents.map { content -> + Thread { + barrier.await() + runCatching { target.writeTextAtomically(content) } + .onFailure { failures.add(it) } + } + } + writers.forEach { it.start() } + writers.forEach { it.join() } + + assertTrue(failures.isEmpty(), "writes failed: ${failures.map { it.toString() }}") + assertContains(contents, target.readText()) + assertEquals(listOf(target), directory.listDirectoryEntries()) + assertTrue(target.exists()) + } +} |
