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
|
||||
|
||||
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<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? {
|
||||
val host = syncPreferences.syncHost().get()
|
||||
val apiKey = syncPreferences.syncAPIKey().get()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user