Skip to content

Commit 3065662

Browse files
committed
Add OkHttp module
1 parent 77e1af3 commit 3065662

File tree

7 files changed

+360
-4
lines changed

7 files changed

+360
-4
lines changed

core/src/test/java/dev/failsafe/testing/Asserts.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public static boolean matches(Throwable actual, Class<? extends Throwable>... th
133133
return true;
134134
}
135135

136+
@SafeVarargs
137+
public static void assertMatches(Throwable actual, Class<? extends Throwable>... throwableHierarchy) {
138+
assertMatches(actual, Arrays.asList(throwableHierarchy));
139+
}
140+
136141
public static void assertMatches(Throwable actual, List<Class<? extends Throwable>> throwableHierarchy) {
137142
Throwable current = actual;
138143
for (Class<? extends Throwable> expected : throwableHierarchy) {

modules/feign/src/test/java/dev/failsafe/feign/testing/FeignTesting.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ public <T> void testInternal(FailsafeExecutor<T> failsafe, Function<FeignService
6363

6464
// Assert completion listener results
6565
ExecutionCompletedEvent<T> completedEvent = completedEventRef.get();
66-
if (expectedExInner.length > 0) {
67-
assertNull(completedEvent.getResult());
68-
assertMatches(completedEvent.getFailure(), Arrays.asList(expectedExInner));
69-
} else {
66+
if (expectedExInner.length == 0) {
7067
assertEquals(completedEvent.getResult(), expectedResult);
7168
assertNull(completedEvent.getFailure());
69+
} else {
70+
assertNull(completedEvent.getResult());
71+
assertMatches(completedEvent.getFailure(), expectedExInner);
7272
}
7373
if (then != null)
7474
then.accept(futureRef.get(), completedEvent);

modules/okhttp/pom.xml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>dev.failsafe</groupId>
8+
<artifactId>failsafe-parent</artifactId>
9+
<version>3.2.1-SNAPSHOT</version>
10+
<relativePath>../../pom.xml</relativePath>
11+
</parent>
12+
13+
<artifactId>failsafe-okhttp</artifactId>
14+
<name>Failsafe OkHttp</name>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>${project.groupId}</groupId>
19+
<artifactId>failsafe</artifactId>
20+
<version>${project.version}</version>
21+
</dependency>
22+
<dependency>
23+
<groupId>com.squareup.okhttp3</groupId>
24+
<artifactId>okhttp</artifactId>
25+
<version>4.9.3</version>
26+
</dependency>
27+
28+
<!-- Test Dependencies -->
29+
<dependency>
30+
<groupId>${project.groupId}</groupId>
31+
<artifactId>failsafe</artifactId>
32+
<version>${project.version}</version>
33+
<type>test-jar</type>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>com.github.tomakehurst</groupId>
38+
<artifactId>wiremock-jre8</artifactId>
39+
<version>2.32.0</version>
40+
<scope>test</scope>
41+
</dependency>
42+
</dependencies>
43+
</project>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.failsafe.okhttp;
2+
3+
import dev.failsafe.FailsafeExecutor;
4+
import okhttp3.Interceptor;
5+
import okhttp3.Response;
6+
import okhttp3.ResponseBody;
7+
8+
class FailsafeInterceptor implements Interceptor {
9+
private final FailsafeExecutor<Response> failsafe;
10+
private volatile ResponseBody previousResponseBody;
11+
12+
FailsafeInterceptor(FailsafeExecutor<Response> failsafe) {
13+
this.failsafe = failsafe;
14+
}
15+
16+
@Override
17+
public Response intercept(Chain chain) {
18+
return failsafe.get(() -> {
19+
// If this is a retry, ensures any previous response body is closed
20+
if (previousResponseBody != null)
21+
previousResponseBody.close();
22+
23+
Response response = chain.proceed(chain.request());
24+
previousResponseBody = response.body();
25+
return response;
26+
});
27+
}
28+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License
15+
*/
16+
package dev.failsafe.okhttp;
17+
18+
import dev.failsafe.Failsafe;
19+
import dev.failsafe.FailsafeExecutor;
20+
import dev.failsafe.Policy;
21+
import dev.failsafe.internal.util.Assert;
22+
import okhttp3.OkHttpClient;
23+
import okhttp3.OkHttpClient.Builder;
24+
import okhttp3.Response;
25+
26+
/**
27+
* Integrates Failsafe with OkHttp.
28+
*
29+
* @author Jonathan Halterman
30+
*/
31+
public final class FailsafeOkHttp {
32+
/**
33+
* Returns an OkHttp Builder for the {@code outerPolicy} and {@code policies}. See {@link Failsafe#with(Policy,
34+
* Policy[])} for docs on how policy composition works.
35+
*
36+
* @throws NullPointerException if {@code outerPolicy} is null
37+
*/
38+
@SafeVarargs
39+
public static <P extends Policy<Response>> Builder builder(P outerPolicy, P... policies) {
40+
return builder(Failsafe.with(outerPolicy, policies));
41+
}
42+
43+
/**
44+
* Returns an OkHttp Builder for the {@code failsafeExecutor}.
45+
*
46+
* @throws NullPointerException if {@code failsafeExecutor} is null
47+
*/
48+
public static Builder builder(FailsafeExecutor<Response> failsafeExecutor) {
49+
Assert.notNull(failsafeExecutor, "failsafeExecutor");
50+
return new OkHttpClient.Builder().addInterceptor(new FailsafeInterceptor(failsafeExecutor));
51+
}
52+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License
15+
*/
16+
package dev.failsafe.okhttp;
17+
18+
import com.github.tomakehurst.wiremock.WireMockServer;
19+
import dev.failsafe.*;
20+
import dev.failsafe.okhttp.testing.OkHttpTesting;
21+
import okhttp3.Request;
22+
import okhttp3.Response;
23+
import okhttp3.ResponseBody;
24+
import org.testng.annotations.AfterMethod;
25+
import org.testng.annotations.BeforeMethod;
26+
import org.testng.annotations.Test;
27+
28+
import java.time.Duration;
29+
30+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
31+
import static org.testng.Assert.assertEquals;
32+
33+
@Test
34+
public class FailsafeOkHttpTest extends OkHttpTesting {
35+
public static final String URL = "http://localhost:8080";
36+
37+
WireMockServer server;
38+
39+
@BeforeMethod
40+
protected void beforeMethod() {
41+
server = new WireMockServer();
42+
server.start();
43+
}
44+
45+
@AfterMethod
46+
protected void afterMethod() {
47+
server.stop();
48+
}
49+
50+
public void testSuccess() {
51+
// Given
52+
mockResponse(200, "foo");
53+
FailsafeExecutor<Response> failsafe = Failsafe.with(RetryPolicy.ofDefaults());
54+
Request request = requestFor("/test");
55+
56+
// When / Then
57+
testRequest(failsafe, request, (f, e) -> {
58+
assertEquals(e.getAttemptCount(), 1);
59+
assertEquals(e.getExecutionCount(), 1);
60+
}, 200, "foo");
61+
assertCalled("/test", 1);
62+
}
63+
64+
public void testRetryPolicyOn400() {
65+
// Given
66+
mockResponse(400, "foo");
67+
RetryPolicy<Response> retryPolicy = RetryPolicy.<Response>builder().handleResultIf(r -> r.code() == 400).build();
68+
FailsafeExecutor<Response> failsafe = Failsafe.with(retryPolicy);
69+
Request request = requestFor("/test");
70+
71+
// When / Then
72+
testRequest(failsafe, request, (f, e) -> {
73+
assertEquals(e.getAttemptCount(), 3);
74+
assertEquals(e.getExecutionCount(), 3);
75+
}, 400, "foo");
76+
assertCalled("/test", 3);
77+
}
78+
79+
public void testRetryPolicyOnResult() {
80+
// Given
81+
mockResponse(200, "bad");
82+
RetryPolicy<Response> retryPolicy = RetryPolicy.<Response>builder()
83+
.handleResultIf(r -> "bad".equals(r.peekBody(Long.MAX_VALUE).string()))
84+
.build();
85+
FailsafeExecutor<Response> failsafe = Failsafe.with(retryPolicy);
86+
Request request = requestFor("/test");
87+
88+
// When / Then
89+
testRequest(failsafe, request, (f, e) -> {
90+
assertEquals(e.getAttemptCount(), 3);
91+
assertEquals(e.getExecutionCount(), 3);
92+
}, 200, "bad");
93+
assertCalled("/test", 3);
94+
}
95+
96+
public void testRetryPolicyFallback() {
97+
// Given
98+
mockResponse(400, "foo");
99+
Fallback<Response> fallback = Fallback.<Response>builder(r -> {
100+
Response response = r.getLastResult();
101+
ResponseBody body = ResponseBody.create("fallback", response.body().contentType());
102+
return response.newBuilder().code(200).body(body).build();
103+
}).handleResultIf(r -> r.code() == 400).build();
104+
RetryPolicy<Response> retryPolicy = RetryPolicy.<Response>builder().handleResultIf(r -> r.code() == 400).build();
105+
FailsafeExecutor<Response> failsafe = Failsafe.with(fallback, retryPolicy);
106+
Request request = requestFor("/test");
107+
108+
// When / Then
109+
testRequest(failsafe, request, (f, e) -> {
110+
assertEquals(e.getAttemptCount(), 3);
111+
assertEquals(e.getExecutionCount(), 3);
112+
}, 200, "fallback");
113+
assertCalled("/test", 3);
114+
}
115+
116+
/**
117+
* Asserts that an open circuit breaker prevents executions from occurring, even with outer retries.
118+
*/
119+
public void testCircuitBreaker() {
120+
// Given
121+
mockResponse(200, "foo");
122+
CircuitBreaker<Response> breaker = CircuitBreaker.ofDefaults();
123+
FailsafeExecutor<Response> failsafe = Failsafe.with(RetryPolicy.ofDefaults(), breaker);
124+
Request request = requestFor("/test");
125+
breaker.open();
126+
127+
// When / Then
128+
testFailure(failsafe, request, (f, e) -> {
129+
assertEquals(e.getAttemptCount(), 3);
130+
assertEquals(e.getExecutionCount(), 0);
131+
}, CircuitBreakerOpenException.class);
132+
assertCalled("/test", 0);
133+
}
134+
135+
public void testTimeout() {
136+
// Given
137+
mockDelayedResponse(200, "foo", 1000);
138+
FailsafeExecutor<Response> failsafe = Failsafe.with(Timeout.of(Duration.ofMillis(100)));
139+
Request request = requestFor("/test");
140+
141+
// When / Then
142+
testFailure(failsafe, request, (f, e) -> {
143+
assertEquals(e.getAttemptCount(), 1);
144+
assertEquals(e.getExecutionCount(), 1);
145+
}, TimeoutExceededException.class);
146+
assertCalled("/test", 1);
147+
}
148+
149+
private Request requestFor(String path) {
150+
return new Request.Builder().url(URL + path).build();
151+
}
152+
153+
private void mockResponse(int responseCode, String body) {
154+
stubFor(get(urlPathEqualTo("/test")).willReturn(
155+
aResponse().withStatus(responseCode).withHeader("Content-Type", "text/plain").withBody(body)));
156+
}
157+
158+
private void mockDelayedResponse(int responseCode, String body, int delayMillis) {
159+
stubFor(get(urlEqualTo("/test")).willReturn(
160+
aResponse().withStatus(responseCode).withFixedDelay(delayMillis).withBody(body)));
161+
}
162+
163+
private void assertCalled(String url, int times) {
164+
verify(times, getRequestedFor(urlPathEqualTo(url)));
165+
}
166+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package dev.failsafe.okhttp.testing;
2+
3+
import dev.failsafe.FailsafeExecutor;
4+
import dev.failsafe.event.EventListener;
5+
import dev.failsafe.event.ExecutionCompletedEvent;
6+
import dev.failsafe.okhttp.FailsafeOkHttp;
7+
import dev.failsafe.testing.Testing;
8+
import okhttp3.OkHttpClient;
9+
import okhttp3.Request;
10+
import okhttp3.Response;
11+
12+
import java.util.concurrent.CompletableFuture;
13+
import java.util.concurrent.atomic.AtomicReference;
14+
15+
import static org.testng.Assert.assertEquals;
16+
import static org.testng.Assert.assertNull;
17+
18+
public class OkHttpTesting extends Testing {
19+
public <T> void testRequest(FailsafeExecutor<Response> failsafe, Request when, Then<Response> then,
20+
int expectedStatus, T expectedResult) {
21+
test(failsafe, when, then, expectedStatus, expectedResult, null);
22+
}
23+
24+
@SafeVarargs
25+
public final void testFailure(FailsafeExecutor<Response> failsafe, Request when, Then<Response> then,
26+
Class<? extends Throwable>... expectedExceptions) {
27+
test(failsafe, when, then, 0, null, expectedExceptions);
28+
}
29+
30+
private <T> void test(FailsafeExecutor<Response> failsafe, Request when, Then<Response> then, int expectedStatus,
31+
T expectedResult, Class<? extends Throwable>[] expectedExceptions) {
32+
AtomicReference<CompletableFuture<Response>> futureRef = new AtomicReference<>();
33+
AtomicReference<ExecutionCompletedEvent<Response>> completedEventRef = new AtomicReference<>();
34+
EventListener<ExecutionCompletedEvent<Response>> setCompletedEventFn = e -> {
35+
completedEventRef.set(e);
36+
};
37+
failsafe.onComplete(setCompletedEventFn);
38+
39+
// Run sync test and assert result
40+
OkHttpClient client = FailsafeOkHttp.builder(failsafe).build();
41+
if (expectedExceptions == null) {
42+
Response response = unwrapExceptions(() -> client.newCall(when).execute());
43+
String result = unwrapExceptions(() -> response.body().string());
44+
assertEquals(response.code(), expectedStatus);
45+
assertEquals(result, expectedResult);
46+
} else {
47+
assertThrows(() -> client.newCall(when).execute(), expectedExceptions);
48+
}
49+
50+
// Assert completion listener results
51+
ExecutionCompletedEvent<Response> completedEvent = completedEventRef.get();
52+
if (expectedExceptions == null) {
53+
assertEquals(completedEvent.getResult().code(), expectedStatus);
54+
assertNull(completedEvent.getFailure());
55+
} else {
56+
assertNull(completedEvent.getResult());
57+
assertMatches(completedEvent.getFailure(), expectedExceptions);
58+
}
59+
if (then != null)
60+
then.accept(futureRef.get(), completedEvent);
61+
}
62+
}

0 commit comments

Comments
 (0)