mirror of
https://github.com/signalapp/Signal-iOS.git
synced 2025-12-05 01:10:41 +00:00
Use Cron for periodic Storage Service refresh
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]) {}
|
||||
|
||||
@@ -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") }
|
||||
|
||||
Reference in New Issue
Block a user