Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/main/java/co/novu/common/base/NovuConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
29 changes: 29 additions & 0 deletions src/main/java/co/novu/common/rest/IdempotencyKeyInterceptor.java
Original file line number Diff line number Diff line change
@@ -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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't necessarily need a dedicated Interceptor for this. We can make it a dynamic header by using @ Headers for the POST and PATCH calls

Copy link
Contributor Author

@git-ashug git-ashug Oct 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use interceptor for adding Idempotency header, do I need to add @Header annotation for POST and PATCH calls in all *Api classes? as that works on method level @mayorjay


@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();
}

}
19 changes: 14 additions & 5 deletions src/main/java/co/novu/common/rest/RestHandler.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,9 +16,6 @@
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import java.io.IOException;
import java.util.Map;

@RequiredArgsConstructor
public class RestHandler {

Expand All @@ -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());
}
Comment on lines +44 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason why this can't be done is the fact that we provide the Retrofit instance lazily (see lines 27 to 29). Doing this would mean that every request will use the same value as the Idempotency-Key, which is not the goal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mayorjay I tried this interceptor and it is inputting new header value for each request made


Gson gson = new GsonBuilder()
.setLenient()
Expand Down
80 changes: 80 additions & 0 deletions src/main/java/co/novu/common/rest/RetryInterceptor.java
Original file line number Diff line number Diff line change
@@ -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<Integer> 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);
}

}