v1.0.32: fix manual sync loop via WorkManager tag monitor
Root cause: manual sync triggered from the UI had no cooldown set in
FileWatchService, so file writes during any manual sync fired FileObserver
→ debounce → another sync → loop.
Fix: startSyncMonitor() subscribes to getWorkInfosByTagFlow("sync_$pairId")
and watches ALL sync work for each pair — manual, catchup, onchange — via
the tag that SyncWorker.buildOneTimeRequest() always adds.
- When any sync is RUNNING or ENQUEUED: cooldown extended to now+120s
- When sync transitions from running to finished: 60s settle cooldown
- Monitor job stored in syncMonitorJobs map and cancelled in clearWatchers()
This means no matter what triggers a sync, FileObserver events from the
resulting file writes are always suppressed until the folder settles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,8 +44,10 @@ class FileWatchService : Service() {
|
|||||||
private val fileObservers = mutableMapOf<Long, MutableList<FileObserver>>()
|
private val fileObservers = mutableMapOf<Long, MutableList<FileObserver>>()
|
||||||
private val contentObservers = mutableMapOf<Long, ContentObserver>()
|
private val contentObservers = mutableMapOf<Long, ContentObserver>()
|
||||||
private val debounceJobs = mutableMapOf<Long, Job>()
|
private val debounceJobs = mutableMapOf<Long, Job>()
|
||||||
// After a watcher-triggered sync completes, suppress FileObserver events for this long
|
// Persistent monitors that watch WorkManager for ANY sync (manual, catchup, onchange)
|
||||||
// to stop the feedback loop: sync writes files → FileObserver fires → another sync → repeat.
|
// so the cooldown is set regardless of who triggered the sync.
|
||||||
|
private val syncMonitorJobs = mutableMapOf<Long, Job>()
|
||||||
|
// After a sync completes, suppress FileObserver events for this long.
|
||||||
private val syncCooldownUntil = mutableMapOf<Long, Long>()
|
private val syncCooldownUntil = mutableMapOf<Long, Long>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -154,9 +156,36 @@ class FileWatchService : Service() {
|
|||||||
syncCooldownUntil[pairId] = System.currentTimeMillis() + 15_000
|
syncCooldownUntil[pairId] = System.currentTimeMillis() + 15_000
|
||||||
watchDirRecursive(dir, pairId, wifiOnly, chargingOnly)
|
watchDirRecursive(dir, pairId, wifiOnly, chargingOnly)
|
||||||
Timber.d("FileWatchService: watching pair $pairId at $path (${fileObservers[pairId]?.size} dirs)")
|
Timber.d("FileWatchService: watching pair $pairId at $path (${fileObservers[pairId]?.size} dirs)")
|
||||||
|
startSyncMonitor(pairId)
|
||||||
scope.launch { catchupScan(pairId, dir, wifiOnly, chargingOnly) }
|
scope.launch { catchupScan(pairId, dir, wifiOnly, chargingOnly) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watches WorkManager for ANY sync tagged sync_$pairId (manual, catchup, onchange).
|
||||||
|
// Sets cooldown while running and for 60s after, so FileObserver events from our
|
||||||
|
// own file writes never trigger a re-sync regardless of what started the sync.
|
||||||
|
private fun startSyncMonitor(pairId: Long) {
|
||||||
|
syncMonitorJobs[pairId]?.cancel()
|
||||||
|
syncMonitorJobs[pairId] = scope.launch {
|
||||||
|
var wasSyncing = false
|
||||||
|
WorkManager.getInstance(applicationContext)
|
||||||
|
.getWorkInfosByTagFlow("sync_$pairId")
|
||||||
|
.collect { infos ->
|
||||||
|
val isSyncing = infos.any {
|
||||||
|
it.state == WorkInfo.State.RUNNING || it.state == WorkInfo.State.ENQUEUED
|
||||||
|
}
|
||||||
|
if (isSyncing) {
|
||||||
|
Timber.d("FileWatchService: sync active for pair $pairId — cooldown extended")
|
||||||
|
syncCooldownUntil[pairId] = System.currentTimeMillis() + 120_000
|
||||||
|
wasSyncing = true
|
||||||
|
} else if (wasSyncing) {
|
||||||
|
Timber.d("FileWatchService: sync finished for pair $pairId — 60s settle cooldown")
|
||||||
|
syncCooldownUntil[pairId] = System.currentTimeMillis() + 60_000
|
||||||
|
wasSyncing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun watchDirRecursive(dir: File, pairId: Long, wifiOnly: Boolean, chargingOnly: Boolean) {
|
private fun watchDirRecursive(dir: File, pairId: Long, wifiOnly: Boolean, chargingOnly: Boolean) {
|
||||||
if (!dir.isDirectory) return
|
if (!dir.isDirectory) return
|
||||||
val mask = FileObserver.CREATE or FileObserver.DELETE or FileObserver.MODIFY or
|
val mask = FileObserver.CREATE or FileObserver.DELETE or FileObserver.MODIFY or
|
||||||
@@ -297,6 +326,8 @@ class FileWatchService : Service() {
|
|||||||
contentObservers.clear()
|
contentObservers.clear()
|
||||||
debounceJobs.values.forEach { it.cancel() }
|
debounceJobs.values.forEach { it.cancel() }
|
||||||
debounceJobs.clear()
|
debounceJobs.clear()
|
||||||
|
syncMonitorJobs.values.forEach { it.cancel() }
|
||||||
|
syncMonitorJobs.clear()
|
||||||
syncCooldownUntil.clear()
|
syncCooldownUntil.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
VERSION_NAME=1.0.31
|
VERSION_NAME=1.0.32
|
||||||
VERSION_CODE=32
|
VERSION_CODE=33
|
||||||
|
|||||||
Reference in New Issue
Block a user