From 506ff7c8cf44beedc502f46d6f287cfdb69add53 Mon Sep 17 00:00:00 2001 From: Ashutosh Date: Sun, 15 Oct 2023 20:09:51 +0530 Subject: [PATCH 1/2] Added Exponential Retry Mechanism with Idempotency Headers --- .../java/co/novu/common/base/NovuConfig.java | 9 ++- .../rest/IdempotencyKeyInterceptor.java | 29 +++++++++ .../java/co/novu/common/rest/RestHandler.java | 19 ++++-- .../co/novu/common/rest/RetryInterceptor.java | 62 +++++++++++++++++++ 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 src/main/java/co/novu/common/rest/IdempotencyKeyInterceptor.java create mode 100644 src/main/java/co/novu/common/rest/RetryInterceptor.java diff --git a/src/main/java/co/novu/common/base/NovuConfig.java b/src/main/java/co/novu/common/base/NovuConfig.java index c3a2ce4b..3ca9e655 100644 --- a/src/main/java/co/novu/common/base/NovuConfig.java +++ b/src/main/java/co/novu/common/base/NovuConfig.java @@ -11,6 +11,13 @@ public NovuConfig(String apiKey) { this.apiKey = apiKey; } - private String apiKey; + private String apiKey; private String baseUrl = "https://api.novu.co/v1/"; + + private int maxRetries = 3; + private int minRetryDelayMillis = 500; // 500 milli seconds + private int maxRetryDelayMillis = 60000; // 60 seconds + private int initialRetryDelayMillis = 1000; // 1 second + private boolean enableRetry = true; // To enable/disable retry logic + private boolean enableIdempotencyKey = true; // To enable/disable idempotency key } \ No newline at end of file diff --git a/src/main/java/co/novu/common/rest/IdempotencyKeyInterceptor.java b/src/main/java/co/novu/common/rest/IdempotencyKeyInterceptor.java new file mode 100644 index 00000000..0e1ac87d --- /dev/null +++ b/src/main/java/co/novu/common/rest/IdempotencyKeyInterceptor.java @@ -0,0 +1,29 @@ +package co.novu.common.rest; + +import java.io.IOException; +import java.util.UUID; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class IdempotencyKeyInterceptor implements Interceptor{ + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Response response = null; + + Request requestWithIdempotencyKey = request.newBuilder() + .header("Idempotency-Key", generateIdempotencyKey()) + .build(); + response = chain.proceed(requestWithIdempotencyKey); + return response; + } + + private String generateIdempotencyKey() { + UUID uuid = UUID. randomUUID(); + return uuid.toString(); + } + +} diff --git a/src/main/java/co/novu/common/rest/RestHandler.java b/src/main/java/co/novu/common/rest/RestHandler.java index e5786322..955d6eff 100644 --- a/src/main/java/co/novu/common/rest/RestHandler.java +++ b/src/main/java/co/novu/common/rest/RestHandler.java @@ -1,9 +1,13 @@ package co.novu.common.rest; -import co.novu.common.base.NovuConfig; -import co.novu.common.contracts.IRequest; +import java.io.IOException; +import java.util.Map; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; + +import co.novu.common.base.NovuConfig; +import co.novu.common.contracts.IRequest; import lombok.RequiredArgsConstructor; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -12,9 +16,6 @@ import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; -import java.io.IOException; -import java.util.Map; - @RequiredArgsConstructor public class RestHandler { @@ -35,6 +36,14 @@ public Retrofit buildRetrofit() { .build(); return chain.proceed(request); }).addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)); + + if(novuConfig.isEnableRetry()) { + clientBuilder.addInterceptor(new RetryInterceptor(novuConfig.getMaxRetries(), novuConfig.getMinRetryDelayMillis() , novuConfig.getMaxRetryDelayMillis() , novuConfig.getInitialRetryDelayMillis())); + } + + if(novuConfig.isEnableIdempotencyKey()) { + clientBuilder.addInterceptor(new IdempotencyKeyInterceptor()); + } Gson gson = new GsonBuilder() .setLenient() diff --git a/src/main/java/co/novu/common/rest/RetryInterceptor.java b/src/main/java/co/novu/common/rest/RetryInterceptor.java new file mode 100644 index 00000000..c86e8d46 --- /dev/null +++ b/src/main/java/co/novu/common/rest/RetryInterceptor.java @@ -0,0 +1,62 @@ +package co.novu.common.rest; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class RetryInterceptor implements Interceptor{ + + private final int maxRetries; + private final int minRetryDelayMillis; + private final int maxRetryDelayMillis; + private final int initialRetryDelayMillis; + + public RetryInterceptor(int maxRetries, int minRetryDelayMillis, int maxRetryDelayMillis, int initialRetryDelayMillis) { + this.maxRetries = maxRetries; + this.minRetryDelayMillis = minRetryDelayMillis; + this.maxRetryDelayMillis = maxRetryDelayMillis; + this.initialRetryDelayMillis = initialRetryDelayMillis; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Response response = null; + IOException lastException = null; + + for (int retry = 0; retry < maxRetries; retry++) { + try { + response = chain.proceed(request); + if (response.isSuccessful()) { + return response; // Request was successful, no need to retry + } + } catch (IOException e) { + lastException = e; + } + + try { + int retryDelay; + if (retry == 0) { + retryDelay = initialRetryDelayMillis; + } else { + retryDelay = (int) (initialRetryDelayMillis * Math.pow(2, retry - 1)); + } + retryDelay = Math.max(retryDelay, minRetryDelayMillis); + retryDelay = Math.min(retryDelay, maxRetryDelayMillis); + Thread.sleep(retryDelay); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + // If all retries failed, throw the last exception + if (lastException != null) { + throw lastException; + } + + return response; + } + +} From bc7e5dbc22cfd197fa9630856a8111ffaa72d388 Mon Sep 17 00:00:00 2001 From: Ashutosh Date: Sun, 22 Oct 2023 23:12:20 +0530 Subject: [PATCH 2/2] Some changes suggested during review --- .../java/co/novu/common/base/NovuConfig.java | 12 ++--- .../co/novu/common/rest/RetryInterceptor.java | 50 +++++++++++++------ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/co/novu/common/base/NovuConfig.java b/src/main/java/co/novu/common/base/NovuConfig.java index 3ca9e655..e071d6e1 100644 --- a/src/main/java/co/novu/common/base/NovuConfig.java +++ b/src/main/java/co/novu/common/base/NovuConfig.java @@ -14,10 +14,10 @@ public NovuConfig(String apiKey) { private String apiKey; private String baseUrl = "https://api.novu.co/v1/"; - private int maxRetries = 3; - private int minRetryDelayMillis = 500; // 500 milli seconds - private int maxRetryDelayMillis = 60000; // 60 seconds - private int initialRetryDelayMillis = 1000; // 1 second - private boolean enableRetry = true; // To enable/disable retry logic - private boolean enableIdempotencyKey = true; // To enable/disable idempotency key + private int maxRetries = 0; + private int minRetryDelayMillis = 1000; // 1 second + private int maxRetryDelayMillis = 2000; // 2 second + private int initialRetryDelayMillis = 500; // 500 milli second + private boolean enableRetry = false; // To enable/disable retry logic + private boolean enableIdempotencyKey = false; // To enable/disable idempotency key } \ No newline at end of file diff --git a/src/main/java/co/novu/common/rest/RetryInterceptor.java b/src/main/java/co/novu/common/rest/RetryInterceptor.java index c86e8d46..602a71f7 100644 --- a/src/main/java/co/novu/common/rest/RetryInterceptor.java +++ b/src/main/java/co/novu/common/rest/RetryInterceptor.java @@ -1,6 +1,8 @@ package co.novu.common.rest; import java.io.IOException; +import java.util.HashSet; +import java.util.Set; import okhttp3.Interceptor; import okhttp3.Request; @@ -12,12 +14,21 @@ public class RetryInterceptor implements Interceptor{ private final int minRetryDelayMillis; private final int maxRetryDelayMillis; private final int initialRetryDelayMillis; + private final Set retryStatusCodes; public RetryInterceptor(int maxRetries, int minRetryDelayMillis, int maxRetryDelayMillis, int initialRetryDelayMillis) { this.maxRetries = maxRetries; this.minRetryDelayMillis = minRetryDelayMillis; this.maxRetryDelayMillis = maxRetryDelayMillis; this.initialRetryDelayMillis = initialRetryDelayMillis; + + retryStatusCodes = new HashSet<>(); + retryStatusCodes.add(408); + retryStatusCodes.add(429); + retryStatusCodes.add(500); + retryStatusCodes.add(502); + retryStatusCodes.add(503); + retryStatusCodes.add(504); } @Override @@ -26,28 +37,30 @@ public Response intercept(Chain chain) throws IOException { Response response = null; IOException lastException = null; - for (int retry = 0; retry < maxRetries; retry++) { + int retry = 0; + while(!response.isSuccessful() && retry < maxRetries) { try { response = chain.proceed(request); - if (response.isSuccessful()) { - return response; // Request was successful, no need to retry - } } catch (IOException e) { lastException = e; } - try { - int retryDelay; - if (retry == 0) { - retryDelay = initialRetryDelayMillis; - } else { - retryDelay = (int) (initialRetryDelayMillis * Math.pow(2, retry - 1)); - } - retryDelay = Math.max(retryDelay, minRetryDelayMillis); - retryDelay = Math.min(retryDelay, maxRetryDelayMillis); - Thread.sleep(retryDelay); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); + if (shouldRetry(response, retry)) { + try { + int retryDelay; + if (retry == 0) { + retryDelay = initialRetryDelayMillis; + } else { + retryDelay = (int) (initialRetryDelayMillis * Math.pow(2, retry - 1)); + } + retryDelay = Math.max(retryDelay, minRetryDelayMillis); + retryDelay = Math.min(retryDelay, maxRetryDelayMillis); + Thread.sleep(retryDelay); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + + retry++; } } @@ -58,5 +71,10 @@ public Response intercept(Chain chain) throws IOException { return response; } + + //utility function to check whether to do retry based on status codes. + private boolean shouldRetry(Response response, int retryCount) { + return response == null || (retryStatusCodes.contains(response.code()) && retryCount < maxRetries); + } }