diff --git a/src/main/java/co/novu/common/base/NovuConfig.java b/src/main/java/co/novu/common/base/NovuConfig.java index c3a2ce4b..e071d6e1 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 = 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/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..602a71f7 --- /dev/null +++ b/src/main/java/co/novu/common/rest/RetryInterceptor.java @@ -0,0 +1,80 @@ +package co.novu.common.rest; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +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; + 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 + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Response response = null; + IOException lastException = null; + + int retry = 0; + while(!response.isSuccessful() && retry < maxRetries) { + try { + response = chain.proceed(request); + } catch (IOException e) { + lastException = e; + } + + 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++; + } + } + + // If all retries failed, throw the last exception + if (lastException != null) { + throw lastException; + } + + 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); + } + +}