Fix Android generation sync retry trigger

This commit is contained in:
fy59 2026-09-18 05:42:48 +02:00
parent 189307b548
commit 1be4ab0397
3 changed files with 36 additions and 4 deletions

View file

@ -112,6 +112,7 @@ internal class SyncGenerationForegroundCoordinator(private val repository: Train
fun run(
directory: File,
timeout: Duration = Duration.ofMinutes(5),
afterPeerPublication: (() -> Unit)? = null,
afterPublication: (() -> Unit)? = null,
): ForegroundGenerationResult =
try {
@ -139,6 +140,13 @@ internal class SyncGenerationForegroundCoordinator(private val repository: Train
)
.toString(),
)
/* WHY: trainlog-syncd is intentionally driven by the established
* Android request artifact, not by polling generation internals.
* CONTRACT: each explicit foreground attempt publishes the peer
* identity before its caller emits one fresh request signal.
* INVARIANT: a failed attempt's request, run, generations, and
* ACK evidence remain immutable; retry creates new correlation. */
afterPeerPublication?.invoke()
val deadline = System.nanoTime() + timeout.toNanos()
val requestRaw =
awaitCorrelated(File(directory, "request-v1.json"), deadline) { candidate ->

View file

@ -54,7 +54,26 @@ internal suspend fun publishBundleAndRequest(
value.opt("version") is Int && value.getInt("version") == 1 && value.opt("enabled") == true
} catch (_: Exception) { false }
if (!enabled) return@withContext SyncRequestResult.Error("Invalid generation synchronization opt-in.")
return@withContext when (val result = SyncGenerationForegroundCoordinator(repository).run(canonicalExchangeDirectory())) {
val opened = exporter.openStorageSnapshot()
if (opened is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Error) {
return@withContext SyncRequestResult.Error("prepare:${opened.message}")
}
val snapshot = (opened as com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Ready).snapshot
return@withContext when (
val result = SyncGenerationForegroundCoordinator(repository).run(
canonicalExchangeDirectory(),
afterPeerPublication = {
when (val request = requestOutbox.requestSync(snapshot)) {
is SyncRequestResult.Requested -> Unit
SyncRequestResult.Unsupported ->
throw IllegalStateException("Generation synchronization is unsupported.")
is SyncRequestResult.Error -> throw IllegalStateException(request.message)
is SyncRequestResult.Completed ->
throw IllegalStateException("Unexpected completed request signal.")
}
},
)
) {
is ForegroundGenerationResult.Completed -> SyncRequestResult.Completed(result.runId)
is ForegroundGenerationResult.Error -> SyncRequestResult.Error(result.message)
}

View file

@ -103,11 +103,15 @@ class SyncDeploymentConversationTest {
repeat(conversations) {
val coordinator = SyncGenerationForegroundCoordinator(repository)
val executor = java.util.concurrent.Executors.newSingleThreadExecutor()
var requestSignals = 0
val result =
executor.submit<ForegroundGenerationResult> {
coordinator.run(transport, Duration.ofSeconds(30)) {
release?.let { awaitFile(File(it)) }
}
coordinator.run(
transport,
Duration.ofSeconds(30),
afterPeerPublication = { requestSignals += 1 },
afterPublication = { release?.let { awaitFile(File(it)) } },
)
}
val peer = JSONObject(awaitFile(File(transport, "android-peer-v1.json")).readText())
if (suppliedTransport == null) {
@ -138,6 +142,7 @@ class SyncDeploymentConversationTest {
"conversation ${it + 1}: $completed",
completed is ForegroundGenerationResult.Completed,
)
assertEquals("one fresh request signal per conversation", 1, requestSignals)
var workerOutput = ""
localWorker?.let { worker ->
workerOutput = worker.inputStream.bufferedReader().readText()