Design a retry handler for API calls that may fail due to transient errors such as timeout, rate limiting, temporary server error, or connection failure.
The retry handler should support fixed delay, exponential backoff, and jitter backoff. The design should allow new backoff strategies to be added later without changing the retry handler logic.
RetryHandler(List<String> retryableErrors)
retryableErrors contains error types that can be retried.String registerApiCall(String apiCallName, String backoffStrategy, int maxRetryAttempts, long baseDelayMillis, long currentMillisecondsNow)
apiCallName.backoffStrategy is one of FIXED, EXPONENTIAL, or JITTER.maxRetryAttempts is the maximum number of retry attempts allowed after the first API attempt.1 + maxRetryAttempts, including attempt 0.baseDelayMillis is used to calculate retry delay.0.0 is not counted in retryAttempts.apiCallName is not already registered, register it and return registered=<apiCallName>, attempt=0, retryAttempts=0.apiCallName is already registered, ignore the new values and return already_registered=<apiCallName>.String updateExecutionResult(String apiCallName, String callResult, long currentMillisecondsNow)
apiCallName.callResult is either SUCCESS,response or ERROR,errorType.SUCCESS,response means the API call succeeded with response value response.ERROR,errorType means the API call failed with error type errorType.updateExecutionResult call for an API call records attempt 0.updateExecutionResult call records one retry attempt.retryAttempts is 0 after attempt 0, 1 after the first retry attempt, and so on.status=<status>, result=<result>, retryAttempts=<retryAttempts>status is SUCCESS, FAILED_RETRYABLE, FAILED_NON_RETRYABLE, or FAILED_RETRY_LIMIT_REACHED.result is the successful response or the error type.retryAttempts is the number of retry attempts made so far, excluding attempt 0.retryAttempts should be -1.FAILED_NON_RETRYABLE, return retryAttempts=-1 as a sentinel value, even if some retry attempts were made before the non-retryable error occurred.long getDelayMillis(String apiCallName, long currentMillisecondsNow)
apiCallName.-1 if the last error for this API call is non-retryable.-2 if the API call has succeeded.-3 if the maximum retry attempt limit has been reached.0 if the next retry can be attempted immediately.currentMillisecondsNow value passed to the latest failed updateExecutionResult call.int attemptsTillNow(String apiCallName, long currentMillisecondsNow)
apiCallName.0, so it is not counted.-1 if the last error for this API call is non-retryable.-1 as a sentinel value instead of the actual retry attempt count.currentMillisecondsNow value in attemptsTillNow is used only for monotonic time validation and does not affect the returned count.currentMillisecondsNow must be monotonically non-decreasing across all method calls.currentMillisecondsNow greater than or equal to the previous call.currentMillisecondsNow value in registerApiCall is used only for monotonic time validation and does not affect retry delay.registerApiCall will be called before updateExecutionResult, getDelayMillis, or attemptsTillNow for the same API call name.apiCallName, only the first registerApiCall call should be used.registerApiCall calls for the same apiCallName should be ignored.updateExecutionResult only after the corresponding API attempt has happened.updateExecutionResult for a retry attempt only when getDelayMillis would return 0 for that API call.SUCCESS, FAILED_NON_RETRYABLE, or FAILED_RETRY_LIMIT_REACHED, no further retry result will be added for that API call name.FIXED, delay is baseDelayMillis.EXPONENTIAL, delay is baseDelayMillis * 2^(retryAttemptNumber - 1).JITTER, delay is baseDelayMillis * 2^(retryAttemptNumber - 1) + retryAttemptNumber.retryAttemptNumber is the retry attempt that is going to happen next.0 fails, the next retry has retryAttemptNumber = 1.1 fails, the next retry has retryAttemptNumber = 2.retryAttemptNumber = retryAttempts + 1, where retryAttempts is the number of retry attempts already made.The registerApiCall method should return one of these formats:
registered=<apiCallName>, attempt=0, retryAttempts=0
already_registered=<apiCallName>
The updateExecutionResult method should return:
status=<status>, result=<result>, retryAttempts=<retryAttempts>
status should be SUCCESS, FAILED_RETRYABLE, FAILED_NON_RETRYABLE, or FAILED_RETRY_LIMIT_REACHED.SUCCESS means the API call succeeded.FAILED_RETRYABLE means the API call failed with a retryable error and at least one retry attempt is still available.FAILED_NON_RETRYABLE means the API call failed with a non-retryable error.FAILED_RETRY_LIMIT_REACHED means the API call failed with a retryable error, but the maximum retry attempt limit has been reached.result should be the successful response or error type.retryAttempts should not include attempt 0.FAILED_NON_RETRYABLE, retryAttempts should be -1.The getDelayMillis method should return:
-1 if the last error is non-retryable.-2 if the API call has succeeded.-3 if the maximum retry attempt limit has been reached.0 if the next retry can be attempted immediately.1 ≤ apiCallName.length() ≤ 1001 ≤ maxRetryAttempts ≤ 1,0000 ≤ baseDelayMillis ≤ 1,000,000,0000 ≤ retryableErrors.size() ≤ 1,0001 ≤ currentMillisecondsNow ≤ 1,000,000,000,000currentMillisecondsNow will be monotonically non-decreasing across all method calls.backoffStrategy will be one of FIXED, EXPONENTIAL, or JITTER.callResult will contain exactly one comma.callResult will be either SUCCESS or ERROR.callResult will be non-empty.Constructor call:
RetryHandler(retryableErrors = List.of("TIMEOUT"))
Method calls and outputs:
registerApiCall(apiCallName = "OrderApi", backoffStrategy = "FIXED", maxRetryAttempts = 3, baseDelayMillis = 200, currentMillisecondsNow = 1000)
"registered=OrderApi, attempt=0, retryAttempts=0"
updateExecutionResult(apiCallName = "OrderApi", callResult = "ERROR,TIMEOUT", currentMillisecondsNow = 1000)
"status=FAILED_RETRYABLE, result=TIMEOUT, retryAttempts=0"
getDelayMillis(apiCallName = "OrderApi", currentMillisecondsNow = 1100)
100
getDelayMillis(apiCallName = "OrderApi", currentMillisecondsNow = 1200)
0
updateExecutionResult(apiCallName = "OrderApi", callResult = "SUCCESS,ORDER_CREATED", currentMillisecondsNow = 1200)
"status=SUCCESS, result=ORDER_CREATED, retryAttempts=1"
getDelayMillis(apiCallName = "OrderApi", currentMillisecondsNow = 1300)
-2
Explanation: The API call has already succeeded, so getDelayMillis returns -2.
Constructor call:
RetryHandler(retryableErrors = List.of("TIMEOUT", "RATE_LIMIT"))
Method calls and outputs:
registerApiCall(apiCallName = "ProfileApi", backoffStrategy = "EXPONENTIAL", maxRetryAttempts = 3, baseDelayMillis = 100, currentMillisecondsNow = 5000)
"registered=ProfileApi, attempt=0, retryAttempts=0"
updateExecutionResult(apiCallName = "ProfileApi", callResult = "ERROR,AUTH_ERROR", currentMillisecondsNow = 5000)
"status=FAILED_NON_RETRYABLE, result=AUTH_ERROR, retryAttempts=-1"
attemptsTillNow(apiCallName = "ProfileApi", currentMillisecondsNow = 5100)
-1
getDelayMillis(apiCallName = "ProfileApi", currentMillisecondsNow = 5100)
-1
Explanation: The last error is non-retryable, so getDelayMillis returns -1.
Constructor call:
RetryHandler(retryableErrors = List.of("RATE_LIMIT"))
Method calls and outputs:
registerApiCall(apiCallName = "InventoryApi", backoffStrategy = "EXPONENTIAL", maxRetryAttempts = 2, baseDelayMillis = 100, currentMillisecondsNow = 1000)
"registered=InventoryApi, attempt=0, retryAttempts=0"
updateExecutionResult(apiCallName = "InventoryApi", callResult = "ERROR,RATE_LIMIT", currentMillisecondsNow = 1000)
"status=FAILED_RETRYABLE, result=RATE_LIMIT, retryAttempts=0"
getDelayMillis(apiCallName = "InventoryApi", currentMillisecondsNow = 1100)
0
updateExecutionResult(apiCallName = "InventoryApi", callResult = "ERROR,RATE_LIMIT", currentMillisecondsNow = 1100)
"status=FAILED_RETRYABLE, result=RATE_LIMIT, retryAttempts=1"
getDelayMillis(apiCallName = "InventoryApi", currentMillisecondsNow = 1200)
100
getDelayMillis(apiCallName = "InventoryApi", currentMillisecondsNow = 1300)
0
updateExecutionResult(apiCallName = "InventoryApi", callResult = "ERROR,RATE_LIMIT", currentMillisecondsNow = 1300)
"status=FAILED_RETRY_LIMIT_REACHED, result=RATE_LIMIT, retryAttempts=2"
attemptsTillNow(apiCallName = "InventoryApi", currentMillisecondsNow = 1300)
2
getDelayMillis(apiCallName = "InventoryApi", currentMillisecondsNow = 1400)
-3
Explanation: The error type is retryable, but the maximum retry attempt limit has been reached. Therefore, updateExecutionResult returns FAILED_RETRY_LIMIT_REACHED and getDelayMillis returns -3.
Constructor call:
RetryHandler(retryableErrors = List.of("TIMEOUT"))
Method calls and outputs:
registerApiCall(apiCallName = "PaymentApi", backoffStrategy = "JITTER", maxRetryAttempts = 3, baseDelayMillis = 100, currentMillisecondsNow = 2000)
"registered=PaymentApi, attempt=0, retryAttempts=0"
updateExecutionResult(apiCallName = "PaymentApi", callResult = "ERROR,TIMEOUT", currentMillisecondsNow = 2000)
"status=FAILED_RETRYABLE, result=TIMEOUT, retryAttempts=0"
getDelayMillis(apiCallName = "PaymentApi", currentMillisecondsNow = 2050)
51
Explanation: After attempt 0 fails, the next retry is retry attempt number 1. For retry attempt number 1, jitter delay is 100 * 2^0 + 1 = 101. Since current time is 50 milliseconds after the failed call, time left is 51.
Constructor call:
RetryHandler(retryableErrors = List.of("TIMEOUT"))
Method calls and outputs:
registerApiCall(apiCallName = "SearchApi", backoffStrategy = "FIXED", maxRetryAttempts = 3, baseDelayMillis = 100, currentMillisecondsNow = 3000)
"registered=SearchApi, attempt=0, retryAttempts=0"
registerApiCall(apiCallName = "SearchApi", backoffStrategy = "EXPONENTIAL", maxRetryAttempts = 10, baseDelayMillis = 500, currentMillisecondsNow = 3000)
"already_registered=SearchApi"
Explanation: The second registration is ignored because SearchApi is already registered.