Skip to content

Commit

Permalink
Merge pull request #498 from Kong/async429
Browse files Browse the repository at this point in the history
Retries for Async Client
  • Loading branch information
ryber authored Oct 8, 2023
2 parents a204020 + 65ec51f commit 8e9e7c6
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 3 deletions.
174 changes: 174 additions & 0 deletions unirest-bdd-tests/src/test/java/BehaviorTests/RetryAsyncTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* The MIT License
*
* Copyright for portions of unirest-java are held by Kong Inc (c) 2013.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package BehaviorTests;

import kong.unirest.core.*;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class RetryAsyncTest extends BddTest {

@Test
void whenSettingIsOff() {
MockServer.retryTimes(100, 429, 1d);
assertEquals(429, Unirest.get(MockServer.GET).asEmpty().getStatus());
assertEquals(429, Unirest.get(MockServer.GET).asString().getStatus());
assertEquals(429, Unirest.get(MockServer.GET).asBytes().getStatus());
assertEquals(429, Unirest.get(MockServer.GET).asObject(RequestCapture.class).getStatus());
}

@Test
void dontRetry_whenHeaderIsMissing(){
MockServer.retryTimes(100, 429, (String)null);
assertDidNotRetry();
}

@Test
void dontRetry_whenHeaderIsUnparseable(){
MockServer.retryTimes(100, 429, "David S Pumpkins");
assertDidNotRetry();
}

@Test
void dontRetry_whenHeaderIsNegative(){
MockServer.retryTimes(100, 429, -5.5);
assertDidNotRetry();
}

@Test
void dontRetry_whenHeaderIsEmpty(){
MockServer.retryTimes(100, 429, "");
assertDidNotRetry();
}

@Test
void willRetryAfterSeconds_AsObject() {
this.<RequestCapture>doWithRetry(r -> r.asObjectAsync(RequestCapture.class))
.assertMethod(HttpMethod.GET);
}

@Test
void willRetryAfterSeconds_AsObjectGenerics() {
List<Foo> o = Arrays.asList(
new Foo("foo"),
new Foo("bar"),
new Foo("baz")
);
MockServer.setJsonAsResponse(o);
List<Foo> cap = (List<Foo>)this.<RequestCapture>doWithRetry(r -> r.asObjectAsync(new GenericType<List<Foo>>(){}));
assertEquals(o, cap);
}

@Test
void willRetryAfterSeconds_AsString() {
MockServer.setStringResponse("Hi Mom");
String cap = this.<String>doWithRetry(r -> r.asStringAsync());
assertEquals("Hi Mom", cap);
}

@Test
void willRetryAfterSeconds_AsJson() {
MockServer.setStringResponse("{\"message\": \"Hi Mom\"}");
JsonNode cap = this.<JsonNode>doWithRetry(r -> r.asJsonAsync());
assertEquals("Hi Mom", cap.getObject().getString("message"));
}

@Test
void willRetryAfterSeconds_AsBytes() {
MockServer.setStringResponse("Hi Mom");
byte[] cap = this.<byte[]>doWithRetry(r -> r.asBytesAsync());
assertEquals("Hi Mom", new String(cap));
}

@Test
void willRetryAfterSeconds_Empty() {
doWithRetry(r -> r.asEmptyAsync());
}

@Test
void willRetryAfterSeconds_AsFile() {
Path path = Paths.get("results.json");
clearFile(path);

MockServer.setStringResponse("Hi Mom");
File cap = (File)this.doWithRetry(r -> r.asFileAsync(path.toString(), StandardCopyOption.REPLACE_EXISTING));
assertTrue(cap.exists());

clearFile(path);
}

@Test
void willReturn429OnceItExceedsMaxAttempts() {
MockServer.retryTimes(10, 429, .01);
Unirest.config().retryAfter(true, 3);

HttpResponse resp = Unirest.get(MockServer.GET).asEmpty();
assertEquals(429, resp.getStatus());
MockServer.assertRequestCount(3);
}

private void clearFile(Path path) {
try {
Files.delete(path);
} catch (Exception ignored) { }
}

private void assertDidNotRetry() {
Unirest.config().retryAfter(true);
assertEquals(429, Unirest.get(MockServer.GET).asEmpty().getStatus());
MockServer.assertRequestCount(1);
}

private <R> R doWithRetry(Function<HttpRequest, CompletableFuture<HttpResponse<R>>> bodyExtractor) {
try {
MockServer.retryTimes(1, 429, 0.01);

Unirest.config().retryAfter(true);

HttpRequest request = Unirest.get(MockServer.GET);

HttpResponse<R> response = bodyExtractor.apply(request).get();
assertEquals(200, response.getStatus());
MockServer.assertRequestCount(2);

return response.getBody();
}catch (Exception e){
throw new AssertionError(e);
}
}
}
28 changes: 25 additions & 3 deletions unirest/src/main/java/kong/unirest/core/BaseRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
import java.io.File;
import java.nio.file.CopyOption;
import java.time.Instant;
import java.util.*;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -337,8 +340,8 @@ public <T> PagedList<T> asPaged(Function<HttpRequest, HttpResponse> mappingFunct
private <E> HttpResponse<E> request(Function<RawResponse, HttpResponse<E>> transformer, Class<?> resultType){
HttpResponse<E> response = config.getClient().request(this, transformer, resultType);

callCount++;
if(config.isAutomaticRetryAfter()) {
callCount++;
var retryAfter = config.getRetryStrategy();
if(retryAfter.isRetryable(response) && callCount < config.maxRetries()) {
long waitTime = retryAfter.getWaitTime(response);
Expand All @@ -355,7 +358,26 @@ private <T> CompletableFuture<HttpResponse<T>> requestAsync(HttpRequest request,
Function<RawResponse, HttpResponse<T>> transformer,
CompletableFuture<HttpResponse<T>> callback,
Class<?> resultType){
return config.getClient().request(request, transformer, callback, resultType);
var asyncR = config.getClient().request(request, transformer, callback, resultType);
if(config.isAutomaticRetryAfter()){
return asyncR.thenApplyAsync(response -> {
callCount++;
var retryAfter = config.getRetryStrategy();
if(retryAfter.isRetryable(response) && callCount < config.maxRetries()) {
long waitTime = retryAfter.getWaitTime(response);
if (waitTime > 0) {
retryAfter.waitFor(waitTime);
try {
return requestAsync(this, transformer, callback, resultType).get();
} catch (Exception e) {
throw new UnirestException(e);
}
}
}
return response;
});
}
return asyncR;
}


Expand Down
9 changes: 9 additions & 0 deletions unirest/src/main/java/kong/unirest/core/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

/**
* The client that does the work.
*/
public interface Client {
/**
* @param <T> the underlying client
Expand Down Expand Up @@ -60,5 +63,11 @@ <T> CompletableFuture<HttpResponse<T>> request(HttpRequest request,
CompletableFuture<HttpResponse<T>> callback,
Class<?> resultType);

/**
* Create a websocket connection
* @param request the connection
* @param listener (in the voice of Cicero) the listener
* @return a WebSocketResponse
*/
WebSocketResponse websocket(WebSocketRequest request, WebSocket.Listener listener);
}
14 changes: 14 additions & 0 deletions unirest/src/main/java/kong/unirest/core/WebSocketResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,33 @@
import java.net.http.WebSocket;
import java.util.concurrent.CompletableFuture;

/**
* Just a silly little class that holds on to the socket and listener
*/
public class WebSocketResponse {
private final CompletableFuture<WebSocket> webSocketFuture;
private final WebSocket.Listener listener;

/**
* ctor
* @param webSocketFuture the ws future
* @param listener the listener
*/
public WebSocketResponse(CompletableFuture<WebSocket> webSocketFuture, WebSocket.Listener listener) {
this.webSocketFuture = webSocketFuture;
this.listener = listener;
}

/**
* @return the ws future
*/
public CompletableFuture<WebSocket> socket(){
return webSocketFuture;
}

/**
* @return the listener
*/
public WebSocket.Listener listener(){
return listener;
}
Expand Down

0 comments on commit 8e9e7c6

Please sign in to comment.