From eb5b1610d0a7637c027b95531a36735353b72459 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Sun, 7 Jan 2024 09:50:30 +1100 Subject: [PATCH] feat: Enhanced SyncYomi with Robust Locking Mechanism - Introduced `LockFile` data class to manage sync status and ownership. - Enumerated `SyncStatus` to clearly indicate the stage of synchronization: Pending, Syncing, or Success. - Implemented a new `beforeSync` method utilizing `OkHttpClient` for secure HTTP calls to manage the lock file. - Added JSON serialization for lock file management and status updates. - Implemented exponential backoff strategy to handle concurrent sync attempts and reduce race conditions. - Added comprehensive logging for debugging and monitoring sync processes. This update enhances the reliability and concurrency handling of SyncYomi's synchronization process, ensuring users experience seamless and efficient data syncing in a self-hosted environment. Signed-off-by: KaiserBh --- .../data/sync/service/SyncYomiSyncService.kt | 93 +++++++++++++++++++ .../tachiyomi/domain/sync/SyncPreferences.kt | 15 +++ 2 files changed, 108 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt index 936da49e3..a1dfc9d88 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -1,9 +1,14 @@ package eu.kanade.tachiyomi.data.sync.service import android.content.Context +import com.google.gson.JsonObject import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.PATCH import eu.kanade.tachiyomi.network.POST +import kotlinx.coroutines.delay +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import logcat.LogPriority @@ -22,6 +27,94 @@ class SyncYomiSyncService( syncPreferences: SyncPreferences, private val notifier: SyncNotifier, ) : SyncService(context, json, syncPreferences) { + + @Serializable + enum class SyncStatus { + @SerialName("pending") + Pending, + @SerialName("syncing") + Syncing, + @SerialName("success") + Success + } + + @Serializable + data class LockFile( + @SerialName("id") + val id: Int?, + @SerialName("user_api_key") + val userApiKey: String?, + @SerialName("acquired_by") + val acquiredBy: String?, + @SerialName("last_synced") + val lastSynced: String?, + @SerialName("status") + val status: SyncStatus, + @SerialName("acquired_at") + val acquiredAt: String?, + @SerialName("expires_at") + val expiresAt: String? + ) + + override suspend fun beforeSync() { + val host = syncPreferences.syncHost().get() + val apiKey = syncPreferences.syncAPIKey().get() + val lockFileApi = "$host/api/sync/lock" + val deviceId = syncPreferences.uniqueDeviceID() + val client = OkHttpClient() + val headers = Headers.Builder().add("X-API-Token", apiKey).build() + val createLockfileJson = JsonObject().apply { + addProperty("acquired_by", deviceId) + } + val patchJson = JsonObject().apply { + addProperty("user_api_key", apiKey) + addProperty("acquired_by", deviceId) + } + + val lockFileRequest = GET( + url = lockFileApi, + headers = headers, + ) + + val lockFileCreate = POST( + url = lockFileApi, + headers = headers, + body = createLockfileJson.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), + ) + + val lockFileUpdate = PATCH( + url = lockFileApi, + headers = headers, + body = patchJson.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), + ) + + // create lock file first + client.newCall(lockFileCreate).execute() + // update lock file acquired_by + client.newCall(lockFileUpdate).execute() + + val json = Json { ignoreUnknownKeys = true } + var backoff = 2000L // Start with 2 seconds + val maxBackoff = 32000L // Maximum backoff time e.g., 32 seconds + var lockFile: LockFile + do { + val response = client.newCall(lockFileRequest).execute() + val responseBody = response.body.string() + lockFile = json.decodeFromString(responseBody) + logcat(LogPriority.DEBUG) { "SyncYomi lock file status: ${lockFile.status}" } + + if (lockFile.status != SyncStatus.Success) { + logcat(LogPriority.DEBUG) { "Lock file not ready, retrying in $backoff ms..." } + delay(backoff) + backoff = (backoff * 2).coerceAtMost(maxBackoff) + } + + } while (lockFile.status != SyncStatus.Success) + + // update lock file acquired_by + client.newCall(lockFileUpdate).execute() + } + override suspend fun pullSyncData(): SyncData? { val host = syncPreferences.syncHost().get() val apiKey = syncPreferences.syncAPIKey().get() diff --git a/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt b/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt index 5f2b6ab98..b9f8d11e2 100644 --- a/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt @@ -2,6 +2,7 @@ package tachiyomi.domain.sync import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore +import java.util.UUID class SyncPreferences( private val preferenceStore: PreferenceStore, @@ -22,4 +23,18 @@ class SyncPreferences( Preference.appStateKey("google_drive_refresh_token"), "", ) + + fun uniqueDeviceID(): String { + val uniqueIDPreference = preferenceStore.getString("unique_device_id", "") + + // Retrieve the current value of the preference + var uniqueID = uniqueIDPreference.get() + if (uniqueID.isBlank()) { + uniqueID = UUID.randomUUID().toString() + uniqueIDPreference.set(uniqueID) + } + + return uniqueID + } + }