fix(android): ignore stale sync coordination objects

This commit is contained in:
fy59 2026-09-17 17:11:35 +02:00
parent d94ff495e3
commit bad79ed926
2 changed files with 43 additions and 7 deletions

View file

@ -36,6 +36,27 @@ internal class SyncGenerationForegroundCoordinator(private val repository: Train
return path
}
private fun awaitCorrelated(
path: File,
deadline: Long,
predicate: (JSONObject) -> Boolean,
): ByteArray {
while (true) {
if (path.isFile && path.length() <= 64 * 1024) {
try {
val raw = path.readBytes()
if (predicate(JSONObject(raw.toString(Charsets.UTF_8)))) return raw
} catch (_: Exception) {
// Publication replaces the file atomically, but an old or
// concurrently observed object is not evidence of failure.
}
}
if (System.nanoTime() >= deadline)
throw SyncGenerationException("timeout waiting for correlated ${path.name}")
Thread.sleep(50)
}
}
fun run(
directory: File,
timeout: Duration = Duration.ofMinutes(5),
@ -88,13 +109,20 @@ internal class SyncGenerationForegroundCoordinator(private val repository: Train
.toString(),
)
afterPublication?.invoke()
service.acceptAcknowledgement(
await(File(directory, "desktop-consumption-ack-v1.json"), deadline).readBytes()
)
val reference =
JSONObject(
await(File(directory, "desktop-generation-v1.json"), deadline).readText()
)
// CONTRACT: durable objects from earlier conversations remain in
// the exchange directory. Only this run's exact generation ACK
// can advance its lineage; stale bytes are retained and ignored.
val desktopAck =
awaitCorrelated(File(directory, "desktop-consumption-ack-v1.json"), deadline) {
it.optString("run_id") == captured.runId &&
it.optString("generation_id") == captured.generationId
}
service.acceptAcknowledgement(desktopAck)
val referenceRaw =
awaitCorrelated(File(directory, "desktop-generation-v1.json"), deadline) {
it.optString("run_id") == captured.runId
}
val reference = JSONObject(referenceRaw.toString(Charsets.UTF_8))
val acknowledgement =
service.consume(File(directory, reference.getString("relative_path")))
publish(File(directory, "android-consumption-ack-v1.json"), acknowledgement)

View file

@ -57,6 +57,14 @@ class SyncDeploymentConversationTest {
var localWorker: Process? = null
try {
transport.mkdirs()
// A previous interrupted exchange leaves durable coordination
// objects behind. The next run must wait for correlated replacements.
File(transport, "desktop-consumption-ack-v1.json").writeText(
"""{"run_id":"sy_11111111-1111-4111-8111-111111111111","generation_id":"gen_11111111-1111-4111-8111-111111111111"}"""
)
File(transport, "desktop-generation-v1.json").writeText(
"""{"run_id":"sy_11111111-1111-4111-8111-111111111111","generation_id":"gen_22222222-2222-4222-8222-222222222222"}"""
)
val exercise =
(repository.createExercise(
NewExerciseProfile(