-
Notifications
You must be signed in to change notification settings - Fork 338
Refactor Retry Handler To Move Into Source Crawler Package #6275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4f03ede
d2bb597
92286e3
5057d70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| * | ||
| */ | ||
|
|
||
| package org.opensearch.dataprepper.plugins.source.source_crawler.utils.retry; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * Default retry strategy with fixed backoff times | ||
| */ | ||
| @Slf4j | ||
| public class DefaultRetryStrategy implements RetryStrategy { | ||
| private static final List<HttpStatus> DEFAULT_RATE_LIMIT_STATUS_CODES = Arrays.asList(HttpStatus.TOO_MANY_REQUESTS); | ||
|
|
||
| private final List<Integer> retryAttemptSleepTime; | ||
| private final List<Integer> rateLimitRetrySleepTime; | ||
| private final List<HttpStatus> rateLimitStatusCodes; | ||
| private final int maxRetries; | ||
|
|
||
| /** | ||
| * Constructor with default sleep times | ||
| */ | ||
| public DefaultRetryStrategy() { | ||
| this.retryAttemptSleepTime = RetryStrategy.DEFAULT_RETRY_ATTEMPT_SLEEP_TIME; | ||
| this.rateLimitRetrySleepTime = RetryStrategy.DEFAULT_RATE_LIMIT_RETRY_SLEEP_TIME; | ||
| this.rateLimitStatusCodes = DEFAULT_RATE_LIMIT_STATUS_CODES; | ||
| this.maxRetries = RetryStrategy.MAX_RETRIES; | ||
| } | ||
|
|
||
| /** | ||
| * Constructor with custom max retries | ||
| * | ||
| * @param maxRetries Maximum number of retries | ||
| */ | ||
| public DefaultRetryStrategy(final int maxRetries) { | ||
| this.retryAttemptSleepTime = RetryStrategy.DEFAULT_RETRY_ATTEMPT_SLEEP_TIME; | ||
| this.rateLimitRetrySleepTime = RetryStrategy.DEFAULT_RATE_LIMIT_RETRY_SLEEP_TIME; | ||
| this.rateLimitStatusCodes = DEFAULT_RATE_LIMIT_STATUS_CODES; | ||
| this.maxRetries = maxRetries; | ||
| } | ||
|
|
||
| /** | ||
| * Constructor with Custom sleep times for rate limit retries and custom rate limit status codes | ||
| * | ||
| * @param rateLimitRetrySleepTime Custom sleep times for rate limit retries (in | ||
| * seconds) | ||
| * @param rateLimitStatusCodes List of status codes that are considered rate limited | ||
| */ | ||
| public DefaultRetryStrategy(List<Integer> rateLimitRetrySleepTime, List<HttpStatus> rateLimitStatusCodes) { | ||
| this.retryAttemptSleepTime = RetryStrategy.DEFAULT_RETRY_ATTEMPT_SLEEP_TIME; | ||
| this.rateLimitRetrySleepTime = rateLimitRetrySleepTime != null | ||
| ? rateLimitRetrySleepTime | ||
| : RetryStrategy.DEFAULT_RATE_LIMIT_RETRY_SLEEP_TIME; | ||
| this.rateLimitStatusCodes = rateLimitStatusCodes != null | ||
| ? rateLimitStatusCodes | ||
| : DEFAULT_RATE_LIMIT_STATUS_CODES; | ||
| this.maxRetries = this.rateLimitRetrySleepTime.size(); | ||
| } | ||
|
|
||
| @Override | ||
| public long calculateSleepTime(Exception ex, int retryCount) { | ||
| Optional<HttpStatus> statusCode = RetryStrategy.getStatusCode(ex); | ||
|
|
||
| List<Integer> sleepTimes = (statusCode.isPresent() && rateLimitStatusCodes.contains(statusCode.get())) | ||
| ? rateLimitRetrySleepTime | ||
| : retryAttemptSleepTime; | ||
|
|
||
| int sleepTimeSeconds = (retryCount < sleepTimes.size()) | ||
| ? sleepTimes.get(retryCount) | ||
| : sleepTimes.get(sleepTimes.size() - 1); | ||
|
|
||
| log.debug("Retrying in {} seconds (attempt {}/{})", | ||
| sleepTimeSeconds, retryCount + 1, getMaxRetries()); | ||
|
|
||
| return sleepTimeSeconds * RetryStrategy.SLEEP_TIME_MULTIPLIER_MS; | ||
| } | ||
|
|
||
| @Override | ||
| public int getMaxRetries() { | ||
| return maxRetries; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| * | ||
| */ | ||
|
|
||
| package org.opensearch.dataprepper.plugins.source.source_crawler.utils.retry; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import java.util.Optional; | ||
|
|
||
| import static org.opensearch.dataprepper.logging.DataPrepperMarkers.NOISY; | ||
|
|
||
| /** | ||
| * Default status code handling - covers common HTTP scenarios | ||
| */ | ||
| @Slf4j | ||
| public class DefaultStatusCodeHandler implements StatusCodeHandler { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We recently found an issue in WindowEvents connector where "tokenExpired" throws 403(FORBIDDEN) instead of 401(UNAUTHORIZED), and we did not run credentialsRenew on it. This caused the connector to stop retrieving data after. Should we consider allowing connectors to pass information whether to retry credsRenewal on UNAUTHORIZED, FORBIDDEN or both? E.g Just one idea, please let me know what you think.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, for cases like this, we have a CustomApiStatusCodeHandler where we can add all status codes that require special handling or are not covered inside DefaultStatusCodeHandler. For all other cases, we simply call super.handleStatusCode as shown below: And in the RestClient: We have covered all such scenarios in the design document, please check: https://quip-amazon.com/vWbpATaYxCeS/Design-And-Refactor-Retry-Handler-To-Move-Into-Source-Crawler-Package.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay makes sense, thanks! That's super helpful. |
||
|
|
||
| @Override | ||
| public RetryDecision handleStatusCode(Exception ex, int retryCount, | ||
| Runnable credentialRenewal) { | ||
| Optional<HttpStatus> statusCode = RetryStrategy.getStatusCode(ex); | ||
| String statusMessage = ex.getMessage(); | ||
|
|
||
| if (statusCode.isEmpty()) { | ||
| return RetryDecision.stop(); | ||
| } | ||
|
|
||
| switch (statusCode.get()) { | ||
| case UNAUTHORIZED: | ||
| log.error(NOISY, "Token expired. Attempting to renew credentials.", ex); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to have an
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| credentialRenewal.run(); | ||
| return RetryDecision.retry(); | ||
|
|
||
| case FORBIDDEN: | ||
| log.error(NOISY, "Access forbidden: {}", statusMessage, ex); | ||
| return RetryDecision.stopWithException( | ||
| new SecurityException("Access forbidden: " + statusMessage)); | ||
|
|
||
| case NOT_FOUND: | ||
| log.warn(NOISY, "Resource not found (404): {}. " + | ||
| "This is expected for deleted/expired resources.", statusMessage); | ||
| return RetryDecision.stop(); | ||
|
|
||
| case TOO_MANY_REQUESTS: | ||
| log.error(NOISY, "Hitting API rate limit. Backing off.", ex); | ||
| return RetryDecision.retry(); | ||
|
|
||
| case SERVICE_UNAVAILABLE: | ||
| log.error(NOISY, "Service unavailable. Will retry.", ex); | ||
| return RetryDecision.retry(); | ||
|
|
||
| default: | ||
| if (statusCode.get().is4xxClientError()) { | ||
| log.error(NOISY, "Client error: {}. Will not retry.", statusCode, ex); | ||
| return RetryDecision.stop(); | ||
| } else if (statusCode.get().is5xxServerError()) { | ||
| log.error(NOISY, "Server error: {}. Will retry.", statusCode, ex); | ||
| return RetryDecision.retry(); | ||
| } else { | ||
| log.error(NOISY, "Unexpected status code: {}. Will not retry.", | ||
| statusCode, ex); | ||
| return RetryDecision.stop(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think if we pass "pluginMetrics" into
RetryHandlerhere so we do not have to passOptional.of(auditLogRequestsFailedCounter)on line #261? Can also be an Optional param.IMO, it would keep the
executeWithRetryclean and concise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't think it would be beneficial. Even if we pass pluginMetrics into the RetryHandler constructor, we would still need to provide the metric name at line 261. Additionally, inside RetryHandler we would have to iterate through pluginMetrics to locate that specific metric. Please correct us if we are wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I was thinking of passing this
searchRequestsFailedCounterdirectly in the constructor. E.gNot a big blocker and will let others chime in.