Skip to content

Commit

Permalink
Add a simple retry for the rest client calls
Browse files Browse the repository at this point in the history
  • Loading branch information
marko-bekhta committed Oct 19, 2024
1 parent 7ebcf5e commit 901f89b
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ static RuntimeException toException(URI uri, Response response) {
return new JiraRestException(
"Encountered an error calling Jira REST API: %s resulting in: %s".formatted(uri,
response.hasEntity() ? response.readEntity(String.class) : "No response body"),
response.getStatus());
response.getStatus(), response.getHeaders());
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package org.hibernate.infra.replicate.jira.service.jira.client;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.hibernate.infra.replicate.jira.JiraConfig;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComment;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComments;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueBulk;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueBulkResponse;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLink;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLinkTypes;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueResponse;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssues;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraRemoteLink;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser;

import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.api.LoggingScope;

Expand All @@ -15,6 +33,7 @@
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import jakarta.ws.rs.core.Response;

public class JiraRestClientBuilder {

Expand All @@ -34,7 +53,7 @@ public static JiraRestClient of(JiraConfig.Instance jira) {
if (jira.logRequests()) {
builder.clientLogger(CustomClientLogger.INSTANCE).loggingScope(LoggingScope.REQUEST_RESPONSE);
}
return builder.build(JiraRestClient.class);
return new JiraRestClientWithRetry(builder.build(JiraRestClient.class));
}

private static class CustomClientLogger implements ClientLogger {
Expand Down Expand Up @@ -102,4 +121,184 @@ private String sanitize(String header, String value) {
}
}
}

