├── 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 --------------------------------------------------------------------------------