I noticed a few times that the scheduler did not seem to match the values I had set. At first I thought I had simply remembered them incorrectly, but it kept happening, so I went through the scheduling code and found something that seems to explain it.
When Syncthing is already inside an active sync window, has connected to at least one device, and all folders are idle, state.idleStart contains the start of the current idle period.
However, updateIdleState() initializes the local value to -1L. If the tracker is already idle, it reaches the "Already idle" branch without assigning anything to that local value. The final state.copy() therefore replaces the existing timestamp with -1L.
The easiest way to trigger this should be:
- Enable the sync schedule and idle timer.
- Wait for an active window, at least one device to connect, and all folders to become idle.
- While the idle countdown is active, change the cycle, sync-window, or idle duration.
- Syncthing remains allowed to run past the expected idle deadline.
This happens because scheduleRefresh immediately recalculates the next alarm. With idleStart cleared, the calculation no longer uses the idle deadline and schedules the end of the full sync window instead.
I reproduced the state transition with a small deterministic Kotlin harness that mirrors the relevant branches and deadline calculation:
Cycle: 60 seconds
Sync window: 30 seconds
Idle time: 10 seconds
Idle started: 2 seconds
Refresh at: 5 seconds
The expected deadline is 12 seconds. With the current code, idleStart becomes -1 and the next deadline moves to 30 seconds.
A later busy-to-idle transition or the next sync window restores the timer, so the problem does not appear to be permanent. However, Syncthing can continue running until the end of the current sync window.
The smallest fix seems to be preserving the timestamp specifically in the "Already idle" branch:
} else {
Log.d(TAG, "Already idle")
+ idleStart = state.idleStart
}
Initializing idleStart from the existing state at the beginning of the function would be unsafe because the other branches intentionally need to clear it when scheduling is disabled, the tracker is outside the window, no device has connected, or a folder is busy.
This appears to have been introduced by #208 and affects BasicSync 3.6 and current master at 2ebe57adf824c1a3722a1ea23eab2b2a3c13a60f.
I noticed a few times that the scheduler did not seem to match the values I had set. At first I thought I had simply remembered them incorrectly, but it kept happening, so I went through the scheduling code and found something that seems to explain it.
When Syncthing is already inside an active sync window, has connected to at least one device, and all folders are idle,
state.idleStartcontains the start of the current idle period.However,
updateIdleState()initializes the local value to-1L. If the tracker is already idle, it reaches the"Already idle"branch without assigning anything to that local value. The finalstate.copy()therefore replaces the existing timestamp with-1L.The easiest way to trigger this should be:
This happens because
scheduleRefreshimmediately recalculates the next alarm. WithidleStartcleared, the calculation no longer uses the idle deadline and schedules the end of the full sync window instead.I reproduced the state transition with a small deterministic Kotlin harness that mirrors the relevant branches and deadline calculation:
The expected deadline is 12 seconds. With the current code,
idleStartbecomes-1and the next deadline moves to 30 seconds.A later busy-to-idle transition or the next sync window restores the timer, so the problem does not appear to be permanent. However, Syncthing can continue running until the end of the current sync window.
The smallest fix seems to be preserving the timestamp specifically in the
"Already idle"branch:} else { Log.d(TAG, "Already idle") + idleStart = state.idleStart }Initializing
idleStartfrom the existing state at the beginning of the function would be unsafe because the other branches intentionally need to clear it when scheduling is disabled, the tracker is outside the window, no device has connected, or a folder is busy.This appears to have been introduced by #208 and affects BasicSync 3.6 and current
masterat2ebe57adf824c1a3722a1ea23eab2b2a3c13a60f.