From ab9dead4d29d66bb136ec5330b9470f1105bdced Mon Sep 17 00:00:00 2001 From: sixwaaaay Date: Thu, 22 Feb 2024 14:42:56 +0800 Subject: [PATCH] feat: client resilience --- graal/pom.xml | 11 +++ .../client/UserClientWrapper.java | 82 ++++++++++++++++++ .../client/VoteClientWrapper.java | 85 +++++++++++++++++++ .../sharingcomment/config/Config.java | 18 ++-- .../SharingCommentApplicationTests.java | 2 - 5 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 graal/src/main/java/io/sixwaaaay/sharingcomment/client/UserClientWrapper.java create mode 100644 graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java diff --git a/graal/pom.xml b/graal/pom.xml index ca8b919b..7b006cde 100644 --- a/graal/pom.xml +++ b/graal/pom.xml @@ -28,6 +28,7 @@ sharing-comment 21 + 2.1.0 @@ -112,6 +113,16 @@ 0.12.3 runtime + + io.github.resilience4j + resilience4j-circuitbreaker + ${resilience4jVersion} + + + io.github.resilience4j + resilience4j-all + ${resilience4jVersion} + diff --git a/graal/src/main/java/io/sixwaaaay/sharingcomment/client/UserClientWrapper.java b/graal/src/main/java/io/sixwaaaay/sharingcomment/client/UserClientWrapper.java new file mode 100644 index 00000000..e1dbb84d --- /dev/null +++ b/graal/src/main/java/io/sixwaaaay/sharingcomment/client/UserClientWrapper.java @@ -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 ids, String token) { + var getManyUserReplySupplier = Decorators.ofSupplier(() -> userClient.getManyUser(ids, token)) + .withCircuitBreaker(circuitBreaker) + .withRetry(retry) + .decorate(); + return getManyUserReplySupplier.get(); + } +} diff --git a/graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java b/graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java new file mode 100644 index 00000000..ed3313df --- /dev/null +++ b/graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java @@ -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); + } + + private VoteExistsReply fallback(Throwable e) { + var reply = new VoteExistsReply(); + reply.setExists(Set.of()); + return reply; + } +} diff --git a/graal/src/main/java/io/sixwaaaay/sharingcomment/config/Config.java b/graal/src/main/java/io/sixwaaaay/sharingcomment/config/Config.java index dbf44690..562fea0c 100644 --- a/graal/src/main/java/io/sixwaaaay/sharingcomment/config/Config.java +++ b/graal/src/main/java/io/sixwaaaay/sharingcomment/config/Config.java @@ -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; @@ -34,9 +36,8 @@ 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 @@ -44,9 +45,14 @@ 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 createService(Class 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); + } } diff --git a/graal/src/test/java/io/sixwaaaay/sharingcomment/SharingCommentApplicationTests.java b/graal/src/test/java/io/sixwaaaay/sharingcomment/SharingCommentApplicationTests.java index 4e613879..af5b2f12 100644 --- a/graal/src/test/java/io/sixwaaaay/sharingcomment/SharingCommentApplicationTests.java +++ b/graal/src/test/java/io/sixwaaaay/sharingcomment/SharingCommentApplicationTests.java @@ -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 @@ -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")