From a847babb6709a6efc839ccfa025a250de51a20d6 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 23 May 2024 15:47:57 -0300 Subject: [PATCH] Implement exponential backoff for optout retries --- .../OperationPreferredDateCalculator.swift | 8 +- ...perationPreferredDateCalculatorTests.swift | 80 ++++++++++++++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift index ca42eaedda..4bb542d01e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift @@ -78,7 +78,7 @@ struct OperationPreferredDateCalculator { return currentPreferredRunDate } case .error: - return date.now.addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) + return date.now.addingTimeInterval(calculateNextRunDate(schedulingConfig: schedulingConfig, historyEvents: historyEvents)) case .optOutStarted, .scanStarted, .noMatchFound: return currentPreferredRunDate case .optOutConfirmed, .optOutRequested: @@ -97,4 +97,10 @@ struct OperationPreferredDateCalculator { let lastRemovalEventDate = lastRemovalEvent.date.addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds) return lastRemovalEventDate < Date() } + + private func calculateNextRunDate(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..df8b19acff 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,89 @@ 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 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,