diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift index ca42eaedda..06b9f0b7e9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift @@ -35,7 +35,6 @@ struct OperationPreferredDateCalculator { extractedProfileID: Int64?, schedulingConfig: DataBrokerScheduleConfig, isDeprecated: Bool = false) throws -> Date? { - guard let lastEvent = historyEvents.last else { throw DataBrokerProtectionError.cantCalculatePreferredRunDate } @@ -63,7 +62,6 @@ struct OperationPreferredDateCalculator { extractedProfileID: Int64?, schedulingConfig: DataBrokerScheduleConfig, date: DateProtocol = SystemDate()) throws -> Date? { - guard let lastEvent = historyEvents.last else { throw DataBrokerProtectionError.cantCalculatePreferredRunDate } @@ -78,7 +76,7 @@ struct OperationPreferredDateCalculator { return currentPreferredRunDate } case .error: - return date.now.addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) + return date.now.addingTimeInterval(calculateNextRunDateOnError(schedulingConfig: schedulingConfig, historyEvents: historyEvents)) case .optOutStarted, .scanStarted, .noMatchFound: return currentPreferredRunDate case .optOutConfirmed, .optOutRequested: @@ -97,4 +95,10 @@ struct OperationPreferredDateCalculator { let lastRemovalEventDate = lastRemovalEvent.date.addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds) return lastRemovalEventDate < Date() } + + private func calculateNextRunDateOnError(schedulingConfig: DataBrokerScheduleConfig, + historyEvents: [HistoryEvent]) -> TimeInterval { + let pastTries = historyEvents.filter { $0.isError }.count + return min(Int(pow(2.0, Double(pastTries))), schedulingConfig.retryError).hoursToSeconds + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift index 959f9555b4..f491a389c9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift @@ -23,7 +23,7 @@ import XCTest final class OperationPreferredDateCalculatorTests: XCTestCase { private let schedulingConfig = DataBrokerScheduleConfig( - retryError: 1000, + retryError: 48, confirmOptOutScan: 2000, maintenanceScan: 3000 ) @@ -322,17 +322,124 @@ final class OperationPreferredDateCalculatorTests: XCTestCase { XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) } - func testError_thenOptOutDateIsRetry() throws { - let expectedOptOutDate = Date().addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) - + func testWhenOptOutFailedOnce_thenWeRetryInTwoHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date())! let historyEvents = [ HistoryEvent(extractedProfileId: 1, brokerId: 1, profileQueryId: 1, type: .error(error: DataBrokerProtectionError.malformedURL))] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedTwice_thenWeRetryInFourHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 4, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedThreeTimes_thenWeRetryInEightHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 8, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedThreeTimes_thenWeRetryInSixteenHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 16, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedThreeTimes_thenWeRetryInThirtyTwoHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 32, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedSixTimes_thenWeRetryInTwoDays() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .day, value: 2, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedMoreThanTheThreshold_thenWeRetryAtTheSchedulingRetry() throws { + let expectedOptOutDate = Date().addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, historyEvents: historyEvents, extractedProfileID: nil,