Skip to content

Commit

Permalink
[#982] Background syncing
Browse files Browse the repository at this point in the history
- Background processing task implemented
- debug version of scheduling for testing purposes (the final one is commented out for now)
- Doc for the Background Synchronization added

[#982] Background syncing

- changelog update
- doc 2nd pass fixes

[#982] Background syncing

- refactor
- custom wifi check

[#982] Background syncing

- code cleanup + refactor
  • Loading branch information
LukasKorba committed Jan 2, 2024
1 parent 7920501 commit 19d7dce
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 47 deletions.
70 changes: 70 additions & 0 deletions BACKGROUND_SYNCING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Background Syncing

## Sources
We encourange you to watch WWDC videos:
- [Advances in App Background Execution]: https://developer.apple.com/videos/play/wwdc2019/707
- [Background execution demystified]: https://developer.apple.com/videos/play/wwdc2020/10063

## Implementation details
There are 4 different APIs and types of background tasks. Each one is specific and can be used for different scenarios. Synchronization of the blockchain data is time and memory consuming operation. Therefore the `BGProcessingTask` has been used. This type of task is designed to run for a longer time when certain conditions are met (watch Background execution demystified).

### Steps to make it work
1. Add a capability of `background modes` in the settings of the xcode project.
2. Turn the `Background Processing` mode on in the new capability.
3. Add `Permitted background task scheduler identifiers` to the info.plist.
4. Create the ID for the background task in the newly created array.
5. Register the BGTask in `application.didFinishLaunchingWithOptions`
```Swift
BGTaskScheduler.shared.register(
forTaskWithIdentifier: <ID>,
using: DispatchQueue.main
) { task in
// see the next steps
}
```
Note: The queue is an optional and most tutorials leave the parameter `nil` but Zashi requires main thread processing due to UI layer - therefore we pass `DispatchQueue.main`.
6. Call a method that schedules the task execution.
7. Start the synchronizer.
8. Set the expiration closure and stop the synchronizer inside it
```swift
task.expirationHandler = {
synchronizer.stop()
}
```
9. The body of the registered task summarized:
```swift
BGTaskScheduler.shared.register(...) { task in
scheduleTask() // 6

synchronizer.star() // 7

task.expirationHandler = {
synchronizer.stop() // 8
}
}
```
10. Call `scheduleTask()` when app goes to the background so there is the initial scheduling done, the next one will be handled by the closure of the registered task. The method usually consists of:
```Swift
let request = BGProcessingTaskRequest(identifier: <ID>)

request.earliestBeginDate = <scheduledTime>
request.requiresExternalPower = true // optional, we require the iPhone to be connected to the power
request.requiresNetworkConnectivity = true // required

do {
try BGTaskScheduler.shared.submit(request)
} catch { // handle error }
```
11. Last step is to call `.setTaskCompleted(success: <bool>)` on the BGTask when the work is done. This is required by the system no matter what. We call it with `true` when the synchronizer finishes the work (up-to-date state) and with `false` for other or failed reasons (stopped state, error state, etc.).

You can see specific details of the Zashi implementation in:
- Xcode project settings, steps 1-4.
- AppDelegate.swift file, steps 5-9.
- SecantApp.swift file, step 10.
- RootInitialization.swift, step 11.

## Gotchas
- The `requiresNetworkConnectivity` flag doesn't specify or deal with the type of connectivity. It simply allows scheduling when the iPhone is connected to the internet. We deal with it when the task is triggered. The custom check wheather the wifi is or is not connected preceeds the start of the synchronizer.
- When the app is killed by a user in the app switcher, the scheduled BGTask is deleted. So the BGTask is triggered at the scheduled time only when the app is suspended or killed by the system. Explicit termination of the app by a user leads to termination of any background processing.


1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ directly impact users rather than highlighting other crucial architectural updat

### Added
- The exported logs also show the shielded balances (total & verified) for every finished sync metric.
- Synchronization in the background. When the iPhone is connected to the power and wifi, the background task will try to synchronize randomly between 3-4am.

### Fixed
- The export buttons are disabled when exporting of the private data is in progress.
Expand Down
2 changes: 1 addition & 1 deletion modules/Sources/Features/Root/RootDestination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ extension RootReducer {
state.splashAppeared = true
return .none

case .tabs, .initialization, .onboarding, .sandbox, .updateStateAfterConfigUpdate, .alert, .phraseDisplay,
case .tabs, .initialization, .onboarding, .sandbox, .updateStateAfterConfigUpdate, .alert, .phraseDisplay, .synchronizerStateChanged,
.welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .exportLogs, .confirmationDialog:
return .none
}
Expand Down
61 changes: 58 additions & 3 deletions modules/Sources/Features/Root/RootInitialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ extension RootReducer {
case nukeWalletRequest
case respondToWalletInitializationState(InitializationState)
case synchronizerStartFailed(ZcashError)
case registerForSynchronizersUpdate
case retryStart
case walletConfigChanged(WalletConfig)
}
Expand All @@ -38,14 +39,53 @@ extension RootReducer {
switch action {
case .initialization(.appDelegate(.didEnterBackground)):
sdkSynchronizer.stop()
return .none
state.bgTask?.setTaskCompleted(success: false)
state.bgTask = nil
return .cancel(id: CancelStateId.timer)

case .initialization(.appDelegate(.backgroundTask(let task))):
state.bgTask = task
return .run { send in
await send(.initialization(.retryStart))
}

case .initialization(.appDelegate(.willEnterForeground)):
return .run { send in
try await mainQueue.sleep(for: .seconds(1))
await send(.initialization(.retryStart))
}

case .synchronizerStateChanged(let latestState):
guard state.bgTask != nil else {
return .none
}

let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.syncStatus)

var finishBGTask = false
var successOfBGTask = false

switch snapshot.syncStatus {
case .upToDate:
successOfBGTask = true
finishBGTask = true
case .stopped, .error:
successOfBGTask = false
finishBGTask = true
default: break
}

LoggerProxy.event("BGTask .synchronizerStateChanged(let latestState): \(snapshot.syncStatus)")

if finishBGTask {
LoggerProxy.event("BGTask setTaskCompleted(success: \(successOfBGTask)) from TCA")
state.bgTask?.setTaskCompleted(success: successOfBGTask)
state.bgTask = nil
return .cancel(id: CancelStateId.timer)
}

return .none

case .initialization(.synchronizerStartFailed):
return .none

Expand All @@ -54,13 +94,28 @@ extension RootReducer {
guard sdkSynchronizer.latestState().syncStatus.isPrepared else {
return .none
}
return .run { send in
return .run { [state] send in
do {
try await sdkSynchronizer.start(true)
if state.bgTask != nil {
LoggerProxy.event("BGTask synchronizer.start() PASSED")
}
await send(.initialization(.registerForSynchronizersUpdate))
} catch {
if state.bgTask != nil {
LoggerProxy.event("BGTask synchronizer.start() failed \(error.toZcashError())")
}
await send(.initialization(.synchronizerStartFailed(error.toZcashError())))
}
}

case .initialization(.registerForSynchronizersUpdate):
return .publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map(RootReducer.Action.synchronizerStateChanged)
}
.cancellable(id: CancelStateId.timer, cancelInFlight: true)

case .initialization(.appDelegate(.didFinishLaunching)):
// TODO: [#704], trigger the review request logic when approved by the team,
Expand Down Expand Up @@ -174,7 +229,7 @@ extension RootReducer {

case .initialization(.initializationSuccessfullyDone(let uAddress)):
state.tabsState.addressDetailsState.uAddress = uAddress
return .none
return .send(.initialization(.registerForSynchronizersUpdate))

case .initialization(.checkBackupPhraseValidation):
guard let storedWallet = state.storedWallet else {
Expand Down
4 changes: 4 additions & 0 deletions modules/Sources/Features/Root/RootStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import Tabs
import CrashReporter
import ReadTransactionsStorage
import RecoveryPhraseDisplay
import BackgroundTasks

public typealias RootStore = Store<RootReducer.State, RootReducer.Action>
public typealias RootViewStore = ViewStore<RootReducer.State, RootReducer.Action>

public struct RootReducer: Reducer {
enum CancelId { case timer }
enum CancelStateId { case timer }
enum SynchronizerCancelId { case timer }
enum WalletConfigCancelId { case timer }
let tokenName: String
Expand All @@ -31,6 +33,7 @@ public struct RootReducer: Reducer {
public struct State: Equatable {
@PresentationState public var alert: AlertState<Action>?
public var appInitializationState: InitializationState = .uninitialized
public var bgTask: BGProcessingTask?
@PresentationState public var confirmationDialog: ConfirmationDialogState<Action.ConfirmationDialog>?
public var debugState: DebugState
public var destinationState: DestinationState
Expand Down Expand Up @@ -92,6 +95,7 @@ public struct RootReducer: Reducer {
case splashFinished
case splashRemovalRequested
case sandbox(SandboxReducer.Action)
case synchronizerStateChanged(SynchronizerState)
case updateStateAfterConfigUpdate(WalletConfig)
case walletConfigLoaded(WalletConfig)
case welcome(WelcomeReducer.Action)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
//
// AppDelegate.swift
// AppDelegateAction.swift
// secant-testnet
//
// Created by Lukáš Korba on 27.03.2022.
//

import Foundation
import BackgroundTasks

public enum AppDelegateAction: Equatable {
case didFinishLaunching
case didEnterBackground
case willEnterForeground
case backgroundTask(BGProcessingTask)
}
10 changes: 8 additions & 2 deletions secant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
9EB35D632A31F1DD00A2149B /* Root in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D622A31F1DD00A2149B /* Root */; };
9EB35D6A2A3A2D7B00A2149B /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D692A3A2D7B00A2149B /* Utils */; };
9EB35D6C2A3A2D9200A2149B /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D6B2A3A2D9200A2149B /* Utils */; };
9EEB06C82B405A0400EEE50F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C72B405A0400EEE50F /* AppDelegate.swift */; };
9EEB06C92B405A0400EEE50F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C72B405A0400EEE50F /* AppDelegate.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -168,6 +170,7 @@
9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySelectionTests.swift; sourceTree = "<group>"; };
9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputTests.swift; sourceTree = "<group>"; };
9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputTests.swift; sourceTree = "<group>"; };
9EEB06C72B405A0400EEE50F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; };
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -242,6 +245,7 @@
isa = PBXGroup;
children = (
9E7FE0B6282D1D9800C374E8 /* Resources */,
9EEB06C72B405A0400EEE50F /* AppDelegate.swift */,
0D4E7A0826B364170058B01E /* SecantApp.swift */,
9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */,
0DEF4766299EA5920032708B /* secant-mainnet-Info.plist */,
Expand Down Expand Up @@ -910,6 +914,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9EEB06C92B405A0400EEE50F /* AppDelegate.swift in Sources */,
0D26AF24299E8196005260EE /* SecantApp.swift in Sources */,
9E2706682AFF99F5000DA6EC /* ReadTransactionsStorageModel.xcdatamodeld in Sources */,
);
Expand All @@ -919,6 +924,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9EEB06C82B405A0400EEE50F /* AppDelegate.swift in Sources */,
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */,
9E2706672AFF99F5000DA6EC /* ReadTransactionsStorageModel.xcdatamodeld in Sources */,
);
Expand Down Expand Up @@ -1011,7 +1017,7 @@
CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
DEVELOPMENT_TEAM = W5KABFU8SV;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "secant/secant-mainnet-Info.plist";
Expand Down Expand Up @@ -1039,7 +1045,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
DEVELOPMENT_TEAM = W5KABFU8SV;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "secant/secant-mainnet-Info.plist";
Expand Down

0 comments on commit 19d7dce

Please sign in to comment.