Use Cron for periodic Storage Service refresh

This commit is contained in:
Max Radermacher
2025-11-25 17:12:55 -06:00
committed by GitHub
parent 45801c76e6
commit 5ed1b58aaf
8 changed files with 87 additions and 12 deletions

View File

@@ -779,6 +779,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
},
)
let storageServiceManager = SSKEnvironment.shared.storageServiceManagerRef
storageServiceManager.registerForCron(cron)
// Note that this does much more than set a flag; it will also run all deferred blocks.
appReadiness.setAppIsReadyUIStillPending()

View File

@@ -55,6 +55,8 @@ public class Cron {
private let metadataStore: NewKeyValueStore
private let jobs: AtomicValue<[(CronContext) async -> Void]>
public static let jitterFactor: Double = 20
/// Unique keys that identify Cron jobs.
///
/// All state related to these keys is cleared when the app's version number
@@ -73,6 +75,7 @@ public class Cron {
case fetchSenderCertificates
case fetchStaleGroup
case fetchStaleProfiles
case fetchStorageService
case fetchSubscriptionConfig
case refreshBackup
case updateAttributes
@@ -168,7 +171,7 @@ public class Cron {
// completed so that we wait for `approximateInterval` before retrying.
Logger.info("job \(uniqueKey) reached terminal result: \(result)")
await db.awaitableWrite { tx in
store.setMostRecentDate(Date(), jitter: approximateInterval / 20, tx: tx)
store.setMostRecentDate(Date(), jitter: approximateInterval / Self.jitterFactor, tx: tx)
}
}
},

View File

@@ -16,6 +16,9 @@ public protocol StorageServiceManager {
/// Called during app launch, registration, and change number.
func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiers)
/// Sets up Cron jobs.
func registerForCron(_ cron: Cron)
/// The version of the latest known Storage Service manifest.
func currentManifestVersion(tx: DBReadTransaction) -> UInt64
/// Whether the latest-known Storage Service manifest contains a `recordIkm`.
@@ -162,10 +165,6 @@ public class StorageServiceManagerImpl: NSObject, StorageServiceManager {
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else { return }
// Schedule a restore. This will do nothing unless we've never
// registered a manifest before.
self.restoreOrCreateManifestIfNecessary(authedDevice: .implicit, masterKeySource: .implicit)
// If we have any pending changes since we last launch, back them up now.
self.backupPendingChanges(authedDevice: .implicit)
}
@@ -176,6 +175,30 @@ public class StorageServiceManagerImpl: NSObject, StorageServiceManager {
}
}
private static let restoreManifestCronKey: Cron.UniqueKey = .fetchStorageService
private static let restoreManifestCronInterval: TimeInterval = .day
public func registerForCron(_ cron: Cron) {
cron.schedulePeriodically(
uniqueKey: Self.restoreManifestCronKey,
approximateInterval: Self.restoreManifestCronInterval,
mustBeRegistered: true,
mustBeConnected: true,
operation: {
try await self._restoreOrCreateManifestIfNecessary(
authedDevice: .implicit,
masterKeySource: .implicit,
isRunningViaCron: true,
).awaitableWithUncooperativeCancellationHandling()
},
)
}
fileprivate static func updateRestoreManifestCronDate(tx: DBWriteTransaction) {
CronStore(uniqueKey: restoreManifestCronKey)
.setMostRecentDate(Date(), jitter: restoreManifestCronInterval / Cron.jitterFactor, tx: tx)
}
@objc
private func willResignActive() {
// If we have any pending changes, start a back up immediately
@@ -243,6 +266,7 @@ public class StorageServiceManagerImpl: NSObject, StorageServiceManager {
struct PendingRestore {
var authedDevice: AuthedDevice
var masterKeySource: StorageService.MasterKeySource
var isRunningViaCron: Bool
var futures: [Future<Void>]
}
var pendingRestore: PendingRestore?
@@ -357,7 +381,7 @@ public class StorageServiceManagerImpl: NSObject, StorageServiceManager {
let restoreOperation = buildOperation(
managerState: managerState,
mode: .restoreOrCreate,
mode: .restoreOrCreate(isRunningViaCron: pendingRestore.isRunningViaCron),
authedDevice: pendingRestore.authedDevice,
masterKeySource: pendingRestore.masterKeySource
)
@@ -521,18 +545,32 @@ public class StorageServiceManagerImpl: NSObject, StorageServiceManager {
@discardableResult
public func restoreOrCreateManifestIfNecessary(
authedDevice: AuthedDevice,
masterKeySource: StorageService.MasterKeySource
masterKeySource: StorageService.MasterKeySource,
) -> Promise<Void> {
return _restoreOrCreateManifestIfNecessary(
authedDevice: authedDevice,
masterKeySource: masterKeySource,
isRunningViaCron: false,
)
}
private func _restoreOrCreateManifestIfNecessary(
authedDevice: AuthedDevice,
masterKeySource: StorageService.MasterKeySource,
isRunningViaCron: Bool,
) -> Promise<Void> {
let (promise, future) = Promise<Void>.pending()
updateManagerState { managerState in
var pendingRestore = managerState.pendingRestore ?? .init(
authedDevice: .implicit,
masterKeySource: .implicit,
isRunningViaCron: false,
futures: []
)
pendingRestore.futures.append(future)
pendingRestore.authedDevice = authedDevice.orIfImplicitUse(pendingRestore.authedDevice)
pendingRestore.masterKeySource = masterKeySource.orIfImplicitUse(pendingRestore.masterKeySource)
pendingRestore.isRunningViaCron = isRunningViaCron || pendingRestore.isRunningViaCron
managerState.pendingRestore = pendingRestore
}
return promise
@@ -680,7 +718,7 @@ class StorageServiceOperation {
fileprivate enum Mode {
case rotateManifest(mode: StorageServiceManager.ManifestRotationMode)
case backup
case restoreOrCreate
case restoreOrCreate(isRunningViaCron: Bool)
case cleanUpUnknownData
}
private let mode: Mode
@@ -715,7 +753,9 @@ class StorageServiceOperation {
// Called every retry, this is where the bulk of the operation's work should go.
private func _run() async throws {
let (currentStateIfRotatingManifest, masterKey) = SSKEnvironment.shared.databaseStorageRef.read { tx in
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
let (currentStateIfRotatingManifest, masterKey) = databaseStorage.read { tx in
let state: State?
switch mode {
case .rotateManifest:
@@ -742,7 +782,7 @@ class StorageServiceOperation {
{
// This is a linked device, and keys are missing. There's nothing that can be done
// until we receive new keys, so send a key sync message and return early.
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
await databaseStorage.awaitableWrite { tx in
SSKEnvironment.shared.syncManagerRef.sendKeysSyncRequestMessage(transaction: tx)
}
} else {
@@ -771,8 +811,15 @@ class StorageServiceOperation {
}
case .backup:
try await backupPendingChanges()
case .restoreOrCreate:
case .restoreOrCreate(let isRunningViaCron):
try await restoreOrCreateManifestIfNecessary()
// If we weren't triggered via Cron, we can report the result to Cron to
// avoid fetching when unnecessary.
if !isRunningViaCron {
await databaseStorage.awaitableWrite { tx in
StorageServiceManagerImpl.updateRestoreManifestCronDate(tx: tx)
}
}
case .cleanUpUnknownData:
await cleanUpUnknownData()
}

View File

@@ -11,6 +11,7 @@ public import SignalRingRTC
public class FakeStorageServiceManager: StorageServiceManager {
public func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiers) {}
public func registerForCron(_ cron: Cron) {}
public func currentManifestVersion(tx: DBReadTransaction) -> UInt64 { 0 }
public func currentManifestHasRecordIkm(tx: DBReadTransaction) -> Bool { false }

View File

@@ -5,6 +5,7 @@
import Foundation
import ObjectiveC
public import SwiftProtobuf
extension Error {
public var hasIsRetryable: Bool {
@@ -76,6 +77,14 @@ extension CancellationError: IsRetryableProvider {
public var isRetryableProvider: Bool { false }
}
extension SwiftProtobuf.BinaryDecodingError: IsRetryableProvider {
public var isRetryableProvider: Bool { false }
}
extension SwiftProtobuf.BinaryEncodingError: IsRetryableProvider {
public var isRetryableProvider: Bool { false }
}
// MARK: -
// NOTE: We typically prefer to use a more specific error.

View File

@@ -6,7 +6,7 @@
import LibSignalClient
public struct StorageService {
public enum StorageError: Error {
public enum StorageError: Error, IsRetryableProvider {
/// We found a manifest with a conflicting version number.
case conflictingManifest(StorageServiceProtoManifestRecord)
@@ -15,6 +15,16 @@ public struct StorageService {
case itemDecryptionFailed(identifier: StorageIdentifier)
case itemProtoDeserializationFailed(identifier: StorageIdentifier)
public var isRetryableProvider: Bool {
switch self {
case .conflictingManifest: true
case .manifestDecryptionFailed: false
case .manifestProtoDeserializationFailed: false
case .itemDecryptionFailed: false
case .itemProtoDeserializationFailed: false
}
}
}
public enum MasterKeySource: Equatable {

View File

@@ -11,6 +11,7 @@ import XCTest
private class MockStorageServiceManager: StorageServiceManager {
func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiers) {}
func registerForCron(_ cron: Cron) {}
func currentManifestVersion(tx: DBReadTransaction) -> UInt64 { 0 }
func currentManifestHasRecordIkm(tx: DBReadTransaction) -> Bool { false }
func recordPendingUpdates(updatedRecipientUniqueIds: [RecipientUniqueId]) {}

View File

@@ -637,6 +637,7 @@ private class MockStorageServiceManager: StorageServiceManager {
}
func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiers) { owsFail("Not implemented!") }
func registerForCron(_ cron: Cron) { owsFail("Not implemented.") }
func currentManifestVersion(tx: DBReadTransaction) -> UInt64 { owsFail("Not implemented") }
func currentManifestHasRecordIkm(tx: DBReadTransaction) -> Bool { owsFail("Not implemented") }
func waitForPendingRestores() async throws { owsFail("Not implemented") }