From 908c38546cb220edadd0f87fdea76943fd28949b Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 17 Sep 2026 19:32:36 +0200 Subject: [PATCH] Use Android-compatible archive durability barrier --- .../SyncGenerationForegroundCoordinator.kt | 9 ++----- .../trainlog/data/SyncGenerationService.kt | 27 ++++++------------- .../data/SyncDeploymentConversationTest.kt | 6 +---- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt b/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt index 061e747..7917c4b 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationForegroundCoordinator.kt @@ -14,13 +14,8 @@ internal sealed interface ForegroundGenerationResult { } /** Owns the bounded, user-initiated Android side of a generation conversation. */ -internal class SyncGenerationForegroundCoordinator( - private val repository: TrainlogRepository, - archiveDirectorySync: ((File) -> Unit)? = null, -) { - private val service = - archiveDirectorySync?.let { SyncGenerationService(repository, it) } - ?: SyncGenerationService(repository) +internal class SyncGenerationForegroundCoordinator(private val repository: TrainlogRepository) { + private val service = SyncGenerationService(repository) private fun requireExactKeys(value: JSONObject, expected: Set, label: String) { val actual = value.keys().asSequence().toSet() diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationService.kt b/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationService.kt index 019207a..d951ac1 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationService.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/SyncGenerationService.kt @@ -1,7 +1,5 @@ package com.labfytools.trainlog.data -import android.system.Os -import android.system.OsConstants import java.io.File import java.nio.ByteBuffer import java.nio.charset.StandardCharsets @@ -27,18 +25,7 @@ internal data class CapturedSyncGeneration( * CONTRACT: artifacts are immutable, bounded and published before the manifest marker. INVARIANT: * causal operation payloads are never rewritten to attach generation identity. */ -internal class SyncGenerationService( - private val repository: TrainlogRepository, - private val syncArchiveDirectory: (File) -> Unit = { directory -> - val descriptor = - Os.open(directory.absolutePath, OsConstants.O_RDONLY or O_DIRECTORY, 0) - try { - Os.fsync(descriptor) - } finally { - Os.close(descriptor) - } - }, -) { +internal class SyncGenerationService(private val repository: TrainlogRepository) { companion object { const val MAX_MANIFEST_BYTES = 64 * 1024 const val MAX_ARTIFACTS = 32 @@ -48,10 +35,6 @@ internal class SyncGenerationService( const val MAX_PATH_BYTES = 240 const val MAX_DIAGNOSTIC_BYTES = 1024 const val MAX_RETAINED_PER_PEER = 8 - // Android's public OsConstants omits Linux O_DIRECTORY even though - // Os.open forwards the platform flag. Keep the fixed Linux ABI value - // local to the directory-fsync boundary. - private const val O_DIRECTORY = 0x10000 private val ID = Regex( "^(gen|peer|sy)_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" @@ -252,7 +235,13 @@ internal class SyncGenerationService( } if (!temporary.renameTo(destination)) throw SyncGenerationException("cannot commit generation archive") - syncArchiveDirectory(root) + /* Android emulated external storage rejects directory fsync. + * Reopen and sync the commit marker after the atomic rename so + * the supported FUSE durability barrier covers the committed + * namespace before the internal SQLite ledger can advance. */ + java.io.FileOutputStream(File(destination, "manifest.json"), true).use { + it.fd.sync() + } } validatePublished(destination, row[3]) val checksum = archiveDigest(destination) diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/SyncDeploymentConversationTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/SyncDeploymentConversationTest.kt index 031fade..de9dd1d 100644 --- a/android/app/src/test/java/com/labfytools/trainlog/data/SyncDeploymentConversationTest.kt +++ b/android/app/src/test/java/com/labfytools/trainlog/data/SyncDeploymentConversationTest.kt @@ -101,11 +101,7 @@ class SyncDeploymentConversationTest { } val conversations = if (suppliedTransport == null) 24 else 1 repeat(conversations) { - // Robolectric cannot open a directory file descriptor. The - // production default performs the real directory fsync; this - // test substitutes only that kernel primitive while retaining - // the production copy, validation, rename and ledger path. - val coordinator = SyncGenerationForegroundCoordinator(repository) {} + val coordinator = SyncGenerationForegroundCoordinator(repository) val executor = java.util.concurrent.Executors.newSingleThreadExecutor() val result = executor.submit {