├── README.md
├── api-calls-and-resilience
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── nickolasfisher
│ │ │ └── webflux
│ │ │ ├── Config.java
│ │ │ ├── WebfluxApplication.java
│ │ │ ├── model
│ │ │ ├── FirstCallDTO.java
│ │ │ ├── MergedCallsDTO.java
│ │ │ ├── SecondCallDTO.java
│ │ │ └── WelcomeMessage.java
│ │ │ └── service
│ │ │ ├── CachingService.java
│ │ │ ├── CombiningCallsService.java
│ │ │ ├── FallbackService.java
│ │ │ └── RetryService.java
│ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── com
│ └── nickolasfisher
│ └── webflux
│ └── service
│ ├── CachingServiceIT.java
│ ├── CombiningCallsServiceIT.java
│ ├── FallbackServiceIT.java
│ └── RetryServiceIT.java
├── context-api
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
├── simple-echo-server
│ └── reflect.py
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── nickolasfisher
│ │ │ └── webfluxcontext
│ │ │ ├── HelloController.java
│ │ │ ├── WebClientConfig.java
│ │ │ ├── WebContextFilter.java
│ │ │ └── WebfluxContextApplication.java
│ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── com
│ └── nickolasfisher
│ └── webfluxcontext
│ └── WebfluxContextApplicationTests.java
├── mocking-and-unit-testing
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── nickolasfisher
│ │ │ └── testing
│ │ │ ├── MyConfig.java
│ │ │ ├── TestingApplication.java
│ │ │ ├── controller
│ │ │ └── MyController.java
│ │ │ ├── dto
│ │ │ ├── DownstreamResponseDTO.java
│ │ │ └── PersonDTO.java
│ │ │ └── service
│ │ │ └── MyService.java
│ └── resources
│ │ └── application.yml
│ └── test
│ └── java
│ └── com
│ └── nickolasfisher
│ └── testing
│ ├── controller
│ └── MyControllerTest.java
│ └── service
│ └── MyServiceTest.java
├── reactive-clustered-redis
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── nickolasfisher
│ │ └── clusteredredis
│ │ ├── ClusteredredisApplication.java
│ │ ├── LettuceConfig.java
│ │ └── PostConstructExecutor.java
│ └── resources
│ └── application.yml
├── reactive-redis
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── infra
│ ├── leader-follower
│ │ └── docker-compose.yaml
│ └── single-leader
│ │ └── docker-compose.yaml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── nickolasfisher
│ │ │ └── reactiveredis
│ │ │ ├── ReactiveredisApplication.java
│ │ │ ├── config
│ │ │ ├── RedisClusteredConfig.java
│ │ │ ├── RedisConfig.java
│ │ │ ├── RedisPrimaryConfig.java
│ │ │ └── RedisReplicaConfig.java
│ │ │ ├── controller
│ │ │ └── SampleController.java
│ │ │ ├── model
│ │ │ └── Thing.java
│ │ │ └── service
│ │ │ ├── RedisDataService.java
│ │ │ └── RedisSubscriptionInitializer.java
│ └── resources
│ │ └── application.yml
│ └── test
│ ├── java
│ └── com
│ │ └── nickolasfisher
│ │ └── reactiveredis
│ │ ├── BaseSetupAndTeardownRedis.java
│ │ ├── DistributedLockingExampleTest.java
│ │ ├── ExpiringSortedSetEntriesTest.java
│ │ ├── HashesTest.java
│ │ ├── ListsTest.java
│ │ ├── LuaScriptTest.java
│ │ ├── MultiSetsTest.java
│ │ ├── PubSubTest.java
│ │ ├── RedisDataServiceTest.java
│ │ ├── RedisStreamsTest.java
│ │ ├── RedisTestContainerTest.java
│ │ ├── SimpleSetsTest.java
│ │ ├── SortedSetsTest.java
│ │ ├── StringTypesTest.java
│ │ └── TransactionsTest.java
│ └── resources
│ └── logback-test.xml
└── reactive-sqs
├── .gitignore
├── .mvn
└── wrapper
│ ├── MavenWrapperDownloader.java
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── infra
├── create-queue-and-topic.sh
├── docker-compose.yaml
├── receive-message.sh
└── send-message.sh
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
└── main
├── java
└── com
│ └── nickolasfisher
│ └── reactivesqs
│ ├── AwsSnsConfig.java
│ ├── AwsSqsConfig.java
│ ├── ReactiveSqsApplication.java
│ ├── SQSListenerBean.java
│ ├── SQSSenderBean.java
│ └── SnsSenderBean.java
└── resources
└── application.properties
/README.md:
--------------------------------------------------------------------------------
1 | # reactive programming with webflux
2 | Some sample code solving specific problems with spring boot webflux. The tutorials accompanying each sample project are all on [nickolasfisher.com](https://nickolasfisher.com/home)
3 |
4 | ## Tutorials
5 |
6 | ### AWS
7 |
8 | - [How to Send SQS Messages to Localstack with the AWS Java SDK 2.0](https://nickolasfisher.com/blog/how-to-send-sqs-messages-to-localstack-with-the-aws-java-sdk-20)
9 | - [How to Setup a Reactive SQS Listener Using the AWS SDK and Spring Boot](https://nickolasfisher.com/blog/how-to-setup-a-reactive-sqs-listener-using-the-aws-sdk-and-spring-boot)
10 | - [Publishing to SNS in Java with the AWS SDK 2.0](https://nickolasfisher.com/blog/publishing-to-sns-in-java-with-the-aws-sdk-20)
11 |
12 | ### WebClient
13 |
14 | - [How to Make Sequential API Calls and Merge the Results In Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-make-sequential-api-calls-and-merge-the-results-in-spring-boot-webflux)
15 | - [How to Make Parallel API calls in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-make-parallel-api-calls-in-spring-boot-webflux)
16 | - [How to Have a Fallback on Errors Calling Downstream Services in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-have-a-fallback-on-errors-calling-downstream-services-in-spring-boot-webflux)
17 | - [How to Automatically Retry on a Webclient Timeout in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-automatically-retry-on-a-webclient-timeout-in-spring-boot-webflux)
18 |
19 | ### Redis
20 |
21 | - [How to use Embedded Redis to Test a Lettuce Client in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-use-embedded-redis-to-test-a-lettuce-client-in-spring-boot-webflux)
22 | - [How to use a Redis Test Container with Lettuce/Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-use-a-redis-test-container-with-lettucespring-boot-webflux)
23 | - [How to Configure Lettuce to connect to a local Redis Instance with Webflux](https://nickolasfisher.com/blog/how-to-configure-lettuce-to-connect-to-a-local-redis-instance-with-webflux)
24 | - [Working with String Types in Redis using Lettuce and Webflux](https://nickolasfisher.com/blog/working-with-string-types-in-redis-using-lettuce-and-webflux)
25 | - [Working with Lists in Redis using Lettuce and Webflux](https://nickolasfisher.com/blog/working-with-lists-in-redis-using-lettuce-and-webflux)
26 | - [Working with Redis Hashes using Lettuce And Webflux](https://nickolasfisher.com/blog/working-with-redis-hashes-using-lettuce-and-webflux)
27 | - [A Guide to Simple Set Operations in Redis with Lettuce](https://nickolasfisher.com/blog/a-guide-to-simple-set-operations-in-redis-with-lettuce)
28 | - [A Guide to Operating on Multiple Sets in Redis with Lettuce](https://nickolasfisher.com/blog/a-guide-to-operating-on-multiple-sets-in-redis-with-lettuce)
29 | - [A Guide to Operating on Sorted Sets in Redis with Lettuce](https://nickolasfisher.com/blog/a-guide-to-operating-on-sorted-sets-in-redis-with-lettuce)
30 | - [Expiring Individual Elements in a Redis Set](https://nickolasfisher.com/blog/expiring-individual-elements-in-a-redis-set)
31 | - [How to Publish and Subscribe to Redis Using Lettuce](https://nickolasfisher.com/blog/how-to-publish-and-subscribe-to-redis-using-lettuce)
32 | - [How to Run a Lua Script against Redis using Lettuce](https://nickolasfisher.com/blog/how-to-run-a-lua-script-against-redis-using-lettuce)
33 | - [Pre Loading a Lua Script into Redis With Lettuce](https://nickolasfisher.com/blog/pre-loading-a-lua-script-into-redis-with-lettuce)
34 | - [Subscribing to Redis Channels with Java, Spring Boot, and Lettuce](https://nickolasfisher.com/blog/subscribing-to-redis-channels-with-java-spring-boot-and-lettuce)
35 | - [Redis Transactions, Reactive Lettuce: Buyer Beware](https://nickolasfisher.com/blog/redis-transactions-reactive-lettuce-buyer-beware)
36 | - [Optimistic Locking in Redis with Reactive Lettuce](https://nickolasfisher.com/blog/optimistic-locking-in-redis-with-reactive-lettuce)
37 | - [Using Redis as a Distributed Lock with Lettuce](https://nickolasfisher.com/blog/using-redis-as-a-distributed-lock-with-lettuce)
38 | - [An Introduction to Redis Streams via Lettuce](https://nickolasfisher.com/blog/an-introduction-to-redis-streams-via-lettuce)
39 |
40 | #### More advanced redis topics
41 |
42 | - [How to Configure Lettuce to use Redis Read Replicas in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-configure-lettuce-to-use-redis-read-replicas-in-spring-boot-webflux)
43 | - [Configuring Lettuce/Webflux to work with Clustered Redis](https://nickolasfisher.com/blog/configuring-lettucewebflux-to-work-with-clustered-redis)
44 | - [Breaking down Lettuce MSET Commands in Clustered Redis](https://nickolasfisher.com/blog/breaking-down-lettuce-mset-commands-in-clustered-redis)
45 | - [Using Hashtags in Clustered Redis with Lettuce and Webflux](https://nickolasfisher.com/blog/using-hashtags-in-clustered-redis-with-lettuce-and-webflux)
46 | - [Lettuce, MSETNX, and Clustered Redis](https://nickolasfisher.com/blog/lettuce-msetnx-and-clustered-redis)
47 | - [Pre Loading Lua Scripts into Clustered Redis with Lettuce](https://nickolasfisher.com/blog/pre-loading-lua-scripts-into-clustered-redis-with-lettuce)
48 | - [Subscribing to Channels in Clustered Redis With Lettuce](https://nickolasfisher.com/blog/subscribing-to-channels-in-clustered-redis-with-lettuce)
49 |
50 | ### Caching
51 |
52 | - [In-Memory Caching in Sprint Boot Webflux/Project Reactor](https://nickolasfisher.com/blog/inmemory-caching-in-sprint-boot-webfluxproject-reactor)
53 | - [How to use Caffeine Caches Effectively in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-use-caffeine-caches-effectively-in-spring-boot-webflux)
54 |
55 | ### Misc/Best Practices
56 |
57 | - [How to Mock Dependencies and Unit Test in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-mock-dependencies-and-unit-test-in-spring-boot-webflux)
58 | - [How to use Mock Server to End to End Test Any WebClient Calls in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-use-mock-server-to-end-to-end-test-any-webclient-calls-in-spring-boot-webflux)
59 | - [How to Forward Request Headers to Downstream Services in Spring Boot Webflux](https://nickolasfisher.com/blog/how-to-forward-request-headers-to-downstream-services-in-spring-boot-webflux)
60 | - [A Guide to Automatic Retries in Reactor](https://nickolasfisher.com/blog/a-guide-to-automatic-retries-in-reactor)
61 |
62 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.net.*;
18 | import java.io.*;
19 | import java.nio.channels.*;
20 | import java.util.Properties;
21 |
22 | public class MavenWrapperDownloader {
23 |
24 | private static final String WRAPPER_VERSION = "0.5.6";
25 | /**
26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
27 | */
28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
30 |
31 | /**
32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
33 | * use instead of the default one.
34 | */
35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
36 | ".mvn/wrapper/maven-wrapper.properties";
37 |
38 | /**
39 | * Path where the maven-wrapper.jar will be saved to.
40 | */
41 | private static final String MAVEN_WRAPPER_JAR_PATH =
42 | ".mvn/wrapper/maven-wrapper.jar";
43 |
44 | /**
45 | * Name of the property which should be used to override the default download url for the wrapper.
46 | */
47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
48 |
49 | public static void main(String args[]) {
50 | System.out.println("- Downloader started");
51 | File baseDirectory = new File(args[0]);
52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
53 |
54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
55 | // wrapperUrl parameter.
56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
57 | String url = DEFAULT_DOWNLOAD_URL;
58 | if (mavenWrapperPropertyFile.exists()) {
59 | FileInputStream mavenWrapperPropertyFileInputStream = null;
60 | try {
61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
62 | Properties mavenWrapperProperties = new Properties();
63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
65 | } catch (IOException e) {
66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
67 | } finally {
68 | try {
69 | if (mavenWrapperPropertyFileInputStream != null) {
70 | mavenWrapperPropertyFileInputStream.close();
71 | }
72 | } catch (IOException e) {
73 | // Ignore ...
74 | }
75 | }
76 | }
77 | System.out.println("- Downloading from: " + url);
78 |
79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
80 | if (!outputFile.getParentFile().exists()) {
81 | if (!outputFile.getParentFile().mkdirs()) {
82 | System.out.println(
83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
84 | }
85 | }
86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
87 | try {
88 | downloadFileFromURL(url, outputFile);
89 | System.out.println("Done");
90 | System.exit(0);
91 | } catch (Throwable e) {
92 | System.out.println("- Error downloading");
93 | e.printStackTrace();
94 | System.exit(1);
95 | }
96 | }
97 |
98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
100 | String username = System.getenv("MVNW_USERNAME");
101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
102 | Authenticator.setDefault(new Authenticator() {
103 | @Override
104 | protected PasswordAuthentication getPasswordAuthentication() {
105 | return new PasswordAuthentication(username, password);
106 | }
107 | });
108 | }
109 | URL website = new URL(urlString);
110 | ReadableByteChannel rbc;
111 | rbc = Channels.newChannel(website.openStream());
112 | FileOutputStream fos = new FileOutputStream(destination);
113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
114 | fos.close();
115 | rbc.close();
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/api-calls-and-resilience/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/api-calls-and-resilience/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.3.4.RELEASE
9 |
10 |
11 | com.nickolasfisher
12 | webflux
13 | 0.0.1-SNAPSHOT
14 | webflux
15 | Calling APIs, resilience patterns in spring boot webflux
16 |
17 |
18 | 11
19 |
20 |
21 |
22 |
23 | org.springframework.boot
24 | spring-boot-starter-webflux
25 |
26 |
27 | org.mock-server
28 | mockserver-netty
29 | 5.11.1
30 |
31 |
32 | org.springframework.boot
33 | spring-boot-starter-test
34 | test
35 |
36 |
37 | org.junit.vintage
38 | junit-vintage-engine
39 |
40 |
41 |
42 |
43 | io.projectreactor
44 | reactor-test
45 | test
46 |
47 |
48 | org.mock-server
49 | mockserver-junit-jupiter
50 | 5.11.1
51 |
52 |
53 | com.github.ben-manes.caffeine
54 | caffeine
55 | 3.0.1
56 |
57 |
58 | org.mock-server
59 | mockserver-netty
60 | 5.11.0
61 | test
62 |
63 |
64 |
65 |
66 |
67 |
68 | org.springframework.boot
69 | spring-boot-maven-plugin
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/Config.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux;
2 |
3 | import io.netty.channel.ChannelOption;
4 | import io.netty.handler.timeout.ReadTimeoutHandler;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.http.client.reactive.ReactorClientHttpConnector;
8 | import org.springframework.web.reactive.function.client.WebClient;
9 | import reactor.netty.http.client.HttpClient;
10 | import reactor.netty.tcp.TcpClient;
11 |
12 | import java.util.concurrent.TimeUnit;
13 | import java.util.function.Function;
14 |
15 | @Configuration
16 | public class Config {
17 |
18 | @Bean("service-a-web-client")
19 | public WebClient serviceAWebClient() {
20 | HttpClient httpClient = HttpClient.create().tcpConfiguration(tcpClient ->
21 | tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
22 | .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(1000, TimeUnit.MILLISECONDS)))
23 | );
24 |
25 | return WebClient.builder()
26 | .baseUrl("http://your-base-url.com")
27 | .clientConnector(new ReactorClientHttpConnector(httpClient))
28 | .build();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/WebfluxApplication.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class WebfluxApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(WebfluxApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/model/FirstCallDTO.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.model;
2 |
3 | public class FirstCallDTO {
4 | private Integer fieldFromFirstCall;
5 |
6 | public Integer getFieldFromFirstCall() {
7 | return fieldFromFirstCall;
8 | }
9 |
10 | public void setFieldFromFirstCall(Integer fieldFromFirstCall) {
11 | this.fieldFromFirstCall = fieldFromFirstCall;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/model/MergedCallsDTO.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.model;
2 |
3 | public class MergedCallsDTO {
4 | private Integer fieldOne;
5 | private String fieldTwo;
6 |
7 | public Integer getFieldOne() {
8 | return fieldOne;
9 | }
10 |
11 | public void setFieldOne(Integer fieldOne) {
12 | this.fieldOne = fieldOne;
13 | }
14 |
15 | public String getFieldTwo() {
16 | return fieldTwo;
17 | }
18 |
19 | public void setFieldTwo(String fieldTwo) {
20 | this.fieldTwo = fieldTwo;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/model/SecondCallDTO.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.model;
2 |
3 | public class SecondCallDTO {
4 | private String fieldFromSecondCall;
5 |
6 | public String getFieldFromSecondCall() {
7 | return fieldFromSecondCall;
8 | }
9 |
10 | public void setFieldFromSecondCall(String fieldFromSecondCall) {
11 | this.fieldFromSecondCall = fieldFromSecondCall;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/model/WelcomeMessage.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.model;
2 |
3 | public class WelcomeMessage {
4 | private String message;
5 |
6 | public WelcomeMessage() {}
7 |
8 | public WelcomeMessage(String message) {
9 | this.message = message;
10 | }
11 |
12 | public String getMessage() {
13 | return message;
14 | }
15 |
16 | public void setMessage(String message) {
17 | this.message = message;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/service/CachingService.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import com.github.benmanes.caffeine.cache.Cache;
4 | import com.github.benmanes.caffeine.cache.Caffeine;
5 | import com.nickolasfisher.webflux.model.WelcomeMessage;
6 | import org.springframework.stereotype.Service;
7 | import reactor.core.publisher.Mono;
8 |
9 | import java.time.Duration;
10 | import java.util.Optional;
11 |
12 | @Service
13 | public class CachingService {
14 |
15 | private final Cache
16 | WELCOME_MESSAGE_CACHE = Caffeine.newBuilder()
17 | .expireAfterWrite(Duration.ofMinutes(5))
18 | .maximumSize(1_000)
19 | .build();
20 |
21 | private final RetryService retryService;
22 | private final Mono cachedEnglishWelcomeMono;
23 |
24 | public CachingService(RetryService retryService) {
25 | this.retryService = retryService;
26 | this.cachedEnglishWelcomeMono = this.retryService.getWelcomeMessageAndHandleTimeout("en_US")
27 | .cache(welcomeMessage -> Duration.ofMinutes(5),
28 | throwable -> Duration.ZERO,
29 | () -> Duration.ZERO
30 | );
31 | }
32 |
33 | public Mono getEnglishLocaleWelcomeMessage() {
34 | return cachedEnglishWelcomeMono;
35 | }
36 |
37 | public Mono getCachedWelcomeMono(String locale) {
38 | Optional message = Optional.ofNullable(WELCOME_MESSAGE_CACHE.getIfPresent(locale));
39 |
40 | return message
41 | .map(Mono::just)
42 | .orElseGet(() ->
43 | this.retryService.getWelcomeMessageAndHandleTimeout(locale)
44 | .doOnNext(welcomeMessage -> WELCOME_MESSAGE_CACHE.put(locale, welcomeMessage))
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/service/CombiningCallsService.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import com.nickolasfisher.webflux.model.FirstCallDTO;
4 | import com.nickolasfisher.webflux.model.MergedCallsDTO;
5 | import com.nickolasfisher.webflux.model.SecondCallDTO;
6 | import org.springframework.beans.factory.annotation.Qualifier;
7 | import org.springframework.stereotype.Service;
8 | import org.springframework.web.reactive.function.client.WebClient;
9 | import reactor.core.publisher.Mono;
10 | import reactor.util.function.Tuple2;
11 |
12 | import java.util.function.Function;
13 |
14 | @Service
15 | public class CombiningCallsService {
16 |
17 | private final WebClient serviceAWebClient;
18 |
19 | public CombiningCallsService(@Qualifier("service-a-web-client") WebClient serviceAWebClient) {
20 | this.serviceAWebClient = serviceAWebClient;
21 | }
22 |
23 | public Mono sequentialCalls(Integer key) {
24 | return this.serviceAWebClient.get()
25 | .uri(uriBuilder -> uriBuilder.path("/first/endpoint/{param}").build(key))
26 | .retrieve()
27 | .bodyToMono(FirstCallDTO.class)
28 | .zipWhen(firstCallDTO ->
29 | serviceAWebClient.get().uri(
30 | uriBuilder ->
31 | uriBuilder.path("/second/endpoint/{param}")
32 | .build(firstCallDTO.getFieldFromFirstCall()))
33 | .retrieve()
34 | .bodyToMono(SecondCallDTO.class),
35 | (firstCallDTO, secondCallDTO) -> secondCallDTO
36 | );
37 | }
38 |
39 | public Mono mergedCalls(Integer firstEndpointParam, Integer secondEndpointParam) {
40 | Mono firstCallDTOMono = this.serviceAWebClient.get()
41 | .uri(uriBuilder -> uriBuilder.path("/first/endpoint/{param}").build(firstEndpointParam))
42 | .retrieve()
43 | .bodyToMono(FirstCallDTO.class);
44 |
45 | Mono secondCallDTOMono = this.serviceAWebClient.get()
46 | .uri(uriBuilder -> uriBuilder.path("/second/endpoint/{param}").build(secondEndpointParam))
47 | .retrieve()
48 | .bodyToMono(SecondCallDTO.class);
49 |
50 | // nothing has been subscribed to, those calls above are not waiting for anything and are not subscribed to, yet
51 |
52 | // zipping the monos will invoke the callback in "map" once both of them have completed, merging the results
53 | // into a tuple.
54 | return Mono.zip(firstCallDTOMono, secondCallDTOMono)
55 | .map(objects -> {
56 | MergedCallsDTO mergedCallsDTO = new MergedCallsDTO();
57 |
58 | mergedCallsDTO.setFieldOne(objects.getT1().getFieldFromFirstCall());
59 | mergedCallsDTO.setFieldTwo(objects.getT2().getFieldFromSecondCall());
60 |
61 | return mergedCallsDTO;
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/service/FallbackService.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import com.nickolasfisher.webflux.model.WelcomeMessage;
4 | import org.springframework.beans.factory.annotation.Qualifier;
5 | import org.springframework.stereotype.Service;
6 | import org.springframework.web.reactive.function.client.WebClient;
7 | import org.springframework.web.reactive.function.client.WebClientResponseException;
8 | import reactor.core.publisher.Mono;
9 |
10 | @Service
11 | public class FallbackService {
12 |
13 | private final WebClient serviceAWebClient;
14 |
15 | public FallbackService(@Qualifier("service-a-web-client")
16 | WebClient serviceAWebClient) {
17 | this.serviceAWebClient = serviceAWebClient;
18 | }
19 |
20 | public Mono getWelcomeMessageByLocale(String locale) {
21 | return this.serviceAWebClient.get()
22 | .uri(uriBuilder -> uriBuilder.path("/locale/{locale}/message").build(locale))
23 | .retrieve()
24 | .bodyToMono(WelcomeMessage.class)
25 | .onErrorReturn(
26 | throwable -> throwable instanceof WebClientResponseException
27 | && ((WebClientResponseException)throwable).getStatusCode().is5xxServerError(),
28 | new WelcomeMessage("hello fallback!")
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/java/com/nickolasfisher/webflux/service/RetryService.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import com.nickolasfisher.webflux.model.WelcomeMessage;
4 | import io.netty.handler.timeout.TimeoutException;
5 | import org.springframework.beans.factory.annotation.Qualifier;
6 | import org.springframework.stereotype.Service;
7 | import org.springframework.web.reactive.function.client.WebClient;
8 | import reactor.core.publisher.Mono;
9 | import reactor.util.retry.Retry;
10 |
11 | import java.time.Duration;
12 |
13 | @Service
14 | public class RetryService {
15 | private final WebClient serviceAWebClient;
16 |
17 | public RetryService(@Qualifier("service-a-web-client") WebClient serviceAWebClient) {
18 | this.serviceAWebClient = serviceAWebClient;
19 | }
20 |
21 | public Mono getWelcomeMessageAndHandleTimeout(String locale) {
22 | return this.serviceAWebClient.get()
23 | .uri(uriBuilder -> uriBuilder.path("/locale/{locale}/message").build(locale))
24 | .retrieve()
25 | .bodyToMono(WelcomeMessage.class)
26 | .retryWhen(
27 | Retry.backoff(2, Duration.ofMillis(25))
28 | .filter(throwable -> throwable instanceof TimeoutException)
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/test/java/com/nickolasfisher/webflux/service/CachingServiceIT.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import com.nickolasfisher.webflux.model.WelcomeMessage;
4 | import org.junit.jupiter.api.AfterEach;
5 | import org.junit.jupiter.api.Test;
6 | import org.mockito.Mockito;
7 | import org.mockito.invocation.InvocationOnMock;
8 | import org.mockito.stubbing.Answer;
9 | import reactor.core.publisher.Mono;
10 | import reactor.test.StepVerifier;
11 |
12 | import java.util.concurrent.atomic.AtomicInteger;
13 |
14 | import static org.junit.jupiter.api.Assertions.assertEquals;
15 | import static org.mockito.ArgumentMatchers.anyString;
16 |
17 | public class CachingServiceIT {
18 |
19 | @Test
20 | public void englishLocaleWelcomeMessage_caches() {
21 | RetryService mockRetryService = Mockito.mock(RetryService.class);
22 |
23 | AtomicInteger counter = new AtomicInteger();
24 | Mockito.when(mockRetryService.getWelcomeMessageAndHandleTimeout("en_US"))
25 | .thenReturn(Mono.defer(() ->
26 | Mono.just(new WelcomeMessage("count " + counter.incrementAndGet()))
27 | )
28 | );
29 |
30 | CachingService cachingService = new CachingService(mockRetryService);
31 |
32 | StepVerifier.create(cachingService.getEnglishLocaleWelcomeMessage())
33 | .expectNextMatches(welcomeMessage -> "count 1".equals(welcomeMessage.getMessage()))
34 | .verifyComplete();
35 |
36 | StepVerifier.create(cachingService.getEnglishLocaleWelcomeMessage())
37 | .expectNextMatches(welcomeMessage -> "count 1".equals(welcomeMessage.getMessage()))
38 | .verifyComplete();
39 | }
40 |
41 | @Test
42 | public void cachesSuccessOnly() {
43 | RetryService mockRetryService = Mockito.mock(RetryService.class);
44 |
45 | AtomicInteger counter = new AtomicInteger();
46 | Mockito.when(mockRetryService.getWelcomeMessageAndHandleTimeout("en_US"))
47 | .thenReturn(Mono.defer(() -> {
48 | if (counter.incrementAndGet() > 1) {
49 | return Mono.just(new WelcomeMessage("count " + counter.get()));
50 | } else {
51 | return Mono.error(new RuntimeException());
52 | }
53 | })
54 | );
55 |
56 | CachingService cachingService = new CachingService(mockRetryService);
57 |
58 | StepVerifier.create(cachingService.getEnglishLocaleWelcomeMessage())
59 | .expectError()
60 | .verify();
61 |
62 | StepVerifier.create(cachingService.getEnglishLocaleWelcomeMessage())
63 | .expectNextMatches(welcomeMessage -> "count 2".equals(welcomeMessage.getMessage()))
64 | .verifyComplete();
65 |
66 | // previous result should be cached
67 | StepVerifier.create(cachingService.getEnglishLocaleWelcomeMessage())
68 | .expectNextMatches(welcomeMessage -> "count 2".equals(welcomeMessage.getMessage()))
69 | .verifyComplete();
70 | }
71 |
72 | @Test
73 | public void getCachedWelcomeMono_cachesSuccess() {
74 | RetryService mockRetryService = Mockito.mock(RetryService.class);
75 |
76 | AtomicInteger timesInvoked = new AtomicInteger(0);
77 | Mockito.when(mockRetryService.getWelcomeMessageAndHandleTimeout(anyString()))
78 | .thenAnswer(new Answer>() {
79 | @Override
80 | public Mono answer(InvocationOnMock invocation) throws Throwable {
81 | String locale_arg = invocation.getArgument(0);
82 | return Mono.defer(() -> {
83 | timesInvoked.incrementAndGet();
84 | return Mono.just(new WelcomeMessage("locale " + locale_arg));
85 | });
86 | }
87 | });
88 |
89 | CachingService cachingService = new CachingService(mockRetryService);
90 |
91 | for (int i = 0; i < 3; i++) {
92 | StepVerifier.create(cachingService.getCachedWelcomeMono("en"))
93 | .expectNextMatches(welcomeMessage -> "locale en".equals(welcomeMessage.getMessage()))
94 | .verifyComplete();
95 | }
96 |
97 | for (int i = 0; i < 5; i++) {
98 | StepVerifier.create(cachingService.getCachedWelcomeMono("ru"))
99 | .expectNextMatches(welcomeMessage -> "locale ru".equals(welcomeMessage.getMessage()))
100 | .verifyComplete();
101 | }
102 |
103 | assertEquals(2, timesInvoked.get());
104 | }
105 |
106 | @Test
107 | public void getCachedWelcomeMono_doesNotCacheFailure() {
108 | RetryService mockRetryService = Mockito.mock(RetryService.class);
109 |
110 | AtomicInteger timesInvoked = new AtomicInteger(0);
111 | Mockito.when(mockRetryService.getWelcomeMessageAndHandleTimeout(anyString()))
112 | .thenAnswer(new Answer>() {
113 | @Override
114 | public Mono answer(InvocationOnMock invocation) throws Throwable {
115 | String locale_arg = invocation.getArgument(0);
116 | return Mono.defer(() -> {
117 | if (timesInvoked.incrementAndGet() > 1) {
118 | return Mono.just(new WelcomeMessage("locale " + locale_arg));
119 | } else {
120 | return Mono.error(new RuntimeException());
121 | }
122 | });
123 | }
124 | });
125 |
126 | CachingService cachingService = new CachingService(mockRetryService);
127 |
128 | StepVerifier.create(cachingService.getCachedWelcomeMono("en"))
129 | .verifyError();
130 |
131 | for (int i = 0; i < 3; i++) {
132 | StepVerifier.create(cachingService.getCachedWelcomeMono("en"))
133 | .expectNextMatches(welcomeMessage -> "locale en".equals(welcomeMessage.getMessage()))
134 | .verifyComplete();
135 | }
136 |
137 | assertEquals(2, timesInvoked.get());
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/test/java/com/nickolasfisher/webflux/service/CombiningCallsServiceIT.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import com.nickolasfisher.webflux.service.CombiningCallsService;
4 | import io.swagger.models.HttpMethod;
5 | import org.junit.jupiter.api.AfterEach;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 | import org.junit.jupiter.api.extension.ExtendWith;
9 | import org.mockserver.integration.ClientAndServer;
10 | import org.mockserver.junit.jupiter.MockServerExtension;
11 | import org.mockserver.model.HttpRequest;
12 | import org.mockserver.model.HttpResponse;
13 | import org.mockserver.model.MediaType;
14 | import org.mockserver.verify.VerificationTimes;
15 | import org.springframework.web.reactive.function.client.WebClient;
16 | import reactor.test.StepVerifier;
17 |
18 | @ExtendWith(MockServerExtension.class)
19 | public class CombiningCallsServiceIT {
20 | private CombiningCallsService combiningCallsService;
21 |
22 | private WebClient webClient;
23 | private ClientAndServer clientAndServer;
24 |
25 | public CombiningCallsServiceIT(ClientAndServer clientAndServer) {
26 | this.clientAndServer = clientAndServer;
27 | this.webClient = WebClient.builder()
28 | .baseUrl("http://localhost:" + clientAndServer.getPort())
29 | .build();
30 | }
31 |
32 | @BeforeEach
33 | public void setup() {
34 | combiningCallsService = new CombiningCallsService(webClient);
35 | }
36 |
37 | @AfterEach
38 | public void reset() {
39 | clientAndServer.reset();
40 | }
41 |
42 | @Test
43 | public void callsFirstAndUsesCallToGetSecond() {
44 | HttpRequest expectedFirstRequest = HttpRequest.request()
45 | .withMethod(HttpMethod.GET.name())
46 | .withPath("/first/endpoint/10");
47 |
48 | this.clientAndServer.when(
49 | expectedFirstRequest
50 | ).respond(
51 | HttpResponse.response()
52 | .withBody("{\"fieldFromFirstCall\": 100}")
53 | .withContentType(MediaType.APPLICATION_JSON)
54 | );
55 |
56 | HttpRequest expectedSecondRequest = HttpRequest.request()
57 | .withMethod(HttpMethod.GET.name())
58 | .withPath("/second/endpoint/100");
59 |
60 | this.clientAndServer.when(
61 | expectedSecondRequest
62 | ).respond(
63 | HttpResponse.response()
64 | .withBody("{\"fieldFromSecondCall\": \"hello\"}")
65 | .withContentType(MediaType.APPLICATION_JSON)
66 | );
67 |
68 | StepVerifier.create(this.combiningCallsService.sequentialCalls(10))
69 | .expectNextMatches(secondCallDTO -> "hello".equals(secondCallDTO.getFieldFromSecondCall()))
70 | .verifyComplete();
71 |
72 | this.clientAndServer.verify(expectedFirstRequest, VerificationTimes.once());
73 | this.clientAndServer.verify(expectedSecondRequest, VerificationTimes.once());
74 | }
75 |
76 | @Test
77 | public void mergedCalls_callsBothEndpointsAndMergesResults() {
78 | HttpRequest expectedFirstRequest = HttpRequest.request()
79 | .withMethod(HttpMethod.GET.name())
80 | .withPath("/first/endpoint/25");
81 |
82 | this.clientAndServer.when(
83 | expectedFirstRequest
84 | ).respond(
85 | HttpResponse.response()
86 | .withBody("{\"fieldFromFirstCall\": 250}")
87 | .withContentType(MediaType.APPLICATION_JSON)
88 | );
89 |
90 | HttpRequest expectedSecondRequest = HttpRequest.request()
91 | .withMethod(HttpMethod.GET.name())
92 | .withPath("/second/endpoint/45");
93 |
94 | this.clientAndServer.when(
95 | expectedSecondRequest
96 | ).respond(
97 | HttpResponse.response()
98 | .withBody("{\"fieldFromSecondCall\": \"something\"}")
99 | .withContentType(MediaType.APPLICATION_JSON)
100 | );
101 |
102 | StepVerifier.create(this.combiningCallsService.mergedCalls(25, 45))
103 | .expectNextMatches(mergedCallsDTO -> 250 == mergedCallsDTO.getFieldOne()
104 | && "something".equals(mergedCallsDTO.getFieldTwo())
105 | )
106 | .verifyComplete();
107 |
108 | this.clientAndServer.verify(expectedFirstRequest, VerificationTimes.once());
109 | this.clientAndServer.verify(expectedSecondRequest, VerificationTimes.once());
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/test/java/com/nickolasfisher/webflux/service/FallbackServiceIT.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import io.swagger.models.HttpMethod;
4 | import org.junit.jupiter.api.AfterEach;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.extension.ExtendWith;
8 | import org.mockserver.integration.ClientAndServer;
9 | import org.mockserver.junit.jupiter.MockServerExtension;
10 | import org.mockserver.model.HttpResponse;
11 | import org.mockserver.model.MediaType;
12 | import org.springframework.web.reactive.function.client.WebClient;
13 | import reactor.test.StepVerifier;
14 |
15 | import static org.mockserver.model.HttpRequest.request;
16 | import static org.mockserver.model.HttpResponse.response;
17 |
18 | @ExtendWith(MockServerExtension.class)
19 | public class FallbackServiceIT {
20 |
21 | private WebClient webClient;
22 | private ClientAndServer clientAndServer;
23 |
24 | private FallbackService fallbackService;
25 |
26 | public FallbackServiceIT(ClientAndServer clientAndServer) {
27 | this.clientAndServer = clientAndServer;
28 | this.webClient = WebClient.builder()
29 | .baseUrl("http://localhost:" + clientAndServer.getPort())
30 | .build();
31 | }
32 |
33 | @BeforeEach
34 | public void setup() {
35 | fallbackService = new FallbackService(webClient);
36 | }
37 |
38 | @AfterEach
39 | public void reset() {
40 | clientAndServer.reset();
41 | }
42 |
43 | @Test
44 | public void welcomeMessage_worksWhenNoErrors() {
45 | this.clientAndServer.when(
46 | request()
47 | .withPath("/locale/en_US/message")
48 | .withMethod(HttpMethod.GET.name())
49 | ).respond(
50 | HttpResponse.response()
51 | .withBody("{\"message\": \"hello\"}")
52 | .withContentType(MediaType.APPLICATION_JSON)
53 | );
54 |
55 | StepVerifier.create(fallbackService.getWelcomeMessageByLocale("en_US"))
56 | .expectNextMatches(welcomeMessage -> "hello".equals(welcomeMessage.getMessage()))
57 | .verifyComplete();
58 | }
59 |
60 | @Test
61 | public void welcomeMessage_fallsBackToEnglishWhenError() {
62 | this.clientAndServer.when(
63 | request()
64 | .withPath("/locale/fr/message")
65 | .withMethod(HttpMethod.GET.name())
66 | ).respond(
67 | HttpResponse.response()
68 | .withStatusCode(503)
69 | );
70 |
71 | StepVerifier.create(fallbackService.getWelcomeMessageByLocale("fr"))
72 | .expectNextMatches(welcomeMessage -> "hello fallback!".equals(welcomeMessage.getMessage()))
73 | .verifyComplete();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/api-calls-and-resilience/src/test/java/com/nickolasfisher/webflux/service/RetryServiceIT.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webflux.service;
2 |
3 | import io.netty.channel.ChannelOption;
4 | import io.netty.handler.timeout.ReadTimeoutHandler;
5 | import io.swagger.models.HttpMethod;
6 | import org.junit.jupiter.api.AfterEach;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Test;
9 | import org.junit.jupiter.api.extension.ExtendWith;
10 | import org.mockserver.integration.ClientAndServer;
11 | import org.mockserver.junit.jupiter.MockServerExtension;
12 | import org.mockserver.mock.action.ExpectationResponseCallback;
13 | import org.mockserver.model.HttpRequest;
14 | import org.mockserver.model.HttpResponse;
15 | import org.mockserver.model.MediaType;
16 | import org.mockserver.verify.VerificationTimes;
17 | import org.springframework.http.client.reactive.ReactorClientHttpConnector;
18 | import org.springframework.http.client.reactive.ReactorResourceFactory;
19 | import org.springframework.web.reactive.function.client.WebClient;
20 | import reactor.netty.http.client.HttpClient;
21 | import reactor.test.StepVerifier;
22 |
23 | import java.util.concurrent.TimeUnit;
24 | import java.util.concurrent.atomic.AtomicInteger;
25 |
26 | import static org.mockserver.model.HttpRequest.request;
27 |
28 | @ExtendWith(MockServerExtension.class)
29 | public class RetryServiceIT {
30 |
31 | public static final int WEBCLIENT_TIMEOUT = 50;
32 | private final ClientAndServer clientAndServer;
33 |
34 | private RetryService retryService;
35 | private WebClient mockWebClient;
36 |
37 | public RetryServiceIT(ClientAndServer clientAndServer) {
38 | this.clientAndServer = clientAndServer;
39 | HttpClient httpClient = HttpClient.create()
40 | .tcpConfiguration(tcpClient ->
41 | tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, WEBCLIENT_TIMEOUT)
42 | .doOnConnected(connection -> connection.addHandlerLast(
43 | new ReadTimeoutHandler(WEBCLIENT_TIMEOUT, TimeUnit.MILLISECONDS))
44 | )
45 | );
46 |
47 | this.mockWebClient = WebClient.builder()
48 | .baseUrl("http://localhost:" + this.clientAndServer.getPort())
49 | .clientConnector(new ReactorClientHttpConnector(httpClient))
50 | .build();
51 | }
52 |
53 | @BeforeEach
54 | public void setup() {
55 | this.retryService = new RetryService(mockWebClient);
56 | }
57 |
58 | @AfterEach
59 | public void clearExpectations() {
60 | this.clientAndServer.reset();
61 | }
62 |
63 | @Test
64 | public void retryOnTimeout() {
65 | AtomicInteger counter = new AtomicInteger();
66 | HttpRequest expectedRequest = request()
67 | .withPath("/locale/en_US/message")
68 | .withMethod(HttpMethod.GET.name());
69 |
70 | this.clientAndServer.when(
71 | expectedRequest
72 | ).respond(
73 | httpRequest -> {
74 | if (counter.incrementAndGet() < 2) {
75 | Thread.sleep(WEBCLIENT_TIMEOUT + 10);
76 | }
77 | return HttpResponse.response()
78 | .withBody("{\"message\": \"hello\"}")
79 | .withContentType(MediaType.APPLICATION_JSON);
80 | }
81 | );
82 |
83 | StepVerifier.create(retryService.getWelcomeMessageAndHandleTimeout("en_US"))
84 | .expectNextMatches(welcomeMessage -> "hello".equals(welcomeMessage.getMessage()))
85 | .verifyComplete();
86 |
87 | this.clientAndServer.verify(expectedRequest, VerificationTimes.exactly(3));
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/context-api/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 | venv
35 |
--------------------------------------------------------------------------------
/context-api/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.net.*;
18 | import java.io.*;
19 | import java.nio.channels.*;
20 | import java.util.Properties;
21 |
22 | public class MavenWrapperDownloader {
23 |
24 | private static final String WRAPPER_VERSION = "0.5.6";
25 | /**
26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
27 | */
28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
30 |
31 | /**
32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
33 | * use instead of the default one.
34 | */
35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
36 | ".mvn/wrapper/maven-wrapper.properties";
37 |
38 | /**
39 | * Path where the maven-wrapper.jar will be saved to.
40 | */
41 | private static final String MAVEN_WRAPPER_JAR_PATH =
42 | ".mvn/wrapper/maven-wrapper.jar";
43 |
44 | /**
45 | * Name of the property which should be used to override the default download url for the wrapper.
46 | */
47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
48 |
49 | public static void main(String args[]) {
50 | System.out.println("- Downloader started");
51 | File baseDirectory = new File(args[0]);
52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
53 |
54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
55 | // wrapperUrl parameter.
56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
57 | String url = DEFAULT_DOWNLOAD_URL;
58 | if (mavenWrapperPropertyFile.exists()) {
59 | FileInputStream mavenWrapperPropertyFileInputStream = null;
60 | try {
61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
62 | Properties mavenWrapperProperties = new Properties();
63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
65 | } catch (IOException e) {
66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
67 | } finally {
68 | try {
69 | if (mavenWrapperPropertyFileInputStream != null) {
70 | mavenWrapperPropertyFileInputStream.close();
71 | }
72 | } catch (IOException e) {
73 | // Ignore ...
74 | }
75 | }
76 | }
77 | System.out.println("- Downloading from: " + url);
78 |
79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
80 | if (!outputFile.getParentFile().exists()) {
81 | if (!outputFile.getParentFile().mkdirs()) {
82 | System.out.println(
83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
84 | }
85 | }
86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
87 | try {
88 | downloadFileFromURL(url, outputFile);
89 | System.out.println("Done");
90 | System.exit(0);
91 | } catch (Throwable e) {
92 | System.out.println("- Error downloading");
93 | e.printStackTrace();
94 | System.exit(1);
95 | }
96 | }
97 |
98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
100 | String username = System.getenv("MVNW_USERNAME");
101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
102 | Authenticator.setDefault(new Authenticator() {
103 | @Override
104 | protected PasswordAuthentication getPasswordAuthentication() {
105 | return new PasswordAuthentication(username, password);
106 | }
107 | });
108 | }
109 | URL website = new URL(urlString);
110 | ReadableByteChannel rbc;
111 | rbc = Channels.newChannel(website.openStream());
112 | FileOutputStream fos = new FileOutputStream(destination);
113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
114 | fos.close();
115 | rbc.close();
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/context-api/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/context-api/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/context-api/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/context-api/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/context-api/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.3.2.RELEASE
9 |
10 |
11 | com.nickolasfisher
12 | webflux-context
13 | 1.0
14 | webflux-context
15 | Examples passing context around in webflux
16 |
17 |
18 | 11
19 |
20 |
21 |
22 |
23 | org.springframework.boot
24 | spring-boot-starter-webflux
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-test
30 | test
31 |
32 |
33 | org.junit.vintage
34 | junit-vintage-engine
35 |
36 |
37 |
38 |
39 | io.projectreactor
40 | reactor-test
41 | test
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.springframework.boot
49 | spring-boot-maven-plugin
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/context-api/simple-echo-server/reflect.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Reflects the requests from HTTP methods GET, POST, PUT, and DELETE
3 | # Written by Nathan Hamiel (2010)
4 |
5 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
6 | from optparse import OptionParser
7 |
8 | class RequestHandler(BaseHTTPRequestHandler):
9 |
10 | def do_GET(self):
11 |
12 | request_path = self.path
13 |
14 | print("\n----- Request Start ----->\n")
15 | print(request_path)
16 | print(self.headers)
17 | print("<----- Request End -----\n")
18 |
19 | self.send_response(200)
20 | self.send_header("Set-Cookie", "foo=bar")
21 | self.end_headers()
22 |
23 | def do_POST(self):
24 |
25 | request_path = self.path
26 |
27 | print("\n----- Request Start ----->\n")
28 | print(request_path)
29 |
30 | request_headers = self.headers
31 | content_length = request_headers.getheaders('content-length')
32 | length = int(content_length[0]) if content_length else 0
33 |
34 | print(request_headers)
35 | print(self.rfile.read(length))
36 | print("<----- Request End -----\n")
37 |
38 | self.send_response(200)
39 | self.end_headers()
40 |
41 | do_PUT = do_POST
42 | do_DELETE = do_GET
43 |
44 | def main():
45 | port = 9000
46 | print('Listening on localhost:%s' % port)
47 | server = HTTPServer(('', port), RequestHandler)
48 | server.serve_forever()
49 |
50 |
51 | if __name__ == "__main__":
52 | parser = OptionParser()
53 | parser.usage = ("Creates an http-server that will echo out any GET or POST parameters\n"
54 | "Run:\n\n"
55 | " reflect")
56 | (options, args) = parser.parse_args()
57 |
58 | main()
59 |
--------------------------------------------------------------------------------
/context-api/src/main/java/com/nickolasfisher/webfluxcontext/HelloController.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webfluxcontext;
2 |
3 | import org.springframework.http.ResponseEntity;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RestController;
6 | import org.springframework.web.reactive.function.client.WebClient;
7 | import reactor.core.publisher.Mono;
8 |
9 | import static com.nickolasfisher.webfluxcontext.WebContextFilter.X_CUSTOM_HEADER;
10 |
11 | @RestController
12 | public class HelloController {
13 |
14 | private final WebClient webClient;
15 |
16 | public HelloController(WebClient webClient) {
17 | this.webClient = webClient;
18 | }
19 |
20 | @GetMapping("/hello")
21 | public Mono> hello() {
22 | return Mono.subscriberContext()
23 | .flatMap(context -> webClient.get()
24 | .uri("/test")
25 | .exchange()
26 | .map(clientResponse -> {
27 | String[] strings = context.get(X_CUSTOM_HEADER);
28 | return ResponseEntity.status(200)
29 | .header(X_CUSTOM_HEADER, strings)
30 | .build();
31 | }));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/context-api/src/main/java/com/nickolasfisher/webfluxcontext/WebClientConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webfluxcontext;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.web.reactive.function.client.*;
6 | import reactor.core.publisher.Mono;
7 | import reactor.util.context.Context;
8 |
9 | import java.util.Arrays;
10 | import java.util.Collections;
11 | import java.util.List;
12 | import java.util.function.Function;
13 |
14 | import static com.nickolasfisher.webfluxcontext.WebContextFilter.X_CUSTOM_HEADER;
15 |
16 | @Configuration
17 | public class WebClientConfig {
18 |
19 | @Bean
20 | public WebClient webClient() {
21 | return WebClient.builder()
22 | .filter(new ExchangeFilterFunction() {
23 | @Override
24 | public Mono filter(ClientRequest clientRequest, ExchangeFunction exchangeFunction) {
25 | return Mono.subscriberContext()
26 | .flatMap(context -> {
27 | String[] wat = context.get(X_CUSTOM_HEADER);
28 | ClientRequest clientReq = ClientRequest.from(clientRequest)
29 | .header(X_CUSTOM_HEADER, wat)
30 | .build();
31 |
32 | return exchangeFunction.exchange(clientReq);
33 | });
34 | }
35 | })
36 | .baseUrl("http://localhost:9000")
37 | .build();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/context-api/src/main/java/com/nickolasfisher/webfluxcontext/WebContextFilter.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webfluxcontext;
2 |
3 | import org.springframework.stereotype.Component;
4 | import org.springframework.web.server.ServerWebExchange;
5 | import org.springframework.web.server.WebFilter;
6 | import org.springframework.web.server.WebFilterChain;
7 | import reactor.core.publisher.Mono;
8 | import reactor.util.context.Context;
9 |
10 | import java.util.List;
11 | import java.util.function.Function;
12 | import java.util.function.Supplier;
13 |
14 | @Component
15 | public class WebContextFilter implements WebFilter {
16 |
17 | public static final String X_CUSTOM_HEADER = "X-Custom-Header";
18 |
19 | @Override
20 | public Mono filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
21 | List customHeaderValues = serverWebExchange.getRequest().getHeaders().get(X_CUSTOM_HEADER);
22 | String singleCustomHeader = customHeaderValues != null && customHeaderValues.size() == 1 ? customHeaderValues.get(0) : null;
23 | serverWebExchange.getResponse();
24 | return webFilterChain.filter(serverWebExchange).subscriberContext(new Function() {
25 | @Override
26 | public Context apply(Context context) {
27 | return singleCustomHeader != null ? context.put(X_CUSTOM_HEADER, new String[] {singleCustomHeader}) : context;
28 | }
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/context-api/src/main/java/com/nickolasfisher/webfluxcontext/WebfluxContextApplication.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webfluxcontext;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class WebfluxContextApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(WebfluxContextApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/context-api/src/main/resources/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/context-api/src/main/resources/application.properties
--------------------------------------------------------------------------------
/context-api/src/test/java/com/nickolasfisher/webfluxcontext/WebfluxContextApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.webfluxcontext;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.boot.test.context.SpringBootTest;
5 |
6 | @SpringBootTest
7 | class WebfluxContextApplicationTests {
8 |
9 | @Test
10 | void contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.net.*;
18 | import java.io.*;
19 | import java.nio.channels.*;
20 | import java.util.Properties;
21 |
22 | public class MavenWrapperDownloader {
23 |
24 | private static final String WRAPPER_VERSION = "0.5.6";
25 | /**
26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
27 | */
28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
30 |
31 | /**
32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
33 | * use instead of the default one.
34 | */
35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
36 | ".mvn/wrapper/maven-wrapper.properties";
37 |
38 | /**
39 | * Path where the maven-wrapper.jar will be saved to.
40 | */
41 | private static final String MAVEN_WRAPPER_JAR_PATH =
42 | ".mvn/wrapper/maven-wrapper.jar";
43 |
44 | /**
45 | * Name of the property which should be used to override the default download url for the wrapper.
46 | */
47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
48 |
49 | public static void main(String args[]) {
50 | System.out.println("- Downloader started");
51 | File baseDirectory = new File(args[0]);
52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
53 |
54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
55 | // wrapperUrl parameter.
56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
57 | String url = DEFAULT_DOWNLOAD_URL;
58 | if (mavenWrapperPropertyFile.exists()) {
59 | FileInputStream mavenWrapperPropertyFileInputStream = null;
60 | try {
61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
62 | Properties mavenWrapperProperties = new Properties();
63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
65 | } catch (IOException e) {
66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
67 | } finally {
68 | try {
69 | if (mavenWrapperPropertyFileInputStream != null) {
70 | mavenWrapperPropertyFileInputStream.close();
71 | }
72 | } catch (IOException e) {
73 | // Ignore ...
74 | }
75 | }
76 | }
77 | System.out.println("- Downloading from: " + url);
78 |
79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
80 | if (!outputFile.getParentFile().exists()) {
81 | if (!outputFile.getParentFile().mkdirs()) {
82 | System.out.println(
83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
84 | }
85 | }
86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
87 | try {
88 | downloadFileFromURL(url, outputFile);
89 | System.out.println("Done");
90 | System.exit(0);
91 | } catch (Throwable e) {
92 | System.out.println("- Error downloading");
93 | e.printStackTrace();
94 | System.exit(1);
95 | }
96 | }
97 |
98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
100 | String username = System.getenv("MVNW_USERNAME");
101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
102 | Authenticator.setDefault(new Authenticator() {
103 | @Override
104 | protected PasswordAuthentication getPasswordAuthentication() {
105 | return new PasswordAuthentication(username, password);
106 | }
107 | });
108 | }
109 | URL website = new URL(urlString);
110 | ReadableByteChannel rbc;
111 | rbc = Channels.newChannel(website.openStream());
112 | FileOutputStream fos = new FileOutputStream(destination);
113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
114 | fos.close();
115 | rbc.close();
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/mocking-and-unit-testing/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/mocking-and-unit-testing/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.3.3.RELEASE
9 |
10 |
11 | com.nickolasfisher
12 | testing
13 | 1.0
14 | testing
15 | Testing in Spring Boot Webflux
16 |
17 |
18 | 11
19 |
20 |
21 |
22 |
23 | org.springframework.boot
24 | spring-boot-starter-webflux
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-test
30 | test
31 |
32 |
33 | org.junit.vintage
34 | junit-vintage-engine
35 |
36 |
37 |
38 |
39 | org.mock-server
40 | mockserver-netty
41 | 5.11.1
42 |
43 |
44 | io.projectreactor
45 | reactor-test
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 | org.springframework.boot
54 | spring-boot-maven-plugin
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/java/com/nickolasfisher/testing/MyConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.http.MediaType;
6 | import org.springframework.web.reactive.function.client.WebClient;
7 |
8 | import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
9 |
10 | @Configuration
11 | public class MyConfig {
12 |
13 | @Bean
14 | public WebClient webClient() {
15 | return WebClient.builder()
16 | .baseUrl("http://localhost:9000")
17 | .defaultHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
18 | .build();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/java/com/nickolasfisher/testing/TestingApplication.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class TestingApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(TestingApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/java/com/nickolasfisher/testing/controller/MyController.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing.controller;
2 |
3 | import com.nickolasfisher.testing.dto.DownstreamResponseDTO;
4 | import com.nickolasfisher.testing.dto.PersonDTO;
5 | import com.nickolasfisher.testing.service.MyService;
6 | import org.springframework.web.bind.annotation.GetMapping;
7 | import org.springframework.web.bind.annotation.RestController;
8 | import reactor.core.publisher.Flux;
9 |
10 | import java.util.function.Function;
11 |
12 | @RestController
13 | public class MyController {
14 |
15 | private final MyService service;
16 |
17 | public MyController(MyService service) {
18 | this.service = service;
19 | }
20 |
21 | @GetMapping("/persons")
22 | public Flux getPersons() {
23 | return service.getAllPeople().map(downstreamResponseDTO -> {
24 | PersonDTO personDTO = new PersonDTO();
25 |
26 | personDTO.setFirstName(downstreamResponseDTO.getFirstName());
27 | personDTO.setLastName(downstreamResponseDTO.getLastName());
28 |
29 | return personDTO;
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/java/com/nickolasfisher/testing/dto/DownstreamResponseDTO.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing.dto;
2 |
3 | import java.util.List;
4 |
5 | public class DownstreamResponseDTO {
6 | private String firstName;
7 | private String lastName;
8 | private String ssn;
9 | private String deepesetFear;
10 |
11 | public String getFirstName() {
12 | return firstName;
13 | }
14 |
15 | public void setFirstName(String firstName) {
16 | this.firstName = firstName;
17 | }
18 |
19 | public String getLastName() {
20 | return lastName;
21 | }
22 |
23 | public void setLastName(String lastName) {
24 | this.lastName = lastName;
25 | }
26 |
27 | public String getSsn() {
28 | return ssn;
29 | }
30 |
31 | public void setSsn(String ssn) {
32 | this.ssn = ssn;
33 | }
34 |
35 | public String getDeepesetFear() {
36 | return deepesetFear;
37 | }
38 |
39 | public void setDeepesetFear(String deepesetFear) {
40 | this.deepesetFear = deepesetFear;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/java/com/nickolasfisher/testing/dto/PersonDTO.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing.dto;
2 |
3 | public class PersonDTO {
4 | private String firstName;
5 | private String lastName;
6 |
7 | public String getFirstName() {
8 | return firstName;
9 | }
10 |
11 | public void setFirstName(String firstName) {
12 | this.firstName = firstName;
13 | }
14 |
15 | public String getLastName() {
16 | return lastName;
17 | }
18 |
19 | public void setLastName(String lastName) {
20 | this.lastName = lastName;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/java/com/nickolasfisher/testing/service/MyService.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing.service;
2 |
3 | import com.nickolasfisher.testing.dto.DownstreamResponseDTO;
4 | import org.springframework.stereotype.Service;
5 | import org.springframework.web.reactive.function.client.WebClient;
6 | import reactor.core.publisher.Flux;
7 | import reactor.util.retry.Retry;
8 |
9 | import java.time.Duration;
10 |
11 | @Service
12 | public class MyService {
13 |
14 | private final WebClient webClient;
15 |
16 | public MyService(WebClient webClient) {
17 | this.webClient = webClient;
18 | }
19 |
20 | public Flux getAllPeople() {
21 | return this.webClient.get()
22 | .uri("/legacy/persons")
23 | .retrieve()
24 | .bodyToFlux(DownstreamResponseDTO.class)
25 | .retryWhen(
26 | Retry.backoff(3, Duration.ofMillis(250))
27 | .minBackoff(Duration.ofMillis(100))
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/test/java/com/nickolasfisher/testing/controller/MyControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing.controller;
2 |
3 | import com.nickolasfisher.testing.dto.DownstreamResponseDTO;
4 | import com.nickolasfisher.testing.service.MyService;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.mockito.Mockito;
8 | import reactor.core.publisher.Flux;
9 | import reactor.test.StepVerifier;
10 |
11 | public class MyControllerTest {
12 | private MyService myServiceMock;
13 |
14 | private MyController myController;
15 |
16 | @BeforeEach
17 | public void setup() {
18 | myServiceMock = Mockito.mock(MyService.class);
19 | myController = new MyController(myServiceMock);
20 | }
21 |
22 | @Test
23 | public void verifyTransformsCorrectly() {
24 | DownstreamResponseDTO downstreamResponseDTO_1 = new DownstreamResponseDTO();
25 | downstreamResponseDTO_1.setFirstName("jack");
26 | downstreamResponseDTO_1.setLastName("attack");
27 | downstreamResponseDTO_1.setDeepesetFear("spiders");
28 | downstreamResponseDTO_1.setSsn("123-45-6789");
29 |
30 | DownstreamResponseDTO downstreamResponseDTO_2 = new DownstreamResponseDTO();
31 | downstreamResponseDTO_2.setFirstName("karen");
32 | downstreamResponseDTO_2.setLastName("cool");
33 | downstreamResponseDTO_2.setDeepesetFear("snakes");
34 | downstreamResponseDTO_2.setSsn("000-00-0000");
35 |
36 | Mockito.when(myServiceMock.getAllPeople())
37 | .thenReturn(Flux.just(downstreamResponseDTO_1, downstreamResponseDTO_2));
38 |
39 | StepVerifier.create(myController.getPersons())
40 | .expectNextMatches(personDTO -> personDTO.getLastName().equals(downstreamResponseDTO_1.getLastName())
41 | && personDTO.getFirstName().equals(downstreamResponseDTO_1.getFirstName()))
42 | .expectNextMatches(personDTO -> personDTO.getLastName().equals(downstreamResponseDTO_2.getLastName())
43 | && personDTO.getFirstName().equals(downstreamResponseDTO_2.getFirstName()))
44 | .verifyComplete();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/mocking-and-unit-testing/src/test/java/com/nickolasfisher/testing/service/MyServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.testing.service;
2 |
3 |
4 | import com.fasterxml.jackson.core.JsonProcessingException;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 | import com.nickolasfisher.testing.dto.DownstreamResponseDTO;
7 | import io.swagger.models.HttpMethod;
8 | import org.junit.jupiter.api.AfterEach;
9 | import org.junit.jupiter.api.BeforeAll;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.mockserver.integration.ClientAndServer;
13 | import org.mockserver.mock.action.ExpectationResponseCallback;
14 | import org.mockserver.model.HttpRequest;
15 | import org.mockserver.model.HttpResponse;
16 | import org.mockserver.model.MediaType;
17 | import org.springframework.http.HttpStatus;
18 | import org.springframework.web.reactive.function.client.WebClient;
19 |
20 | import java.util.Arrays;
21 | import java.util.List;
22 | import java.util.concurrent.atomic.AtomicInteger;
23 |
24 | import static org.junit.jupiter.api.Assertions.assertEquals;
25 | import static org.junit.jupiter.api.Assertions.assertTrue;
26 | import static org.mockserver.model.HttpRequest.*;
27 | import static org.mockserver.model.HttpResponse.response;
28 |
29 | public class MyServiceTest {
30 |
31 | private ClientAndServer mockServer;
32 |
33 | private MyService myService;
34 |
35 | private static final ObjectMapper serializer = new ObjectMapper();
36 |
37 | @BeforeEach
38 | public void setupMockServer() {
39 | mockServer = ClientAndServer.startClientAndServer(2001);
40 | myService = new MyService(WebClient.builder()
41 | .baseUrl("http://localhost:" + mockServer.getLocalPort()).build());
42 | }
43 |
44 | @AfterEach
45 | public void tearDownServer() {
46 | mockServer.stop();
47 | }
48 |
49 | @Test
50 | public void testTheThing() throws JsonProcessingException {
51 | String responseBody = getDownstreamResponseDTOAsString();
52 | mockServer.when(
53 | request()
54 | .withMethod(HttpMethod.GET.name())
55 | .withPath("/legacy/persons")
56 | ).respond(
57 | response()
58 | .withStatusCode(HttpStatus.OK.value())
59 | .withContentType(MediaType.APPLICATION_JSON)
60 | .withBody(responseBody)
61 | );
62 |
63 | List responses = myService.getAllPeople().collectList().block();
64 |
65 | assertEquals(1, responses.size());
66 | assertEquals("first", responses.get(0).getFirstName());
67 | assertEquals("last", responses.get(0).getLastName());
68 |
69 | mockServer.verify(
70 | request().withMethod(HttpMethod.GET.name())
71 | .withPath("/legacy/persons")
72 | );
73 | }
74 |
75 | private String getDownstreamResponseDTOAsString() throws JsonProcessingException {
76 | DownstreamResponseDTO downstreamResponseDTO = new DownstreamResponseDTO();
77 |
78 | downstreamResponseDTO.setLastName("last");
79 | downstreamResponseDTO.setFirstName("first");
80 | downstreamResponseDTO.setSsn("123-12-1231");
81 | downstreamResponseDTO.setDeepesetFear("alligators");
82 |
83 | return serializer.writeValueAsString(Arrays.asList(downstreamResponseDTO));
84 | }
85 |
86 | @Test
87 | public void retriesOnFailure() throws JsonProcessingException {
88 | String responseBody = getDownstreamResponseDTOAsString();
89 |
90 | AtomicInteger counter = new AtomicInteger(0);
91 | mockServer.when(
92 | request()
93 | .withMethod(HttpMethod.GET.name())
94 | .withPath("/legacy/persons")
95 | ).respond(
96 | new ExpectationResponseCallback() {
97 | @Override
98 | public HttpResponse handle(HttpRequest httpRequest) throws Exception {
99 | int attempt = counter.incrementAndGet();
100 | if (attempt >= 2) {
101 | return response().
102 | withBody(responseBody)
103 | .withContentType(MediaType.APPLICATION_JSON)
104 | .withStatusCode(HttpStatus.OK.value());
105 | } else {
106 | return response().withStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
107 | }
108 | }
109 | }
110 | );
111 |
112 | List responses = myService.getAllPeople().collectList().block();
113 |
114 | assertEquals(1, responses.size());
115 | assertEquals("first", responses.get(0).getFirstName());
116 | assertEquals("last", responses.get(0).getLastName());
117 |
118 | mockServer.verify(
119 | request().withMethod(HttpMethod.GET.name())
120 | .withPath("/legacy/persons")
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.net.*;
18 | import java.io.*;
19 | import java.nio.channels.*;
20 | import java.util.Properties;
21 |
22 | public class MavenWrapperDownloader {
23 |
24 | private static final String WRAPPER_VERSION = "0.5.6";
25 | /**
26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
27 | */
28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
30 |
31 | /**
32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
33 | * use instead of the default one.
34 | */
35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
36 | ".mvn/wrapper/maven-wrapper.properties";
37 |
38 | /**
39 | * Path where the maven-wrapper.jar will be saved to.
40 | */
41 | private static final String MAVEN_WRAPPER_JAR_PATH =
42 | ".mvn/wrapper/maven-wrapper.jar";
43 |
44 | /**
45 | * Name of the property which should be used to override the default download url for the wrapper.
46 | */
47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
48 |
49 | public static void main(String args[]) {
50 | System.out.println("- Downloader started");
51 | File baseDirectory = new File(args[0]);
52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
53 |
54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
55 | // wrapperUrl parameter.
56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
57 | String url = DEFAULT_DOWNLOAD_URL;
58 | if (mavenWrapperPropertyFile.exists()) {
59 | FileInputStream mavenWrapperPropertyFileInputStream = null;
60 | try {
61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
62 | Properties mavenWrapperProperties = new Properties();
63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
65 | } catch (IOException e) {
66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
67 | } finally {
68 | try {
69 | if (mavenWrapperPropertyFileInputStream != null) {
70 | mavenWrapperPropertyFileInputStream.close();
71 | }
72 | } catch (IOException e) {
73 | // Ignore ...
74 | }
75 | }
76 | }
77 | System.out.println("- Downloading from: " + url);
78 |
79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
80 | if (!outputFile.getParentFile().exists()) {
81 | if (!outputFile.getParentFile().mkdirs()) {
82 | System.out.println(
83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
84 | }
85 | }
86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
87 | try {
88 | downloadFileFromURL(url, outputFile);
89 | System.out.println("Done");
90 | System.exit(0);
91 | } catch (Throwable e) {
92 | System.out.println("- Error downloading");
93 | e.printStackTrace();
94 | System.exit(1);
95 | }
96 | }
97 |
98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
100 | String username = System.getenv("MVNW_USERNAME");
101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
102 | Authenticator.setDefault(new Authenticator() {
103 | @Override
104 | protected PasswordAuthentication getPasswordAuthentication() {
105 | return new PasswordAuthentication(username, password);
106 | }
107 | });
108 | }
109 | URL website = new URL(urlString);
110 | ReadableByteChannel rbc;
111 | rbc = Channels.newChannel(website.openStream());
112 | FileOutputStream fos = new FileOutputStream(destination);
113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
114 | fos.close();
115 | rbc.close();
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/reactive-clustered-redis/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/reactive-clustered-redis/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.4.5
9 |
10 |
11 | com.nickolasfisher
12 | clusteredredis
13 | 0.0.1-SNAPSHOT
14 | clusteredredis
15 | Clustered Redis interactions with lettuce
16 |
17 | 11
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-webflux
23 |
24 |
25 |
26 | org.springframework.boot
27 | spring-boot-starter-test
28 | test
29 |
30 |
31 | io.projectreactor
32 | reactor-test
33 | test
34 |
35 |
36 | io.lettuce
37 | lettuce-core
38 | 6.1.0.RELEASE
39 |
40 |
41 |
42 |
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-maven-plugin
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/src/main/java/com/nickolasfisher/clusteredredis/ClusteredredisApplication.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.clusteredredis;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class ClusteredredisApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(ClusteredredisApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/src/main/java/com/nickolasfisher/clusteredredis/LettuceConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.clusteredredis;
2 |
3 | import io.lettuce.core.cluster.RedisClusterClient;
4 | import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
5 | import io.lettuce.core.cluster.pubsub.api.reactive.RedisClusterPubSubReactiveCommands;
6 | import io.lettuce.core.resource.ClientResources;
7 | import org.springframework.boot.context.properties.ConfigurationProperties;
8 | import org.springframework.context.annotation.Bean;
9 | import org.springframework.context.annotation.Configuration;
10 |
11 | @Configuration
12 | @ConfigurationProperties("redis-cluster")
13 | public class LettuceConfig {
14 | private String host;
15 | private int port;
16 |
17 | public String getHost() {
18 | return host;
19 | }
20 |
21 | public void setHost(String host) {
22 | this.host = host;
23 | }
24 |
25 | public int getPort() {
26 | return port;
27 | }
28 |
29 | public void setPort(int port) {
30 | this.port = port;
31 | }
32 |
33 | @Bean("redis-cluster-commands")
34 | public RedisClusterReactiveCommands redisPrimaryReactiveCommands(RedisClusterClient redisClusterClient) {
35 | return redisClusterClient.connect().reactive();
36 | }
37 |
38 | @Bean
39 | public RedisClusterClient redisClient() {
40 | return RedisClusterClient.create(
41 | // adjust things like thread pool size with client resources
42 | ClientResources.builder().build(),
43 | "redis://" + this.getHost() + ":" + this.getPort()
44 | );
45 | }
46 |
47 | @Bean("redis-cluster-pub-sub")
48 | public RedisClusterPubSubReactiveCommands redisClusterPubSub(RedisClusterClient redisClusterClient) {
49 | return redisClusterClient.connectPubSub().reactive();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/reactive-clustered-redis/src/main/java/com/nickolasfisher/clusteredredis/PostConstructExecutor.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.clusteredredis;
2 |
3 | import io.lettuce.core.ScriptOutputType;
4 | import io.lettuce.core.SetArgs;
5 | import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
6 | import io.lettuce.core.cluster.pubsub.api.reactive.RedisClusterPubSubReactiveCommands;
7 | import org.springframework.beans.factory.annotation.Qualifier;
8 | import org.springframework.stereotype.Service;
9 | import reactor.core.publisher.Mono;
10 | import reactor.util.Logger;
11 | import reactor.util.Loggers;
12 |
13 | import javax.annotation.PostConstruct;
14 | import java.time.Duration;
15 | import java.util.ArrayList;
16 | import java.util.HashMap;
17 | import java.util.List;
18 | import java.util.Map;
19 |
20 | @Service
21 | public class PostConstructExecutor {
22 |
23 | private static final Logger LOG = Loggers.getLogger(PostConstructExecutor.class);
24 |
25 | private final RedisClusterReactiveCommands redisClusterReactiveCommands;
26 | private final RedisClusterPubSubReactiveCommands redisClusterPubSubReactiveCommands;
27 |
28 | public PostConstructExecutor(@Qualifier("redis-cluster-commands") RedisClusterReactiveCommands redisClusterReactiveCommands,
29 | @Qualifier("redis-cluster-pub-sub") RedisClusterPubSubReactiveCommands redisClusterPubSubReactiveCommands) {
30 | this.redisClusterReactiveCommands = redisClusterReactiveCommands;
31 | this.redisClusterPubSubReactiveCommands = redisClusterPubSubReactiveCommands;
32 | }
33 |
34 | @PostConstruct
35 | public void doStuffOnClusteredRedis() {
36 | subscribeToChannel();
37 | scriptLoad();
38 | setHellos();
39 | showMsetAcrossCluster();
40 | hashTagging();
41 | msetNxDifferentHashSlots();
42 | msetNxSameHashSlots();
43 | }
44 |
45 | private void subscribeToChannel() {
46 | List channels = new ArrayList<>();
47 | for (int i = 1; i <= 100; i++) {
48 | channels.add("channel-" + i);
49 | }
50 | redisClusterPubSubReactiveCommands.subscribe(channels.toArray(new String[0]))
51 | .subscribe();
52 |
53 | redisClusterPubSubReactiveCommands.observeChannels().doOnNext(channelAndMessage -> {
54 | LOG.info("channel {}, message {}", channelAndMessage.getChannel(), channelAndMessage.getMessage());
55 | }).subscribe();
56 | }
57 |
58 | private void scriptLoad() {
59 | LOG.info("starting script load");
60 | String hashOfScript = redisClusterReactiveCommands.scriptLoad("return redis.call('set',KEYS[1],ARGV[1],'ex',ARGV[2])")
61 | .block();
62 |
63 | redisClusterReactiveCommands.evalsha(hashOfScript, ScriptOutputType.BOOLEAN, new String[]{"foo1"}, "bar1", "10").blockLast();
64 |
65 | redisClusterReactiveCommands.evalsha(hashOfScript, ScriptOutputType.BOOLEAN, new String[] {"foo2"}, "bar2", "10").blockLast();
66 | redisClusterReactiveCommands.evalsha(hashOfScript, ScriptOutputType.BOOLEAN, new String[] {"foo4"}, "bar4", "10").blockLast();
67 | }
68 |
69 | private void msetNxDifferentHashSlots() {
70 | Mono successMono = redisClusterReactiveCommands.msetnx(
71 | Map.of(
72 | "key-1", "value-1",
73 | "key-2", "value-2",
74 | "key-3", "value-3",
75 | "key-4", "value-4"
76 | )
77 | );
78 |
79 | Boolean wasSuccessful = successMono.block();
80 |
81 | LOG.info("msetnx success response: {}", wasSuccessful);
82 | }
83 |
84 | private void msetNxSameHashSlots() {
85 | Mono successMono = redisClusterReactiveCommands.msetnx(
86 | Map.of(
87 | "{same-hash-slot}.key-1", "value-1",
88 | "{same-hash-slot}.key-2", "value-2",
89 | "{same-hash-slot}.key-3", "value-3"
90 | )
91 | );
92 |
93 | Boolean wasSuccessful = successMono.block();
94 |
95 | LOG.info("msetnx success response: {}", wasSuccessful);
96 | }
97 |
98 | private void setHellos() {
99 | SetArgs setArgs = new SetArgs();
100 | setArgs.ex(Duration.ofSeconds(10));
101 | Mono command = Mono.empty();
102 | for (int i = 0; i < 10; i++) {
103 | command = command.then(redisClusterReactiveCommands.set("hello-" + i, "no " + i, setArgs));
104 | }
105 | command.block();
106 | }
107 |
108 | private void showMsetAcrossCluster() {
109 | LOG.info("starting mset");
110 |
111 | Map map = new HashMap<>();
112 | for (int i = 0; i < 10; i++) {
113 | map.put("key" + i, "value" + i);
114 | }
115 |
116 | // can follow with MONITOR to see the MSETs for just that node written, under the hood lettuce breaks
117 | // up the map, gets the hash slot and sends it to that node for you.
118 | redisClusterReactiveCommands
119 | .mset(map)
120 | .block();
121 | LOG.info("done with mset");
122 | }
123 |
124 | private void hashTagging() {
125 | for (int i = 0; i < 10; i++) {
126 | String candidateKey = "not-hashtag." + i;
127 | Long keySlotNumber = redisClusterReactiveCommands.clusterKeyslot(candidateKey).block();
128 | LOG.info("key slot number for {} is {}", candidateKey, keySlotNumber);
129 | redisClusterReactiveCommands.set(candidateKey, "value").block();
130 | }
131 |
132 | for (int i = 0; i < 10; i++) {
133 | String candidateHashTaggedKey = "{some:hashtag}." + i;
134 | Long keySlotNumber = redisClusterReactiveCommands.clusterKeyslot(candidateHashTaggedKey).block();
135 | LOG.info("key slot number for {} is {}", candidateHashTaggedKey, keySlotNumber);
136 | redisClusterReactiveCommands.set(candidateHashTaggedKey, "value").block();
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/reactive-clustered-redis/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 |
2 | # refer to https://nickolasfisher.com/blog/Bootstrap-a-Local-Sharded-Redis-Cluster-in-Five-Minutes
3 | # for how to bootstrap a redis cluster quickly
4 | redis-cluster:
5 | host: 127.0.0.1
6 | port: 30001
7 |
8 |
--------------------------------------------------------------------------------
/reactive-redis/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 | redis-6.2.1*
35 |
--------------------------------------------------------------------------------
/reactive-redis/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.net.*;
18 | import java.io.*;
19 | import java.nio.channels.*;
20 | import java.util.Properties;
21 |
22 | public class MavenWrapperDownloader {
23 |
24 | private static final String WRAPPER_VERSION = "0.5.6";
25 | /**
26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
27 | */
28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
30 |
31 | /**
32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
33 | * use instead of the default one.
34 | */
35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
36 | ".mvn/wrapper/maven-wrapper.properties";
37 |
38 | /**
39 | * Path where the maven-wrapper.jar will be saved to.
40 | */
41 | private static final String MAVEN_WRAPPER_JAR_PATH =
42 | ".mvn/wrapper/maven-wrapper.jar";
43 |
44 | /**
45 | * Name of the property which should be used to override the default download url for the wrapper.
46 | */
47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
48 |
49 | public static void main(String args[]) {
50 | System.out.println("- Downloader started");
51 | File baseDirectory = new File(args[0]);
52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
53 |
54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
55 | // wrapperUrl parameter.
56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
57 | String url = DEFAULT_DOWNLOAD_URL;
58 | if (mavenWrapperPropertyFile.exists()) {
59 | FileInputStream mavenWrapperPropertyFileInputStream = null;
60 | try {
61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
62 | Properties mavenWrapperProperties = new Properties();
63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
65 | } catch (IOException e) {
66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
67 | } finally {
68 | try {
69 | if (mavenWrapperPropertyFileInputStream != null) {
70 | mavenWrapperPropertyFileInputStream.close();
71 | }
72 | } catch (IOException e) {
73 | // Ignore ...
74 | }
75 | }
76 | }
77 | System.out.println("- Downloading from: " + url);
78 |
79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
80 | if (!outputFile.getParentFile().exists()) {
81 | if (!outputFile.getParentFile().mkdirs()) {
82 | System.out.println(
83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
84 | }
85 | }
86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
87 | try {
88 | downloadFileFromURL(url, outputFile);
89 | System.out.println("Done");
90 | System.exit(0);
91 | } catch (Throwable e) {
92 | System.out.println("- Error downloading");
93 | e.printStackTrace();
94 | System.exit(1);
95 | }
96 | }
97 |
98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
100 | String username = System.getenv("MVNW_USERNAME");
101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
102 | Authenticator.setDefault(new Authenticator() {
103 | @Override
104 | protected PasswordAuthentication getPasswordAuthentication() {
105 | return new PasswordAuthentication(username, password);
106 | }
107 | });
108 | }
109 | URL website = new URL(urlString);
110 | ReadableByteChannel rbc;
111 | rbc = Channels.newChannel(website.openStream());
112 | FileOutputStream fos = new FileOutputStream(destination);
113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
114 | fos.close();
115 | rbc.close();
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/reactive-redis/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/reactive-redis/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/reactive-redis/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/reactive-redis/infra/leader-follower/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | leader:
5 | image: redis
6 | ports:
7 | - "6379:6379"
8 | - 6379
9 | networks:
10 | - local
11 | follower:
12 | image: redis
13 | ports:
14 | - "6380:6379"
15 | - 6379
16 | networks:
17 | - local
18 | command: ["--replicaof", "leader", "6379"]
19 |
20 | networks:
21 | local:
22 | driver: bridge
23 |
--------------------------------------------------------------------------------
/reactive-redis/infra/single-leader/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 | services:
3 | redis:
4 | image: "redis:alpine"
5 | ports:
6 | - "6379:6379"
7 |
--------------------------------------------------------------------------------
/reactive-redis/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/reactive-redis/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.4.4
9 |
10 |
11 | com.nickolasfisher
12 | reactiveredis
13 | 0.0.1-SNAPSHOT
14 | reactiveredis
15 | Reactive Redis tinkering
16 |
17 | 11
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-webflux
23 |
24 |
25 | org.projectlombok
26 | lombok
27 | 1.18.20
28 | provided
29 |
30 |
31 | io.lettuce
32 | lettuce-core
33 | 6.1.0.RELEASE
34 |
35 |
36 | com.github.kstyrc
37 | embedded-redis
38 | 0.6
39 | test
40 |
41 |
42 | org.testcontainers
43 | testcontainers
44 | 1.15.2
45 | test
46 |
47 |
48 | org.testcontainers
49 | junit-jupiter
50 | 1.15.2
51 | test
52 |
53 |
54 | org.springframework.boot
55 | spring-boot-starter-test
56 | test
57 |
58 |
59 | io.projectreactor
60 | reactor-test
61 | test
62 |
63 |
64 |
65 |
66 |
67 |
68 | org.springframework.boot
69 | spring-boot-maven-plugin
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/ReactiveredisApplication.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class ReactiveredisApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(ReactiveredisApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/config/RedisClusteredConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.config;
2 |
3 | public class RedisClusteredConfig {
4 | }
5 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/config/RedisConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.config;
2 |
3 | import io.lettuce.core.ReadFrom;
4 | import io.lettuce.core.RedisClient;
5 | import io.lettuce.core.RedisURI;
6 | import io.lettuce.core.api.reactive.RedisStringReactiveCommands;
7 | import io.lettuce.core.cluster.RedisClusterClient;
8 | import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
9 | import io.lettuce.core.codec.StringCodec;
10 | import io.lettuce.core.internal.LettuceFactories;
11 | import io.lettuce.core.masterreplica.MasterReplica;
12 | import io.lettuce.core.masterreplica.StatefulRedisMasterReplicaConnection;
13 | import io.lettuce.core.pubsub.api.reactive.RedisPubSubReactiveCommands;
14 | import io.lettuce.core.resource.ClientResources;
15 | import org.springframework.beans.factory.annotation.Qualifier;
16 | import org.springframework.boot.context.properties.ConfigurationProperties;
17 | import org.springframework.context.annotation.Bean;
18 | import org.springframework.context.annotation.Configuration;
19 |
20 | @Configuration
21 | public class RedisConfig {
22 | @Bean("redis-primary-commands")
23 | public RedisStringReactiveCommands redisPrimaryReactiveCommands(RedisClient redisClient) {
24 | return redisClient.connect().reactive();
25 | }
26 |
27 | @Bean("redis-subscription-commands")
28 | public RedisPubSubReactiveCommands redisPubSubReactiveCommands(RedisClient redisClient) {
29 | return redisClient.connectPubSub().reactive();
30 | }
31 |
32 | @Bean("redis-primary-client")
33 | public RedisClient redisClient(RedisPrimaryConfig redisPrimaryConfig) {
34 | return RedisClient.create(
35 | // adjust things like thread pool size with client resources
36 | ClientResources.builder().build(),
37 | "redis://" + redisPrimaryConfig.getHost() + ":" + redisPrimaryConfig.getPort()
38 | );
39 | }
40 |
41 | @Bean("redis-replica-commands")
42 | public RedisStringReactiveCommands redisReplicaReactiveCommands(RedisPrimaryConfig redisPrimaryConfig) {
43 | RedisURI redisPrimaryURI = RedisURI.builder()
44 | .withHost(redisPrimaryConfig.getHost())
45 | .withPort(redisPrimaryConfig.getPort())
46 | .build();
47 |
48 | RedisClient redisClient = RedisClient.create(
49 | redisPrimaryURI
50 | );
51 |
52 | StatefulRedisMasterReplicaConnection primaryAndReplicaConnection = MasterReplica.connect(
53 | redisClient,
54 | StringCodec.UTF8,
55 | redisPrimaryURI
56 | );
57 |
58 | primaryAndReplicaConnection.setReadFrom(ReadFrom.REPLICA);
59 |
60 | return primaryAndReplicaConnection.reactive();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/config/RedisPrimaryConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.config;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 | import org.springframework.context.annotation.Configuration;
5 |
6 | @Configuration
7 | @ConfigurationProperties(prefix = "redis-primary")
8 | public class RedisPrimaryConfig {
9 | private String host;
10 | private Integer port;
11 |
12 | public String getHost() {
13 | return host;
14 | }
15 |
16 | public void setHost(String host) {
17 | this.host = host;
18 | }
19 |
20 | public Integer getPort() {
21 | return port;
22 | }
23 |
24 | public void setPort(Integer port) {
25 | this.port = port;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/config/RedisReplicaConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.config;public class RedisReplicaConfig {
2 | }
3 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/controller/SampleController.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.controller;
2 |
3 | import com.nickolasfisher.reactiveredis.model.Thing;
4 | import com.nickolasfisher.reactiveredis.service.RedisDataService;
5 | import org.springframework.http.ResponseEntity;
6 | import org.springframework.web.bind.annotation.GetMapping;
7 | import org.springframework.web.bind.annotation.PathVariable;
8 | import org.springframework.web.bind.annotation.RestController;
9 | import reactor.core.publisher.Mono;
10 |
11 | @RestController
12 | public class SampleController {
13 | private final RedisDataService redisDataService;
14 |
15 | public SampleController(RedisDataService redisDataService) {
16 | this.redisDataService = redisDataService;
17 | }
18 |
19 | @GetMapping("/redis/{key}")
20 | public Mono> getRedisValue(@PathVariable("key") Integer key) {
21 | return redisDataService.getThing(key)
22 | .flatMap(thing -> Mono.just(ResponseEntity.ok(thing)))
23 | .defaultIfEmpty(ResponseEntity.notFound().build());
24 | }
25 |
26 | @GetMapping("/primary-redis/{key}")
27 | public Mono> getPrimaryRedisValue(@PathVariable("key") Integer key) {
28 | return redisDataService.getThingPrimary(key)
29 | .flatMap(thing -> Mono.just(ResponseEntity.ok(thing)))
30 | .defaultIfEmpty(ResponseEntity.notFound().build());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/model/Thing.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.model;
2 |
3 | import lombok.Builder;
4 | import lombok.Getter;
5 |
6 | @Builder
7 | @Getter
8 | public class Thing {
9 | private Integer id;
10 | private String value;
11 | }
12 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/service/RedisDataService.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.service;
2 |
3 | import com.nickolasfisher.reactiveredis.model.Thing;
4 | import io.lettuce.core.api.reactive.RedisStringReactiveCommands;
5 | import lombok.extern.log4j.Log4j2;
6 | import org.springframework.beans.factory.annotation.Qualifier;
7 | import org.springframework.stereotype.Service;
8 | import reactor.core.publisher.Mono;
9 |
10 | @Service
11 | @Log4j2
12 | public class RedisDataService {
13 |
14 | private final RedisStringReactiveCommands redisPrimaryCommands;
15 | private RedisStringReactiveCommands redisReplicaCommands;
16 |
17 | public RedisDataService(
18 | @Qualifier("redis-primary-commands") RedisStringReactiveCommands redisPrimaryCommands,
19 | @Qualifier("redis-replica-commands") RedisStringReactiveCommands redisReplicaCommands
20 | ) {
21 | this.redisPrimaryCommands = redisPrimaryCommands;
22 | this.redisReplicaCommands = redisReplicaCommands;
23 | }
24 |
25 | public Mono writeThing(Thing thing) {
26 | return this.redisPrimaryCommands
27 | .set(thing.getId().toString(), thing.getValue())
28 | .then();
29 | }
30 |
31 | public Mono getThing(Integer id) {
32 | log.info("getting {} from replica", id);
33 | return this.redisReplicaCommands.get(id.toString())
34 | .map(response -> Thing.builder().id(id).value(response).build());
35 | }
36 |
37 | public Mono getThingPrimary(Integer id) {
38 | log.info("getting {} from primary", id);
39 | return this.redisPrimaryCommands.get(id.toString())
40 | .map(response -> Thing.builder().id(id).value(response).build());
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/java/com/nickolasfisher/reactiveredis/service/RedisSubscriptionInitializer.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis.service;
2 |
3 | import io.lettuce.core.api.reactive.RedisStringReactiveCommands;
4 | import io.lettuce.core.pubsub.api.reactive.RedisPubSubReactiveCommands;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.stereotype.Service;
8 |
9 | import javax.annotation.PostConstruct;
10 |
11 | @Service
12 | public class RedisSubscriptionInitializer {
13 |
14 | private final Logger LOG = LoggerFactory.getLogger(RedisSubscriptionInitializer.class);
15 |
16 | private final RedisPubSubReactiveCommands redisPubSubReactiveCommands;
17 |
18 | public RedisSubscriptionInitializer(RedisPubSubReactiveCommands redisPubSubReactiveCommands) {
19 | this.redisPubSubReactiveCommands = redisPubSubReactiveCommands;
20 | }
21 |
22 | @PostConstruct
23 | public void setupSubscriber() {
24 | redisPubSubReactiveCommands.subscribe("channel-1").subscribe();
25 |
26 | redisPubSubReactiveCommands.observeChannels().doOnNext(stringStringChannelMessage -> {
27 | if ("channel-1".equals(stringStringChannelMessage.getChannel())) {
28 | LOG.info("found message in channel 1: {}", stringStringChannelMessage.getMessage());
29 | }
30 | }).subscribe();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/reactive-redis/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 |
2 | redis-primary:
3 | host: 127.0.0.1
4 | port: 6379
5 |
6 | #optionally enable debug logs to sanity check this actually works
7 | #logging.level.io.lettuce.core: DEBUG
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/BaseSetupAndTeardownRedis.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.RedisClient;
4 | import io.lettuce.core.api.reactive.RedisReactiveCommands;
5 | import org.junit.jupiter.api.AfterEach;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.testcontainers.containers.GenericContainer;
8 | import org.testcontainers.junit.jupiter.Container;
9 | import org.testcontainers.junit.jupiter.Testcontainers;
10 | import org.testcontainers.utility.DockerImageName;
11 |
12 | @Testcontainers
13 | public abstract class BaseSetupAndTeardownRedis {
14 |
15 | @Container
16 | public static GenericContainer genericContainer = new GenericContainer(
17 | DockerImageName.parse("redis:5.0.3-alpine")
18 | ).withExposedPorts(6379);
19 |
20 | protected RedisClient redisClient;
21 | protected RedisReactiveCommands redisReactiveCommands;
22 |
23 | @BeforeEach
24 | public void setupRedisClient() {
25 | redisClient = RedisClient.create("redis://" + genericContainer.getHost() + ":" + genericContainer.getMappedPort(6379));
26 | redisReactiveCommands = redisClient.connect().reactive();
27 | }
28 |
29 | @AfterEach
30 | public void removeAllDataFromRedis() {
31 | redisClient.connect().reactive().flushall().block();
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/DistributedLockingExampleTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.SetArgs;
4 | import org.junit.jupiter.api.Test;
5 | import reactor.core.publisher.Mono;
6 | import reactor.test.StepVerifier;
7 |
8 | import java.util.concurrent.atomic.AtomicInteger;
9 | import java.util.function.Function;
10 |
11 | import static org.junit.jupiter.api.Assertions.assertEquals;
12 |
13 | public class DistributedLockingExampleTest extends BaseSetupAndTeardownRedis {
14 |
15 | @Test
16 | public void distributedLocking() {
17 | AtomicInteger numTimesCalled = new AtomicInteger(0);
18 | Mono justLogItMono = Mono.defer(() -> {
19 | System.out.println("not doing anything, just logging");
20 | numTimesCalled.incrementAndGet();
21 | return Mono.empty();
22 | });
23 |
24 | StepVerifier.create(simpleDoIfLockAcquired("lock-123", justLogItMono))
25 | .verifyComplete();
26 |
27 | StepVerifier.create(simpleDoIfLockAcquired("lock-123", justLogItMono))
28 | .verifyComplete();
29 |
30 | StepVerifier.create(simpleDoIfLockAcquired("lock-123", justLogItMono))
31 | .verifyComplete();
32 |
33 | assertEquals(1, numTimesCalled.get());
34 | }
35 |
36 | public Mono simpleDoIfLockAcquired(String lockKey, Mono thingToDo) {
37 | return redisReactiveCommands.setnx(lockKey, "ACQUIRED")
38 | .flatMap(acquired -> {
39 | if (acquired) {
40 | System.out.println("lock acquired, returning mono");
41 | return thingToDo;
42 | } else {
43 | System.out.println("lock not acquired, doing nothing");
44 | return Mono.empty();
45 | }
46 | });
47 | }
48 |
49 | @Test
50 | public void distributedLockingAndErrorHandling() {
51 | AtomicInteger numTimesCalled = new AtomicInteger(0);
52 | Mono errorMono = Mono.defer(() -> {
53 | System.out.println("returning an error");
54 | numTimesCalled.incrementAndGet();
55 | return Mono.error(new RuntimeException("ahhhh"));
56 | });
57 |
58 | Mono successMono = Mono.defer(() -> {
59 | System.out.println("returning success");
60 | numTimesCalled.incrementAndGet();
61 | return Mono.empty();
62 | });
63 |
64 | StepVerifier.create(doIfLockAcquiredAndHandleErrors("lock-123", errorMono))
65 | .verifyError();
66 |
67 | StepVerifier.create(doIfLockAcquiredAndHandleErrors("lock-123", errorMono))
68 | .verifyError();
69 |
70 | StepVerifier.create(doIfLockAcquiredAndHandleErrors("lock-123", errorMono))
71 | .verifyError();
72 |
73 | StepVerifier.create(doIfLockAcquiredAndHandleErrors("lock-123", successMono))
74 | .verifyComplete();
75 |
76 | // errors should cause the lock to be released
77 | assertEquals(4, numTimesCalled.get());
78 |
79 | // we should have finally succeeded, which means the lock is marked as processed
80 | StepVerifier.create(redisReactiveCommands.get("lock-123"))
81 | .expectNext("PROCESSED")
82 | .verifyComplete();
83 | }
84 |
85 | public Mono doIfLockAcquiredAndHandleErrors(String lockKey, Mono thingToDo) {
86 | SetArgs setArgs = new SetArgs().nx().ex(20);
87 | return redisReactiveCommands
88 | .set(lockKey, "PROCESSING", setArgs)
89 | .switchIfEmpty(Mono.defer(() -> {
90 | System.out.println("lock not acquired, doing nothing");
91 | return Mono.empty();
92 | }))
93 | .flatMap(acquired -> {
94 | if (acquired.equals("OK")) {
95 | System.out.println("lock acquired, returning mono");
96 | return thingToDo
97 | .onErrorResume(throwable ->
98 | redisReactiveCommands
99 | .del(lockKey)
100 | .then(Mono.error(throwable))
101 | )
102 | .then(redisReactiveCommands.set(lockKey, "PROCESSED", new SetArgs().ex(200)).then());
103 | }
104 | return Mono.error(new RuntimeException("whoops!"));
105 | });
106 | }
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/ExpiringSortedSetEntriesTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.Range;
4 | import io.lettuce.core.ScoredValue;
5 | import org.junit.jupiter.api.Test;
6 | import reactor.core.publisher.Mono;
7 | import reactor.test.StepVerifier;
8 |
9 | import java.time.Instant;
10 | import java.time.temporal.ChronoUnit;
11 |
12 | public class ExpiringSortedSetEntriesTest extends BaseSetupAndTeardownRedis {
13 |
14 | @Test
15 | public void expireElementsPeriodically() throws Exception {
16 | String setKey = "values-set-key";
17 |
18 | addValueToSet(setKey, "first", Instant.now().plus(450, ChronoUnit.MILLIS).toEpochMilli());
19 | Thread.sleep(100);
20 |
21 | addValueToSet(setKey, "second", Instant.now().plus(150, ChronoUnit.MILLIS).toEpochMilli());
22 | Thread.sleep(100);
23 |
24 | addValueToSet(setKey, "third", Instant.now().plus(500, ChronoUnit.MILLIS).toEpochMilli());
25 | Thread.sleep(100);
26 |
27 | // expire everything based on score, or time to expire as epoch millisecond
28 | Mono expireOldEntriesMono = redisReactiveCommands.zremrangebyscore(setKey,
29 | Range.create(0, Instant.now().toEpochMilli())
30 | );
31 |
32 | StepVerifier.create(expireOldEntriesMono)
33 | .expectNext(1L).verifyComplete();
34 |
35 | // get all entries
36 | StepVerifier.create(redisReactiveCommands.zrevrangebyscore(setKey, Range.unbounded()))
37 | .expectNextMatches(val -> "third".equals(val))
38 | .expectNextMatches(val -> "first".equals(val))
39 | .verifyComplete();
40 | }
41 |
42 | private void addValueToSet(String setKey, String value, long epochMilliToExpire) {
43 | Mono addValueWithEpochMilliScore = redisReactiveCommands.zadd(
44 | setKey,
45 | ScoredValue.just(epochMilliToExpire, value)
46 | );
47 |
48 | StepVerifier.create(addValueWithEpochMilliScore)
49 | .expectNext(1L)
50 | .verifyComplete();
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/HashesTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.KeyValue;
4 | import io.lettuce.core.api.reactive.RedisReactiveCommands;
5 | import org.junit.jupiter.api.Test;
6 | import reactor.core.publisher.Mono;
7 | import reactor.test.StepVerifier;
8 |
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | public class HashesTest extends BaseSetupAndTeardownRedis {
13 |
14 | @Test
15 | public void setSingleHashAndGetWholeHash() {
16 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
17 |
18 | Mono multiSetMono = redisReactiveCommands.hmset("hash-set-key", Map.of(
19 | "key-1", "value-1",
20 | "key-2", "value-2",
21 | "key-3", "value-3"
22 | ));
23 |
24 | StepVerifier.create(multiSetMono)
25 | .expectNextMatches(response -> "OK".equals(response))
26 | .verifyComplete();
27 |
28 | Mono>> allKeyValuesMono = redisReactiveCommands.hgetall("hash-set-key").collectList();
29 |
30 | StepVerifier.create(allKeyValuesMono)
31 | .expectNextMatches(keyValues -> keyValues.size() == 3
32 | && keyValues.stream()
33 | .anyMatch(keyValue -> keyValue.getValue().equals("value-2")
34 | && keyValue.getKey().equals("key-2"))
35 | )
36 | .verifyComplete();
37 | }
38 |
39 | @Test
40 | public void getAndSetSingleValueInHash() {
41 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
42 |
43 | Mono multiSetMono = redisReactiveCommands.hmset("hash-set-key", Map.of(
44 | "key-1", "value-1",
45 | "key-2", "value-2",
46 | "key-3", "value-3"
47 | ));
48 |
49 | StepVerifier.create(multiSetMono)
50 | .expectNextMatches(response -> "OK".equals(response))
51 | .verifyComplete();
52 |
53 | StepVerifier.create(redisReactiveCommands.hget("hash-set-key", "key-1"))
54 | .expectNextMatches(val -> val.equals("value-1"))
55 | .verifyComplete();
56 |
57 | StepVerifier.create(redisReactiveCommands.hset("hash-set-key", "key-2", "new-value-2"))
58 | // returns false if no new fields were added--in this case we're changing an existing field
59 | .expectNextMatches(response -> !response)
60 | .verifyComplete();
61 |
62 | StepVerifier.create(redisReactiveCommands.hget("hash-set-key", "key-2"))
63 | .expectNextMatches(val -> "new-value-2".equals(val))
64 | .verifyComplete();
65 |
66 | // different value in the same hash is unchanged
67 | StepVerifier.create(redisReactiveCommands.hget("hash-set-key", "key-1"))
68 | .expectNextMatches(val -> "value-1".equals(val))
69 | .verifyComplete();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/ListsTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.KeyValue;
4 | import io.lettuce.core.api.reactive.RedisReactiveCommands;
5 | import org.junit.jupiter.api.Test;
6 | import reactor.core.publisher.Mono;
7 | import reactor.test.StepVerifier;
8 |
9 | import java.time.Duration;
10 | import java.time.Instant;
11 |
12 | import static org.junit.Assert.assertTrue;
13 |
14 | public class ListsTest extends BaseSetupAndTeardownRedis {
15 |
16 | @Test
17 | public void addAndRemoveFromTheLeft() {
18 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
19 |
20 | StepVerifier.create(redisReactiveCommands.lpush("list-key", "fourth-element", "third-element"))
21 | .expectNextMatches(sizeOfList -> 2L == sizeOfList)
22 | .verifyComplete();
23 |
24 | StepVerifier.create(redisReactiveCommands.lpush("list-key","second-element", "first-element"))
25 | // pushes to the left of the same list
26 | .expectNextMatches(sizeOfList -> 4L == sizeOfList)
27 | .verifyComplete();
28 |
29 | StepVerifier.create(redisReactiveCommands.lpop("list-key"))
30 | .expectNextMatches(poppedElement -> "first-element".equals(poppedElement))
31 | .verifyComplete();
32 | }
33 |
34 | @Test
35 | public void blockingGet() {
36 | RedisReactiveCommands redisReactiveCommands1 = redisClient.connect().reactive();
37 | RedisReactiveCommands redisReactiveCommands2 = redisClient.connect().reactive();
38 |
39 | long startingTime = Instant.now().toEpochMilli();
40 | StepVerifier.create(Mono.zip(
41 | redisReactiveCommands1.blpop(1, "list-key").switchIfEmpty(Mono.just(KeyValue.empty("list-key"))),
42 | Mono.delay(Duration.ofMillis(500)).then(redisReactiveCommands2.lpush("list-key", "an-element"))
43 | ).map(tuple -> tuple.getT1().getValue())
44 | )
45 | .expectNextMatches(value -> "an-element".equals(value))
46 | .verifyComplete();
47 | long endingTime = Instant.now().toEpochMilli();
48 |
49 | assertTrue(endingTime - startingTime > 400);
50 | }
51 |
52 | @Test
53 | public void getRange() {
54 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
55 |
56 | StepVerifier.create(redisReactiveCommands.lpush("list-key", "third-element", "second-element", "first-element"))
57 | .expectNextMatches(sizeOfList -> 3L == sizeOfList)
58 | .verifyComplete();
59 |
60 | StepVerifier.create(redisReactiveCommands.lrange("list-key", 0, 1))
61 | .expectNextMatches(first -> "first-element".equals(first))
62 | .expectNextMatches(second -> "second-element".equals(second))
63 | .verifyComplete();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/LuaScriptTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import com.github.dockerjava.zerodep.shaded.org.apache.commons.codec.binary.Hex;
4 | import io.lettuce.core.ScriptOutputType;
5 | import org.junit.jupiter.api.Test;
6 | import reactor.test.StepVerifier;
7 |
8 | import java.nio.charset.StandardCharsets;
9 | import java.security.MessageDigest;
10 | import java.util.Arrays;
11 |
12 | import static org.junit.jupiter.api.Assertions.assertEquals;
13 |
14 | public class LuaScriptTest extends BaseSetupAndTeardownRedis {
15 |
16 | public static final String SAMPLE_LUA_SCRIPT = "return redis.call('set',KEYS[1],ARGV[1],'ex',ARGV[2])";
17 |
18 | @Test
19 | public void executeLuaScript() {
20 | String script = SAMPLE_LUA_SCRIPT;
21 |
22 | StepVerifier.create(redisReactiveCommands.eval(script, ScriptOutputType.BOOLEAN,
23 | // keys as an array
24 | Arrays.asList("foo1").toArray(new String[0]),
25 | // other arguments
26 | "bar1", "10"
27 | )
28 | )
29 | .expectNext(true)
30 | .verifyComplete();
31 |
32 | StepVerifier.create(redisReactiveCommands.get("foo1"))
33 | .expectNext("bar1")
34 | .verifyComplete();
35 |
36 | StepVerifier.create(redisReactiveCommands.ttl("foo1"))
37 | .expectNextMatches(ttl -> 7 < ttl && 11 > ttl)
38 | .verifyComplete();
39 | }
40 |
41 | @Test
42 | public void scriptLoadFromResponse() {
43 | String shaOfScript = redisReactiveCommands.scriptLoad(SAMPLE_LUA_SCRIPT).block();
44 |
45 | StepVerifier.create(redisReactiveCommands.evalsha(
46 | shaOfScript,
47 | ScriptOutputType.BOOLEAN,
48 | // keys as an array
49 | Arrays.asList("foo1").toArray(new String[0]),
50 | // other arguments
51 | "bar1", "10")
52 | )
53 | .expectNext(true)
54 | .verifyComplete();
55 | }
56 |
57 | @Test
58 | public void scriptLoadFromDigest() throws Exception {
59 | MessageDigest md = MessageDigest.getInstance("SHA-1");
60 | byte[] digestAsBytes = md.digest(SAMPLE_LUA_SCRIPT.getBytes(StandardCharsets.UTF_8));
61 | String hexadecimalStringOfScriptSha1 = Hex.encodeHexString(digestAsBytes);
62 | String hexStringFromRedis = redisReactiveCommands.scriptLoad(SAMPLE_LUA_SCRIPT).block();
63 |
64 | // they're the same
65 | assertEquals(hexadecimalStringOfScriptSha1, hexStringFromRedis);
66 |
67 | StepVerifier.create(redisReactiveCommands.evalsha(
68 | hexadecimalStringOfScriptSha1,
69 | ScriptOutputType.BOOLEAN,
70 | // keys as an array
71 | Arrays.asList("foo1").toArray(new String[0]),
72 | // other arguments
73 | "bar1", "10")
74 | )
75 | .expectNext(true)
76 | .verifyComplete();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/MultiSetsTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import reactor.core.publisher.Mono;
5 | import reactor.test.StepVerifier;
6 |
7 | import java.util.List;
8 |
9 | //https://redis.io/commands/#set
10 | public class MultiSetsTest extends BaseSetupAndTeardownRedis {
11 |
12 | @Test
13 | public void subtractingMultipleSets() {
14 | String firstSetKey = "first-set-key";
15 | String secondSetKey = "second-set-key";
16 | Mono setupFirstSetMono = redisReactiveCommands.sadd(firstSetKey, "value-1", "value-2");
17 |
18 | StepVerifier.create(setupFirstSetMono).expectNext(2L).verifyComplete();
19 |
20 | Mono setupSecondSetMono = redisReactiveCommands.sadd(secondSetKey, "value-1", "value-3");
21 |
22 | StepVerifier.create(setupSecondSetMono).expectNext(2L).verifyComplete();
23 |
24 | Mono> subtractSecondFromFirstCollection = redisReactiveCommands.sdiff(firstSetKey, secondSetKey).collectList();
25 |
26 | StepVerifier.create(subtractSecondFromFirstCollection)
27 | .expectNextMatches(collection ->
28 | collection.size() == 1
29 | && collection.contains("value-2"))
30 | .verifyComplete();
31 |
32 | Mono> subtractFirstFromSecondCollection = redisReactiveCommands.sdiff(secondSetKey, firstSetKey).collectList();
33 |
34 | StepVerifier.create(subtractFirstFromSecondCollection)
35 | .expectNextMatches(collection ->
36 | collection.size() == 1
37 | && collection.contains("value-3"))
38 | .verifyComplete();
39 |
40 | Mono> originalSetUnchangedMono = redisReactiveCommands.smembers(firstSetKey).collectList();
41 |
42 | StepVerifier.create(originalSetUnchangedMono)
43 | .expectNextMatches(firstSetMembers ->
44 | firstSetMembers.size() == 2
45 | && firstSetMembers.contains("value-1")
46 | && firstSetMembers.contains("value-2")
47 | ).verifyComplete();
48 | }
49 |
50 | @Test
51 | public void intersectingMultipleSets() {
52 | String firstSetKey = "first-set-key";
53 | String secondSetKey = "second-set-key";
54 | Mono setupFirstSetMono = redisReactiveCommands
55 | .sadd(firstSetKey, "value-1", "value-2");
56 |
57 | StepVerifier.create(setupFirstSetMono).expectNext(2L).verifyComplete();
58 |
59 | Mono setupSecondSetMono = redisReactiveCommands
60 | .sadd(secondSetKey, "value-1", "value-3");
61 |
62 | StepVerifier.create(setupSecondSetMono).expectNext(2L).verifyComplete();
63 |
64 | Mono> intersectedSets = redisReactiveCommands
65 | .sinter(firstSetKey, secondSetKey).collectList();
66 |
67 | StepVerifier.create(intersectedSets)
68 | .expectNextMatches(collection ->
69 | collection.size() == 1
70 | && collection.contains("value-1")
71 | && !collection.contains("value-2")
72 | )
73 | .verifyComplete();
74 | }
75 |
76 | @Test
77 | public void addingMultipleSetsTogether() {
78 | String firstSetKey = "first-set-key";
79 | String secondSetKey = "second-set-key";
80 | Mono setupFirstSetMono = redisReactiveCommands
81 | .sadd(firstSetKey, "value-1", "value-2");
82 |
83 | StepVerifier.create(setupFirstSetMono).expectNext(2L).verifyComplete();
84 |
85 | Mono setupSecondSetMono = redisReactiveCommands
86 | .sadd(secondSetKey, "value-1", "value-3");
87 |
88 | StepVerifier.create(setupSecondSetMono).expectNext(2L).verifyComplete();
89 |
90 | Mono> unionedSets = redisReactiveCommands
91 | .sunion(firstSetKey, secondSetKey).collectList();
92 |
93 | StepVerifier.create(unionedSets)
94 | .expectNextMatches(collection ->
95 | collection.size() == 3
96 | && collection.contains("value-1")
97 | && collection.contains("value-2")
98 | && collection.contains("value-3")
99 | )
100 | .verifyComplete();
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/PubSubTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
4 | import io.lettuce.core.pubsub.api.reactive.RedisPubSubReactiveCommands;
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.util.concurrent.atomic.AtomicBoolean;
9 |
10 | public class PubSubTest extends BaseSetupAndTeardownRedis {
11 |
12 | @Test
13 | public void publishAndSubscribe() throws Exception {
14 | StatefulRedisPubSubConnection pubSubConnection =
15 | redisClient.connectPubSub();
16 |
17 | AtomicBoolean messageReceived = new AtomicBoolean(false);
18 | RedisPubSubReactiveCommands reactivePubSubCommands = pubSubConnection.reactive();
19 | reactivePubSubCommands.subscribe("some-channel").subscribe();
20 |
21 | reactivePubSubCommands.observeChannels()
22 | .doOnNext(stringStringChannelMessage -> messageReceived.set(true))
23 | .subscribe();
24 |
25 | Thread.sleep(25);
26 |
27 | redisClient.connectPubSub()
28 | .reactive()
29 | .publish("some-channel", "some-message")
30 | .subscribe();
31 |
32 | Thread.sleep(25);
33 |
34 | Assertions.assertTrue(messageReceived.get());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/RedisDataServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import com.nickolasfisher.reactiveredis.model.Thing;
4 | import com.nickolasfisher.reactiveredis.service.RedisDataService;
5 | import io.lettuce.core.RedisClient;
6 | import org.junit.jupiter.api.AfterAll;
7 | import org.junit.jupiter.api.BeforeAll;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.Test;
10 | import reactor.core.publisher.Mono;
11 | import reactor.test.StepVerifier;
12 | import redis.embedded.RedisServer;
13 | import java.net.ServerSocket;
14 |
15 | public class RedisDataServiceTest {
16 | private static RedisServer redisServer;
17 |
18 | private static int getOpenPort() {
19 | try {
20 | int port = -1;
21 | try (ServerSocket socket = new ServerSocket(0)) {
22 | port = socket.getLocalPort();
23 | }
24 | return port;
25 | } catch (Exception e) {
26 | throw new RuntimeException();
27 | }
28 | }
29 |
30 | private static int port = getOpenPort();
31 |
32 | private RedisDataService redisDataService;
33 |
34 | @BeforeAll
35 | public static void setupRedisServer() throws Exception {
36 | redisServer = new RedisServer(port);
37 | redisServer.start();
38 | }
39 |
40 | @BeforeEach
41 | public void setupRedisClient() {
42 | RedisClient redisClient = RedisClient.create("redis://localhost:" + port);
43 | redisDataService = new RedisDataService(redisClient.connect().reactive(), redisClient.connect().reactive());
44 | }
45 |
46 | @Test
47 | public void canWriteAndReadThing() {
48 | Mono writeMono = redisDataService.writeThing(Thing.builder().id(1).value("hello-redis").build());
49 |
50 | StepVerifier.create(writeMono).verifyComplete();
51 |
52 | StepVerifier.create(redisDataService.getThing(1))
53 | .expectNextMatches(thing ->
54 | thing.getId() == 1 &&
55 | "hello-redis".equals(thing.getValue())
56 | )
57 | .verifyComplete();
58 | }
59 |
60 | @AfterAll
61 | public static void teardownRedisServer() {
62 | redisServer.stop();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/RedisStreamsTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.Range;
4 | import io.lettuce.core.StreamMessage;
5 | import io.lettuce.core.XReadArgs;
6 | import io.lettuce.core.protocol.CommandArgs;
7 | import org.junit.jupiter.api.Test;
8 | import reactor.test.StepVerifier;
9 |
10 | import java.util.Map;
11 | import java.util.concurrent.atomic.AtomicInteger;
12 |
13 | import static org.junit.jupiter.api.Assertions.assertEquals;
14 |
15 | public class RedisStreamsTest extends BaseSetupAndTeardownRedis {
16 |
17 | @Test
18 | public void streamsEx() throws InterruptedException {
19 | StepVerifier.create(redisReactiveCommands
20 | .xadd("some-stream", Map.of("first", "1", "second", "2")))
21 | .expectNextMatches(resp -> resp.endsWith("-0"))
22 | .verifyComplete();
23 |
24 | StepVerifier.create(redisReactiveCommands.xlen("some-stream"))
25 | .expectNext(1L)
26 | .verifyComplete();
27 |
28 | StepVerifier.create(redisReactiveCommands.xrange("some-stream", Range.create("-", "+")))
29 | .expectNextMatches(streamMessage ->
30 | streamMessage.getBody().get("first").equals("1") &&
31 | streamMessage.getBody().get("second").equals("2")
32 | ).verifyComplete();
33 |
34 | AtomicInteger elementsSeen = new AtomicInteger(0);
35 | redisClient.connectPubSub().reactive()
36 | .xread(
37 | new XReadArgs().block(2000),
38 | XReadArgs.StreamOffset.from("some-stream", "0")
39 | )
40 | .subscribe(stringStringStreamMessage -> {
41 | elementsSeen.incrementAndGet();
42 | });
43 |
44 | StepVerifier.create(redisReactiveCommands
45 | .xadd("some-stream", Map.of("third", "3", "fourth", "4")))
46 | .expectNextCount(1)
47 | .verifyComplete();
48 |
49 | Thread.sleep(500);
50 |
51 | assertEquals(2, elementsSeen.get());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/RedisTestContainerTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import com.nickolasfisher.reactiveredis.model.Thing;
4 | import com.nickolasfisher.reactiveredis.service.RedisDataService;
5 | import io.lettuce.core.RedisClient;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 | import org.testcontainers.containers.GenericContainer;
9 | import org.testcontainers.junit.jupiter.Container;
10 | import org.testcontainers.junit.jupiter.Testcontainers;
11 | import org.testcontainers.utility.DockerImageName;
12 | import reactor.core.publisher.Mono;
13 | import reactor.test.StepVerifier;
14 |
15 | @Testcontainers
16 | public class RedisTestContainerTest {
17 |
18 | @Container
19 | public static GenericContainer genericContainer = new GenericContainer(
20 | DockerImageName.parse("redis:5.0.3-alpine")
21 | ).withExposedPorts(6379);
22 |
23 | private RedisDataService redisDataService;
24 |
25 | @BeforeEach
26 | public void setupRedisClient() {
27 | RedisClient redisClient = RedisClient.create("redis://" + genericContainer.getHost() + ":" + genericContainer.getMappedPort(6379));
28 | redisDataService = new RedisDataService(redisClient.connect().reactive(), redisClient.connect().reactive());
29 | }
30 |
31 | @Test
32 | public void canWriteAndReadThing() {
33 | Mono writeMono = redisDataService.writeThing(Thing.builder().id(1).value("hello-redis").build());
34 |
35 | StepVerifier.create(writeMono).verifyComplete();
36 |
37 | StepVerifier.create(redisDataService.getThing(1))
38 | .expectNextMatches(thing ->
39 | thing.getId() == 1 &&
40 | "hello-redis".equals(thing.getValue())
41 | )
42 | .verifyComplete();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/SimpleSetsTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import reactor.core.publisher.Flux;
5 | import reactor.core.publisher.Mono;
6 | import reactor.test.StepVerifier;
7 |
8 | import java.util.List;
9 |
10 | //https://redis.io/commands/#set
11 | public class SimpleSetsTest extends BaseSetupAndTeardownRedis {
12 |
13 | @Test
14 | public void sAdd_and_sRem() {
15 | String setKey = "set-key-1";
16 | Mono saddMono = redisReactiveCommands.sadd(setKey, "value-1", "value-2");
17 |
18 | StepVerifier.create(saddMono)
19 | .expectNextMatches(numberOfElementsAdded -> 2L == numberOfElementsAdded)
20 | .verifyComplete();
21 |
22 | Mono saddOneRepeatingValueMono = redisReactiveCommands.sadd(setKey, "value-1", "value-3");
23 |
24 | StepVerifier.create(saddOneRepeatingValueMono)
25 | .expectNextMatches(numberOfElementsAdded -> 1L == numberOfElementsAdded)
26 | .verifyComplete();
27 |
28 | Mono> smembersCollectionMono = redisReactiveCommands.smembers(setKey).collectList();
29 |
30 | StepVerifier.create(smembersCollectionMono)
31 | .expectNextMatches(setMembers -> setMembers.size() == 3 && setMembers.contains("value-3"))
32 | .verifyComplete();
33 |
34 | Mono sremValue3Mono = redisReactiveCommands.srem(setKey, "value-3");
35 |
36 | StepVerifier.create(sremValue3Mono)
37 | .expectNextMatches(numRemoved -> numRemoved == 1L)
38 | .verifyComplete();
39 |
40 | StepVerifier.create(smembersCollectionMono)
41 | .expectNextMatches(setMembers -> setMembers.size() == 2 && !setMembers.contains("value-3"));
42 | }
43 |
44 | @Test
45 | public void sisMember() {
46 | String setKey = "set-key-1";
47 | Mono saddMono = redisReactiveCommands.sadd(setKey, "value-1", "value-2");
48 |
49 | StepVerifier.create(saddMono)
50 | .expectNextMatches(numberOfElementsAdded -> 2L == numberOfElementsAdded)
51 | .verifyComplete();
52 |
53 | Mono shouldNotExistInSetMono = redisReactiveCommands.sismember(setKey, "value-3");
54 |
55 | StepVerifier.create(shouldNotExistInSetMono)
56 | .expectNext(false)
57 | .verifyComplete();
58 |
59 | Mono shouldExistInSetMono = redisReactiveCommands.sismember(setKey, "value-2");
60 |
61 | StepVerifier.create(shouldExistInSetMono)
62 | .expectNext(true)
63 | .verifyComplete();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/SortedSetsTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.Range;
4 | import io.lettuce.core.ScoredValue;
5 | import org.junit.jupiter.api.Test;
6 | import reactor.core.publisher.Mono;
7 | import reactor.test.StepVerifier;
8 |
9 | import java.util.List;
10 |
11 | // https://redis.io/commands/#sorted_set
12 | public class SortedSetsTest extends BaseSetupAndTeardownRedis {
13 |
14 | @Test
15 | public void zAddAndUpdate() {
16 | String setKey = "set-key-1";
17 | Mono addOneHundredScoreMono = redisReactiveCommands.zadd(setKey, ScoredValue.just(100, "one hundred"));
18 |
19 | StepVerifier.create(addOneHundredScoreMono)
20 | .expectNextMatches(numAdded -> 1L == numAdded).verifyComplete();
21 |
22 | Mono getOneHundredScoreMono = redisReactiveCommands.zscore(setKey, "one hundred");
23 |
24 | StepVerifier.create(getOneHundredScoreMono)
25 | .expectNextMatches(score -> score < 100.01 && score > 99.99)
26 | .verifyComplete();
27 |
28 | Mono elementDoesNotExistMono = redisReactiveCommands.zscore(setKey, "not here");
29 |
30 | StepVerifier.create(elementDoesNotExistMono)
31 | .verifyComplete();
32 |
33 | Mono updateOneHundredScoreMono = redisReactiveCommands.zadd(setKey, ScoredValue.just(105, "one hundred"));
34 |
35 | StepVerifier.create(updateOneHundredScoreMono)
36 | // updated, not added, so 0
37 | .expectNextMatches(numAdded -> 0L == numAdded)
38 | .verifyComplete();
39 |
40 | StepVerifier.create(getOneHundredScoreMono)
41 | .expectNextMatches(score -> score < 105.01 && score > 104.99)
42 | .verifyComplete();
43 | }
44 |
45 | @Test
46 | public void zRange_Rank_AndScore() {
47 | String setKey = "set-key-1";
48 | Mono addOneHundredScoreMono = redisReactiveCommands.zadd(setKey, ScoredValue.just(100, "one hundred"));
49 |
50 | StepVerifier.create(addOneHundredScoreMono)
51 | .expectNextMatches(numAdded -> 1L == numAdded).verifyComplete();
52 |
53 | Mono>> allCollectedElementsMono = redisReactiveCommands
54 | .zrangebyscoreWithScores(setKey, Range.unbounded()).collectList();
55 |
56 | StepVerifier.create(allCollectedElementsMono)
57 | .expectNextMatches(allElements -> allElements.size() == 1
58 | && allElements.stream().allMatch(
59 | scoredValue -> scoredValue.getScore() == 100
60 | && scoredValue.getValue().equals("one hundred")
61 | )
62 | ).verifyComplete();
63 |
64 | Mono addFiftyMono = redisReactiveCommands.zadd(setKey, ScoredValue.just(50, "fifty"));
65 |
66 | StepVerifier.create(addFiftyMono)
67 | .expectNextMatches(numAdded -> 1L == numAdded)
68 | .verifyComplete();
69 |
70 | // by default, lowest score is at the front, or zero index
71 | StepVerifier.create(allCollectedElementsMono)
72 | .expectNextMatches(
73 | allElements -> allElements.size() == 2
74 | && allElements.get(0).equals(ScoredValue.just(50, "fifty"))
75 | && allElements.get(1).equals(ScoredValue.just(100, "one hundred"))
76 | ).verifyComplete();
77 | }
78 |
79 | @Test
80 | public void zRevRangeByScore() {
81 | String setKey = "set-key-1";
82 | Mono addOneHundredScoreMono = redisReactiveCommands
83 | .zadd(
84 | setKey,
85 | ScoredValue.just(100, "first"),
86 | ScoredValue.just(200, "second"),
87 | ScoredValue.just(300, "third")
88 | );
89 |
90 | StepVerifier.create(addOneHundredScoreMono)
91 | .expectNextMatches(numAdded -> 3L == numAdded).verifyComplete();
92 |
93 | Mono removeElementsByScoreMono = redisReactiveCommands
94 | .zremrangebyscore(setKey, Range.create(90, 210));
95 |
96 | StepVerifier.create(removeElementsByScoreMono)
97 | .expectNext(2L)
98 | .verifyComplete();
99 |
100 | Mono>> allCollectedElementsMono = redisReactiveCommands
101 | .zrangebyscoreWithScores(setKey, Range.unbounded()).collectList();
102 |
103 | StepVerifier.create(allCollectedElementsMono)
104 | .expectNextMatches(allElements -> allElements.size() == 1
105 | && allElements.stream().allMatch(
106 | scoredValue -> scoredValue.getScore() == 300
107 | && scoredValue.getValue().equals("third")
108 | )
109 | ).verifyComplete();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/StringTypesTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 |
4 | import io.lettuce.core.KeyValue;
5 | import io.lettuce.core.SetArgs;
6 | import io.lettuce.core.api.reactive.RedisReactiveCommands;
7 | import org.junit.jupiter.api.Test;
8 | import reactor.core.publisher.Flux;
9 | import reactor.test.StepVerifier;
10 |
11 | import java.util.Map;
12 |
13 |
14 | public class StringTypesTest extends BaseSetupAndTeardownRedis{
15 |
16 | @Test
17 | public void setAndGet() {
18 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
19 |
20 | // vanilla get and set
21 | StepVerifier.create(redisReactiveCommands.set("some-key-1", "some-value-1"))
22 | .expectNextMatches(response -> "OK".equals(response)).verifyComplete();
23 |
24 | StepVerifier.create(redisReactiveCommands.get("some-key-1"))
25 | .expectNextMatches(response -> "some-value-1".equals(response))
26 | .verifyComplete();
27 |
28 | // adding an additional argument like nx will cause it to return nothing if it doesn't get set
29 | StepVerifier.create(redisReactiveCommands.set("some-key-1", "some-value-2", new SetArgs().nx()))
30 | .verifyComplete();
31 |
32 | // prove the value is the same
33 | StepVerifier.create(redisReactiveCommands.get("some-key-1"))
34 | .expectNextMatches(response -> "some-value-1".equals(response))
35 | .verifyComplete();
36 | }
37 |
38 | @Test
39 | public void setNx() throws Exception {
40 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
41 |
42 | StepVerifier.create(redisReactiveCommands.setnx("key-1", "value-1"))
43 | .expectNextMatches(success -> success)
44 | .verifyComplete();
45 |
46 | StepVerifier.create(redisReactiveCommands.setnx("key-1", "value-1"))
47 | .expectNextMatches(success -> !success)
48 | .verifyComplete();
49 |
50 | StepVerifier.create(redisReactiveCommands.setex("key-1", 1, "value-2"))
51 | .expectNextMatches(response -> "OK".equals(response))
52 | .verifyComplete();
53 |
54 | // key-1 expires in 1 second
55 | Thread.sleep(1500);
56 |
57 | StepVerifier.create(redisReactiveCommands.get("key-1"))
58 | // no value
59 | .verifyComplete();
60 | }
61 |
62 | @Test
63 | public void append() {
64 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
65 |
66 | StepVerifier.create(redisReactiveCommands.set("key-10", "value-10"))
67 | .expectNextMatches(response -> "OK".equals(response))
68 | .verifyComplete();
69 |
70 | StepVerifier.create(redisReactiveCommands.append("key-10", "-more-stuff"))
71 | // length of new value is returned
72 | .expectNextMatches(response -> 19L == response)
73 | .verifyComplete();
74 |
75 | StepVerifier.create(redisReactiveCommands.get("key-10"))
76 | .expectNextMatches(response ->
77 | "value-10-more-stuff".equals(response))
78 | .verifyComplete();
79 | }
80 |
81 | @Test
82 | public void incrBy() {
83 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
84 |
85 | StepVerifier.create(redisReactiveCommands.set("key-counter", "7"))
86 | .expectNextMatches(response -> "OK".equals(response))
87 | .verifyComplete();
88 |
89 | StepVerifier.create(redisReactiveCommands.incrby("key-counter", 8L))
90 | .expectNextMatches(val -> 15 == val)
91 | .verifyComplete();
92 | }
93 |
94 | @Test
95 | public void mget() {
96 | RedisReactiveCommands redisReactiveCommands = redisClient.connect().reactive();
97 |
98 | StepVerifier.create(redisReactiveCommands.mset(Map.of(
99 | "key-1", "val-1",
100 | "key-2", "val-2",
101 | "key-3", "val-3"
102 | )))
103 | .expectNextMatches(response -> "OK".equals(response))
104 | .verifyComplete();
105 |
106 | Flux> mgetValuesFlux = redisReactiveCommands.mget("key-1", "key-2", "key-3");
107 | StepVerifier.create(mgetValuesFlux.collectList())
108 | .expectNextMatches(collectedValues ->
109 | collectedValues.size() == 3
110 | && collectedValues.stream()
111 | .anyMatch(stringStringKeyValue ->
112 | stringStringKeyValue.getKey().equals("key-1")
113 | && stringStringKeyValue.getValue().equals("val-1")
114 | )
115 | )
116 | .verifyComplete();
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/java/com/nickolasfisher/reactiveredis/TransactionsTest.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactiveredis;
2 |
3 | import io.lettuce.core.api.reactive.RedisReactiveCommands;
4 | import org.junit.jupiter.api.Test;
5 | import reactor.test.StepVerifier;
6 |
7 | public class TransactionsTest extends BaseSetupAndTeardownRedis {
8 |
9 | @Test
10 | public void transactions() throws InterruptedException {
11 | RedisReactiveCommands firstConnection =
12 | redisClient.connect().reactive();
13 |
14 | RedisReactiveCommands secondConnection =
15 | redisClient.connect().reactive();
16 |
17 | StepVerifier.create(firstConnection.multi())
18 | .expectNext("OK")
19 | .verifyComplete();
20 |
21 | // This will block and never return, because lettuce is has issued it
22 | // but is waiting for the response from exec
23 | // StepVerifier.create(firstConnection.set("key-1", "value-1"))
24 | // .expectNext("OK")
25 | // .verifyComplete();
26 |
27 | firstConnection.set("key-1", "value-1")
28 | .subscribe(resp ->
29 | System.out.println(
30 | "response from set within transaction: " + resp
31 | )
32 | );
33 |
34 | // no records yet, transaction not committed
35 | StepVerifier.create(secondConnection.get("key-1"))
36 | .verifyComplete();
37 |
38 | Thread.sleep(20);
39 | System.out.println("running exec");
40 | StepVerifier.create(firstConnection.exec())
41 | .expectNextMatches(tr -> {
42 | System.out.println("exec responded");
43 | return tr.size() == 1 && tr.get(0).equals("OK");
44 | })
45 | .verifyComplete();
46 |
47 | StepVerifier.create(secondConnection.get("key-1"))
48 | .expectNext("value-1")
49 | .verifyComplete();
50 | }
51 |
52 | @Test
53 | public void optLocking() {
54 | RedisReactiveCommands firstConnection =
55 | redisClient.connect().reactive();
56 |
57 | RedisReactiveCommands secondConnection =
58 | redisClient.connect().reactive();
59 |
60 | firstConnection.watch("key-1").subscribe();
61 | firstConnection.multi().subscribe();
62 | firstConnection.incr("key-1").subscribe();
63 |
64 | secondConnection.set("key-1", "10").subscribe();
65 |
66 | StepVerifier.create(firstConnection.exec())
67 | // transaction not committed
68 | .expectNextMatches(tr -> tr.wasDiscarded())
69 | .verifyComplete();
70 |
71 | StepVerifier.create(secondConnection.get("key-1"))
72 | .expectNextMatches(val -> "10".equals(val))
73 | .verifyComplete();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/reactive-redis/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/reactive-sqs/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/reactive-sqs/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.net.*;
18 | import java.io.*;
19 | import java.nio.channels.*;
20 | import java.util.Properties;
21 |
22 | public class MavenWrapperDownloader {
23 |
24 | private static final String WRAPPER_VERSION = "0.5.6";
25 | /**
26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
27 | */
28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
30 |
31 | /**
32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
33 | * use instead of the default one.
34 | */
35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
36 | ".mvn/wrapper/maven-wrapper.properties";
37 |
38 | /**
39 | * Path where the maven-wrapper.jar will be saved to.
40 | */
41 | private static final String MAVEN_WRAPPER_JAR_PATH =
42 | ".mvn/wrapper/maven-wrapper.jar";
43 |
44 | /**
45 | * Name of the property which should be used to override the default download url for the wrapper.
46 | */
47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
48 |
49 | public static void main(String args[]) {
50 | System.out.println("- Downloader started");
51 | File baseDirectory = new File(args[0]);
52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
53 |
54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
55 | // wrapperUrl parameter.
56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
57 | String url = DEFAULT_DOWNLOAD_URL;
58 | if (mavenWrapperPropertyFile.exists()) {
59 | FileInputStream mavenWrapperPropertyFileInputStream = null;
60 | try {
61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
62 | Properties mavenWrapperProperties = new Properties();
63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
65 | } catch (IOException e) {
66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
67 | } finally {
68 | try {
69 | if (mavenWrapperPropertyFileInputStream != null) {
70 | mavenWrapperPropertyFileInputStream.close();
71 | }
72 | } catch (IOException e) {
73 | // Ignore ...
74 | }
75 | }
76 | }
77 | System.out.println("- Downloading from: " + url);
78 |
79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
80 | if (!outputFile.getParentFile().exists()) {
81 | if (!outputFile.getParentFile().mkdirs()) {
82 | System.out.println(
83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
84 | }
85 | }
86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
87 | try {
88 | downloadFileFromURL(url, outputFile);
89 | System.out.println("Done");
90 | System.exit(0);
91 | } catch (Throwable e) {
92 | System.out.println("- Error downloading");
93 | e.printStackTrace();
94 | System.exit(1);
95 | }
96 | }
97 |
98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
100 | String username = System.getenv("MVNW_USERNAME");
101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
102 | Authenticator.setDefault(new Authenticator() {
103 | @Override
104 | protected PasswordAuthentication getPasswordAuthentication() {
105 | return new PasswordAuthentication(username, password);
106 | }
107 | });
108 | }
109 | URL website = new URL(urlString);
110 | ReadableByteChannel rbc;
111 | rbc = Channels.newChannel(website.openStream());
112 | FileOutputStream fos = new FileOutputStream(destination);
113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
114 | fos.close();
115 | rbc.close();
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/reactive-sqs/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nfisher23/reactive-programming-webflux/a9d6d0ad8b35c545771cfed0fea046499309e8e5/reactive-sqs/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/reactive-sqs/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/reactive-sqs/infra/create-queue-and-topic.sh:
--------------------------------------------------------------------------------
1 |
2 |
3 | export AWS_SECRET_ACCESS_KEY="FAKE"
4 | export AWS_ACCESS_KEY_ID="FAKE"
5 | export AWS_DEFAULT_REGION=us-east-1
6 |
7 | QUEUE_NAME="my-queue"
8 | TOPIC_NAME="my-topic"
9 |
10 | QUEUE_URL=$(aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name "$QUEUE_NAME" --output text)
11 | echo "queue url: $QUEUE_URL"
12 |
13 | TOPIC_ARN=$(aws --endpoint-url http://localhost:4566 sns create-topic --output text --name "$TOPIC_NAME")
14 | echo "topic arn: $TOPIC_ARN"
15 |
16 | QUEUE_ARN=$(aws --endpoint-url http://localhost:4566 sqs get-queue-attributes --queue-url "$QUEUE_URL" | jq -r ".Attributes.QueueArn")
17 | echo "queue arn: $QUEUE_ARN"
18 |
19 | SUBSCRIPTION_ARN=$(aws --endpoint-url http://localhost:4566 sns subscribe --topic-arn "$TOPIC_ARN" --protocol sqs --notification-endpoint "$QUEUE_ARN" --output text)
20 |
21 | # modify to raw message delivery true
22 | aws --endpoint-url http://localhost:4566 sns set-subscription-attributes \
23 | --subscription-arn "$SUBSCRIPTION_ARN" --attribute-name RawMessageDelivery --attribute-value true
24 |
--------------------------------------------------------------------------------
/reactive-sqs/infra/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '2.1'
2 |
3 | services:
4 | localstack:
5 | container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
6 | image: localstack/localstack
7 | ports:
8 | - "4566-4599:4566-4599"
9 | - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
10 | environment:
11 | - SERVICES=${SERVICES- }
12 | - DEBUG=${DEBUG- }
13 | - DATA_DIR=${DATA_DIR- }
14 | - PORT_WEB_UI=${PORT_WEB_UI- }
15 | - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
16 | - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
17 | - DOCKER_HOST=unix:///var/run/docker.sock
18 | - HOST_TMP_FOLDER=${TMPDIR}
19 | volumes:
20 | - "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
21 | - "/var/run/docker.sock:/var/run/docker.sock"
22 |
--------------------------------------------------------------------------------
/reactive-sqs/infra/receive-message.sh:
--------------------------------------------------------------------------------
1 | export AWS_SECRET_ACCESS_KEY="FAKE"
2 | export AWS_ACCESS_KEY_ID="FAKE"
3 | export AWS_DEFAULT_REGION=us-east-1
4 |
5 |
6 | Q_URL=$(aws --endpoint-url http://localhost:4566 sqs get-queue-url --queue-name "my-queue" --output text)
7 | aws --endpoint-url http://localhost:4566 sqs receive-message --queue-url "$Q_URL"
8 |
--------------------------------------------------------------------------------
/reactive-sqs/infra/send-message.sh:
--------------------------------------------------------------------------------
1 | export AWS_SECRET_ACCESS_KEY="FAKE"
2 | export AWS_ACCESS_KEY_ID="FAKE"
3 | export AWS_DEFAULT_REGION=us-east-1
4 |
5 |
6 | Q_URL=$(aws --endpoint-url http://localhost:4566 sqs get-queue-url --queue-name "my-queue" --output text)
7 | aws --endpoint-url http://localhost:4566 sqs send-message --queue-url "$Q_URL" --message-body "hey there"
8 |
--------------------------------------------------------------------------------
/reactive-sqs/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/reactive-sqs/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.3.3.RELEASE
9 |
10 |
11 | com.nickolasfisher
12 | reactive-sqs
13 | 0.0.1-SNAPSHOT
14 | reactive-sqs
15 | spring boot and reactive SQS listening
16 |
17 |
18 |
19 |
20 | software.amazon.awssdk
21 | bom
22 | 2.5.5
23 | pom
24 | import
25 |
26 |
27 |
28 |
29 |
30 | 11
31 |
32 |
33 |
34 |
35 | org.springframework.boot
36 | spring-boot-starter-webflux
37 |
38 |
39 |
40 | software.amazon.awssdk
41 | sqs
42 |
43 |
44 | software.amazon.awssdk
45 | sns
46 |
47 |
48 |
49 | org.springframework.boot
50 | spring-boot-starter-test
51 | test
52 |
53 |
54 | org.junit.vintage
55 | junit-vintage-engine
56 |
57 |
58 |
59 |
60 | io.projectreactor
61 | reactor-test
62 | test
63 |
64 |
65 |
66 |
67 |
68 |
69 | org.springframework.boot
70 | spring-boot-maven-plugin
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/java/com/nickolasfisher/reactivesqs/AwsSnsConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactivesqs;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import software.amazon.awssdk.auth.credentials.AwsCredentials;
6 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
7 | import software.amazon.awssdk.regions.Region;
8 | import software.amazon.awssdk.services.sns.SnsAsyncClient;
9 |
10 | import java.net.URI;
11 |
12 | @Configuration
13 | public class AwsSnsConfig {
14 |
15 | @Bean
16 | public SnsAsyncClient amazonSNSAsyncClient() {
17 | return SnsAsyncClient.builder()
18 | .endpointOverride(URI.create("http://localhost:4566"))
19 | .region(Region.US_EAST_1)
20 | .credentialsProvider(StaticCredentialsProvider.create(new AwsCredentials() {
21 | @Override
22 | public String accessKeyId() {
23 | return "FAKE";
24 | }
25 |
26 | @Override
27 | public String secretAccessKey() {
28 | return "FAKE";
29 | }
30 | }))
31 | .build();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/java/com/nickolasfisher/reactivesqs/AwsSqsConfig.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactivesqs;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
6 | import software.amazon.awssdk.auth.credentials.AwsCredentials;
7 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
8 | import software.amazon.awssdk.regions.Region;
9 | import software.amazon.awssdk.services.sqs.SqsAsyncClient;
10 |
11 | import java.net.URI;
12 |
13 | @Configuration
14 | public class AwsSqsConfig {
15 |
16 | @Bean
17 | public SqsAsyncClient amazonSQSAsyncClient() {
18 | return SqsAsyncClient.builder()
19 | .endpointOverride(URI.create("http://localhost:4566"))
20 | .region(Region.US_EAST_1)
21 | .credentialsProvider(StaticCredentialsProvider.create(new AwsCredentials() {
22 | @Override
23 | public String accessKeyId() {
24 | return "FAKE";
25 | }
26 |
27 | @Override
28 | public String secretAccessKey() {
29 | return "FAKE";
30 | }
31 | }))
32 | .build();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/java/com/nickolasfisher/reactivesqs/ReactiveSqsApplication.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactivesqs;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class ReactiveSqsApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(ReactiveSqsApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/java/com/nickolasfisher/reactivesqs/SQSListenerBean.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactivesqs;
2 |
3 | import org.reactivestreams.Publisher;
4 | import org.reactivestreams.Subscriber;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.stereotype.Component;
8 | import reactor.core.publisher.Flux;
9 | import reactor.core.publisher.FluxSink;
10 | import reactor.core.publisher.Mono;
11 | import reactor.core.publisher.Signal;
12 | import reactor.core.scheduler.Schedulers;
13 | import reactor.util.retry.RetrySpec;
14 | import software.amazon.awssdk.services.sqs.SqsAsyncClient;
15 | import software.amazon.awssdk.services.sqs.model.*;
16 |
17 | import javax.annotation.PostConstruct;
18 | import java.time.Duration;
19 | import java.util.List;
20 | import java.util.Objects;
21 | import java.util.concurrent.CompletableFuture;
22 | import java.util.concurrent.TimeUnit;
23 | import java.util.function.*;
24 |
25 | @Component
26 | public class SQSListenerBean {
27 |
28 | public static final Logger LOGGER = LoggerFactory.getLogger(SQSListenerBean.class);
29 | private final SqsAsyncClient sqsAsyncClient;
30 | private final String queueUrl;
31 |
32 | public SQSListenerBean(SqsAsyncClient sqsAsyncClient) {
33 | this.sqsAsyncClient = sqsAsyncClient;
34 | try {
35 | this.queueUrl = this.sqsAsyncClient.getQueueUrl(GetQueueUrlRequest.builder().queueName("my-queue").build()).get().queueUrl();
36 | } catch (Exception e) {
37 | throw new RuntimeException(e);
38 | }
39 | }
40 |
41 | @PostConstruct
42 | public void continuousListener() {
43 | Mono receiveMessageResponseMono = Mono.fromFuture(() ->
44 | sqsAsyncClient.receiveMessage(
45 | ReceiveMessageRequest.builder()
46 | .maxNumberOfMessages(5)
47 | .queueUrl(queueUrl)
48 | .waitTimeSeconds(10)
49 | .visibilityTimeout(30)
50 | .build()
51 | )
52 | );
53 |
54 | receiveMessageResponseMono
55 | .repeat()
56 | .retry()
57 | .map(ReceiveMessageResponse::messages)
58 | .map(Flux::fromIterable)
59 | .flatMap(messageFlux -> messageFlux)
60 | .subscribe(message -> {
61 | LOGGER.info("message body: " + message.body());
62 |
63 | sqsAsyncClient.deleteMessage(DeleteMessageRequest.builder().queueUrl(queueUrl).receiptHandle(message.receiptHandle()).build())
64 | .thenAccept(deleteMessageResponse -> {
65 | LOGGER.info("deleted message with handle " + message.receiptHandle());
66 | });
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/java/com/nickolasfisher/reactivesqs/SQSSenderBean.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactivesqs;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.stereotype.Component;
7 | import reactor.core.publisher.Mono;
8 | import reactor.util.retry.Retry;
9 | import software.amazon.awssdk.services.sqs.SqsAsyncClient;
10 | import software.amazon.awssdk.services.sqs.model.GetQueueUrlRequest;
11 | import software.amazon.awssdk.services.sqs.model.GetQueueUrlResponse;
12 | import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
13 |
14 | import javax.annotation.PostConstruct;
15 | import java.time.ZonedDateTime;
16 | import java.util.concurrent.CompletableFuture;
17 |
18 | @Component
19 | public class SQSSenderBean {
20 |
21 | private Logger LOG = LoggerFactory.getLogger(SQSSenderBean.class);
22 |
23 | private final SqsAsyncClient sqsAsyncClient;
24 |
25 | public SQSSenderBean(SqsAsyncClient sqsAsyncClient) {
26 | this.sqsAsyncClient = sqsAsyncClient;
27 | }
28 |
29 | @PostConstruct
30 | public void sendHelloMessage() throws Exception {
31 | LOG.info("hello!!!");
32 | CompletableFuture wat = sqsAsyncClient.getQueueUrl(GetQueueUrlRequest.builder().queueName("my-queue").build());
33 | GetQueueUrlResponse getQueueUrlResponse = wat.get();
34 |
35 | Mono.fromFuture(() -> sqsAsyncClient.sendMessage(
36 | SendMessageRequest.builder()
37 | .queueUrl(getQueueUrlResponse.queueUrl())
38 | .messageBody("new message at second " + ZonedDateTime.now().getSecond())
39 | .build()
40 | ))
41 | .retryWhen(Retry.max(3))
42 | .repeat(5)
43 | .subscribe();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/java/com/nickolasfisher/reactivesqs/SnsSenderBean.java:
--------------------------------------------------------------------------------
1 | package com.nickolasfisher.reactivesqs;
2 |
3 | import org.springframework.stereotype.Component;
4 | import reactor.core.publisher.Mono;
5 | import software.amazon.awssdk.services.sns.SnsAsyncClient;
6 | import software.amazon.awssdk.services.sns.model.PublishRequest;
7 |
8 | import javax.annotation.PostConstruct;
9 |
10 | @Component
11 | public class SnsSenderBean {
12 |
13 | private final SnsAsyncClient snsAsyncClient;
14 |
15 | // ARN's are immutable. In reality, you'll want to pass this in as config per environment
16 | private static final String topicARN = "arn:aws:sns:us-east-1:000000000000:my-topic";
17 |
18 | public SnsSenderBean(SnsAsyncClient snsAsyncClient) {
19 | this.snsAsyncClient = snsAsyncClient;
20 | }
21 |
22 | @PostConstruct
23 | public void sendHelloToSNS() {
24 | Mono.fromFuture(() -> snsAsyncClient.publish(PublishRequest.builder().topicArn(topicARN).message("message-from-sns").build()))
25 | .repeat(3)
26 | .subscribe();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/reactive-sqs/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | logging.level.root=INFO
2 | server.port=9001
--------------------------------------------------------------------------------