Prevent backups from being scheduled twice within the jitter window.

Fixes #13559.
This commit is contained in:
Nicholas Tinsley 2024-07-01 17:01:02 -04:00 committed by Cody Henthorne
parent b113eec940
commit aec0a9951a
4 changed files with 43 additions and 9 deletions

View file

@ -47,7 +47,7 @@ public class LocalBackupListener extends PersistentAlarmManagerListener {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
int hour = SignalStore.settings().getBackupHour(); int hour = SignalStore.settings().getBackupHour();
int minute = SignalStore.settings().getBackupMinute(); int minute = SignalStore.settings().getBackupMinute();
LocalDateTime next = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, hour, minute, BACKUP_JITTER_WINDOW_SECONDS); LocalDateTime next = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, hour, minute, BACKUP_JITTER_WINDOW_SECONDS, new Random());
long nextTime = JavaTimeExtensionsKt.toMillis(next); long nextTime = JavaTimeExtensionsKt.toMillis(next);

View file

@ -13,7 +13,8 @@ import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.toMillis import org.thoughtcrime.securesms.util.toMillis
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.Random import java.util.Random
import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
class MessageBackupListener : PersistentAlarmManagerListener() { class MessageBackupListener : PersistentAlarmManagerListener() {
override fun shouldScheduleExact(): Boolean { override fun shouldScheduleExact(): Boolean {
@ -33,8 +34,8 @@ class MessageBackupListener : PersistentAlarmManagerListener() {
} }
companion object { companion object {
private val BACKUP_JITTER_WINDOW_SECONDS = Math.toIntExact(TimeUnit.MINUTES.toSeconds(10)) private val BACKUP_JITTER_WINDOW_SECONDS = 10.minutes.inWholeSeconds.toInt()
private val BACKUP_MEDIA_SYNC_INTERVAL = TimeUnit.DAYS.toMillis(7) private val BACKUP_MEDIA_SYNC_INTERVAL = 7.days.inWholeMilliseconds
@JvmStatic @JvmStatic
fun schedule(context: Context?) { fun schedule(context: Context?) {
@ -44,22 +45,23 @@ class MessageBackupListener : PersistentAlarmManagerListener() {
} }
@JvmStatic @JvmStatic
fun getNextDailyBackupTimeFromNowWithJitter(now: LocalDateTime, hour: Int, minute: Int, maxJitterSeconds: Int): LocalDateTime { fun getNextDailyBackupTimeFromNowWithJitter(now: LocalDateTime, hour: Int, minute: Int, maxJitterSeconds: Int, randomSource: Random = Random()): LocalDateTime {
var next = now.withHour(hour).withMinute(minute).withSecond(0) var next = now.withHour(hour).withMinute(minute).withSecond(0)
if (!now.plusSeconds(maxJitterSeconds.toLong() / 2).isBefore(next)) { val endOfJitterWindowForNow = now.plusSeconds(maxJitterSeconds.toLong() / 2)
while (!endOfJitterWindowForNow.isBefore(next)) {
next = next.plusDays(1) next = next.plusDays(1)
} }
val jitter = Random().nextInt(BACKUP_JITTER_WINDOW_SECONDS) - BACKUP_JITTER_WINDOW_SECONDS / 2 val jitter = randomSource.nextInt(maxJitterSeconds) - maxJitterSeconds / 2
return next.plusSeconds(jitter.toLong()) return next.plusSeconds(jitter.toLong())
} }
fun setNextBackupTimeToIntervalFromNow(): Long { fun setNextBackupTimeToIntervalFromNow(maxJitterSeconds: Int = BACKUP_JITTER_WINDOW_SECONDS): Long {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val hour = SignalStore.settings.backupHour val hour = SignalStore.settings.backupHour
val minute = SignalStore.settings.backupMinute val minute = SignalStore.settings.backupMinute
var next = getNextDailyBackupTimeFromNowWithJitter(now, hour, minute, BACKUP_JITTER_WINDOW_SECONDS) var next = getNextDailyBackupTimeFromNowWithJitter(now, hour, minute, maxJitterSeconds)
next = when (SignalStore.backup.backupFrequency) { next = when (SignalStore.backup.backupFrequency) {
BackupFrequency.MANUAL -> next.plusDays(364) BackupFrequency.MANUAL -> next.plusDays(364)
BackupFrequency.MONTHLY -> next.plusDays(29) BackupFrequency.MONTHLY -> next.plusDays(29)

View file

@ -8,8 +8,12 @@ package org.thoughtcrime.securesms.service
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.thoughtcrime.securesms.BaseUnitTest import org.thoughtcrime.securesms.BaseUnitTest
import org.thoughtcrime.securesms.testutil.MockRandom
import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
class BackListenerTest : BaseUnitTest() { class BackListenerTest : BaseUnitTest() {
@ -44,4 +48,14 @@ class BackListenerTest : BaseUnitTest() {
val next = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, 3, 0, jitterWindowSeconds) val next = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, 3, 0, jitterWindowSeconds)
Assert.assertEquals(8, next.dayOfMonth) Assert.assertEquals(8, next.dayOfMonth)
} }
@Test
fun testBackupJitterWhenScheduledForMidnightButJitterMakesItRunJustBefore() {
val mockRandom = MockRandom(listOf(1.minutes.inWholeSeconds.toInt()))
val jitterWindowSeconds = 10.minutes.inWholeSeconds.toInt()
val now: LocalDateTime = LocalDateTime.of(2024, 6, 27, 23, 57, 0)
val next: LocalDateTime = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, 0, 0, jitterWindowSeconds, mockRandom)
Assert.assertTrue(Duration.between(now, next).toSeconds() > (1.days.inWholeSeconds - jitterWindowSeconds))
}
} }

View file

@ -0,0 +1,18 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testutil
import java.util.LinkedList
import java.util.Random
class MockRandom(initialInts: List<Int>) : Random() {
val nextInts = LinkedList(initialInts)
override fun nextInt(): Int {
return nextInts.remove()
}
}