mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
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 <kaiserbh@proton.me>
This commit is contained in:
parent
39d5714a1b
commit
eb5b1610d0
@ -1,9 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.sync.service
|
package eu.kanade.tachiyomi.data.sync.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.PATCH
|
||||||
import eu.kanade.tachiyomi.network.POST
|
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.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
@ -22,6 +27,94 @@ class SyncYomiSyncService(
|
|||||||
syncPreferences: SyncPreferences,
|
syncPreferences: SyncPreferences,
|
||||||
private val notifier: SyncNotifier,
|
private val notifier: SyncNotifier,
|
||||||
) : SyncService(context, json, syncPreferences) {
|
) : 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<LockFile>(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? {
|
override suspend fun pullSyncData(): SyncData? {
|
||||||
val host = syncPreferences.syncHost().get()
|
val host = syncPreferences.syncHost().get()
|
||||||
val apiKey = syncPreferences.syncAPIKey().get()
|
val apiKey = syncPreferences.syncAPIKey().get()
|
||||||
|
@ -2,6 +2,7 @@ package tachiyomi.domain.sync
|
|||||||
|
|
||||||
import tachiyomi.core.preference.Preference
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class SyncPreferences(
|
class SyncPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
@ -22,4 +23,18 @@ class SyncPreferences(
|
|||||||
Preference.appStateKey("google_drive_refresh_token"),
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user