Skip to content

Commit

Permalink
[MOBILE-4696] Fix automation engine energy use when offline (#3210)
Browse files Browse the repository at this point in the history
* [MOBILE-4696] Fix automation engine energy use when offline

* Test

* Fix tests

* Fix
  • Loading branch information
rlepinski authored Sep 23, 2024
1 parent 8a595cc commit d44016f
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,6 @@ fileprivate extension AutomationEngine {
return
}


guard
try await self.store.getSchedule(scheduleID: scheduleID) == data
else {
Expand Down Expand Up @@ -564,39 +563,41 @@ fileprivate extension AutomationEngine {
triggerSessionID: data.triggerSessionID
)

AirshipLogger.trace("Preparing schedule \(data) result: \(prepareResult)")
if case .cancel = prepareResult {
try await self.store.deleteSchedules(scheduleIDs: [data.schedule.identifier])
return nil
}

let updated = try await self.updateState(identifier: data.schedule.identifier) { [date] data in
guard data.isInState([.triggered]) else { return }
AirshipLogger.trace("Finished preparing schedule \(data) result: \(prepareResult)")

switch (prepareResult) {
case .prepared(let preparedSchedule):
switch prepareResult {
case .prepared(let preparedSchedule):
let updated = try await self.updateState(identifier: data.schedule.identifier) { [date] data in
data.prepared(info: preparedSchedule.info, date: date.now)
case .invalidate, .cancel:
break
case .skip:
data.prepareCancelled(date: date.now, penalize: false)
case .penalize:
data.prepareCancelled(date: date.now, penalize: true)
}
}

switch prepareResult {
case .prepared(let info):
// Make sure its updated
guard let updated else {
await preparer.cancelled(schedule: data.schedule)
return nil
}

return PreparedData(
scheduleData: updated ?? data,
preparedSchedule: info
scheduleData: updated,
preparedSchedule: preparedSchedule
)
case .invalidate:
await self.startTaskToProcessTriggeredSchedule(
scheduleID: data.schedule.identifier
)
return nil
case .cancel, .skip, .penalize:
case .cancel:
try await self.store.deleteSchedules(scheduleIDs: [data.schedule.identifier])
return nil
case .skip:
_ = try await self.updateState(identifier: data.schedule.identifier) { [date] data in
data.prepareCancelled(date: date.now, penalize: false)
}
return nil
case .penalize:
_ = try await self.updateState(identifier: data.schedule.identifier) { [date] data in
data.prepareCancelled(date: date.now, penalize: true)
}
return nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ final class AutomationExecutor: AutomationExecutorProtocol {

@MainActor
func isValid(schedule: AutomationSchedule) async -> Bool {
guard await self.remoteDataAccess.isCurrent(schedule: schedule) else {
return false
}

return true
return await self.remoteDataAccess.isCurrent(schedule: schedule)
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ struct AutomationPreparer: AutomationPreparerProtocol {
)
} catch {
AirshipLogger.error("Failed to fetch frequency checker for schedule \(schedule.identifier) error: \(error)")
await self.remoteDataAccess.notifyOutdated(schedule: schedule)
return .success(result: .invalidate)
return .success(result: .skip)
}

let deviceInfoProvider = self.deviceInfoProviderFactory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ final class AutomationRemoteDataAccess: AutomationRemoteDataAccessProtocol {
return true
}

// if we are connected wait for refresh
// if we are connected wait for refresh attempt
if (await network.isConnected) {
await remoteData.waitRefreshAttempt(source: source)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,54 @@ final class AutomationPreparerTest: XCTestCase {
XCTAssertNotNil(prepared.frequencyChecker)
}

func testPrepareMessageCheckerError() async throws {
let automationSchedule = AutomationSchedule(
identifier: UUID().uuidString,
data: .inAppMessage(
InAppMessage(name: "name", displayContent: .custom(.null))
),
triggers: [],
created: Date(),
lastUpdated: Date(),
audience: AutomationAudience(
audienceSelector: DeviceAudienceSelector(),
missBehavior: .penalize
),
campaigns: .string("campaigns"),
frequencyConstraintIDs: ["constraint"]
)

self.remoteDataAccess.contactIDBlock = { _ in
return "contact ID"
}

self.remoteDataAccess.requiresUpdateBlock = { _ in
return false
}

self.remoteDataAccess.bestEffortRefreshBlock = { _ in
return true
}

self.audienceChecker.onEvaluate = { _, _, provider in
return true
}

await self.frequencyLimits.setCheckerBlock { _ in
throw AirshipErrors.error("Failed")
}

let triggerSessionID = UUID().uuidString

let result = await self.preparer.prepare(
schedule: automationSchedule,
triggerContext: triggerContext,
triggerSessionID: triggerSessionID
)

XCTAssertTrue(result.isSkipped)
}

func testAdditionalAudienceMiss() async throws {
let automationSchedule = AutomationSchedule(
identifier: UUID().uuidString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final actor TestFrequencyLimitManager: FrequencyLimitManagerProtocol {
private var checkerBlock: (@Sendable ([String]) async throws -> FrequencyCheckerProtocol)?


func setCheckerBlock(_ checkerBlock: @Sendable @escaping ([String]) -> FrequencyCheckerProtocol) {
func setCheckerBlock(_ checkerBlock: @Sendable @escaping ([String]) throws -> FrequencyCheckerProtocol) {
self.checkerBlock = checkerBlock
}

Expand Down
54 changes: 14 additions & 40 deletions Airship/AirshipCore/Source/RemoteData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ final class RemoteData: AirshipComponent, RemoteDataProtocol {
public func notifyOutdated(remoteDataInfo: RemoteDataInfo) async {
for provider in self.providers {
if (provider.source == remoteDataInfo.source) {
await provider.notifyOutdated(remoteDataInfo: remoteDataInfo)
if (await provider.notifyOutdated(remoteDataInfo: remoteDataInfo)) {
enqueueRefreshTask()
}
return
}
}
Expand Down Expand Up @@ -313,14 +315,13 @@ final class RemoteData: AirshipComponent, RemoteDataProtocol {
return success ? .success : .failure
}

@discardableResult
public func refresh() async -> Bool {
return await self.refresh(sources: self.providers.map { $0.source})
}

@discardableResult
public func refresh(source: RemoteDataSource) async -> Bool {
return await self.refresh(sources: [source])
public func forceRefresh() async {
self.updateChangeToken()
enqueueRefreshTask()
let sources = self.providers.map { $0.source }
for source in sources {
await self.waitRefreshAttempt(source: source)
}
}

public func waitRefresh(source: RemoteDataSource) async {
Expand All @@ -331,9 +332,9 @@ final class RemoteData: AirshipComponent, RemoteDataProtocol {
source: RemoteDataSource,
maxTime: TimeInterval?
) async {
AirshipLogger.trace("Waiting for remote data to refresh \(source)")
AirshipLogger.trace("Waiting for remote data to refresh succesfully \(source)")
await waitRefreshStatus(source: source, maxTime: maxTime) { status in
status != .none
status == .success
}
}

Expand All @@ -345,7 +346,7 @@ final class RemoteData: AirshipComponent, RemoteDataProtocol {
source: RemoteDataSource,
maxTime: TimeInterval?
) async {
AirshipLogger.trace("Waiting for remote data to refresh successfully \(source)")
AirshipLogger.trace("Waiting for remote refresh attempt \(source)")
await waitRefreshStatus(source: source, maxTime: maxTime) { status in
status != .none
}
Expand Down Expand Up @@ -384,34 +385,7 @@ final class RemoteData: AirshipComponent, RemoteDataProtocol {

AirshipLogger.trace("Remote data refresh: \(source), status: \(result)")
}

private func refresh(sources: [RemoteDataSource]) async -> Bool {
// Refresh task will refresh all remoteDataHandlers. If we only care about
// a subset of the sources, we need filter out those results and collect
// the expected count of sources.
var cancellable: AnyCancellable?
let result = await withCheckedContinuation { continuation in
cancellable = self.refreshResultSubject
.filter { result in
sources.contains(result.source)
}
.collect(sources.count)
.map { results in
!results.contains { result in
result.result == .failed
}
}
.first()
.sink { result in
continuation.resume(returning: result)
}

enqueueRefreshTask()
}
cancellable?.cancel()
return result
}


public func payloads(types: [String]) async -> [RemoteDataPayload] {
var payloads: [RemoteDataPayload] = []
for provider in self.providers {
Expand Down
27 changes: 21 additions & 6 deletions Airship/AirshipCore/Source/RemoteDataProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,29 @@ public protocol RemoteDataProtocol: AnyObject, Sendable {
func publisher(types: [String]) -> AnyPublisher<[RemoteDataPayload], Never>
func payloads(types: [String]) async -> [RemoteDataPayload]

/// Waits for a successful refresh
/// - Parameters:
/// - source: The remote data source.
/// - maxTime: The max time to wait
func waitRefresh(source: RemoteDataSource, maxTime: TimeInterval?) async
func waitRefreshAttempt(source: RemoteDataSource, maxTime: TimeInterval?) async

/// Waits for a successful refresh
/// - Parameters:
/// - source: The remote data source.
func waitRefresh(source: RemoteDataSource) async
func waitRefreshAttempt(source: RemoteDataSource) async

@discardableResult
func refresh() async -> Bool
/// Waits for a refresh attempt for the session.
/// - Parameters:
/// - source: The remote data source.
/// - maxTime: The max time to wait
func waitRefreshAttempt(source: RemoteDataSource, maxTime: TimeInterval?) async

/// Waits for a refresh attempt for the session.
/// - Parameters:
/// - source: The remote data source.
func waitRefreshAttempt(source: RemoteDataSource) async

@discardableResult
func refresh(source: RemoteDataSource) async -> Bool
/// Forces a refresh attempt. This should generally never be called externally. Currently being exposed for
/// test apps.
func forceRefresh() async
}
9 changes: 6 additions & 3 deletions Airship/AirshipCore/Source/RemoteDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,13 @@ actor RemoteDataProvider: RemoteDataProviderProtocol {
}
}

func notifyOutdated(remoteDataInfo: RemoteDataInfo) {
if (self.refreshState?.remoteDataInfo == remoteDataInfo) {
self.refreshState = nil
func notifyOutdated(remoteDataInfo: RemoteDataInfo) -> Bool {
guard self.refreshState?.remoteDataInfo == remoteDataInfo else {
return false
}

self.refreshState = nil
return true
}

func isCurrent(locale: Locale, randomeValue: Int) async -> Bool {
Expand Down
3 changes: 2 additions & 1 deletion Airship/AirshipCore/Source/RemoteDataProviderProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ protocol RemoteDataProviderProtocol: Actor {
/// Notifies that the remote-data info is outdated. This will cause the next refresh to
/// to fetch data.
/// - Parameter remoteDataInfo: The remote data info.
func notifyOutdated(remoteDataInfo: RemoteDataInfo)
/// - Returns true if cleared, otherwise false.
func notifyOutdated(remoteDataInfo: RemoteDataInfo) -> Bool

/// Checks if the source is current.
/// - Parameter locale: The current locale.
Expand Down
Loading

0 comments on commit d44016f

Please sign in to comment.