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..0e6bb414 --- /dev/null +++ b/graal/src/main/java/io/sixwaaaay/sharingcomment/client/UserClientWrapper.java @@ -0,0 +1,70 @@ +/* + * 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 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)) + .build(); + retry = Retry.of(name, retryConfig); + var circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(50) + .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..07b4bcc9 --- /dev/null +++ b/graal/src/main/java/io/sixwaaaay/sharingcomment/client/VoteClientWrapper.java @@ -0,0 +1,81 @@ +/* + * 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(1000)) + .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); + } + + @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/main/java/io/sixwaaaay/sharingcomment/util/RetryWrapper.java b/graal/src/main/java/io/sixwaaaay/sharingcomment/util/RetryWrapper.java deleted file mode 100644 index d36ed145..00000000 --- a/graal/src/main/java/io/sixwaaaay/sharingcomment/util/RetryWrapper.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.util; - -import java.util.concurrent.Callable; -import java.util.function.Predicate; - - - -/* -example usage: -RetryWrapper retryWrapper = new RetryWrapper<>( - () -> userClient.getUser(id, token), - 3, - 1000, - 2, - e -> e instanceof IOException -); -GetUserReply reply = retryWrapper.retry(); -*/ - - -public class RetryWrapper { - private final Callable callable; - private final int maxAttempts; - private final long initialDelay; - private final double delayMultiplier; - private final Predicate retryOn; - - public RetryWrapper(Callable callable, int maxAttempts, long initialDelay, double delayMultiplier, Predicate retryOn) { - this.callable = callable; - this.maxAttempts = maxAttempts; - this.initialDelay = initialDelay; - this.delayMultiplier = delayMultiplier; - this.retryOn = retryOn; - } - - public RetryWrapper(Callable callable, int maxAttempts, long initialDelay, double delayMultiplier, Class exceptionClass) { - this.callable = callable; - this.maxAttempts = maxAttempts; - this.initialDelay = initialDelay; - this.delayMultiplier = delayMultiplier; - this.retryOn = exceptionClass::isInstance; - } - - public T retry() throws Exception { - long delay = initialDelay; - for (int i = 0; i < maxAttempts; i++) { - try { - return callable.call(); - } catch (Exception e) { - if (i == maxAttempts - 1 || !retryOn.test(e)) { - throw e; - } - Thread.sleep(delay); - delay *= (long) delayMultiplier; - } - } - throw new Exception("Retry attempts exceeded"); - } - -} 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")