fix(sync): carry ack recovery over transports

This commit is contained in:
fy59 2026-09-23 16:40:20 +02:00
parent 0750a47cef
commit 41d0f1ff96
9 changed files with 67 additions and 9 deletions

View file

@ -41,6 +41,17 @@ private const val MAX_BT_FILE_BYTES = 64L * 1024L * 1024L
private const val MAX_BT_FILES = 128
private const val AUTO_REQUEST_COOLDOWN_MS = 5L * 60L * 1000L
internal val BLUETOOTH_ANDROID_PULL_COORDINATION =
listOf(
"android-peer-v1.json",
FULL_GENERATION_REQUEST_NAME,
LEGACY_SYNC_REQUEST_NAME,
"android-generation-v1.json",
"android-consumption-ack-v1.json",
"android-archive-acknowledgements-v1.json",
"android-generation-error-v1.json",
)
internal data class BondedBluetoothDesktop(
val name: String,
val address: String,
@ -248,14 +259,7 @@ private class BluetoothExchangeArchive(
val totals = longArrayOf(0L, 0L)
try {
ZipOutputStream(BufferedOutputStream(FileOutputStream(output))).use { zip ->
listOf(
"android-peer-v1.json",
FULL_GENERATION_REQUEST_NAME,
LEGACY_SYNC_REQUEST_NAME,
"android-generation-v1.json",
"android-consumption-ack-v1.json",
"android-generation-error-v1.json",
).forEach { name ->
BLUETOOTH_ANDROID_PULL_COORDINATION.forEach { name ->
addFile(zip, File(exchangeDirectory, name), name, seen, totals)
}
val reference = File(exchangeDirectory, "android-generation-v1.json")

View file

@ -195,7 +195,8 @@ internal class DriveDocumentTransport(private val context: Context, treeUri: Uri
localRoot.mkdirs()
val coordination = listOf(
"android-peer-v1.json", "android-generation-v1.json", "android-consumption-ack-v1.json",
"android-generation-error-v1.json", "request-v1.json",
"android-archive-acknowledgements-v1.json", "android-generation-error-v1.json",
"request-v1.json",
"desktop-archive-acknowledgements-v1.json", "desktop-consumption-ack-v1.json",
"desktop-generation-v1.json",
)
@ -261,6 +262,13 @@ internal class DriveDocumentTransport(private val context: Context, treeUri: Uri
upload("android-generation-v1.json", reference)
}
fun pushRecoveryAcknowledgements(localRoot: File) {
upload(
"android-archive-acknowledgements-v1.json",
File(localRoot, "android-archive-acknowledgements-v1.json"),
)
}
fun pushAcknowledgement(localRoot: File) {
upload(
"android-consumption-ack-v1.json",
@ -318,6 +326,10 @@ class DriveSyncWorker(context: Context, parameters: WorkerParameters) :
local,
Duration.ofMinutes(10),
afterPeerPublication = { transport.pushPeer(local) },
afterRecoveryAcknowledgements = {
stage = "recovery-acknowledgement"
transport.pushRecoveryAcknowledgements(local)
},
afterPublication = {
stage = "generation-publication"
transport.pushGeneration(local)

View file

@ -404,6 +404,7 @@ internal class SyncGenerationCoordinator(
directory: File,
timeout: Duration = Duration.ofMinutes(5),
afterPeerPublication: (() -> Unit)? = null,
afterRecoveryAcknowledgements: (() -> Unit)? = null,
afterPublication: (() -> Unit)? = null,
pollTransport: (() -> Unit)? = null,
afterAcknowledgement: (() -> Unit)? = null,
@ -509,6 +510,7 @@ internal class SyncGenerationCoordinator(
)
publish(recoveryPath, recoveryEnvelope)
visibility.confirm(listOf(recoveryPath))
afterRecoveryAcknowledgements?.invoke()
phase(
runId,
"android_archive_ack_published",

View file

@ -0,0 +1,15 @@
package com.labfytools.trainlog.data
import org.junit.Assert.assertTrue
import org.junit.Test
class BluetoothSyncTransportContractTest {
@Test
fun androidPullArchiveCarriesGenerationAckRecoveryEvidence() {
assertTrue(
BLUETOOTH_ANDROID_PULL_COORDINATION.contains(
"android-archive-acknowledgements-v1.json"
)
)
}
}

View file

@ -116,6 +116,8 @@ class DriveTransportTest(unittest.TestCase):
source = Path(raw) / "source"
source.mkdir()
self.generation(source)
recovery = source / "android-archive-acknowledgements-v1.json"
recovery.write_text('{"format":"trainlog-sync-archive-acknowledgements","version":1}')
remote = MemoryDrive()
drive.push(remote, source)
destination = Path(raw) / "destination"
@ -124,6 +126,10 @@ class DriveTransportTest(unittest.TestCase):
(destination / "desktop-objects/generations/gen_one/history.json").read_bytes(),
b"{}\n",
)
self.assertEqual(
(destination / "android-archive-acknowledgements-v1.json").read_bytes(),
recovery.read_bytes(),
)
remote.downloads.clear()
drive.pull(remote, destination)

View file

@ -20,6 +20,7 @@ COORDINATION = (
"android-peer-v1.json",
"android-generation-v1.json",
"android-consumption-ack-v1.json",
"android-archive-acknowledgements-v1.json",
"android-generation-error-v1.json",
"request-v1.json",
"desktop-archive-acknowledgements-v1.json",

View file

@ -242,6 +242,7 @@ def mirror_current_exchange(transport_root: Path, remote: str, timeout: int) ->
"android-peer-v1.json",
"android-generation-v1.json",
"android-consumption-ack-v1.json",
"android-archive-acknowledgements-v1.json",
"android-generation-error-v1.json",
"request-v1.json",
"desktop-archive-acknowledgements-v1.json",

View file

@ -622,6 +622,14 @@ TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
status = pull_named_file(
io, &selection, selection.root, "android-consumption-ack-v1.json", local, false);
}
if (status == TRAINLOG_STATUS_OK) {
status = pull_named_file(io,
&selection,
selection.root,
"android-archive-acknowledgements-v1.json",
local,
false);
}
if (status == TRAINLOG_STATUS_OK) {
status = pull_named_file(
io, &selection, selection.root, "android-generation-error-v1.json", local, false);

View file

@ -250,6 +250,12 @@ static int test_pull_and_manifest_last(void) {
"trainlog-sync-full-generation-request-v1.json",
"{\"format\":\"trainlog-sync-request\",\"version\":1,\"request_id\":\"sr_11111111-"
"1111-4111-8111-111111111111\",\"requested_at\":\"2026-09-20T12:00:00Z\"}");
add(15,
2,
false,
"android-archive-acknowledgements-v1.json",
"{\"format\":\"trainlog-sync-archive-acknowledgements\",\"version\":1,"
"\"acknowledgements\":[]}");
add(5, 2, true, "android-objects", NULL);
add(6, 5, true, "generations", NULL);
add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
@ -272,6 +278,9 @@ static int test_pull_and_manifest_last(void) {
CHECK(access(path, R_OK) == 0);
CHECK(trainlog_test_join_path(path, sizeof(path), root, "trainlog-sync-request-v1.json"));
CHECK(access(path, R_OK) == 0);
CHECK(trainlog_test_join_path(
path, sizeof(path), root, "android-archive-acknowledgements-v1.json"));
CHECK(access(path, R_OK) == 0);
CHECK(trainlog_test_join_path(path, sizeof(path), root, "out"));
CHECK(mkdir(path, 0700) == 0);
char gen[512];