// TODO: remove it once we figure out how to correctly integrate smallrye fault-tolerance
// (simply adding annotations on the REST interface does not work)
private static class JiraRestClientWithRetry implements JiraRestClient {

private final JiraRestClient delegate;

private JiraRestClientWithRetry(JiraRestClient delegate) {
this.delegate = delegate;
}

@Override
public JiraIssue getIssue(String key) {
return withRetry(() -> delegate.getIssue(key));
}

@Override
public JiraIssue getIssue(Long id) {
return delegate.getIssue(id);
}

@Override
public JiraIssueResponse create(JiraIssue issue) {
return withRetry(() -> delegate.create(issue));
}

@Override
public JiraIssueBulkResponse create(JiraIssueBulk bulk) {
return withRetry(() -> delegate.create(bulk));
}

@Override
public JiraIssueResponse update(String key, JiraIssue issue) {
return withRetry(() -> delegate.update(key, issue));
}

@Override
public void upsertRemoteLink(String key, JiraRemoteLink remoteLink) {
withRetry(() -> delegate.upsertRemoteLink(key, remoteLink));
}

@Override
public JiraComment getComment(Long issueId, Long commentId) {
return withRetry(() -> delegate.getComment(issueId, commentId));
}

@Override
public JiraComment getComment(String issueKey, Long commentId) {
return withRetry(() -> delegate.getComment(issueKey, commentId));
}

@Override
public JiraComments getComments(Long issueId, int startAt, int maxResults) {
return withRetry(() -> delegate.getComments(issueId, startAt, maxResults));
}

@Override
public JiraComments getComments(String issueKey, int startAt, int maxResults) {
return withRetry(() -> delegate.getComments(issueKey, startAt, maxResults));
}

@Override
public JiraIssueResponse create(String issueKey, JiraComment comment) {
return withRetry(() -> delegate.create(issueKey, comment));
}

@Override
public JiraIssueResponse update(String issueKey, String commentId, JiraComment comment) {
return withRetry(() -> delegate.update(issueKey, commentId, comment));
}

@Override
public List<JiraSimpleObject> getPriorities() {
return withRetry(delegate::getPriorities);
}

@Override
public List<JiraSimpleObject> getIssueTypes() {
return withRetry(delegate::getIssueTypes);
}

@Override
public List<JiraSimpleObject> getStatues() {
return withRetry(delegate::getStatues);
}

@Override
public JiraIssueLinkTypes getIssueLinkTypes() {
return withRetry(delegate::getIssueLinkTypes);
}

@Override
public List<JiraUser> findUser(String email) {
return withRetry(() -> delegate.findUser(email));
}

@Override
public JiraIssueLink getIssueLink(Long id) {
return withRetry(() -> delegate.getIssueLink(id));
}

@Override
public void upsertIssueLink(JiraIssueLink link) {
withRetry(() -> delegate.upsertIssueLink(link));
}

@Override
public void deleteComment(String issueKey, String commentId) {
withRetry(() -> delegate.deleteComment(issueKey, commentId));
}

@Override
public void deleteIssueLink(String linkId) {
withRetry(() -> delegate.deleteIssueLink(linkId));
}

@Override
public JiraIssues find(String query, int startAt, int maxResults) {
return withRetry(() -> delegate.find(query, startAt, maxResults));
}

@Override
public void transition(String issueKey, JiraTransition transition) {
withRetry(() -> delegate.transition(issueKey, transition));
}

private static final int RETRIES = 5;
private static final Duration WAIT_BETWEEN_RETRIES = Duration.of(2, ChronoUnit.SECONDS);

private void withRetry(Runnable runnable) {
withRetry(() -> {
runnable.run();
return null;
});
}

private <T> T withRetry(Supplier<T> supplier) {
for (int i = 0; i < RETRIES; i++) {
try {
return supplier.get();
} catch (RuntimeException exception) {
if (!shouldRetryOnException(exception)) {
throw exception;
}
}
try {
Thread.sleep(WAIT_BETWEEN_RETRIES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
throw new IllegalStateException("Shouldn't really reach this far.");
}

private boolean shouldRetryOnException(Throwable throwable) {
if (throwable instanceof JiraRestException exception) {
if (exception.statusCode() == RestResponse.StatusCode.UNAUTHORIZED
|| exception.statusCode() == RestResponse.StatusCode.FORBIDDEN) {
Log.warnf(exception,
"Will make an attempt to retry the REST API call because of the authentication problem. Response headers %s",
exception.headers());
return true;
}
if (exception.statusCode() == RestResponse.StatusCode.NOT_FOUND) {
// not found is fine :)
Log.warn("Will make no retry attempt of a REST API call for a NOT_FOUND response.");
return false;
}
if (Response.Status.Family.SERVER_ERROR
.equals(Response.Status.Family.familyOf(exception.statusCode()))) {
Log.warnf(exception,
"Will make an attempt to retry the REST API call because of the internal server problem. Response headers %s",
exception.headers());
return true;
}
}
return false;
}
}

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package org.hibernate.infra.replicate.jira.service.jira.client;

import java.util.List;
import java.util.Map;

public class JiraRestException extends RuntimeException {
private final int statusCode;
private final Map<String, List<Object>> headers;

public JiraRestException(String message, int statusCode) {
public JiraRestException(String message, int statusCode, Map<String, List<Object>> headers) {
super(message);
this.statusCode = statusCode;
this.headers = headers;
}

public int statusCode() {
return statusCode;
}

public Map<String, List<Object>> headers() {
return headers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -41,15 +42,15 @@ public class SampleJiraRestClient implements JiraRestClient {
@Override
public JiraIssue getIssue(String key) {
if (Pattern.compile(itemCannotBeFound.get()).matcher(key).matches()) {
throw new JiraRestException("No issue %s".formatted(key), 404);
throw new JiraRestException("No issue %s".formatted(key), 404, Map.of());
}
return sample(1L, key);
}

@Override
public JiraIssue getIssue(Long id) {
if (Pattern.compile(itemCannotBeFound.get()).matcher(Long.toString(id)).matches()) {
throw new JiraRestException("No issue %s".formatted(id), 404);
throw new JiraRestException("No issue %s".formatted(id), 404, Map.of());
}
return sample(id, jiraKey(id));
}
Expand Down Expand Up @@ -87,7 +88,7 @@ public void upsertRemoteLink(String key, JiraRemoteLink remoteLink) {
@Override
public JiraComment getComment(Long issueId, Long commentId) {
if (Pattern.compile(itemCannotBeFound.get()).matcher("%d - %d".formatted(issueId, commentId)).matches()) {
throw new JiraRestException("No comment %s".formatted(commentId), 404);
throw new JiraRestException("No comment %s".formatted(commentId), 404, Map.of());
}
return sampleComment(issueId, commentId);
}
Expand Down Expand Up @@ -177,7 +178,9 @@ public void deleteIssueLink(String linkId) {

@Override
public JiraIssues find(String query, int startAt, int maxResults) {
return new JiraIssues();
JiraIssues issues = new JiraIssues();
issues.issues = List.of();
return issues;
}

@Override
Expand Down

0 comments on commit 901f89b

Please sign in to comment.