├── .gitignore ├── .idea └── compiler.xml ├── README.md ├── agent-smith-service ├── build.gradle └── src │ └── main │ ├── java │ └── ru │ │ └── spring │ │ └── demo │ │ └── reactive │ │ └── smith │ │ ├── AgentSmithApplication.java │ │ ├── configuration │ │ └── ReactiveConfiguration.java │ │ ├── decider │ │ └── GuardDecider.java │ │ └── notifier │ │ └── Notifier.java │ └── resources │ ├── application.yml │ └── logback-spring.xml ├── big-brother-service ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── java │ │ └── ru │ │ │ └── spring │ │ │ └── demo │ │ │ └── reactive │ │ │ └── bigbro │ │ │ ├── BigBrotherApplication.java │ │ │ ├── configuration │ │ │ └── ReactiveConfiguration.java │ │ │ └── services │ │ │ └── LetterDecoder.java │ └── resources │ │ ├── application.yml │ │ └── logback-spring.xml │ └── test │ └── java │ └── com │ └── naya │ └── gameofthrones │ └── signuterdecoderinformer │ └── BigBrotherApplicationTests.java ├── build.gradle ├── console-dashboard ├── build.gradle └── src │ └── main │ ├── groovy │ └── ru │ │ └── spring │ │ └── demo │ │ └── reactive │ │ └── dashboard │ │ └── console │ │ ├── ConsoleDashboard.groovy │ │ ├── ConsoleDashboardApplication.java │ │ ├── DashboardProperties.java │ │ ├── model │ │ └── RateStatus.java │ │ └── service │ │ └── FetchRatesService.groovy │ └── resources │ └── application.yml ├── gradle ├── logback-spring.xml └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── http-requests ├── requests.http └── rest-client.env.json ├── metrics-starter ├── build.gradle └── src │ └── main │ ├── java │ └── ru │ │ └── spring │ │ └── demo │ │ └── reactive │ │ └── starter │ │ └── metrics │ │ ├── RatesMetricsConfiguration.java │ │ └── RatesMetricsEndpoint.java │ └── resources │ └── META-INF │ └── spring.factories ├── pechkin-service ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── java │ │ └── ru │ │ │ └── spring │ │ │ └── demo │ │ │ └── reactive │ │ │ └── pechkin │ │ │ ├── PechkinApplication.java │ │ │ ├── configuration │ │ │ ├── FakeDataConfiguration.java │ │ │ └── ReactiveConfiguration.java │ │ │ ├── controllers │ │ │ └── FeedbackController.java │ │ │ ├── producer │ │ │ └── LetterProducer.java │ │ │ └── services │ │ │ └── LetterDistributor.java │ └── resources │ │ ├── application.yml │ │ └── logback-spring.xml │ └── test │ └── java │ └── ru │ └── spring │ └── demo │ └── reactive │ └── pechkin │ └── producer │ └── LetterProducerTest.java ├── settings.gradle ├── speed-adjuster-starter ├── .gitignore ├── build.gradle └── src │ └── main │ ├── java │ └── ru │ │ └── spring │ │ └── demo │ │ └── reactive │ │ └── starter │ │ └── speed │ │ ├── AdjustmentProperties.java │ │ ├── configuration │ │ └── SpeedAdjusterConfiguration.java │ │ ├── controllers │ │ └── RequestController.java │ │ ├── model │ │ ├── DecodedLetter.java │ │ ├── Letter.java │ │ └── Notification.java │ │ ├── rsocket │ │ └── ReconnectingRSocket.java │ │ └── services │ │ └── LetterRequesterService.java │ └── resources │ └── META-INF │ └── spring.factories └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | ### JetBrains template 8 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 9 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 10 | 11 | # User-specific stuff 12 | .idea/ 13 | 14 | # Gradle 15 | # CMake 16 | cmake-build-debug/ 17 | cmake-build-release/ 18 | 19 | # Mongo Explorer plugin 20 | .idea/**/mongoSettings.xml 21 | 22 | # File-based project format 23 | *.iws 24 | 25 | # IntelliJ 26 | out/ 27 | 28 | # mpeltonen/sbt-idea plugin 29 | .idea_modules/ 30 | 31 | # JIRA plugin 32 | atlassian-ide-plugin.xml 33 | 34 | # Cursive Clojure plugin 35 | .idea/replstate.xml 36 | 37 | # Crashlytics plugin (for Android Studio and IntelliJ) 38 | com_crashlytics_export_strings.xml 39 | crashlytics.properties 40 | crashlytics-build.properties 41 | fabric.properties 42 | 43 | # Editor-based Rest Client 44 | .idea/httpRequests 45 | ### Gradle template 46 | .gradle 47 | /build/ 48 | 49 | # Ignore Gradle GUI config 50 | gradle-app.setting 51 | 52 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 53 | !gradle-wrapper.jar 54 | 55 | # Cache of project 56 | .gradletasknamecache 57 | 58 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 59 | # gradle/wrapper/gradle-wrapper.properties 60 | ### Java template 61 | # Compiled class file 62 | *.class 63 | 64 | # Log file 65 | *.log 66 | 67 | # BlueJ files 68 | *.ctxt 69 | 70 | # Mobile Tools for Java (J2ME) 71 | .mtj.tmp/ 72 | 73 | # Package Files # 74 | *.jar 75 | *.war 76 | *.nar 77 | *.ear 78 | *.zip 79 | *.tar.gz 80 | *.rar 81 | 82 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 83 | hs_err_pid* 84 | 85 | !/.idea/compiler.xml 86 | ### macOS template 87 | # General 88 | .DS_Store 89 | .AppleDouble 90 | .LSOverride 91 | 92 | # Icon must end with two \r 93 | Icon 94 | 95 | # Thumbnails 96 | ._* 97 | 98 | # Files that might appear in the root of a volume 99 | .DocumentRevisions-V100 100 | .fseventsd 101 | .Spotlight-V100 102 | .TemporaryItems 103 | .Trashes 104 | .VolumeIcon.icns 105 | .com.apple.timemachine.donotpresent 106 | 107 | # Directories potentially created on remote AFP share 108 | .AppleDB 109 | .AppleDesktop 110 | Network Trash Folder 111 | Temporary Items 112 | .apdisk 113 | 114 | 115 | *.iml 116 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring reactive programming demo 2 | 3 | Projects: 4 | 1. `console-dashboard` — spring boot app for grab metrics from all service and visualize it in terminal. `./gradlew :console-dashboard:bootRun` 5 | 2. `metrics-starter` — starter for expose metrics from service. Use this metrics in console-dashboard 6 | 3. `speed-adjuster-starter` — starter. Provide shared logic for request(n) through http and use in each demo service 7 | 4. `pechkin-service` — generate letter and push to `big-brother-service` 8 | 5. `big-brother-service` — decode letter and send it to `agent-smith-service` 9 | 6. `agent-smith-service` — analyse decoded letter and send feedback to `pechking-service` 10 | 11 | -------------------------------------------------------------------------------- /agent-smith-service/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.springframework.boot' 2 | 3 | dependencies { 4 | implementation project(':speed-adjuster-starter'), 5 | project(':metrics-starter'), 6 | 'com.github.CollaborationInEncapsulation.spring-boot-rsocket:spring-boot-starter-rsocket' 7 | 8 | runtimeClasspath 'org.springframework.boot:spring-boot-devtools' 9 | } 10 | 11 | -------------------------------------------------------------------------------- /agent-smith-service/src/main/java/ru/spring/demo/reactive/smith/AgentSmithApplication.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.smith; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.scheduling.annotation.EnableAsync; 8 | 9 | import java.util.concurrent.RejectedExecutionHandler; 10 | 11 | @Slf4j 12 | @EnableAsync 13 | @SpringBootApplication 14 | public class AgentSmithApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(AgentSmithApplication.class, args); 18 | } 19 | 20 | @Bean 21 | public RejectedExecutionHandler rejectedExecutionHandler() { 22 | return (r, executor) -> log.info("Miss. Not enough soldiers!"); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /agent-smith-service/src/main/java/ru/spring/demo/reactive/smith/configuration/ReactiveConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.smith.configuration; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.rsocket.AbstractRSocket; 5 | import io.rsocket.Payload; 6 | import io.rsocket.SocketAcceptor; 7 | import lombok.SneakyThrows; 8 | import org.reactivestreams.Publisher; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | import ru.spring.demo.reactive.smith.decider.GuardDecider; 14 | import ru.spring.demo.reactive.starter.speed.model.DecodedLetter; 15 | 16 | import java.util.concurrent.ThreadPoolExecutor; 17 | 18 | @Configuration 19 | public class ReactiveConfiguration { 20 | 21 | @Bean 22 | public SocketAcceptor socketAcceptor( 23 | GuardDecider guardDecider, 24 | ObjectMapper mapper, 25 | ThreadPoolExecutor threadPoolExecutor 26 | ) { 27 | return (setup, sendingSocket) -> Mono.just(new AbstractRSocket() { 28 | @Override 29 | public Flux requestChannel(Publisher payloads) { 30 | return Flux.from(payloads) 31 | .map(payload -> getDecodedLetter(payload, mapper)) 32 | .flatMap(guardDecider::decideDeferred, 33 | threadPoolExecutor.getMaximumPoolSize() 34 | ) 35 | .log() 36 | .thenMany(Flux.empty()); 37 | } 38 | }); 39 | } 40 | 41 | @SneakyThrows 42 | private DecodedLetter getDecodedLetter(Payload payload, ObjectMapper mapper) { 43 | return mapper.readValue(payload.getData().array(), DecodedLetter.class); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /agent-smith-service/src/main/java/ru/spring/demo/reactive/smith/decider/GuardDecider.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.smith.decider; 2 | 3 | import io.micrometer.core.instrument.Counter; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | import reactor.core.publisher.Mono; 10 | import reactor.core.scheduler.Schedulers; 11 | import ru.spring.demo.reactive.smith.notifier.Notifier; 12 | import ru.spring.demo.reactive.starter.speed.AdjustmentProperties; 13 | import ru.spring.demo.reactive.starter.speed.model.DecodedLetter; 14 | import ru.spring.demo.reactive.starter.speed.model.Notification; 15 | import ru.spring.demo.reactive.starter.speed.services.LetterRequesterService; 16 | 17 | import java.util.concurrent.BlockingQueue; 18 | import java.util.concurrent.ThreadPoolExecutor; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | @Slf4j 22 | @Service 23 | public class GuardDecider { 24 | private final AdjustmentProperties adjustmentProperties; 25 | private final Notifier notifier; 26 | private final Counter counter; 27 | private final ThreadPoolExecutor letterProcessorExecutor; 28 | private final LetterRequesterService letterRequesterService; 29 | private final BlockingQueue workQueue; 30 | 31 | public GuardDecider( 32 | AdjustmentProperties adjustmentProperties, 33 | Notifier notifier, 34 | MeterRegistry meterRegistry, 35 | ThreadPoolExecutor letterProcessorExecutor, 36 | LetterRequesterService letterRequesterService) { 37 | this.adjustmentProperties = adjustmentProperties; 38 | this.notifier = notifier; 39 | this.letterProcessorExecutor = letterProcessorExecutor; 40 | this.letterRequesterService = letterRequesterService; 41 | 42 | counter = meterRegistry.counter("letter.rps"); 43 | workQueue = letterProcessorExecutor.getQueue(); 44 | } 45 | 46 | public void decide(DecodedLetter notification) { 47 | letterProcessorExecutor.execute( 48 | getCommand(notification) 49 | ); 50 | } 51 | 52 | private GuardTask getCommand(DecodedLetter notification) { 53 | return new GuardTask( 54 | adjustmentProperties, 55 | notification, 56 | notifier, 57 | counter, 58 | letterRequesterService, 59 | workQueue 60 | ); 61 | } 62 | 63 | public Mono decideDeferred(DecodedLetter decodedLetter) { 64 | return Mono.fromRunnable(getCommand(decodedLetter)) 65 | .subscribeOn(Schedulers.fromExecutor(letterProcessorExecutor)); 66 | } 67 | 68 | @Slf4j 69 | @RequiredArgsConstructor 70 | public static class GuardTask implements Runnable { 71 | private final AdjustmentProperties adjustmentProperties; 72 | private final DecodedLetter decodedLetter; 73 | private final Notifier notifier; 74 | private final Counter counter; 75 | private final LetterRequesterService letterRequesterService; 76 | private final BlockingQueue workQueue; 77 | 78 | @SneakyThrows 79 | private String getDecision() { 80 | TimeUnit.MILLISECONDS.sleep(adjustmentProperties.getProcessingTime()); 81 | int decision = (int) ((Math.random() * (2)) + 1); 82 | if(decision == 1) { 83 | return "Nothing"; 84 | } else { 85 | return "Block"; 86 | } 87 | } 88 | 89 | @Override 90 | public void run() { 91 | String decision = getDecision(); 92 | 93 | Notification notification = Notification.builder() 94 | .author(decodedLetter.getAuthor()) 95 | .action(decision) 96 | .build(); 97 | 98 | notifier.sendNotification(notification); 99 | counter.increment(); 100 | // if(workQueue.size() == 0) { 101 | // letterRequesterService.request(letterRequesterService.getAdjustmentProperties().getLetterProcessorConcurrencyLevel()); 102 | // } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /agent-smith-service/src/main/java/ru/spring/demo/reactive/smith/notifier/Notifier.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.smith.notifier; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.web.client.RestTemplate; 8 | import ru.spring.demo.reactive.starter.speed.model.Notification; 9 | 10 | 11 | @Slf4j 12 | @Service 13 | @RequiredArgsConstructor 14 | public class Notifier { 15 | private final RestTemplate restTemplate; 16 | 17 | public void sendNotification(Notification notification) { 18 | try { 19 | restTemplate.postForObject("http://localhost:8080/letter-status", notification, ResponseEntity.class); 20 | log.info("Guard notification sent"); 21 | } catch (Exception e) { 22 | log.error("no sender url found"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /agent-smith-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server.port: 8082 2 | 3 | datasender: 4 | letter-box-size: 10 5 | letter-processor-concurrency-level: 2 6 | url: http://localhost:8081/request 7 | processing-time: 500 8 | 9 | management: 10 | endpoints: 11 | web: 12 | exposure: 13 | include: '*' 14 | logging: 15 | level: 16 | org.springframework.boot.devtools.autoconfigure: DEBUG 17 | 18 | spring: 19 | devtools: 20 | livereload: 21 | port: 10510 22 | -------------------------------------------------------------------------------- /agent-smith-service/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | ../../../../gradle/logback-spring.xml -------------------------------------------------------------------------------- /big-brother-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /big-brother-service/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.springframework.boot' 2 | 3 | dependencies { 4 | implementation project(':speed-adjuster-starter'), 5 | project(':metrics-starter'), 6 | 'com.github.CollaborationInEncapsulation.spring-boot-rsocket:spring-boot-starter-rsocket' 7 | 8 | runtimeClasspath 'org.springframework.boot:spring-boot-devtools' 9 | } 10 | -------------------------------------------------------------------------------- /big-brother-service/src/main/java/ru/spring/demo/reactive/bigbro/BigBrotherApplication.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.bigbro; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | import org.springframework.web.reactive.config.EnableWebFlux; 9 | 10 | @Slf4j 11 | @EnableAsync 12 | @EnableWebFlux 13 | @EnableScheduling 14 | @SpringBootApplication 15 | public class BigBrotherApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(BigBrotherApplication.class, args); 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /big-brother-service/src/main/java/ru/spring/demo/reactive/bigbro/configuration/ReactiveConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.bigbro.configuration; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.rsocket.*; 5 | import io.rsocket.transport.netty.client.WebsocketClientTransport; 6 | import io.rsocket.util.DefaultPayload; 7 | import lombok.SneakyThrows; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.reactivestreams.Publisher; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import reactor.core.publisher.DirectProcessor; 13 | import reactor.core.publisher.Flux; 14 | import reactor.core.publisher.Mono; 15 | import reactor.core.scheduler.Schedulers; 16 | import reactor.netty.http.client.HttpClient; 17 | import reactor.netty.tcp.TcpClient; 18 | import ru.spring.demo.reactive.bigbro.services.LetterDecoder; 19 | import ru.spring.demo.reactive.starter.speed.model.DecodedLetter; 20 | import ru.spring.demo.reactive.starter.speed.model.Letter; 21 | import ru.spring.demo.reactive.starter.speed.rsocket.ReconnectingRSocket; 22 | 23 | import java.time.Duration; 24 | import java.util.concurrent.ThreadPoolExecutor; 25 | 26 | @Slf4j 27 | @Configuration 28 | public class ReactiveConfiguration { 29 | 30 | @Bean 31 | public RSocket guardRSocket() { 32 | return new ReconnectingRSocket( 33 | RSocketFactory.connect() 34 | .transport(WebsocketClientTransport.create( 35 | HttpClient.from(TcpClient.create() 36 | .host("localhost") 37 | .port(8082)), 38 | "/rs" 39 | )) 40 | .start(), Duration.ofMillis(200), Duration.ofMillis(1000)); 41 | 42 | 43 | } 44 | 45 | 46 | final DirectProcessor directProcessor = DirectProcessor.create(); 47 | 48 | 49 | 50 | 51 | @Bean 52 | public SocketAcceptor ioRsocketSocketAcceptor( 53 | ThreadPoolExecutor letterProcessorExecutor, 54 | ObjectMapper objectMapper, 55 | LetterDecoder decoder, 56 | RSocket guardRSocket 57 | ) { 58 | return ((setup, sendingSocket) -> Mono.just(new AbstractRSocket() { 59 | @Override 60 | public Flux requestChannel(Publisher payloads) { 61 | guardRSocket.requestChannel(directProcessor.onBackpressureDrop()) 62 | .subscribe(); 63 | 64 | 65 | return Flux.from(payloads) 66 | // .onBackpressureBuffer(256) 67 | // .onBackpressureDrop(payload -> log.error("Drop {}", payload.getDataUtf8())) 68 | .map(this::convertToLetter) 69 | .flatMap(letter -> Mono.fromCallable(() -> decoder.decode(letter)) 70 | .subscribeOn(Schedulers.fromExecutor(letterProcessorExecutor)), 71 | letterProcessorExecutor.getMaximumPoolSize() + 1) 72 | .doOnRequest(value -> log.info("request seq {}", value)) 73 | .doOnError(throwable -> log.error("payloads error", throwable)) 74 | .map(this::convertToPayload) 75 | .subscribeWith(directProcessor) 76 | .thenMany(Flux.empty()); 77 | // .compose(guardRSocket::requestChannel) 78 | // .thenMany(Flux.empty()) 79 | // .doOnError(t -> log.error("Got Error in sending", t)); 80 | } 81 | 82 | @SneakyThrows 83 | private Letter convertToLetter(Payload payload) { 84 | return objectMapper.readValue(payload.getData().array(), Letter.class); 85 | } 86 | 87 | @SneakyThrows 88 | private Payload convertToPayload(DecodedLetter payload) { 89 | return DefaultPayload.create(objectMapper.writeValueAsBytes(payload)); 90 | } 91 | })); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /big-brother-service/src/main/java/ru/spring/demo/reactive/bigbro/services/LetterDecoder.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.bigbro.services; 2 | 3 | import io.micrometer.core.instrument.Counter; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import lombok.SneakyThrows; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import ru.spring.demo.reactive.starter.speed.AdjustmentProperties; 9 | import ru.spring.demo.reactive.starter.speed.model.DecodedLetter; 10 | import ru.spring.demo.reactive.starter.speed.model.Letter; 11 | 12 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 13 | 14 | /** 15 | * @author Evgeny Borisov 16 | */ 17 | @Service 18 | @Slf4j 19 | public class LetterDecoder { 20 | private final AdjustmentProperties adjustmentProperties; 21 | private final Counter counter; 22 | 23 | public LetterDecoder(AdjustmentProperties adjustmentProperties, 24 | MeterRegistry meterRegistry) { 25 | this.adjustmentProperties = adjustmentProperties; 26 | counter = meterRegistry.counter("letter.rps"); 27 | } 28 | 29 | @SneakyThrows 30 | public DecodedLetter decode(Letter letter) { 31 | MILLISECONDS.sleep( 32 | adjustmentProperties.getProcessingTime() 33 | ); 34 | 35 | counter.increment(); 36 | 37 | return DecodedLetter.builder() 38 | .author(letter.secretMethodForDecodeSignature()) 39 | .location(letter.getLocation()) 40 | .content(letter.getContent()) 41 | .build(); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /big-brother-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | datasender: 2 | slow-multiplier: 1 3 | letter-processor-concurrency-level: 8 4 | url: http://localhost:8080/request 5 | 6 | server: 7 | port: 8081 8 | logging: 9 | level: 10 | root: info 11 | management: 12 | endpoints: 13 | web: 14 | exposure: 15 | include: '*' 16 | 17 | spring: 18 | devtools: 19 | livereload: 20 | port: 10501 21 | -------------------------------------------------------------------------------- /big-brother-service/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | ../../../../gradle/logback-spring.xml -------------------------------------------------------------------------------- /big-brother-service/src/test/java/com/naya/gameofthrones/signuterdecoderinformer/BigBrotherApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.naya.gameofthrones.signuterdecoderinformer; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class BigBrotherApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.1.3.RELEASE' apply false 3 | } 4 | 5 | apply plugin: 'base' 6 | 7 | allprojects { 8 | group 'ru.spring.reactive.in.practice' 9 | } 10 | 11 | subprojects { 12 | apply plugin: 'java' 13 | 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | 17 | repositories { 18 | jcenter() 19 | maven { url 'https://jitpack.io' } 20 | } 21 | 22 | dependencies { 23 | [enforcedPlatform('org.springframework.boot:spring-boot-dependencies:2.1.3.RELEASE'), 24 | platform('org.junit:junit-bom:5.4.0'), 25 | ].each { 26 | implementation it 27 | annotationProcessor it 28 | testAnnotationProcessor it 29 | } 30 | 31 | constraints { 32 | ['org.projectlombok:lombok:1.18.2'].each { 33 | compileOnly it 34 | annotationProcessor it 35 | testAnnotationProcessor it 36 | } 37 | 38 | ['com.github.javafaker:javafaker:0.16', 39 | 'io.dropwizard.metrics:metrics-core:+', 40 | 'org.beryx:text-io:3.3.0', 41 | 'org.beryx:text-io-web:3.3.0', 42 | 'org.fusesource.jansi:jansi:1.17.1', 43 | 'org.codehaus.groovy:groovy-all:2.5.6', 44 | 'com.google.guava:guava:27.1-jre', 45 | 'com.github.CollaborationInEncapsulation.spring-boot-rsocket:spring-boot-starter-rsocket:0.0.1.RELEASE', 46 | 'io.rsocket:rsocket-transport-netty:0.11.21', 47 | 'io.rsocket:rsocket-core:0.11.21', 48 | ].each { 49 | implementation it 50 | } 51 | 52 | ['org.mockito:mockito-core:2.22.0', 53 | 'org.mockito:mockito-junit-jupiter:2.22.0', 54 | 'io.github.benas:random-beans:3.7.0', 55 | ].each { 56 | testImplementation it 57 | } 58 | } 59 | 60 | //declare dependencies 61 | ['org.projectlombok:lombok', 62 | 'org.springframework.boot:spring-boot-configuration-processor', 63 | ].each { 64 | compileOnly it 65 | testCompileOnly it 66 | annotationProcessor it 67 | testAnnotationProcessor it 68 | } 69 | 70 | implementation 'org.springframework.boot:spring-boot-starter-webflux', 71 | 'org.springframework.boot:spring-boot-starter-actuator', 72 | 'io.rsocket:rsocket-transport-netty', 73 | 'com.google.guava:guava', 74 | 'io.dropwizard.metrics:metrics-core', 75 | 'org.beryx:text-io', 76 | 'org.beryx:text-io-web', 77 | 'org.fusesource.jansi:jansi', 78 | 'io.projectreactor.addons:reactor-extra' 79 | 80 | testImplementation 'org.mockito:mockito-core', 81 | 'org.mockito:mockito-junit-jupiter', 82 | 'org.junit.jupiter:junit-jupiter-engine', 83 | 'org.springframework.boot:spring-boot-starter-test' 84 | } 85 | 86 | test { 87 | useJUnitPlatform() 88 | testLogging { 89 | exceptionFormat = 'full' 90 | events 'passed', 'skipped', 'failed' 91 | } 92 | reports { 93 | html.enabled = true 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /console-dashboard/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'org.springframework.boot' 3 | 4 | dependencies { 5 | implementation 'org.codehaus.groovy:groovy-all' 6 | runtimeClasspath 'org.springframework.boot:spring-boot-devtools' 7 | } 8 | -------------------------------------------------------------------------------- /console-dashboard/src/main/groovy/ru/spring/demo/reactive/dashboard/console/ConsoleDashboard.groovy: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.dashboard.console 2 | 3 | import groovy.transform.CompileStatic 4 | import lombok.RequiredArgsConstructor 5 | import lombok.extern.slf4j.Slf4j 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.scheduling.annotation.Scheduled 8 | import org.springframework.stereotype.Component 9 | import ru.spring.demo.reactive.dashboard.console.model.RateStatus 10 | import ru.spring.demo.reactive.dashboard.console.service.FetchRatesService 11 | 12 | import java.util.concurrent.CompletableFuture 13 | 14 | import static java.lang.String.format 15 | import static java.util.concurrent.CompletableFuture.allOf 16 | import static org.fusesource.jansi.Ansi.Color.BLACK 17 | import static org.fusesource.jansi.Ansi.ansi 18 | 19 | @Slf4j 20 | @Component 21 | @CompileStatic 22 | @RequiredArgsConstructor 23 | class ConsoleDashboard { 24 | public static final int PAD = 20 25 | 26 | @Autowired FetchRatesService fetchRatesService 27 | @Autowired DashboardProperties properties 28 | 29 | @Scheduled(fixedDelay = 50L) 30 | void run() { 31 | def whenDone = sequence([ 32 | fetchRatesService.getRateStatus(properties.getLetterGrabberUrl()).thenApply({ status -> status.setComponent("pechkin-service") }), 33 | fetchRatesService.getRateStatus(properties.getLetterSignatureUrl()).thenApply({ status -> status.setComponent("big-brother-service") }), 34 | fetchRatesService.getRateStatus(properties.getGuardUrl()).thenApply({ status -> status.setComponent("agent-smith-service") }) 35 | ]) 36 | 37 | def builder = new StringBuilder() 38 | whenDone.thenAccept { statuses -> 39 | int COL0_MAXSIZE = statuses*.component*.size().max() 40 | int TABLE_LINESIZE = PAD * 2 + COL0_MAXSIZE 41 | 42 | ansi().restoreCursorPosition() 43 | 44 | (statuses.size() + 3).times { 45 | builder.append ansi().cursorUpLine().eraseLine() 46 | } 47 | 48 | builder.append("┏${('━' * TABLE_LINESIZE)}┓\n") 49 | builder.append '┃' 50 | builder.append ansi().fgBright(BLACK) 51 | builder.append 'service name'.padRight(COL0_MAXSIZE + 2) 52 | builder.append 'speed'.center(8) 53 | builder.append 'buffers'.center(12) 54 | builder.append 'workers'.center(4) 55 | builder.append ' '.padLeft(11) 56 | builder.append ansi().reset() 57 | builder.append '┃\n' 58 | 59 | statuses.each { status -> 60 | builder.append '┃' 61 | builder.append formatComponent(status, COL0_MAXSIZE) 62 | builder.append "${formatRate(status)} ${formatBuffer(status)}".padLeft(PAD * 2 - 8, '.') 63 | builder.append '┃\n'.padLeft(PAD - 7) 64 | } 65 | 66 | builder.append("┗${('━' * TABLE_LINESIZE)}┛\n") 67 | 68 | print builder 69 | }.join() 70 | } 71 | 72 | private String formatBuffer(RateStatus status) { 73 | def buffer = status.buffers?.get(0) 74 | return buffer?.with { 75 | def result = ansi() 76 | 77 | if (remaining <= maxSize * 0.75) { 78 | result.fgBrightRed() 79 | } else { 80 | result.fgBrightGreen() 81 | } 82 | 83 | result.format('%6d/%-6d', remaining, maxSize) 84 | result.format('%2d/%-2d', activeWorker, workersCount) 85 | return result.reset().toString() 86 | } ?: format('%6d/%-6d%2d/%-2d', 0, 0, 0, 0) 87 | } 88 | 89 | private String formatRate(RateStatus status) { 90 | ansi().fgBrightCyan().format('%8.2f', status.getLetterRps()).reset().toString() 91 | } 92 | 93 | private String formatComponent(RateStatus status, int colSize) { 94 | ansi().fgBrightGreen().a(status.getComponent().padRight(colSize + 2, '.')).reset().toString() 95 | } 96 | 97 | static CompletableFuture> sequence(List> futures) { 98 | allOf(futures as CompletableFuture[]).thenApply({ ignored -> 99 | return futures.collect { it.join() } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /console-dashboard/src/main/groovy/ru/spring/demo/reactive/dashboard/console/ConsoleDashboardApplication.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.dashboard.console; 2 | 3 | import org.springframework.boot.Banner; 4 | import org.springframework.boot.WebApplicationType; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.builder.SpringApplicationBuilder; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.boot.web.client.RestTemplateBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.scheduling.annotation.EnableAsync; 11 | import org.springframework.scheduling.annotation.EnableScheduling; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | @EnableAsync 15 | @EnableScheduling 16 | @SpringBootApplication 17 | @EnableConfigurationProperties(DashboardProperties.class) 18 | public class ConsoleDashboardApplication { 19 | 20 | public static void main(String[] args) { 21 | System.setProperty("jansi.passthrough", "true"); 22 | new SpringApplicationBuilder(ConsoleDashboardApplication.class) 23 | .headless(true) 24 | .web(WebApplicationType.NONE) 25 | .bannerMode(Banner.Mode.OFF) 26 | .run(args); 27 | } 28 | 29 | @Bean 30 | public RestTemplate restTemplate(RestTemplateBuilder builder) { 31 | return builder.build(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /console-dashboard/src/main/groovy/ru/spring/demo/reactive/dashboard/console/DashboardProperties.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.dashboard.console; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties(prefix = "dashboard") 8 | public class DashboardProperties { 9 | private String letterSignatureUrl; 10 | private String letterGrabberUrl; 11 | private String guardUrl; 12 | } 13 | -------------------------------------------------------------------------------- /console-dashboard/src/main/groovy/ru/spring/demo/reactive/dashboard/console/model/RateStatus.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.dashboard.console.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | 9 | import java.util.List; 10 | 11 | @Data 12 | @Accessors(chain = true) 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class RateStatus { 16 | private String component; 17 | private double letterRps; 18 | 19 | @JsonProperty("buffer.size") 20 | private int bufferSize; 21 | 22 | @JsonProperty("buffer.capacity") 23 | private int bufferCapacity; 24 | 25 | private List buffers; 26 | 27 | @Data 28 | public static class BufferStatus { 29 | private int remaining; 30 | private int maxSize; 31 | private int activeWorker; 32 | private int workersCount; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /console-dashboard/src/main/groovy/ru/spring/demo/reactive/dashboard/console/service/FetchRatesService.groovy: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.dashboard.console.service 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.scheduling.annotation.Async 5 | import org.springframework.stereotype.Service 6 | import org.springframework.web.client.RestTemplate 7 | import ru.spring.demo.reactive.dashboard.console.model.RateStatus 8 | 9 | import java.util.concurrent.CompletableFuture 10 | 11 | import static java.util.concurrent.CompletableFuture.completedFuture 12 | 13 | @Service 14 | class FetchRatesService { 15 | @Autowired RestTemplate restTemplate 16 | 17 | @Async 18 | CompletableFuture getRateStatus(String letterSignatureUrl) { 19 | try { 20 | completedFuture restTemplate.getForObject(letterSignatureUrl, RateStatus.class) 21 | } catch (ignored) { 22 | completedFuture new RateStatus().setLetterRps(-1d) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /console-dashboard/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | dashboard: 2 | letter-grabber-url: http://localhost:8080/actuator/rates 3 | guard-url: http://localhost:8082/actuator/rates 4 | letter-signature-url: http://localhost:8081/actuator/rates 5 | 6 | -------------------------------------------------------------------------------- /gradle/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | true 13 | 14 | %r [%highlight(%thread)] %gray(%-5level) %msg %n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /http-requests/requests.http: -------------------------------------------------------------------------------- 1 | # remaning requests in producer 2 | GET {{letter-grabber-host}}/request 3 | Accept: application/json 4 | 5 | > {% 6 | client.test("Should return remaining requests count", function() { 7 | client.assert(response.status === 200, "Response status is not 200"); 8 | 9 | client.global.set("requests_for_clean", -1*response.body); 10 | }); 11 | %} 12 | 13 | ### 14 | 15 | # send X request to consumer 16 | GET {{letter-grabber-host}}/request/1000 17 | Accept: application/json 18 | 19 | ### 20 | 21 | # clean requests queue 22 | GET {{letter-grabber-host}}/request/{{requests_for_clean}} 23 | Accept: application/json 24 | 25 | ### 26 | 27 | # show metrics 28 | GET {{letter-grabber-host}}/actuator/rates 29 | Accept: application/json 30 | 31 | ### 32 | 33 | GET {{letter-signature-decoder-host}}/actuator/rates 34 | Accept: application/json 35 | 36 | ### 37 | 38 | GET {{guard-host}}/actuator/rates 39 | Accept: application/json 40 | 41 | ### 42 | -------------------------------------------------------------------------------- /http-requests/rest-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "demo": { 3 | "guard-host": "http://localhost:8082", 4 | "letter-grabber-host": "http://localhost:8080", 5 | "letter-signature-decoder-host": "http://localhost:8081" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /metrics-starter/build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavcraft/spring-react-or-not-react/52959732e37bfb1fb1407cbb98d9cda4c88923f7/metrics-starter/build.gradle -------------------------------------------------------------------------------- /metrics-starter/src/main/java/ru/spring/demo/reactive/starter/metrics/RatesMetricsConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.metrics; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | import io.micrometer.core.instrument.Clock; 5 | import io.micrometer.core.instrument.MeterRegistry; 6 | import io.micrometer.core.instrument.dropwizard.DropwizardConfig; 7 | import io.micrometer.core.instrument.dropwizard.DropwizardMeterRegistry; 8 | import io.micrometer.core.instrument.util.HierarchicalNameMapper; 9 | import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; 10 | import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; 11 | import org.springframework.boot.autoconfigure.AutoConfigureBefore; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.concurrent.ThreadPoolExecutor; 18 | 19 | @Configuration 20 | @ConditionalOnEnabledEndpoint(endpoint = RatesMetricsEndpoint.class) 21 | @AutoConfigureBefore(SimpleMetricsExportAutoConfiguration.class) 22 | public class RatesMetricsConfiguration { 23 | @Bean 24 | public MetricRegistry dropwizardRegistry() { 25 | return new MetricRegistry(); 26 | } 27 | 28 | @Bean 29 | public MeterRegistry consoleLoggingRegistry(MetricRegistry dropwizardRegistry) { 30 | DropwizardConfig consoleConfig = new DropwizardConfig() { 31 | 32 | @Override 33 | public String prefix() { 34 | return "console"; 35 | } 36 | 37 | @Override 38 | public String get(String key) { 39 | return null; 40 | } 41 | 42 | }; 43 | 44 | return new DropwizardMeterRegistry(consoleConfig, dropwizardRegistry, HierarchicalNameMapper.DEFAULT, Clock.SYSTEM) { 45 | @Override 46 | protected Double nullGaugeValue() { 47 | return null; 48 | } 49 | }; 50 | } 51 | 52 | @Bean 53 | public RatesMetricsEndpoint ratesMetricsEndpoint(MetricRegistry m, 54 | Optional> executors) { 55 | return new RatesMetricsEndpoint( 56 | m, 57 | executors 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /metrics-starter/src/main/java/ru/spring/demo/reactive/starter/metrics/RatesMetricsEndpoint.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.metrics; 2 | 3 | import com.codahale.metrics.Meter; 4 | import com.codahale.metrics.MetricRegistry; 5 | import com.google.common.collect.ImmutableMap; 6 | import lombok.Data; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.Accessors; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 11 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 12 | import org.springframework.boot.actuate.endpoint.annotation.Selector; 13 | 14 | import java.util.*; 15 | import java.util.Map.Entry; 16 | import java.util.concurrent.ThreadPoolExecutor; 17 | import java.util.stream.Collector; 18 | import java.util.stream.Stream; 19 | 20 | import static java.util.Map.entry; 21 | import static java.util.stream.Collectors.toMap; 22 | 23 | @Slf4j 24 | @Endpoint(id = "rates") 25 | @RequiredArgsConstructor 26 | public class RatesMetricsEndpoint { 27 | private static final Collector, ?, Map> ENTRY_MAP_COLLECTOR = toMap( 28 | Entry::getKey, 29 | Entry::getValue, 30 | (o, o2) -> { 31 | if(o instanceof List && o2 instanceof List) { 32 | ((List) o).addAll((Collection) o2); 33 | } 34 | return o; 35 | } 36 | ); 37 | private final MetricRegistry metricRegistry; 38 | 39 | @ReadOperation 40 | public Map allrates() { 41 | return extractRatesFromMeters(); 42 | } 43 | 44 | @ReadOperation 45 | public Map rates(@Selector String arg0) { 46 | Map rates = extractMeterRates( 47 | metricRegistry.getMeters().entrySet().stream() 48 | .filter(stringMeterEntry -> stringMeterEntry.getKey().contains(arg0)) 49 | ); 50 | 51 | rates.putAll(extractPoolStats()); 52 | 53 | return rates; 54 | } 55 | 56 | private Map extractRatesFromMeters() { 57 | Map rates = extractMeterRates(metricRegistry.getMeters().entrySet().stream()); 58 | 59 | rates.putAll(extractPoolStats()); 60 | 61 | return rates; 62 | } 63 | 64 | private Map extractPoolStats() { 65 | return executors 66 | .map(Map::entrySet) 67 | .map(Collection::stream) 68 | .map(threadPoolExecutorStream -> threadPoolExecutorStream 69 | .map(entry -> extractThreadPoolStats(entry.getKey(), entry.getValue()))) 70 | .map(mapStream -> mapStream 71 | .map(Map::entrySet) 72 | .flatMap(Collection::stream) 73 | .collect(ENTRY_MAP_COLLECTOR) 74 | ).orElse(Collections.emptyMap()); 75 | } 76 | 77 | private Map extractMeterRates(Stream> stream) { 78 | return stream 79 | .map(metric -> entry(metric.getKey(), (Object) metric.getValue().getOneMinuteRate())) 80 | .collect(ENTRY_MAP_COLLECTOR); 81 | } 82 | 83 | private Map extractThreadPoolStats(String name, ThreadPoolExecutor threadPoolExecutor) { 84 | int remainingCapacity = threadPoolExecutor.getQueue().remainingCapacity(); 85 | int queueSize = threadPoolExecutor.getQueue().size() + remainingCapacity; 86 | int threadInWork = threadPoolExecutor.getActiveCount(); 87 | int maximumPoolSize = threadPoolExecutor.getMaximumPoolSize(); 88 | 89 | return new ImmutableMap.Builder() 90 | .put("buffers", Arrays.asList( 91 | new BufferStats() 92 | .setName(name) 93 | .setRemaining(remainingCapacity) 94 | .setMaxSize(queueSize) 95 | .setActiveWorker(threadInWork) 96 | .setWorkersCount(maximumPoolSize) 97 | ) 98 | ) 99 | .put("buffer.size", remainingCapacity) 100 | .put("buffer.capacity", queueSize) 101 | .build(); 102 | } 103 | 104 | @Data 105 | @Accessors(chain = true) 106 | public static class BufferStats { 107 | String name; 108 | int remaining; 109 | int maxSize; 110 | int activeWorker; 111 | int workersCount; 112 | } 113 | 114 | private final Optional> executors; 115 | } 116 | -------------------------------------------------------------------------------- /metrics-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=ru.spring.demo.reactive.starter.metrics.RatesMetricsConfiguration 2 | -------------------------------------------------------------------------------- /pechkin-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /pechkin-service/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.springframework.boot' 2 | 3 | dependencies { 4 | implementation 'com.github.javafaker:javafaker', 5 | project(':speed-adjuster-starter'), 6 | project(':metrics-starter') 7 | 8 | runtimeClasspath 'org.springframework.boot:spring-boot-devtools' 9 | } 10 | -------------------------------------------------------------------------------- /pechkin-service/src/main/java/ru/spring/demo/reactive/pechkin/PechkinApplication.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @EnableScheduling 8 | @SpringBootApplication 9 | public class PechkinApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(PechkinApplication.class, args); 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /pechkin-service/src/main/java/ru/spring/demo/reactive/pechkin/configuration/FakeDataConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin.configuration; 2 | 3 | import com.github.javafaker.Faker; 4 | import com.github.javafaker.RickAndMorty; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * @author Evgeny Borisov 10 | */ 11 | @Configuration 12 | public class FakeDataConfiguration { 13 | @Bean 14 | public RickAndMorty faker() { 15 | return new Faker().rickAndMorty(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pechkin-service/src/main/java/ru/spring/demo/reactive/pechkin/configuration/ReactiveConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin.configuration; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import reactor.core.publisher.EmitterProcessor; 6 | 7 | @Configuration 8 | public class ReactiveConfiguration { 9 | @Bean 10 | public EmitterProcessor unicastProcessorLetter() { 11 | return EmitterProcessor.create(false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pechkin-service/src/main/java/ru/spring/demo/reactive/pechkin/controllers/FeedbackController.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin.controllers; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | import org.springframework.web.bind.annotation.RestController; 7 | import ru.spring.demo.reactive.starter.speed.model.Notification; 8 | 9 | @Slf4j 10 | @RestController 11 | public class FeedbackController { 12 | 13 | @PostMapping("/letter-status") 14 | public void feedback(@RequestBody Notification feedback) { 15 | log.info("feedback = " + feedback); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /pechkin-service/src/main/java/ru/spring/demo/reactive/pechkin/producer/LetterProducer.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin.producer; 2 | 3 | import com.github.javafaker.RickAndMorty; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | import reactor.core.publisher.Flux; 10 | import ru.spring.demo.reactive.starter.speed.model.Letter; 11 | 12 | import java.util.concurrent.LinkedBlockingQueue; 13 | 14 | /** 15 | * @author Evgeny Borisov 16 | */ 17 | @Slf4j 18 | @Service 19 | @Setter 20 | @RequiredArgsConstructor 21 | public class LetterProducer { 22 | private final RickAndMorty faker; 23 | 24 | 25 | @SneakyThrows 26 | public Letter getLetter() { 27 | return randomLetter(); 28 | } 29 | 30 | LinkedBlockingQueue letterQueue = new LinkedBlockingQueue(); 31 | 32 | public Flux letterFlux() { 33 | return Flux.generate(synchronousSink -> synchronousSink.next(randomLetter())); 34 | } 35 | 36 | private Letter randomLetter() { 37 | String character = faker.character(); 38 | return Letter.builder() 39 | .content(faker.quote()) 40 | .location(faker.location()) 41 | .signature(character) 42 | ._original(character) 43 | .build(); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /pechkin-service/src/main/java/ru/spring/demo/reactive/pechkin/services/LetterDistributor.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin.services; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.micrometer.core.instrument.Counter; 5 | import io.micrometer.core.instrument.MeterRegistry; 6 | import io.rsocket.RSocket; 7 | import io.rsocket.RSocketFactory; 8 | import io.rsocket.transport.netty.client.WebsocketClientTransport; 9 | import io.rsocket.util.DefaultPayload; 10 | import lombok.Getter; 11 | import lombok.SneakyThrows; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.boot.context.event.ApplicationStartedEvent; 14 | import org.springframework.context.event.EventListener; 15 | import org.springframework.stereotype.Service; 16 | import reactor.core.publisher.Flux; 17 | import reactor.core.publisher.Mono; 18 | import reactor.netty.http.client.HttpClient; 19 | import reactor.netty.tcp.TcpClient; 20 | import ru.spring.demo.reactive.pechkin.producer.LetterProducer; 21 | import ru.spring.demo.reactive.starter.speed.model.Letter; 22 | import ru.spring.demo.reactive.starter.speed.rsocket.ReconnectingRSocket; 23 | 24 | import java.time.Duration; 25 | import java.util.concurrent.ThreadPoolExecutor; 26 | 27 | /** 28 | * @author Evgeny Borisov 29 | */ 30 | @Slf4j 31 | @Getter 32 | @Service 33 | public class LetterDistributor { 34 | private final LetterProducer producer; 35 | private final Counter counter; 36 | private final ThreadPoolExecutor letterProcessorExecutor; 37 | private final ObjectMapper objectMapper; 38 | 39 | private final Mono bigBrotherRSocketMono; 40 | 41 | public LetterDistributor(LetterProducer producer, 42 | MeterRegistry meterRegistry, 43 | ThreadPoolExecutor letterProcessorExecutor, 44 | ObjectMapper objectMapper) { 45 | this.producer = producer; 46 | this.counter = meterRegistry.counter("letter.rps"); 47 | this.letterProcessorExecutor = letterProcessorExecutor; 48 | this.objectMapper = objectMapper; 49 | 50 | bigBrotherRSocketMono = RSocketFactory.connect() 51 | .transport(WebsocketClientTransport.create( 52 | HttpClient.from(TcpClient.create() 53 | .host("localhost") 54 | .port(8081)), 55 | "/rs" 56 | )) 57 | .start(); 58 | } 59 | 60 | @EventListener(ApplicationStartedEvent.class) 61 | public void init() { 62 | new ReconnectingRSocket(bigBrotherRSocketMono, Duration.ofMillis(200), Duration.ofMillis(1000)) 63 | .requestChannel( 64 | Flux.defer(() -> producer.letterFlux() 65 | .log() 66 | .doOnNext(payload -> counter.increment()) 67 | .map(letter -> DefaultPayload.create(convertToBytes(letter)))) 68 | ) 69 | .doOnError(e -> log.error("Got App Error ", e)) 70 | .retryBackoff(Integer.MAX_VALUE, Duration.ofMillis(200), Duration.ofMillis(1000)) 71 | .subscribe(); 72 | } 73 | 74 | @SneakyThrows 75 | private byte[] convertToBytes(Letter letter) { 76 | return objectMapper.writeValueAsBytes(letter); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /pechkin-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | devtools: 6 | livereload: 7 | port: 10580 8 | 9 | management: 10 | endpoints: 11 | web: 12 | exposure: 13 | include: '*' 14 | 15 | -------------------------------------------------------------------------------- /pechkin-service/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | ../../../../gradle/logback-spring.xml -------------------------------------------------------------------------------- /pechkin-service/src/test/java/ru/spring/demo/reactive/pechkin/producer/LetterProducerTest.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.pechkin.producer; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import ru.spring.demo.reactive.starter.speed.model.Letter; 10 | 11 | /** 12 | * @author Evgeny Borisov 13 | */ 14 | @SpringBootTest 15 | @RunWith(SpringRunner.class) 16 | public class LetterProducerTest { 17 | 18 | @Autowired 19 | LetterProducer producer; 20 | 21 | @Test 22 | public void testProducer() { 23 | Letter letter = producer.getLetter(); 24 | Assert.assertNotNull(letter); 25 | Assert.assertNotNull(letter.getContent()); 26 | Assert.assertNotNull(letter.getLocation()); 27 | Assert.assertNotNull(letter.getSignature()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-react-or-not-react' 2 | 3 | fileTree('.') { 4 | include '**/build.gradle' 5 | exclude 'build.gradle' 6 | }.collect { 7 | relativePath(it.parent).replace(File.separator, ':') 8 | }.each { 9 | include it 10 | } 11 | -------------------------------------------------------------------------------- /speed-adjuster-starter/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | -------------------------------------------------------------------------------- /speed-adjuster-starter/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/AdjustmentProperties.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | /** 9 | * @author Evgeny Borisov 10 | */ 11 | @Data 12 | @ConfigurationProperties("datasender") 13 | public class AdjustmentProperties { 14 | private String url; 15 | private int letterBoxSize = 100; 16 | private int letterProcessorConcurrencyLevel = 1; 17 | private int processingTime = 500; 18 | 19 | private AtomicInteger request = new AtomicInteger(0); 20 | } 21 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/configuration/SpeedAdjusterConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.configuration; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.lang3.concurrent.BasicThreadFactory; 5 | import org.springframework.beans.factory.ObjectProvider; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.boot.web.client.RestTemplateBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.scheduling.annotation.EnableScheduling; 12 | import org.springframework.web.client.RestTemplate; 13 | import reactor.core.publisher.EmitterProcessor; 14 | import ru.spring.demo.reactive.starter.speed.AdjustmentProperties; 15 | import ru.spring.demo.reactive.starter.speed.controllers.RequestController; 16 | import ru.spring.demo.reactive.starter.speed.services.LetterRequesterService; 17 | 18 | import java.util.concurrent.LinkedBlockingQueue; 19 | import java.util.concurrent.RejectedExecutionHandler; 20 | import java.util.concurrent.ThreadPoolExecutor; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | /** 24 | * @author Evgeny Borisov 25 | */ 26 | @Slf4j 27 | @Configuration 28 | @EnableScheduling 29 | @EnableConfigurationProperties(AdjustmentProperties.class) 30 | public class SpeedAdjusterConfiguration { 31 | 32 | @Bean 33 | public RequestController requestController(AdjustmentProperties properties, 34 | ObjectProvider> lettersProcessor) { 35 | return new RequestController(properties, lettersProcessor); 36 | } 37 | 38 | @Bean 39 | @ConditionalOnMissingBean 40 | public RestTemplate restTemplate(RestTemplateBuilder builder) { 41 | return builder.build(); 42 | } 43 | 44 | @Bean 45 | public LetterRequesterService letterRequesterService(AdjustmentProperties properties, 46 | RestTemplate restTemplate) { 47 | return new LetterRequesterService(restTemplate, properties); 48 | } 49 | 50 | @Bean 51 | @ConditionalOnMissingBean 52 | RejectedExecutionHandler rejectedExecutionHandler() { 53 | return (r, executor) -> log.info("task failed — {}", r); 54 | } 55 | 56 | @Bean 57 | public ThreadPoolExecutor letterProcessorExecutor( 58 | AdjustmentProperties adjustmentProperties, 59 | RejectedExecutionHandler rejectedExecutionHandler 60 | ) { 61 | ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 62 | adjustmentProperties.getLetterProcessorConcurrencyLevel(), 63 | adjustmentProperties.getLetterProcessorConcurrencyLevel(), 64 | 0L, TimeUnit.MILLISECONDS, 65 | new LinkedBlockingQueue<>(adjustmentProperties.getLetterBoxSize()), 66 | new BasicThreadFactory.Builder() 67 | .namingPattern("letter-%d") 68 | .daemon(true) 69 | .priority(Thread.MAX_PRIORITY) 70 | .build() 71 | ); 72 | 73 | threadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); 74 | 75 | threadPoolExecutor.prestartAllCoreThreads(); 76 | return threadPoolExecutor; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/controllers/RequestController.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.controllers; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.ObjectProvider; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import reactor.core.publisher.EmitterProcessor; 9 | import ru.spring.demo.reactive.starter.speed.AdjustmentProperties; 10 | 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | @Slf4j 14 | @RestController 15 | public class RequestController { 16 | private final AtomicInteger remainginRequests; 17 | private final AdjustmentProperties adjustmentProperties; 18 | private final ObjectProvider> lettersProcessor; 19 | 20 | public RequestController(AdjustmentProperties adjustmentProperties, 21 | ObjectProvider> lettersProcessor) { 22 | this.remainginRequests = adjustmentProperties.getRequest(); 23 | this.adjustmentProperties = adjustmentProperties; 24 | this.lettersProcessor = lettersProcessor; 25 | } 26 | 27 | @GetMapping("/request/{request}") 28 | public void request(@PathVariable int request) { 29 | lettersProcessor.ifAvailable(longEmitterProcessor -> { 30 | log.info("request controller {}", request); 31 | longEmitterProcessor.onNext((long) request); 32 | }); 33 | 34 | remainginRequests.addAndGet(request); 35 | } 36 | 37 | @GetMapping("/request") 38 | public int getAtomicInteger() { 39 | return remainginRequests.get(); 40 | } 41 | 42 | @GetMapping("/speed/{level}") 43 | public String setSpeed(@PathVariable int level) { 44 | adjustmentProperties.setProcessingTime(level); 45 | 46 | return "{ \"status\": \"ok\"}"; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/model/DecodedLetter.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author Evgeny Borisov 10 | */ 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class DecodedLetter { 16 | private String author; 17 | private String content; 18 | private String location; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/model/Letter.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author Evgeny Borisov 10 | */ 11 | @Data 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class Letter { 16 | private String content; 17 | private String signature; 18 | private String location; 19 | private String _original; 20 | 21 | public String getSignature() { 22 | return getEncrypted(); 23 | } 24 | 25 | private String getEncrypted() { 26 | return Integer.toBinaryString(signature.hashCode()); 27 | } 28 | 29 | public String secretMethodForDecodeSignature() { 30 | return this._original; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/model/Notification.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @Builder 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class Notification { 13 | private String author; 14 | private String action; 15 | } 16 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/rsocket/ReconnectingRSocket.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.rsocket; 2 | 3 | import java.nio.channels.ClosedChannelException; 4 | import java.time.Duration; 5 | import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; 6 | 7 | import io.rsocket.Closeable; 8 | import io.rsocket.Payload; 9 | import io.rsocket.RSocket; 10 | import org.reactivestreams.Publisher; 11 | import org.reactivestreams.Subscription; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import reactor.core.publisher.BaseSubscriber; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | import reactor.core.publisher.MonoProcessor; 18 | 19 | @SuppressWarnings("unchecked") 20 | public class ReconnectingRSocket extends BaseSubscriber implements RSocket { 21 | 22 | private static final Logger LOGGER = LoggerFactory.getLogger(ReconnectingRSocket.class); 23 | 24 | private final Duration backoff; 25 | private final Duration backoffMax; 26 | 27 | private volatile MonoProcessor rSocketMono; 28 | private static final AtomicReferenceFieldUpdater RSOCKET_MONO 29 | = AtomicReferenceFieldUpdater.newUpdater(ReconnectingRSocket.class, MonoProcessor.class, "rSocketMono"); 30 | 31 | public ReconnectingRSocket(Mono rSocketMono, Duration backoff, Duration backoffMax) { 32 | this.backoff = backoff; 33 | this.backoffMax = backoffMax; 34 | this.rSocketMono = MonoProcessor.create(); 35 | 36 | rSocketMono.retryBackoff(Long.MAX_VALUE, backoff) 37 | .repeat() 38 | .subscribe(this); 39 | } 40 | 41 | @Override 42 | protected void hookOnSubscribe(Subscription subscription) { 43 | subscription.request(1); 44 | } 45 | 46 | @Override 47 | protected void hookOnNext(RSocket value) { 48 | LOGGER.info("Connected."); 49 | value.onClose() 50 | .subscribe(null, this::reconnect, this::reconnect); 51 | rSocketMono.onNext(value); 52 | } 53 | 54 | private void reconnect(Throwable t) { 55 | LOGGER.error("Error.", t); 56 | reconnect(); 57 | } 58 | 59 | private void reconnect() { 60 | LOGGER.info("Reconnecting..."); 61 | request(1); 62 | } 63 | 64 | @Override 65 | public Mono fireAndForget(Payload payload) { 66 | return Mono 67 | .defer(() -> { 68 | MonoProcessor rSocketMono = this.rSocketMono; 69 | if(rSocketMono.isSuccess()) { 70 | return rSocketMono.peek() 71 | .fireAndForget(payload) 72 | .doOnError(throwable -> { 73 | if(throwable instanceof ClosedChannelException) { 74 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 75 | } 76 | }); 77 | } else { 78 | return rSocketMono.flatMap(rSocket -> 79 | rSocket.fireAndForget(payload) 80 | .doOnError(throwable -> { 81 | if(throwable instanceof ClosedChannelException) { 82 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 83 | } 84 | }) 85 | ); 86 | } 87 | }); 88 | } 89 | 90 | @Override 91 | public Mono requestResponse(Payload payload) { 92 | return Mono 93 | .defer(() -> { 94 | MonoProcessor rSocketMono = this.rSocketMono; 95 | if(rSocketMono.isSuccess()) { 96 | return rSocketMono.peek() 97 | .requestResponse(payload) 98 | .doOnError(throwable -> { 99 | if(throwable instanceof ClosedChannelException) { 100 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 101 | } 102 | }); 103 | } else { 104 | return rSocketMono.flatMap(rSocket -> 105 | rSocket.requestResponse(payload) 106 | .doOnError(throwable -> { 107 | if(throwable instanceof ClosedChannelException) { 108 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 109 | } 110 | }) 111 | ); 112 | } 113 | }); 114 | } 115 | 116 | @Override 117 | public Flux requestStream(Payload payload) { 118 | return Flux 119 | .defer(() -> { 120 | MonoProcessor rSocketMono = this.rSocketMono; 121 | if(rSocketMono.isSuccess()) { 122 | return rSocketMono.peek() 123 | .requestStream(payload) 124 | .doOnError(throwable -> { 125 | if(throwable instanceof ClosedChannelException) { 126 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 127 | } 128 | }); 129 | } else { 130 | return rSocketMono.flatMapMany(rSocket -> 131 | rSocket.requestStream(payload) 132 | .doOnError(throwable -> { 133 | if(throwable instanceof ClosedChannelException) { 134 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 135 | } 136 | }) 137 | ); 138 | } 139 | }); 140 | } 141 | 142 | @Override 143 | public Flux requestChannel(Publisher payloads) { 144 | return Flux 145 | .defer(() -> { 146 | MonoProcessor rSocketMono = this.rSocketMono; 147 | if(rSocketMono.isSuccess()) { 148 | return rSocketMono.peek() 149 | .requestChannel(payloads) 150 | .doOnError(throwable -> { 151 | if(throwable instanceof ClosedChannelException) { 152 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 153 | } 154 | }); 155 | } else { 156 | return rSocketMono.flatMapMany(rSocket -> 157 | rSocket.requestChannel(payloads) 158 | .doOnError(throwable -> { 159 | if(throwable instanceof ClosedChannelException) { 160 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 161 | } 162 | }) 163 | ); 164 | } 165 | }); 166 | } 167 | 168 | @Override 169 | public Mono metadataPush(Payload payload) { 170 | return Mono 171 | .defer(() -> { 172 | MonoProcessor rSocketMono = this.rSocketMono; 173 | if(rSocketMono.isSuccess()) { 174 | return rSocketMono.peek() 175 | .metadataPush(payload) 176 | .doOnError(throwable -> { 177 | if(throwable instanceof ClosedChannelException) { 178 | RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create()); 179 | } 180 | }); 181 | } else { 182 | return rSocketMono.flatMap(rSocket -> 183 | rSocket.metadataPush(payload) 184 | .doOnError(__ -> RSOCKET_MONO.compareAndSet(this, rSocketMono, MonoProcessor.create())) 185 | ); 186 | } 187 | }); 188 | } 189 | 190 | @Override 191 | public double availability() { 192 | return rSocketMono.isSuccess() ? rSocketMono.peek().availability() : 0d; 193 | } 194 | 195 | @Override 196 | public void dispose() { 197 | super.dispose(); 198 | rSocketMono.dispose(); 199 | } 200 | 201 | @Override 202 | public boolean isDisposed() { 203 | return super.isDisposed(); 204 | } 205 | 206 | @Override 207 | public Mono onClose() { 208 | if(rSocketMono.isSuccess()) { 209 | return rSocketMono.peek().onClose(); 210 | } else { 211 | return rSocketMono.flatMap(Closeable::onClose); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/java/ru/spring/demo/reactive/starter/speed/services/LetterRequesterService.java: -------------------------------------------------------------------------------- 1 | package ru.spring.demo.reactive.starter.speed.services; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.web.client.RestClientException; 7 | import org.springframework.web.client.RestTemplate; 8 | import ru.spring.demo.reactive.starter.speed.AdjustmentProperties; 9 | 10 | /** 11 | * @author Evgeny Borisov 12 | */ 13 | @Slf4j 14 | @Data 15 | @RequiredArgsConstructor 16 | public class LetterRequesterService { 17 | private final RestTemplate restTemplate; 18 | private final AdjustmentProperties adjustmentProperties; 19 | 20 | public void request(int n) { 21 | syncSpeed(n); 22 | } 23 | 24 | private void syncSpeed(int n) { 25 | try { 26 | restTemplate.getForObject( 27 | adjustmentProperties.getUrl() + "/" + n, 28 | Void.class); 29 | } catch (RestClientException e) { 30 | log.error("cant send request(n)", e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /speed-adjuster-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=ru.spring.demo.reactive.starter.speed.configuration.SpeedAdjusterConfiguration 2 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 2 | ┃letter-grabber-producer...........14.23 [##### ] ┃ 3 | ┃letter-signature-consumer.........14.10 [##### ] ┃ 4 | ┃guard-consumer.....................0.00 [##] ┃ 5 | ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 6 | 7 | 1. [v] пририсовать буферы на console-dashboard 8 | 2. [v] guard который дропает реквесты 9 | 3. начать переписывать с spring-react (webflux + tcp backpressure) 10 | 1. Напороться на doNext 11 | 2. Переписать на subscriber 12 | 3. Напороться 13 | 4. сдедать свою реализацию Observable с request(n) таким же как на шаге 2 14 | 5. переписать реализацию на честный backpressure c RSocket 15 | 16 | --- b 17 | a ---| 18 | --- c 19 | 20 | 6. Reactor - Netty просит по 32 элемента. У него своц BP между "нетворком" и "клиентом" 21 | 6.5 (Олег) Почему тогда при добавлении onBPBuffer(256) начинает просить MAX_LONG а не 32 или 256? 22 | 7. reactor-extra и более адекватный backoff стратегия 23 | 24 | 8. RSocket — resumability показать 25 | 26 | 9. в спринге RSocket support будет в 5.2. можно будет на одном порту держать и webflux, и rsocket 27 | --------------------------------------------------------------------------------