Skip to content

Commit

Permalink
feat: client resilience
Browse files Browse the repository at this point in the history
  • Loading branch information
sixwaaaay committed Feb 28, 2024
1 parent d27b74b commit ab9dead
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 8 deletions.
11 changes: 11 additions & 0 deletions graal/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<description>sharing-comment</description>
<properties>
<java.version>21</java.version>
<resilience4jVersion>2.1.0</resilience4jVersion>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -112,6 +113,16 @@
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2023-2024 sixwaaaay.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.sixwaaaay.sharingcomment.client;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.decorators.Decorators;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.sixwaaaay.sharingcomment.transmission.GetMultipleUserReply;
import io.sixwaaaay.sharingcomment.transmission.GetUserReply;
import org.springframework.web.client.HttpClientErrorException;

import java.time.Duration;
import java.util.Collection;

public class UserClientWrapper implements UserClient {
private final UserClient userClient;

private final CircuitBreaker circuitBreaker;

private final Retry retry;

public UserClientWrapper(UserClient userClient) {

this.userClient = userClient;
var name = "userClient";
var retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(1000))
.ignoreExceptions(
HttpClientErrorException.NotFound.class,
HttpClientErrorException.Forbidden.class,
HttpClientErrorException.MethodNotAllowed.class,
HttpClientErrorException.Unauthorized.class
).build();
retry = Retry.of(name, retryConfig);
var circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.ignoreExceptions(
HttpClientErrorException.NotFound.class,
HttpClientErrorException.Forbidden.class,
HttpClientErrorException.MethodNotAllowed.class,
HttpClientErrorException.Unauthorized.class
)
.waitDurationInOpenState(Duration.ofMillis(1000))
.permittedNumberOfCallsInHalfOpenState(2)
.slidingWindowSize(2)
.build();
circuitBreaker = CircuitBreaker.of(name, circuitBreakerConfig);
}

@Override
public GetUserReply getUser(long id, String token) {
var getUserReplySupplier = Decorators.ofSupplier(() -> userClient.getUser(id, token))
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.decorate();
return getUserReplySupplier.get();
}


@Override
public GetMultipleUserReply getManyUser(Collection<Long> ids, String token) {
var getManyUserReplySupplier = Decorators.ofSupplier(() -> userClient.getManyUser(ids, token))
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.decorate();
return getManyUserReplySupplier.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2023-2024 sixwaaaay.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.sixwaaaay.sharingcomment.client;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.decorators.Decorators;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.sixwaaaay.sharingcomment.transmission.*;

import java.time.Duration;
import java.util.Set;

public class VoteClientWrapper implements VoteClient {


private final VoteClient voteClient;

public VoteClientWrapper(VoteClient voteClient) {
this.voteClient = voteClient;
}

private final String name = "voteClient";

private final CircuitBreaker circuitBreaker = CircuitBreaker.of(name, CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(2000))
.permittedNumberOfCallsInHalfOpenState(2)
.slidingWindowSize(2)
.build()
);

private final Retry retry = Retry.of(name, RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(1000))
.build()
);

@Override
public VoteReply itemAdd(VoteReq req) {
return voteClient.itemAdd(req);
}

@Override
public VoteReply itemDelete(VoteReq req) {
return voteClient.itemDelete(req);
}

/**
* wrap the exists method with resilience4j circuit breaker and retry
* fallback with an empty set if the circuit breaker is open
*/
@Override
public VoteExistsReply exists(VoteExistsReq req) {
var existsReplySupplier = Decorators.ofSupplier(() -> voteClient.exists(req))
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.withFallback(this::fallback)
.decorate();
return existsReplySupplier.get();
}

@Override
public ScanVotedReply scan(ScanVotedReq req) {
return voteClient.scan(req);

Check warning on line 77 in graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java

View check run for this annotation

Codecov / codecov/patch

graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java#L77

Added line #L77 was not covered by tests
}

private VoteExistsReply fallback(Throwable e) {
var reply = new VoteExistsReply();
reply.setExists(Set.of());
return reply;

Check warning on line 83 in graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java

View check run for this annotation

Codecov / codecov/patch

graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java#L81-L83

Added lines #L81 - L83 were not covered by tests
}
}
18 changes: 12 additions & 6 deletions graal/src/main/java/io/sixwaaaay/sharingcomment/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
package io.sixwaaaay.sharingcomment.config;

import io.sixwaaaay.sharingcomment.client.UserClient;
import io.sixwaaaay.sharingcomment.client.UserClientWrapper;
import io.sixwaaaay.sharingcomment.client.VoteClient;
import io.sixwaaaay.sharingcomment.client.VoteClientWrapper;
import io.sixwaaaay.sharingcomment.request.Principal;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -34,19 +36,23 @@ VoteClient repositoryService(
@Value("${service.vote.base-url}") String baseUrl,
RestClient.Builder restClientBuilder
) {
return HttpServiceProxyFactory.builderFor(
RestClientAdapter.create(restClientBuilder.baseUrl(baseUrl).build())
).build().createClient(VoteClient.class);
var client = createService(VoteClient.class, baseUrl, restClientBuilder);
return new VoteClientWrapper(client);
}

@Bean
UserClient userService(
@Value("${service.user.base-url}") String baseUrl,
RestClient.Builder restClientBuilder
) {
return HttpServiceProxyFactory.builderFor(
RestClientAdapter.create(restClientBuilder.baseUrl(baseUrl).build())
).build().createClient(UserClient.class);
var client = createService(UserClient.class, baseUrl, restClientBuilder);
return new UserClientWrapper(client);
}

private <T> T createService(Class<T> clazz, String baseUrl, RestClient.Builder restClientBuilder) {
var restClient = restClientBuilder.baseUrl(baseUrl).build();
var adapter = RestClientAdapter.create(restClient);
var proxyFactory = HttpServiceProxyFactory.builderFor(adapter);
return proxyFactory.build().createClient(clazz);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertFalse;

@SpringBootTest
@AutoConfigureMockMvc
Expand Down Expand Up @@ -101,7 +100,6 @@ public void createCommentTest() throws Exception {

@Test
public void deleteComments() throws Exception {
// todo: add payload
var token = jwtUtil.generateToken("n", "1");
var json = "{ \"content\": \"This is a test comment\", \"reply_to\": null, \"belong_to\": 1 }";
mockMvc.perform(MockMvcRequestBuilders.delete("/comments/21")
Expand Down

0 comments on commit ab9dead

Please sign in to comment.