Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions bot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@
</dependency>

<!-- Tests -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down Expand Up @@ -130,10 +136,31 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.1</version>
<scope>test</scope>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>jcache</artifactId>
</dependency>
<dependency>
<groupId>edu.java</groupId>
<artifactId>retry</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>

Expand Down
5 changes: 4 additions & 1 deletion bot/src/main/java/edu/java/bot/BotApplication.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package edu.java.bot;

import edu.java.RetryQueryConfiguration;
import edu.java.bot.configuration.ApplicationConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableConfigurationProperties(ApplicationConfig.class)
@EnableCaching
@EnableConfigurationProperties({ApplicationConfig.class, RetryQueryConfiguration.class})
public class BotApplication {
public static void main(String[] args) {
SpringApplication.run(BotApplication.class, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package edu.java.bot.configuration;

import edu.java.RetryFactory;
import edu.java.RetryQueryConfiguration;
import edu.java.bot.client.scrapper.ScrapperClient;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -9,6 +11,7 @@
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@Configuration
@Log4j2
Expand All @@ -18,10 +21,12 @@ public class ScrapperClientConfiguration {
private String scrapperUrl;

@Bean
public ScrapperClient scrapperClient() {
public ScrapperClient scrapperClient(RetryQueryConfiguration retryQueryConfiguration) {
Retry retry = RetryFactory.createRetry(retryQueryConfiguration, "scrapper");
WebClient webClient = WebClient.builder()
.defaultStatusHandler(httpStatusCode -> true, clientResponse -> Mono.empty())
.defaultHeader("Content-Type", "application/json")
.filter(RetryFactory.createFilter(retry))
.baseUrl(scrapperUrl).build();

HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
Expand Down
30 changes: 30 additions & 0 deletions bot/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ spring:
name: bot
jackson:
time-zone: UTC
cache:
jcache:
provider: com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
caffeine:
spec: maximumSize=100000,expireAfterAccess=3600s
cache-names:
- rate-limit-bucket

server:
port: 8090
Expand All @@ -19,3 +26,26 @@ springdoc:

scrapper:
url: http://localhost:8080

retry-query:
retries:
- target: scrapper
type: exponential
max-attempts: 3
min-delay: 1s
max-delay: 10s
codes: 429

bucket4j:
enabled: true
filters:
- cache-name: rate-limit-bucket
url: .*
http-status-code: too_many_requests
rate-limits:
- bandwidths:
- capacity: 1000
time: 1
unit: hours
refill-speed: interval
cache-key: getRemoteAddr()
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<module>bot</module>
<module>scrapper</module>
<module>scrapper-jooq</module>
<module>retry</module>
</modules>

<dependencyManagement>
Expand Down
47 changes: 47 additions & 0 deletions retry/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>edu.java</groupId>
<artifactId>root</artifactId>
<version>${revision}</version>
</parent>

<artifactId>retry</artifactId>
<version>${revision}</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

</project>
21 changes: 21 additions & 0 deletions retry/src/main/java/edu/java/ErrorFilterPredicate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package edu.java;

import java.util.List;
import java.util.function.Predicate;
import org.springframework.web.reactive.function.client.WebClientResponseException;

public class ErrorFilterPredicate implements Predicate<Throwable> {
private final List<Integer> retryCodes;

public ErrorFilterPredicate(List<Integer> retryCodes) {
this.retryCodes = retryCodes;
}

@Override
public boolean test(Throwable throwable) {
if (throwable instanceof WebClientResponseException e) {
return retryCodes.contains(e.getStatusCode().value());
}
return true;
}
}
93 changes: 93 additions & 0 deletions retry/src/main/java/edu/java/LinearRetryBackoffSpec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package edu.java;

import java.time.Duration;
import java.util.function.Predicate;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.reactivestreams.Publisher;
import org.springframework.retry.ExhaustedRetryException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;


@RequiredArgsConstructor
public class LinearRetryBackoffSpec extends Retry {
private static final Duration MAX_BACKOFF = Duration.ofMillis(Long.MAX_VALUE);
private final Duration minBackoff;
private final Duration maxBackoff;
private final double factor;
private final int maxAttempts;
private final Predicate<Throwable> errorFilter;
private final Supplier<Scheduler> schedulerSupplier;

public LinearRetryBackoffSpec factor(double factor) {
return new LinearRetryBackoffSpec(
this.minBackoff,
this.maxBackoff,
factor,
this.maxAttempts,
this.errorFilter,
this.schedulerSupplier
);
}

public LinearRetryBackoffSpec filter(Predicate<Throwable> errorFilter) {
return new LinearRetryBackoffSpec(
this.minBackoff,
this.maxBackoff,
this.factor,
this.maxAttempts,
errorFilter,
this.schedulerSupplier
);
}

public static LinearRetryBackoffSpec linear(int maxAttempts, Duration minDelay) {
return new LinearRetryBackoffSpec(
minDelay,
MAX_BACKOFF,
1.0,
maxAttempts,
e -> true,
Schedulers::parallel
);
}

@Override
public Publisher<?> generateCompanion(Flux<RetrySignal> retrySignals) {
return Flux.deferContextual(cv ->
retrySignals.contextWrite(cv)
.concatMap(retryWhenState -> {
RetrySignal copy = retryWhenState.copy();
Throwable currentFailure = copy.failure();
long iteration = copy.totalRetries();
if (currentFailure == null) {
return Mono.error(new IllegalStateException(
"Retry.RetrySignal#failure() not expected to be null"));
}
if (!errorFilter.test(currentFailure)) {
return Mono.error(currentFailure);
}
if (iteration >= maxAttempts) {
return Mono.error(new ExhaustedRetryException("Retry exhausted: " + this));
}

Duration nextBackoff;
try {
nextBackoff = minBackoff.multipliedBy((long) (iteration * factor));
if (nextBackoff.compareTo(maxBackoff) > 0) {
nextBackoff = maxBackoff;
}
} catch (ArithmeticException overflow) {
nextBackoff = maxBackoff;
}

return Mono.delay(nextBackoff, schedulerSupplier.get()).contextWrite(cv);
})
.onErrorStop()
);
}
}
16 changes: 16 additions & 0 deletions retry/src/main/java/edu/java/RetryElement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.java;

import java.time.Duration;
import java.util.List;
import org.jetbrains.annotations.NotNull;

public record RetryElement(
@NotNull String target,
@NotNull String type,
int maxAttempts,
double factor,
Duration minDelay,
Duration maxDelay,
List<Integer> codes
) {
}
41 changes: 41 additions & 0 deletions retry/src/main/java/edu/java/RetryFactory.java
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

чтобы не создавать и хранить все политики, а использовать только нужное -
лучше сделать конфигурацию и использовать  @ConditionalOnProperty для выбора политики повтора

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я попробовал применить @ConditionalOnProperty и, мне кажется, он не очень здесь подходит, так как у меня для каждого сервиса могут быть различные типы и указывать для каждого с помощью этой аннотации не удобно. Если подскажете как красиво это сделать, то я могу попытаться, но я бы хотел оставить так

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

сейчас модульный монолит, а если далее менять архитектуру и разделять сервисы?
можешь просто сделать для каждого сервиса свою retry-конфигурацию (да, они будут почти одинаковые)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Для каждого сервиса сделал свою retry-конфигурацию в дз9

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package edu.java;

import edu.java.builders.ExponentialRetryBuilder;
import edu.java.builders.FixedRetryBuilder;
import edu.java.builders.LinearRetryBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import lombok.experimental.UtilityClass;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@UtilityClass
public class RetryFactory {

private static final Map<String, Function<RetryElement, Retry>> RETRY_BUILDERS = new HashMap<>();

static {
RETRY_BUILDERS.put("fixed", new FixedRetryBuilder());
RETRY_BUILDERS.put("linear", new LinearRetryBuilder());
RETRY_BUILDERS.put("exponential", new ExponentialRetryBuilder());
}

public static ExchangeFilterFunction createFilter(Retry retry) {
return (response, next) -> next.exchange(response)
.flatMap(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.createError();
} else {
return Mono.just(clientResponse);
}
}).retryWhen(retry);
}

public static Retry createRetry(RetryQueryConfiguration config, String target) {
return config.retries().stream().filter(element -> element.target().equals(target)).findFirst()
.map(element -> RETRY_BUILDERS.get(element.type()).apply(element))
.orElseThrow(() -> new RuntimeException("Unknown target " + target));
}
}
10 changes: 10 additions & 0 deletions retry/src/main/java/edu/java/RetryQueryConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package edu.java;

import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "retry-query", ignoreUnknownFields = false)
public record RetryQueryConfiguration(List<RetryElement> retries) {
}
16 changes: 16 additions & 0 deletions retry/src/main/java/edu/java/builders/ExponentialRetryBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.java.builders;

import edu.java.ErrorFilterPredicate;
import edu.java.RetryElement;
import java.util.function.Function;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;

public class ExponentialRetryBuilder implements Function<RetryElement, Retry> {
@Override
public Retry apply(RetryElement retryElement) {
return RetryBackoffSpec.backoff(retryElement.maxAttempts(), retryElement.minDelay())
.maxBackoff(retryElement.maxDelay())
.filter(new ErrorFilterPredicate(retryElement.codes()));
}
}
Loading