fix(sync): carry ack recovery over transports
This commit is contained in:
parent
0750a47cef
commit
41d0f1ff96
9 changed files with 67 additions and 9 deletions
|
|
@ -41,6 +41,17 @@ private const val MAX_BT_FILE_BYTES = 64L * 1024L * 1024L
|
||||||
private const val MAX_BT_FILES = 128
|
private const val MAX_BT_FILES = 128
|
||||||
private const val AUTO_REQUEST_COOLDOWN_MS = 5L * 60L * 1000L
|
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(
|
internal data class BondedBluetoothDesktop(
|
||||||
val name: String,
|
val name: String,
|
||||||
val address: String,
|
val address: String,
|
||||||
|
|
@ -248,14 +259,7 @@ private class BluetoothExchangeArchive(
|
||||||
val totals = longArrayOf(0L, 0L)
|
val totals = longArrayOf(0L, 0L)
|
||||||
try {
|
try {
|
||||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(output))).use { zip ->
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(output))).use { zip ->
|
||||||
listOf(
|
BLUETOOTH_ANDROID_PULL_COORDINATION.forEach { name ->
|
||||||
"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 ->
|
|
||||||
addFile(zip, File(exchangeDirectory, name), name, seen, totals)
|
addFile(zip, File(exchangeDirectory, name), name, seen, totals)
|
||||||
}
|
}
|
||||||
val reference = File(exchangeDirectory, "android-generation-v1.json")
|
val reference = File(exchangeDirectory, "android-generation-v1.json")
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,8 @@ internal class DriveDocumentTransport(private val context: Context, treeUri: Uri
|
||||||
localRoot.mkdirs()
|
localRoot.mkdirs()
|
||||||
val coordination = listOf(
|
val coordination = listOf(
|
||||||
"android-peer-v1.json", "android-generation-v1.json", "android-consumption-ack-v1.json",
|
"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-archive-acknowledgements-v1.json", "desktop-consumption-ack-v1.json",
|
||||||
"desktop-generation-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)
|
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) {
|
fun pushAcknowledgement(localRoot: File) {
|
||||||
upload(
|
upload(
|
||||||
"android-consumption-ack-v1.json",
|
"android-consumption-ack-v1.json",
|
||||||
|
|
@ -318,6 +326,10 @@ class DriveSyncWorker(context: Context, parameters: WorkerParameters) :
|
||||||
local,
|
local,
|
||||||
Duration.ofMinutes(10),
|
Duration.ofMinutes(10),
|
||||||
afterPeerPublication = { transport.pushPeer(local) },
|
afterPeerPublication = { transport.pushPeer(local) },
|
||||||
|
afterRecoveryAcknowledgements = {
|
||||||
|
stage = "recovery-acknowledgement"
|
||||||
|
transport.pushRecoveryAcknowledgements(local)
|
||||||
|
},
|
||||||
afterPublication = {
|
afterPublication = {
|
||||||
stage = "generation-publication"
|
stage = "generation-publication"
|
||||||
transport.pushGeneration(local)
|
transport.pushGeneration(local)
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,7 @@ internal class SyncGenerationCoordinator(
|
||||||
directory: File,
|
directory: File,
|
||||||
timeout: Duration = Duration.ofMinutes(5),
|
timeout: Duration = Duration.ofMinutes(5),
|
||||||
afterPeerPublication: (() -> Unit)? = null,
|
afterPeerPublication: (() -> Unit)? = null,
|
||||||
|
afterRecoveryAcknowledgements: (() -> Unit)? = null,
|
||||||
afterPublication: (() -> Unit)? = null,
|
afterPublication: (() -> Unit)? = null,
|
||||||
pollTransport: (() -> Unit)? = null,
|
pollTransport: (() -> Unit)? = null,
|
||||||
afterAcknowledgement: (() -> Unit)? = null,
|
afterAcknowledgement: (() -> Unit)? = null,
|
||||||
|
|
@ -509,6 +510,7 @@ internal class SyncGenerationCoordinator(
|
||||||
)
|
)
|
||||||
publish(recoveryPath, recoveryEnvelope)
|
publish(recoveryPath, recoveryEnvelope)
|
||||||
visibility.confirm(listOf(recoveryPath))
|
visibility.confirm(listOf(recoveryPath))
|
||||||
|
afterRecoveryAcknowledgements?.invoke()
|
||||||
phase(
|
phase(
|
||||||
runId,
|
runId,
|
||||||
"android_archive_ack_published",
|
"android_archive_ack_published",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -116,6 +116,8 @@ class DriveTransportTest(unittest.TestCase):
|
||||||
source = Path(raw) / "source"
|
source = Path(raw) / "source"
|
||||||
source.mkdir()
|
source.mkdir()
|
||||||
self.generation(source)
|
self.generation(source)
|
||||||
|
recovery = source / "android-archive-acknowledgements-v1.json"
|
||||||
|
recovery.write_text('{"format":"trainlog-sync-archive-acknowledgements","version":1}')
|
||||||
remote = MemoryDrive()
|
remote = MemoryDrive()
|
||||||
drive.push(remote, source)
|
drive.push(remote, source)
|
||||||
destination = Path(raw) / "destination"
|
destination = Path(raw) / "destination"
|
||||||
|
|
@ -124,6 +126,10 @@ class DriveTransportTest(unittest.TestCase):
|
||||||
(destination / "desktop-objects/generations/gen_one/history.json").read_bytes(),
|
(destination / "desktop-objects/generations/gen_one/history.json").read_bytes(),
|
||||||
b"{}\n",
|
b"{}\n",
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(destination / "android-archive-acknowledgements-v1.json").read_bytes(),
|
||||||
|
recovery.read_bytes(),
|
||||||
|
)
|
||||||
|
|
||||||
remote.downloads.clear()
|
remote.downloads.clear()
|
||||||
drive.pull(remote, destination)
|
drive.pull(remote, destination)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ COORDINATION = (
|
||||||
"android-peer-v1.json",
|
"android-peer-v1.json",
|
||||||
"android-generation-v1.json",
|
"android-generation-v1.json",
|
||||||
"android-consumption-ack-v1.json",
|
"android-consumption-ack-v1.json",
|
||||||
|
"android-archive-acknowledgements-v1.json",
|
||||||
"android-generation-error-v1.json",
|
"android-generation-error-v1.json",
|
||||||
"request-v1.json",
|
"request-v1.json",
|
||||||
"desktop-archive-acknowledgements-v1.json",
|
"desktop-archive-acknowledgements-v1.json",
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ def mirror_current_exchange(transport_root: Path, remote: str, timeout: int) ->
|
||||||
"android-peer-v1.json",
|
"android-peer-v1.json",
|
||||||
"android-generation-v1.json",
|
"android-generation-v1.json",
|
||||||
"android-consumption-ack-v1.json",
|
"android-consumption-ack-v1.json",
|
||||||
|
"android-archive-acknowledgements-v1.json",
|
||||||
"android-generation-error-v1.json",
|
"android-generation-error-v1.json",
|
||||||
"request-v1.json",
|
"request-v1.json",
|
||||||
"desktop-archive-acknowledgements-v1.json",
|
"desktop-archive-acknowledgements-v1.json",
|
||||||
|
|
|
||||||
|
|
@ -622,6 +622,14 @@ TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
|
||||||
status = pull_named_file(
|
status = pull_named_file(
|
||||||
io, &selection, selection.root, "android-consumption-ack-v1.json", local, false);
|
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) {
|
if (status == TRAINLOG_STATUS_OK) {
|
||||||
status = pull_named_file(
|
status = pull_named_file(
|
||||||
io, &selection, selection.root, "android-generation-error-v1.json", local, false);
|
io, &selection, selection.root, "android-generation-error-v1.json", local, false);
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,12 @@ static int test_pull_and_manifest_last(void) {
|
||||||
"trainlog-sync-full-generation-request-v1.json",
|
"trainlog-sync-full-generation-request-v1.json",
|
||||||
"{\"format\":\"trainlog-sync-request\",\"version\":1,\"request_id\":\"sr_11111111-"
|
"{\"format\":\"trainlog-sync-request\",\"version\":1,\"request_id\":\"sr_11111111-"
|
||||||
"1111-4111-8111-111111111111\",\"requested_at\":\"2026-09-20T12:00:00Z\"}");
|
"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(5, 2, true, "android-objects", NULL);
|
||||||
add(6, 5, true, "generations", NULL);
|
add(6, 5, true, "generations", NULL);
|
||||||
add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", 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(access(path, R_OK) == 0);
|
||||||
CHECK(trainlog_test_join_path(path, sizeof(path), root, "trainlog-sync-request-v1.json"));
|
CHECK(trainlog_test_join_path(path, sizeof(path), root, "trainlog-sync-request-v1.json"));
|
||||||
CHECK(access(path, R_OK) == 0);
|
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(trainlog_test_join_path(path, sizeof(path), root, "out"));
|
||||||
CHECK(mkdir(path, 0700) == 0);
|
CHECK(mkdir(path, 0700) == 0);
|
||||||
char gen[512];
|
char gen[512];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue