├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── jmh ├── java │ └── io │ │ └── codearte │ │ └── duramen │ │ └── benchmark │ │ ├── DuramenProcessingBenchmark.java │ │ ├── config │ │ ├── ComplexEventConfig.java │ │ ├── FiledataConfig.java │ │ ├── InMemoryConfig.java │ │ └── SimpleEventConfig.java │ │ └── event │ │ ├── ComplexEvent.java │ │ └── SimpleEvent.java └── resources │ └── logback-test.xml ├── main └── java │ └── io │ └── codearte │ └── duramen │ ├── DuramenPackageMarker.java │ ├── EventBus.java │ ├── annotation │ └── EnableDuramen.java │ ├── config │ ├── DuramenConfiguration.java │ ├── DuramenConfigurator.java │ ├── DuramenSpringConfiguration.java │ └── EvenBusContext.java │ ├── datastore │ ├── Datastore.java │ ├── FileData.java │ └── InMemory.java │ ├── event │ ├── Event.java │ ├── ProcessAfterCommit.java │ └── RetryableEvent.java │ ├── generator │ ├── IdGenerator.java │ └── RandomIdGenerator.java │ ├── handler │ ├── EventHandler.java │ └── ExceptionHandler.java │ └── internal │ ├── EventJsonSerializer.java │ └── LoggingExceptionHandler.java └── test ├── groovy └── io │ └── codearte │ └── duramen │ ├── EventBusSpecIT.groovy │ ├── PersistenceSpec.groovy │ ├── RetryableSpec.groovy │ ├── TestUtil.groovy │ └── datastore │ └── DatastoreSpecIT.groovy ├── java └── test │ └── codearte │ └── duramen │ ├── EventConsumer.java │ ├── EventProducer.java │ ├── RetryableEventProducer.java │ ├── TestEvent.java │ └── TestRetryableEvent.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | duramen.iml 3 | target/ 4 | duramen.data 5 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devskiller/duramen/d4503e425a04e1a70741a6675e29b6b44c2fbcb2/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk11 5 | 6 | script: ./mvnw clean verify jacoco:report 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Duramen [![Build Status](https://travis-ci.org/Devskiller/duramen.svg?branch=master)](https://travis-ci.org/Devskiller/duramen) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.devskiller.duramen/duramen/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.devskiller.duramen/duramen) 2 | 3 | Persistent event bus implementation for Java. Easily integrates with Spring Framework. By default uses file backed database. Guarantees that event will be dispatched. 4 | 5 | ## Usage 6 | 7 | 1) Add Duramen dependency: 8 | `com.devskiller.duramen:duramen:1.1.2` 9 | 10 | 2) Use ```@EnableDuramen``` annotation to import Duramen into your project: 11 | ```java 12 | @Configuration 13 | @ComponentScan 14 | @EnableDuramen 15 | public class FooConfiguration { 16 | 17 | } 18 | ``` 19 | 20 | 3) Implement custom event class. Remember that this class must contain (even private) default constructor 21 | ```java 22 | public class FooEvent implements Event { 23 | private String message; 24 | 25 | // getters and setters 26 | } 27 | ``` 28 | 29 | 4) To produce events you have to implement producer component: 30 | ```java 31 | import io.codearte.duramen.EventBus; 32 | 33 | @Component 34 | public class FooEventProducer { 35 | 36 | private final EventBus eventBus; 37 | 38 | @Autowired 39 | public FooEventProducer(EventBus eventBus) { 40 | this.eventBus = eventBus; 41 | } 42 | 43 | /** 44 | * You will invoke this method somewhere in your code 45 | */ 46 | public void produceEvent() { 47 | FooEvent event = new FooEvent(); 48 | event.setMessage("Test message"); 49 | eventBus.publish(event); 50 | } 51 | } 52 | ``` 53 | 54 | 5) To receive events you have to implement consumer. Generic type in ```EventHandler``` will decide which events will be processed in particular consumer: 55 | ```java 56 | import io.codearte.duramen.handler.EventHandler; 57 | 58 | @Component 59 | public class FooEventConsumer implements EventHandler { 60 | 61 | @Override 62 | public void onEvent(FooEvent event) { 63 | System.out.println("Received message: " + event.getMessage()); 64 | } 65 | 66 | } 67 | ``` 68 | 69 | All Spring beans implementing ```EventHandler``` interface will be automatically registered as handlers. It's also possible to manually register ```EventHandler``` by invoking ```eventBus.register(qualifiedEventClassName, eventHandler)``` method. 70 | 71 | 6) If you want to retry events after some error occur you just have to implement ```RetryableEvent``` instead of ```Event``` 72 | 73 | 7) If you want to use transaction-aware events just please annotate ```Event``` classes with `@ProcessAfterCommit`. As the name suggests such events will be processed after committing the transaction. 74 | 75 | ## Testing 76 | 77 | Usually in test scope we don't want to persist our events. To achieve such behaviour we can configure custom bean: 78 | ```java 79 | import io.codearte.duramen.datastore.InMemory(); 80 | 81 | @Bean 82 | public Datastore inMemoryDatastore() { 83 | return new InMemory(); 84 | } 85 | ``` 86 | 87 | ## Error handling 88 | 89 | When ```EventHandler``` processing bean throws an exception, it will be logged with event data serialized to JSON. 90 | 91 | You can specify custom ```ExceptionHandler``` by creating bean implementing ```io.codearte.duramen.handler.ExceptionHandler``` interface. 92 | 93 | ## Available datastores 94 | 95 | In Duramen there are 2 ```Datastore``` objects. 96 | 97 | ### FileData 98 | 99 | Default implementation. Backed by [Chronicle Map](https://github.com/OpenHFT/Chronicle-Map). It stores events in binary file (by default duramen.data). 100 | To use this implementation you don't have to do anything, as long as you accept default values (see "Specifying messages limits"). 101 | To change defaults you need create own bean: 102 | 103 | ```java 104 | import io.codearte.duramen.datastore.FileData; 105 | 106 | @Bean 107 | public Datastore fileDatastore() { 108 | return new FileData("/tmp/myfile.data", /*entries*/ 10, /*entrySize*/ 8192); 109 | } 110 | ``` 111 | 112 | ### In memory 113 | 114 | We've already described ```InMemory``` datastore in "Testing" section 115 | 116 | ## Customizing default configuration 117 | 118 | As you can see to use Duramen no configuration is required. However if you want, there are some options to customize. 119 | To do that just provide ```DuramenConfiguration``` bean: 120 | 121 | ```java 122 | @Bean 123 | public DuramenConfiguration duramenConfiguration() { 124 | return DuramenConfiguration.builder().retryDelayInSeconds(30).build() 125 | } 126 | ``` 127 | 128 | ### Specifying messages limits 129 | 130 | By default message size is set to 4096 bytes. You can change this value by defining bean: 131 | 132 | ```java 133 | DuramenConfiguration.builder().maxMessageSize(8192).build() 134 | ``` 135 | 136 | Message count limit is set to 1024 events in queue. You can change this value by defining bean: 137 | 138 | ```java 139 | DuramenConfiguration.builder().maxMessageCount(2048).build() 140 | } 141 | ``` 142 | 143 | ### Retrying options 144 | 145 | By default Duramen reties all ```RetryableEvent``` events after any exception 3 times with 5 seconds delay. Of course you can customize 146 | those settings by ```DuramenConfiguration```: 147 | 148 | ```java 149 | DuramenConfiguration.builder().retryDelayInSeconds(5).retryCount(10) 150 | .retryableExceptions(SocketTimeoutException.class).build() 151 | ``` 152 | 153 | ### Processing options 154 | 155 | By default Duramen uses daemon threads, but it can be easily changes by declaring: 156 | 157 | ```java 158 | DuramenConfiguration.builder().useDaemonThreads(false).build() 159 | ``` 160 | 161 | Also number of threads processing events (default we use only one thread) can be increased: 162 | 163 | ```java 164 | DuramenConfiguration.builder().maxProcessingThreads(2).build() 165 | ``` 166 | 167 | Finally, if you want, there is a possibility to use own ExecutorService for processing events. 168 | ```java 169 | @Bean 170 | public ExecutorService duramenExecutorService() { 171 | return Executors.newCachedThreadPool(); 172 | } 173 | ``` 174 | 175 | ## Performance 176 | 177 | Performance tests executed using JMH on Linux-4.12.5, Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz 178 | 179 | | Datastore | Event type* | Events / second | 180 | | --------- |-------------| ---------------:| 181 | | Filedata | Simple | 943 431 | 182 | | Filedata | Complex | 326 803 | 183 | | InMemory | Simple | 1 494 560 | 184 | | InMemory | Complex | 384 291 | 185 | 186 | - Simple event contains one short String field 187 | - Complex event contains 512 chars String field, one BigDecimal field and 6 element ArrayList of Integers 188 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /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 http://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 Maven2 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 key stroke 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 enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.devskiller.duramen 7 | duramen 8 | 1.1.4-SNAPSHOT 9 | jar 10 | 11 | duramen 12 | Java lightweight persistent event bus 13 | https://github.com/Devskiller/duramen 14 | 15 | 16 | 17 | Apache 2 18 | http://www.apache.org/licenses/LICENSE-2.0.txt 19 | repo 20 | A business-friendly OSS license 21 | 22 | 23 | 24 | 25 | 26 | jkubrynski 27 | Jakub Kubrynski 28 | 29 | 30 | 31 | 32 | 11 33 | ${java.version} 34 | ${java.version} 35 | UTF-8 36 | 37 | 1.19 38 | 5.0.10.RELEASE 39 | 1.7.25 40 | 1.2-groovy-2.5 41 | 42 | 43 | 44 | 45 | org.slf4j 46 | slf4j-api 47 | ${slf4j.version} 48 | 49 | 50 | org.springframework 51 | spring-context 52 | ${spring.version} 53 | 54 | 55 | org.springframework 56 | spring-tx 57 | ${spring.version} 58 | 59 | 60 | com.google.guava 61 | guava 62 | 27.0-jre 63 | 64 | 65 | de.ruedigermoeller 66 | fst 67 | 2.57 68 | 69 | 70 | com.fasterxml.jackson.core 71 | jackson-databind 72 | 2.9.7 73 | 74 | 75 | 76 | net.openhft 77 | chronicle-map 78 | 3.16.4 79 | 80 | 81 | com.sun.java 82 | tools 83 | 84 | 85 | 86 | 87 | javax.annotation 88 | javax.annotation-api 89 | 1.2 90 | 91 | 92 | org.springframework 93 | spring-test 94 | ${spring.version} 95 | test 96 | 97 | 98 | org.spockframework 99 | spock-core 100 | ${spock.version} 101 | test 102 | 103 | 104 | org.spockframework 105 | spock-spring 106 | ${spock.version} 107 | test 108 | 109 | 110 | org.mockito 111 | mockito-all 112 | 1.10.19 113 | test 114 | 115 | 116 | ch.qos.logback 117 | logback-classic 118 | 1.2.3 119 | test 120 | 121 | 122 | org.slf4j 123 | jcl-over-slf4j 124 | ${slf4j.version} 125 | test 126 | 127 | 128 | org.awaitility 129 | awaitility-groovy 130 | 3.1.2 131 | test 132 | 133 | 134 | org.codehaus.groovy 135 | groovy-all 136 | 137 | 138 | 139 | 140 | org.codehaus.groovy 141 | groovy 142 | 2.5.3 143 | test 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | maven-clean-plugin 152 | 3.0.0 153 | 154 | 155 | maven-install-plugin 156 | 2.5.2 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-release-plugin 161 | 2.5.3 162 | 163 | false 164 | true 165 | true 166 | 167 | 168 | 169 | org.jacoco 170 | jacoco-maven-plugin 171 | 0.8.2 172 | 173 | 174 | prepare-agent 175 | 176 | prepare-agent 177 | 178 | 179 | 180 | 181 | 182 | org.codehaus.gmavenplus 183 | gmavenplus-plugin 184 | 1.6.1 185 | 186 | 187 | 188 | compileTests 189 | 190 | 191 | 192 | 193 | 1.8 194 | 195 | 196 | 197 | maven-surefire-plugin 198 | 2.18.1 199 | 200 | 201 | **/*Spec.java 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | scm:git:https://github.com/Devskiller/duramen.git 210 | scm:git:git@github.com:Devskiller/duramen.git 211 | https://github.com/Devskiller/duramen/ 212 | HEAD 213 | 214 | 215 | 216 | 217 | sonatype-nexus-snapshots 218 | Sonatype Nexus Snapshots 219 | http://oss.sonatype.org/content/repositories/snapshots 220 | 221 | 222 | sonatype-nexus-staging 223 | Nexus Release Repository 224 | http://oss.sonatype.org/service/local/staging/deploy/maven2/ 225 | 226 | 227 | 228 | 229 | 230 | release 231 | 232 | 233 | performRelease 234 | true 235 | 236 | 237 | 238 | 239 | 240 | org.apache.maven.plugins 241 | maven-gpg-plugin 242 | 1.6 243 | 244 | 245 | sign-artifacts 246 | verify 247 | 248 | sign 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | jmh 258 | 259 | 260 | 261 | org.codehaus.mojo 262 | build-helper-maven-plugin 263 | 3.0.0 264 | 265 | 266 | 267 | add-test-source 268 | add-test-resource 269 | 270 | 271 | 272 | src/jmh/java 273 | 274 | 275 | 276 | src/jmh/resources 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-compiler-plugin 286 | 3.8.0 287 | 288 | 289 | 290 | testCompile 291 | 292 | 293 | 294 | 295 | org.openjdk.jmh 296 | jmh-generator-annprocess 297 | ${jmh.version} 298 | 299 | 300 | ${java.version} 301 | 302 | 303 | 304 | 305 | 306 | org.codehaus.mojo 307 | exec-maven-plugin 308 | 1.6.0 309 | 310 | 311 | run-benchmarks 312 | integration-test 313 | 314 | exec 315 | 316 | 317 | test 318 | java 319 | 320 | -classpath 321 | 322 | org.openjdk.jmh.Main 323 | .* 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | org.openjdk.jmh 334 | jmh-core 335 | ${jmh.version} 336 | test 337 | 338 | 339 | 340 | 341 | 342 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/DuramenProcessingBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark; 2 | 3 | import io.codearte.duramen.EventBus; 4 | import io.codearte.duramen.event.Event; 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.Fork; 7 | import org.openjdk.jmh.annotations.Level; 8 | import org.openjdk.jmh.annotations.Measurement; 9 | import org.openjdk.jmh.annotations.Param; 10 | import org.openjdk.jmh.annotations.Scope; 11 | import org.openjdk.jmh.annotations.Setup; 12 | import org.openjdk.jmh.annotations.State; 13 | import org.openjdk.jmh.annotations.TearDown; 14 | import org.openjdk.jmh.annotations.Warmup; 15 | 16 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.lang.reflect.Constructor; 21 | import java.lang.reflect.InvocationTargetException; 22 | 23 | /** 24 | * Created by jkubrynski@gmail.com / 2014-05-20 25 | */ 26 | @State(Scope.Benchmark) 27 | @SuppressWarnings({"unchecked", "unused"}) 28 | @Warmup(iterations = 10) 29 | @Measurement(iterations = 20) 30 | @Fork(2) 31 | public class DuramenProcessingBenchmark { 32 | 33 | private EventBus eventBus; 34 | private AnnotationConfigApplicationContext applicationContext; 35 | 36 | @Param({"SimpleEvent", "ComplexEvent"}) 37 | private String eventTestCase; 38 | 39 | @Param({"Filedata", "InMemory"}) 40 | private String datastoreTestCase; 41 | 42 | private Constructor eventConstructor; 43 | 44 | 45 | @Benchmark 46 | public int testSimpleEvent() throws IllegalAccessException, InvocationTargetException, InstantiationException { 47 | Event test = eventConstructor.newInstance(); 48 | eventBus.publish(test); 49 | return test.hashCode(); 50 | } 51 | 52 | @Setup(Level.Iteration) 53 | public void init() throws IOException, ClassNotFoundException, NoSuchMethodException { 54 | Class eventConfigClass = getClassForSimpleName("config." + eventTestCase + "Config"); 55 | Class datastoreConfigClass = getClassForSimpleName("config." + datastoreTestCase + "Config"); 56 | eventConstructor = (Constructor) getClassForSimpleName("event." + eventTestCase).getDeclaredConstructor(); 57 | applicationContext = new AnnotationConfigApplicationContext(datastoreConfigClass, eventConfigClass); 58 | eventBus = applicationContext.getBean(EventBus.class); 59 | } 60 | 61 | private Class getClassForSimpleName(String simpleName) throws ClassNotFoundException { 62 | return Class.forName("io.codearte.duramen.benchmark." + simpleName); 63 | } 64 | 65 | @TearDown(Level.Iteration) 66 | public void cleanup() throws IOException, InterruptedException { 67 | // wait for event to process before closing context 68 | Thread.sleep(1000); 69 | 70 | applicationContext.close(); 71 | 72 | // cleanup files 73 | for (File f : new File("/tmp").listFiles()) { 74 | if (f.getName().startsWith("perf_duramen")) { 75 | f.delete(); 76 | } 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/config/ComplexEventConfig.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark.config; 2 | 3 | import io.codearte.duramen.config.DuramenConfiguration; 4 | import io.codearte.duramen.handler.EventHandler; 5 | import io.codearte.duramen.benchmark.event.ComplexEvent; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | /** 9 | * @author Jakub Kubrynski 10 | */ 11 | public class ComplexEventConfig { 12 | 13 | @Bean 14 | public DuramenConfiguration duramenConfiguration() { 15 | return DuramenConfiguration.builder() 16 | .maxMessageCount(1024) 17 | .maxMessageSize(1536) 18 | .maxProcessingThreads(1) 19 | .build(); 20 | } 21 | 22 | @Bean 23 | public EventHandler eventHandler() { 24 | return new EventHandler() { 25 | @Override 26 | public void onEvent(ComplexEvent event) { 27 | } 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/config/FiledataConfig.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark.config; 2 | 3 | import io.codearte.duramen.annotation.EnableDuramen; 4 | import io.codearte.duramen.config.DuramenConfiguration; 5 | import io.codearte.duramen.datastore.Datastore; 6 | import io.codearte.duramen.datastore.FileData; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import java.io.IOException; 12 | import java.util.UUID; 13 | 14 | /** 15 | * @author Jakub Kubrynski 16 | */ 17 | @Configuration 18 | @EnableDuramen 19 | class FiledataConfig { 20 | 21 | @Autowired 22 | private DuramenConfiguration duramenConfiguration; 23 | 24 | @Bean 25 | public Datastore datastore() throws IOException { 26 | return new FileData("/tmp/perf_duramen_" + UUID.randomUUID().toString(), 27 | duramenConfiguration.getMaxMessageCount(), duramenConfiguration.getMaxMessageSize()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/config/InMemoryConfig.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark.config; 2 | 3 | import io.codearte.duramen.annotation.EnableDuramen; 4 | import io.codearte.duramen.datastore.Datastore; 5 | import io.codearte.duramen.datastore.InMemory; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * @author Jakub Kubrynski 13 | */ 14 | @Configuration 15 | @EnableDuramen 16 | class InMemoryConfig { 17 | 18 | @Bean 19 | public Datastore datastore() throws IOException { 20 | return new InMemory(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/config/SimpleEventConfig.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark.config; 2 | 3 | import io.codearte.duramen.config.DuramenConfiguration; 4 | import io.codearte.duramen.handler.EventHandler; 5 | import io.codearte.duramen.benchmark.event.SimpleEvent; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | /** 9 | * @author Jakub Kubrynski 10 | */ 11 | public class SimpleEventConfig { 12 | 13 | @Bean 14 | public DuramenConfiguration duramenConfiguration() { 15 | return DuramenConfiguration.builder() 16 | .maxMessageCount(1024) 17 | .maxMessageSize(256) 18 | .maxProcessingThreads(1) 19 | .build(); 20 | } 21 | @Bean 22 | public EventHandler eventHandler() { 23 | return new EventHandler() { 24 | @Override 25 | public void onEvent(SimpleEvent event) { 26 | } 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/event/ComplexEvent.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark.event; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.codearte.duramen.event.Event; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.ArrayList; 8 | 9 | /** 10 | * @author Jakub Kubrynski 11 | */ 12 | public class ComplexEvent implements Event { 13 | 14 | private String stringPayload; 15 | private BigDecimal bigDecimalPayload; 16 | private ArrayList integers; 17 | 18 | public ComplexEvent() { 19 | stringPayload = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 20 | bigDecimalPayload = new BigDecimal(12345.6789); 21 | integers = Lists.newArrayList(123, 456, 789, 1234, 2345, 3456); 22 | } 23 | 24 | public String getStringPayload() { 25 | return stringPayload; 26 | } 27 | 28 | public BigDecimal getBigDecimalPayload() { 29 | return bigDecimalPayload; 30 | } 31 | 32 | public ArrayList getIntegers() { 33 | return integers; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/jmh/java/io/codearte/duramen/benchmark/event/SimpleEvent.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.benchmark.event; 2 | 3 | import io.codearte.duramen.event.Event; 4 | 5 | /** 6 | * @author Jakub Kubrynski 7 | */ 8 | public class SimpleEvent implements Event { 9 | 10 | private String payload; 11 | 12 | public SimpleEvent() { 13 | payload = "TEST"; 14 | } 15 | 16 | public String getPayload() { 17 | return payload; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/jmh/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} ${LEVEL:-%6p} [%-9t] %-42logger{39} : %m%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/DuramenPackageMarker.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen; 2 | 3 | /** 4 | * Marker interface for Spring component scan 5 | * 6 | * @author Jakub Kubrynski 7 | */ 8 | public interface DuramenPackageMarker { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/EventBus.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen; 2 | 3 | import com.google.common.collect.HashMultimap; 4 | import com.google.common.collect.Multimap; 5 | import io.codearte.duramen.config.EvenBusContext; 6 | import io.codearte.duramen.event.Event; 7 | import io.codearte.duramen.event.ProcessAfterCommit; 8 | import io.codearte.duramen.event.RetryableEvent; 9 | import io.codearte.duramen.handler.EventHandler; 10 | import org.nustaq.serialization.simpleapi.DefaultCoder; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.core.annotation.AnnotationUtils; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.transaction.support.TransactionSynchronizationAdapter; 17 | import org.springframework.transaction.support.TransactionSynchronizationManager; 18 | 19 | import java.lang.invoke.MethodHandles; 20 | import java.util.Collection; 21 | import java.util.Map; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.ConcurrentMap; 24 | import java.util.concurrent.Semaphore; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.concurrent.atomic.AtomicInteger; 27 | 28 | import static com.google.common.base.Preconditions.checkNotNull; 29 | 30 | /** 31 | * Main Duramen class implementing EventBus pattern 32 | * 33 | * @author Jakub Kubrynski 34 | */ 35 | @Component 36 | public class EventBus { 37 | 38 | private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 39 | 40 | private final EvenBusContext evenBusContext; 41 | 42 | private final Multimap handlers; 43 | private final Semaphore semaphore; 44 | 45 | private final DefaultCoder defaultCoder = new DefaultCoder(); 46 | 47 | private final ConcurrentMap retryingEventsMap = new ConcurrentHashMap<>(); 48 | 49 | @Autowired 50 | public EventBus(EvenBusContext evenBusContext) { 51 | this.evenBusContext = evenBusContext; 52 | semaphore = new Semaphore(evenBusContext.getMaxMessageCount(), true); 53 | handlers = HashMultimap.create(); 54 | } 55 | 56 | /** 57 | * This method can be used to register custom handler not included in Spring application context 58 | * 59 | * @param eventDiscriminator full qualified name of event class 60 | * @param eventHandler {@link io.codearte.duramen.handler.EventHandler} instance 61 | */ 62 | public void register(String eventDiscriminator, EventHandler eventHandler) { 63 | if (checkEventClassCorrectness(eventDiscriminator)) { 64 | handlers.put(eventDiscriminator, eventHandler); 65 | } 66 | } 67 | 68 | /** 69 | * Checks if class contains default constructor required by Kryo deserializer 70 | * 71 | * @param eventDiscriminator full qualified name of event class 72 | * @return true if class is correct event class 73 | */ 74 | private boolean checkEventClassCorrectness(String eventDiscriminator) { 75 | try { 76 | Class.forName(eventDiscriminator).getDeclaredConstructor(); 77 | return true; 78 | } catch (NoSuchMethodException e) { 79 | LOG.error("Event {} does not contain default (even private) constructor!", eventDiscriminator); 80 | } catch (ClassNotFoundException e) { 81 | LOG.error("Could not retrieve event class {}", eventDiscriminator); 82 | } 83 | return false; 84 | } 85 | 86 | /** 87 | * Method used to publish event. Event will be persisted and then processed 88 | * in {@link java.util.concurrent.ExecutorService} 89 | * After successful processing event will be deleted from persistent store 90 | * 91 | * @param event event to process 92 | */ 93 | @SuppressWarnings("unchecked") 94 | public void publish(final Event event) { 95 | checkNotNull(event); 96 | semaphore.acquireUninterruptibly(); 97 | final Long eventId; 98 | synchronized (defaultCoder) { 99 | eventId = evenBusContext.getDatastore().saveEvent(defaultCoder.toByteArray(event)); 100 | } 101 | if (LOG.isDebugEnabled()) { 102 | LOG.debug("Publishing event [id = {}, queueSize={}, body={}]", 103 | eventId, evenBusContext.getDatastore().size(), evenBusContext.getEventJsonSerializer().serializeToJson(event)); 104 | } 105 | 106 | if (TransactionSynchronizationManager.isActualTransactionActive() && isTransactionAwareEvent(event)) { 107 | if (LOG.isDebugEnabled()) { 108 | LOG.debug("Event will be processed after transaction commit: id={}", eventId); 109 | } 110 | TransactionSynchronizationManager.registerSynchronization( 111 | new TransactionSynchronizationAdapter() { 112 | @Override 113 | public void afterCommit() { 114 | evenBusContext.getExecutorService().submit(getRunnableProcessor(event, eventId)); 115 | } 116 | 117 | @Override 118 | public void afterCompletion(int status) { 119 | if (status == STATUS_ROLLED_BACK) { 120 | if (LOG.isDebugEnabled()) { 121 | LOG.debug("Transaction rolled back -> removing event: id={}", eventId); 122 | } 123 | evenBusContext.getDatastore().deleteEvent(eventId); 124 | } 125 | } 126 | }); 127 | } else { 128 | evenBusContext.getExecutorService().submit(getRunnableProcessor(event, eventId)); 129 | } 130 | } 131 | 132 | private boolean isTransactionAwareEvent(Event event) { 133 | return AnnotationUtils.isAnnotationDeclaredLocally(ProcessAfterCommit.class, event.getClass()); 134 | } 135 | 136 | @SuppressWarnings("unchecked") 137 | private void processEvent(Long eventId, Event event) { 138 | checkNotNull(event); 139 | Collection eventHandlers = handlers.get(event.getClass().getCanonicalName()); 140 | if (LOG.isDebugEnabled()) { 141 | LOG.debug("Processing event with id = {} - found {} valid handlers", eventId, eventHandlers.size()); 142 | } 143 | // we should delete if there are no handlers for this event 144 | boolean shouldBeDeleted = true; 145 | boolean shouldBeRetried = false; 146 | for (EventHandler handler : eventHandlers) { 147 | shouldBeDeleted = false; 148 | try { 149 | handler.onEvent(event); 150 | shouldBeDeleted = true; 151 | } catch (Throwable e) { 152 | if (!shouldBeRetried) { 153 | if (eventShouldBeRetried(event, eventId, e)) { 154 | shouldBeRetried = true; 155 | } else { 156 | evenBusContext.getExceptionHandler().handleException(event, e, handler); 157 | shouldBeDeleted = true; 158 | } 159 | } 160 | } 161 | } 162 | if (shouldBeRetried) { 163 | evenBusContext.getExecutorService().schedule(getRunnableProcessor(event, eventId), 164 | evenBusContext.getRetryDelayInSeconds(), TimeUnit.SECONDS); 165 | } 166 | if (shouldBeDeleted) { 167 | if (LOG.isDebugEnabled()) { 168 | LOG.debug("Deleting event {}", eventId); 169 | } 170 | evenBusContext.getDatastore().deleteEvent(eventId); 171 | semaphore.release(); 172 | } 173 | } 174 | 175 | private Runnable getRunnableProcessor(final Event event, final Long eventId) { 176 | return new Runnable() { 177 | @Override 178 | public void run() { 179 | processEvent(eventId, event); 180 | } 181 | }; 182 | } 183 | 184 | private boolean eventShouldBeRetried(Event event, Long eventId, Throwable e) { 185 | if (!(event instanceof RetryableEvent)) { 186 | return false; 187 | } 188 | 189 | boolean isProperException = false; 190 | for (Class exceptionClass : evenBusContext.getRetryableExceptions()) { 191 | if (exceptionClass.isAssignableFrom(e.getClass())) { 192 | isProperException = true; 193 | break; 194 | } 195 | } 196 | 197 | if (!retryingEventsMap.containsKey(eventId) && isProperException) { 198 | retryingEventsMap.put(eventId, new AtomicInteger()); 199 | } 200 | 201 | boolean retryCountExceeded = retryingEventsMap.get(eventId).incrementAndGet() > evenBusContext.getRetryCount(); 202 | if (retryCountExceeded || !isProperException) { 203 | retryingEventsMap.remove(eventId); 204 | } 205 | return !retryCountExceeded && isProperException; 206 | } 207 | 208 | /** 209 | * This method process all event persisted and not processed before application restart. 210 | */ 211 | @SuppressWarnings("unchecked") 212 | public void processSavedEvents() { 213 | Map events = evenBusContext.getDatastore().getStoredEvents(); 214 | LOG.info("Processing stored events. Found {} events to process", events.size()); 215 | semaphore.acquireUninterruptibly(events.size()); 216 | for (Long eventId : events.keySet()) { 217 | Object eventToProcess; 218 | synchronized (defaultCoder) { 219 | eventToProcess = defaultCoder.toObject(events.get(eventId)); 220 | } 221 | processEvent(eventId, (Event) eventToProcess); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/annotation/EnableDuramen.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.annotation; 2 | 3 | import io.codearte.duramen.config.DuramenSpringConfiguration; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * Root Duramen configuration annotation 13 | * Annotate your @Configuration class to enable Duramen in your project 14 | * 15 | * @author Jakub Kubrynski 16 | */ 17 | @Target(ElementType.TYPE) 18 | @Retention(RetentionPolicy.RUNTIME) 19 | @Import(DuramenSpringConfiguration.class) 20 | public @interface EnableDuramen { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/config/DuramenConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.config; 2 | 3 | import com.google.common.collect.Sets; 4 | 5 | import java.util.Set; 6 | 7 | /** 8 | * @author Jakub Kubrynski 9 | */ 10 | public class DuramenConfiguration { 11 | 12 | private final Integer maxMessageSize; 13 | private final Integer maxMessageCount; 14 | private final Integer maxProcessingThreads; 15 | private final Integer retryDelayInSeconds; 16 | private final Integer retryCount; 17 | private final Boolean useDaemonThreads; 18 | private final Set> retryableExceptions; 19 | 20 | private DuramenConfiguration(Integer maxMessageSize, Integer maxMessageCount, Integer maxProcessingThreads, 21 | Integer retryDelayInSeconds, Integer retryCount, Boolean useDaemonThreads, 22 | Set> retryableExceptions) { 23 | 24 | this.maxMessageSize = maxMessageSize; 25 | this.maxMessageCount = maxMessageCount; 26 | this.maxProcessingThreads = maxProcessingThreads; 27 | this.retryDelayInSeconds = retryDelayInSeconds; 28 | this.retryCount = retryCount; 29 | this.useDaemonThreads = useDaemonThreads; 30 | this.retryableExceptions = retryableExceptions; 31 | } 32 | 33 | public Integer getMaxMessageSize() { 34 | return maxMessageSize; 35 | } 36 | 37 | public Integer getMaxMessageCount() { 38 | return maxMessageCount; 39 | } 40 | 41 | public Integer getMaxProcessingThreads() { 42 | return maxProcessingThreads; 43 | } 44 | 45 | public Integer getRetryDelayInSeconds() { 46 | return retryDelayInSeconds; 47 | } 48 | 49 | public Integer getRetryCount() { 50 | return retryCount; 51 | } 52 | 53 | public Boolean getUseDaemonThreads() { 54 | return useDaemonThreads; 55 | } 56 | 57 | public Set> getRetryableExceptions() { 58 | return retryableExceptions; 59 | } 60 | 61 | public static DuramenConfigurationBuilder builder() { 62 | return new DuramenConfigurationBuilder(); 63 | } 64 | 65 | public static class DuramenConfigurationBuilder { 66 | 67 | private Integer maxMessageSize = 4096; 68 | private Integer maxMessageCount = 1024; 69 | private Integer maxProcessingThreads = 1; 70 | private Integer retryDelayInSeconds = 5; 71 | private Integer retryCount = 3; 72 | private Boolean useDaemonThreads = true; 73 | @SuppressWarnings("unchecked") 74 | private Set> retryableExceptions = Sets.>newHashSet(Throwable.class); 75 | 76 | public DuramenConfiguration build() { 77 | return new DuramenConfiguration(maxMessageSize, maxMessageCount, maxProcessingThreads, 78 | retryDelayInSeconds, retryCount, useDaemonThreads, retryableExceptions); 79 | } 80 | 81 | public DuramenConfigurationBuilder maxMessageSize(Integer maxMessageSize) { 82 | this.maxMessageSize = maxMessageSize; 83 | return this; 84 | } 85 | 86 | public DuramenConfigurationBuilder maxMessageCount(Integer maxMessageCount) { 87 | this.maxMessageCount = maxMessageCount; 88 | return this; 89 | } 90 | 91 | public DuramenConfigurationBuilder maxProcessingThreads(Integer maxProcessingThreads) { 92 | this.maxProcessingThreads = maxProcessingThreads; 93 | return this; 94 | } 95 | 96 | public DuramenConfigurationBuilder retryDelayInSeconds(Integer retryDelayInSeconds) { 97 | this.retryDelayInSeconds = retryDelayInSeconds; 98 | return this; 99 | } 100 | 101 | public DuramenConfigurationBuilder retryCount(Integer retryCount) { 102 | this.retryCount = retryCount; 103 | return this; 104 | } 105 | 106 | public DuramenConfigurationBuilder useDaemonThreads(Boolean useDaemonThreads) { 107 | this.useDaemonThreads = useDaemonThreads; 108 | return this; 109 | } 110 | 111 | public DuramenConfigurationBuilder retryableExceptions(Class... retryableExceptions) { 112 | this.retryableExceptions = Sets.newHashSet(retryableExceptions); 113 | return this; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/config/DuramenConfigurator.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.config; 2 | 3 | import io.codearte.duramen.EventBus; 4 | import io.codearte.duramen.handler.EventHandler; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; 8 | import org.springframework.beans.factory.NoUniqueBeanDefinitionException; 9 | import org.springframework.context.ApplicationContext; 10 | import org.springframework.context.ApplicationListener; 11 | import org.springframework.context.event.ContextRefreshedEvent; 12 | import org.springframework.core.GenericTypeResolver; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.lang.invoke.MethodHandles; 16 | import java.util.Collection; 17 | import java.util.Map; 18 | 19 | /** 20 | * This class scans application context and lookup for all beans implementing 21 | * {@link io.codearte.duramen.handler.EventHandler} interface. Those beans will 22 | * be registered as event handlers automatically. 23 | * 24 | * @author Jakub Kubrynski 25 | */ 26 | @Component 27 | class DuramenConfigurator implements ApplicationListener { 28 | 29 | private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 30 | 31 | @Override 32 | public void onApplicationEvent(ContextRefreshedEvent event) { 33 | ApplicationContext applicationContext = event.getApplicationContext(); 34 | 35 | EventBus eventBus; 36 | 37 | try { 38 | eventBus = applicationContext.getBean(EventBus.class); 39 | } catch (NoUniqueBeanDefinitionException e) { 40 | LOG.error("More than one EventBus bean found!"); 41 | return; 42 | } catch (NoSuchBeanDefinitionException e) { 43 | LOG.error("No EventBus bean has been found"); 44 | return; 45 | } 46 | 47 | Map beans = applicationContext.getBeansOfType(EventHandler.class); 48 | Collection eventHandlers = beans.values(); 49 | 50 | if (eventHandlers.isEmpty()) { 51 | LOG.warn("No event handler beans have been found"); 52 | return; 53 | } 54 | 55 | for (EventHandler eventHandler : eventHandlers) { 56 | Class generic = GenericTypeResolver.resolveTypeArgument(eventHandler.getClass(), EventHandler.class); 57 | LOG.debug("Registering {} handler for processing {} events", 58 | eventHandler.getClass().getSimpleName(), 59 | generic.getSimpleName()); 60 | eventBus.register(generic.getCanonicalName(), eventHandler); 61 | } 62 | 63 | Thread thread = new Thread(eventBus::processSavedEvents); 64 | thread.setDaemon(true); 65 | thread.setName("Duramen-Saved-Events"); 66 | thread.start(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/config/DuramenSpringConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.config; 2 | 3 | import io.codearte.duramen.DuramenPackageMarker; 4 | import io.codearte.duramen.datastore.Datastore; 5 | import io.codearte.duramen.datastore.FileData; 6 | import io.codearte.duramen.handler.ExceptionHandler; 7 | import io.codearte.duramen.internal.EventJsonSerializer; 8 | import io.codearte.duramen.internal.LoggingExceptionHandler; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Qualifier; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.ComponentScan; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | import java.io.IOException; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.ScheduledExecutorService; 18 | import java.util.concurrent.ThreadFactory; 19 | import java.util.concurrent.atomic.AtomicInteger; 20 | 21 | /** 22 | * Main configuration class. Handles all optional dependencies which allows 23 | * user to override default settings 24 | * 25 | * @author Jakub Kubrynski 26 | */ 27 | @SuppressWarnings("FieldCanBeLocal") 28 | @Configuration 29 | @ComponentScan(basePackageClasses = DuramenPackageMarker.class, excludeFilters = @ComponentScan.Filter(Configuration.class)) 30 | public class DuramenSpringConfiguration { 31 | 32 | @Autowired(required = false) 33 | private DuramenConfiguration duramenConfiguration; 34 | 35 | @Autowired(required = false) 36 | private Datastore datastore; 37 | 38 | @Autowired(required = false) 39 | @Qualifier("duramenExecutorService") 40 | private ScheduledExecutorService executorService; 41 | 42 | @Autowired(required = false) 43 | private ExceptionHandler exceptionHandler; 44 | 45 | @Autowired 46 | private EventJsonSerializer eventJsonSerializer; 47 | 48 | @Bean 49 | public EvenBusContext evenBusProperties() throws IOException { 50 | if (duramenConfiguration == null) { 51 | duramenConfiguration = DuramenConfiguration.builder().build(); 52 | } 53 | if (executorService == null) { 54 | executorService = Executors.newScheduledThreadPool(duramenConfiguration.getMaxProcessingThreads(), buildThreadFactory()); 55 | } 56 | if (datastore == null) { 57 | datastore = new FileData(FileData.DEFAULT_FILENAME, duramenConfiguration.getMaxMessageCount(), duramenConfiguration.getMaxMessageSize()); 58 | } 59 | if (exceptionHandler == null) { 60 | exceptionHandler = new LoggingExceptionHandler(eventJsonSerializer); 61 | } 62 | return new EvenBusContext(duramenConfiguration, executorService, datastore, eventJsonSerializer, exceptionHandler); 63 | } 64 | 65 | private ThreadFactory buildThreadFactory() { 66 | final AtomicInteger threadNumerator = new AtomicInteger(0); 67 | 68 | return new ThreadFactory() { 69 | @Override 70 | public Thread newThread(Runnable r) { 71 | Thread thread = new Thread(r); 72 | thread.setDaemon(duramenConfiguration.getUseDaemonThreads()); 73 | thread.setName("duramen-" + threadNumerator.incrementAndGet()); 74 | return thread; 75 | } 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/config/EvenBusContext.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.config; 2 | 3 | import io.codearte.duramen.datastore.Datastore; 4 | import io.codearte.duramen.handler.ExceptionHandler; 5 | import io.codearte.duramen.internal.EventJsonSerializer; 6 | 7 | import java.util.Set; 8 | import java.util.concurrent.ScheduledExecutorService; 9 | 10 | /** 11 | * Class grouping all dependencies needed to processing events. 12 | * 13 | * @author Jakub Kubrynski 14 | */ 15 | public class EvenBusContext { 16 | 17 | private final DuramenConfiguration duramenConfiguration; 18 | private final ScheduledExecutorService executorService; 19 | private final Datastore datastore; 20 | private final EventJsonSerializer eventJsonSerializer; 21 | private final ExceptionHandler exceptionHandler; 22 | 23 | public EvenBusContext(DuramenConfiguration duramenConfiguration, ScheduledExecutorService executorService, 24 | Datastore datastore, EventJsonSerializer eventJsonSerializer, 25 | ExceptionHandler exceptionHandler) { 26 | this.duramenConfiguration = duramenConfiguration; 27 | this.executorService = executorService; 28 | this.datastore = datastore; 29 | this.eventJsonSerializer = eventJsonSerializer; 30 | this.exceptionHandler = exceptionHandler; 31 | } 32 | 33 | public Integer getMaxMessageSize() { 34 | return duramenConfiguration.getMaxMessageSize(); 35 | } 36 | 37 | public Integer getMaxMessageCount() { 38 | return duramenConfiguration.getMaxMessageCount(); 39 | } 40 | 41 | public Integer getRetryDelayInSeconds() { 42 | return duramenConfiguration.getRetryDelayInSeconds(); 43 | } 44 | 45 | public Integer getRetryCount() { 46 | return duramenConfiguration.getRetryCount(); 47 | } 48 | 49 | public ScheduledExecutorService getExecutorService() { 50 | return executorService; 51 | } 52 | 53 | public Datastore getDatastore() { 54 | return datastore; 55 | } 56 | 57 | public EventJsonSerializer getEventJsonSerializer() { 58 | return eventJsonSerializer; 59 | } 60 | 61 | public ExceptionHandler getExceptionHandler() { 62 | return exceptionHandler; 63 | } 64 | 65 | public Set> getRetryableExceptions() { 66 | return duramenConfiguration.getRetryableExceptions(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/datastore/Datastore.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.datastore; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Generic datastore interface. Can be implemented to handle custom 7 | * data repositories. 8 | * 9 | * @author Jakub Kubrynski 10 | */ 11 | public interface Datastore { 12 | 13 | /** 14 | * Persists event under unique id 15 | * 16 | * @param eventAsBytes serialized event 17 | * @return generated event unique identifier 18 | */ 19 | Long saveEvent(byte[] eventAsBytes); 20 | 21 | /** 22 | * Deletes event from persistent store 23 | * 24 | * @param eventId event identifier 25 | */ 26 | void deleteEvent(Long eventId); 27 | 28 | /** 29 | * Returns all persisted events. Used after starting event bus to process 30 | * events persisted before closing context and not yet processed 31 | * 32 | * @return events Map<eventId, eventAsBytes> 33 | */ 34 | Map getStoredEvents(); 35 | 36 | long size(); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/datastore/FileData.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.datastore; 2 | 3 | import javax.annotation.PreDestroy; 4 | import java.io.Closeable; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import com.google.common.base.Strings; 11 | import io.codearte.duramen.generator.IdGenerator; 12 | import io.codearte.duramen.generator.RandomIdGenerator; 13 | import net.openhft.chronicle.map.ChronicleMapBuilder; 14 | 15 | import static com.google.common.base.Preconditions.checkArgument; 16 | 17 | /** 18 | * Fast implementation based on SharedHashMap from HugeCollections 19 | * 20 | * @author Jakub Kubrynski 21 | */ 22 | public class FileData implements Datastore { 23 | 24 | public static final String DEFAULT_FILENAME = "/tmp/duramen.data"; 25 | 26 | private final IdGenerator randomIdGenerator = new RandomIdGenerator(); 27 | 28 | private final Map sharedHashMap; 29 | 30 | /** 31 | * Most detailed constructor allowing creation of fully customized map 32 | * 33 | * @param path full path including file name 34 | * @param entries maximum number of events persisted 35 | * @param entrySize maximum size of single event 36 | * @throws IOException when creating/opening file fails 37 | */ 38 | public FileData(String path, int entries, int entrySize) throws IOException { 39 | checkArgument(!Strings.isNullOrEmpty(path)); 40 | checkArgument(entries > 0); 41 | checkArgument(entrySize > 0); 42 | 43 | sharedHashMap = ChronicleMapBuilder.of(Long.class, byte[].class) 44 | .entries(entries) 45 | .averageValueSize(entrySize) 46 | .createPersistedTo(new File(path)); 47 | } 48 | 49 | @Override 50 | public Long saveEvent(byte[] eventAsBytes) { 51 | long id = randomIdGenerator.getNextId(); 52 | sharedHashMap.put(id, eventAsBytes); 53 | return id; 54 | } 55 | 56 | @Override 57 | public void deleteEvent(Long eventId) { 58 | sharedHashMap.remove(eventId); 59 | } 60 | 61 | @Override 62 | public Map getStoredEvents() { 63 | return new HashMap<>(sharedHashMap); 64 | } 65 | 66 | @Override 67 | public long size() { 68 | return sharedHashMap.size(); 69 | } 70 | 71 | /** 72 | * Closes map before destroying application context 73 | * 74 | * @throws IOException inherited from {@link java.io.Closeable}. Will not be thrown 75 | */ 76 | @PreDestroy 77 | public void close() throws IOException { 78 | ((Closeable) sharedHashMap).close(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/datastore/InMemory.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.datastore; 2 | 3 | import io.codearte.duramen.generator.IdGenerator; 4 | import io.codearte.duramen.generator.RandomIdGenerator; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * Implementation mainly for testing purposes. Does not persist events 12 | * 13 | * @author Jakub Kubrynski 14 | */ 15 | public class InMemory implements Datastore { 16 | 17 | private final IdGenerator randomIdGenerator = new RandomIdGenerator(); 18 | private Map hashMap; 19 | 20 | @SuppressWarnings("UnusedDeclaration") 21 | public InMemory() { 22 | hashMap = new ConcurrentHashMap<>(); 23 | } 24 | 25 | @Override 26 | public Long saveEvent(byte[] eventAsBytes) { 27 | Long id = randomIdGenerator.getNextId(); 28 | hashMap.put(id, eventAsBytes); 29 | return id; 30 | } 31 | 32 | @Override 33 | public void deleteEvent(Long eventId) { 34 | hashMap.remove(eventId); 35 | } 36 | 37 | @Override 38 | public Map getStoredEvents() { 39 | return new HashMap<>(hashMap); 40 | } 41 | 42 | @Override 43 | public long size() { 44 | return hashMap.size(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/event/Event.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.event; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Generic event interface. 7 | * Remember that class implementing Event have to provide (even private) default constructor. 8 | * 9 | * @author Jakub Kubrynski 10 | */ 11 | public interface Event extends Serializable { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/event/ProcessAfterCommit.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.event; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks that event should be processed after transaction commit 10 | * @author Jakub Kubrynski 11 | */ 12 | @Target(ElementType.TYPE) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface ProcessAfterCommit { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/event/RetryableEvent.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.event; 2 | 3 | /** 4 | * Retryable event interface 5 | * Remember that class implementing Event have to provide (even private) default constructor. 6 | * 7 | * @author Jakub Kubrynski 8 | */ 9 | public interface RetryableEvent extends Event { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/generator/IdGenerator.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.generator; 2 | 3 | /** 4 | * @author Jakub Kubrynski 5 | */ 6 | public interface IdGenerator { 7 | 8 | /** 9 | * Generates unique event identifier 10 | * 11 | * @return generated event id 12 | */ 13 | Long getNextId(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/generator/RandomIdGenerator.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.generator; 2 | 3 | import java.util.Random; 4 | 5 | /** 6 | * @author Jakub Kubrynski 7 | */ 8 | public class RandomIdGenerator implements IdGenerator { 9 | 10 | private Random random = new Random(); 11 | 12 | @Override 13 | public Long getNextId() { 14 | return System.nanoTime() * 10 + random.nextInt(10); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/handler/EventHandler.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.handler; 2 | 3 | import io.codearte.duramen.event.Event; 4 | 5 | /** 6 | * Implement that interface in your event handler to receive events of generic E class 7 | * 8 | * @author Jakub Kubrynski 9 | */ 10 | public interface EventHandler { 11 | 12 | void onEvent(E event); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/handler/ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.handler; 2 | 3 | import io.codearte.duramen.event.Event; 4 | 5 | /** 6 | * @author Jakub Kubrynski 7 | */ 8 | public interface ExceptionHandler { 9 | 10 | void handleException(Event event, Throwable e, EventHandler handler); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/internal/EventJsonSerializer.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.internal; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.codearte.duramen.event.Event; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.lang.invoke.MethodHandles; 11 | 12 | /** 13 | * @author Jakub Kubrynski 14 | */ 15 | @Component 16 | public class EventJsonSerializer { 17 | 18 | private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 19 | 20 | private ObjectMapper objectMapper; 21 | 22 | public EventJsonSerializer() { 23 | this.objectMapper = new ObjectMapper(); 24 | } 25 | 26 | public String serializeToJson(Event event) { 27 | String json; 28 | try { 29 | json = objectMapper.writeValueAsString(event); 30 | } catch (JsonProcessingException jsonException) { 31 | LOG.error("Error during serializing event to json", jsonException); 32 | json = "ERROR"; 33 | } 34 | return json; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/codearte/duramen/internal/LoggingExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.internal; 2 | 3 | import io.codearte.duramen.event.Event; 4 | import io.codearte.duramen.handler.EventHandler; 5 | import io.codearte.duramen.handler.ExceptionHandler; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.lang.invoke.MethodHandles; 10 | 11 | /** 12 | * @author Jakub Kubrynski 13 | */ 14 | public class LoggingExceptionHandler implements ExceptionHandler { 15 | 16 | private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 17 | 18 | private final EventJsonSerializer eventJsonSerializer; 19 | 20 | public LoggingExceptionHandler(EventJsonSerializer eventJsonSerializer) { 21 | this.eventJsonSerializer = eventJsonSerializer; 22 | } 23 | 24 | @Override 25 | public void handleException(Event event, Throwable e, EventHandler handler) { 26 | LOG.error("Error during processing event [{}] in handler {}", 27 | eventJsonSerializer.serializeToJson(event), handler.getClass().getSimpleName(), e); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/groovy/io/codearte/duramen/EventBusSpecIT.groovy: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen 2 | 3 | import io.codearte.duramen.annotation.EnableDuramen 4 | import io.codearte.duramen.datastore.Datastore 5 | import io.codearte.duramen.datastore.InMemory 6 | import org.mockito.Mockito 7 | import spock.lang.Specification 8 | import test.codearte.duramen.EventConsumer 9 | import test.codearte.duramen.EventProducer 10 | import test.codearte.duramen.TestEvent 11 | 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.context.annotation.Bean 14 | import org.springframework.context.annotation.ComponentScan 15 | import org.springframework.context.annotation.Configuration 16 | import org.springframework.test.context.ContextConfiguration 17 | 18 | import static org.mockito.Matchers.isA 19 | import static org.mockito.Mockito.timeout 20 | import static org.mockito.Mockito.verify 21 | 22 | /** 23 | * Created by jkubrynski@gmail.com / 2014-02-10 24 | */ 25 | @ContextConfiguration(classes = SampleConfiguration) 26 | class EventBusSpecIT extends Specification { 27 | 28 | @Autowired 29 | EventProducer eventProducer 30 | 31 | @Autowired 32 | EventConsumer eventConsumer 33 | 34 | def "should publish and receive event"() { 35 | when: 36 | eventProducer.produce() 37 | then: 38 | verify(eventConsumer, timeout(500)).onEvent(isA(TestEvent)) 39 | } 40 | 41 | @Configuration 42 | @ComponentScan(basePackages = "test.codearte.duramen") 43 | @EnableDuramen 44 | static class SampleConfiguration { 45 | 46 | @Bean 47 | Datastore datastore() { 48 | new InMemory() 49 | } 50 | 51 | @Bean 52 | EventConsumer eventConsumer() { 53 | return Mockito.mock(EventConsumer) 54 | } 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /src/test/groovy/io/codearte/duramen/PersistenceSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import io.codearte.duramen.annotation.EnableDuramen 6 | import io.codearte.duramen.handler.EventHandler 7 | import org.springframework.context.annotation.AnnotationConfigApplicationContext 8 | import org.springframework.context.annotation.Bean 9 | import spock.lang.Specification 10 | import test.codearte.duramen.EventProducer 11 | import test.codearte.duramen.TestEvent 12 | 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | import java.util.concurrent.locks.LockSupport 15 | 16 | import static org.awaitility.Awaitility.await 17 | import static org.hamcrest.core.IsEqual.equalTo 18 | 19 | /** 20 | * @author Jakub Kubrynski 21 | */ 22 | class PersistenceSpec extends Specification { 23 | 24 | static AtomicBoolean wrongHandlerInvoked = new AtomicBoolean(false) 25 | static AtomicBoolean goodHandlerInvoked = new AtomicBoolean(false) 26 | 27 | def "should process event after crash"() { 28 | given: 29 | TestUtil.cleanupDatastore() 30 | def context = new AnnotationConfigApplicationContext(SampleConfigurationWrongConsumer) 31 | def eventProducer = context.getBean(EventProducer) 32 | when: 33 | eventProducer.produce() 34 | await().atMost(2, TimeUnit.SECONDS).untilAtomic(wrongHandlerInvoked, equalTo(true)) 35 | context.close() 36 | and: 37 | new AnnotationConfigApplicationContext(SampleConfigurationGoodConsumer) 38 | then: 39 | await().atMost(2, TimeUnit.SECONDS).untilAtomic(goodHandlerInvoked, equalTo(true)) 40 | } 41 | 42 | @EnableDuramen 43 | static class SampleConfigurationWrongConsumer { 44 | 45 | @Bean 46 | EventProducer eventProducer(EventBus eventBus) { 47 | return new EventProducer(eventBus) 48 | } 49 | 50 | @Bean 51 | EventHandler eventHandler() { 52 | return new EventHandler() { 53 | @Override 54 | void onEvent(TestEvent event) { 55 | wrongHandlerInvoked.set(true) 56 | while (true) { 57 | LockSupport.park() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | @EnableDuramen 65 | static class SampleConfigurationGoodConsumer { 66 | 67 | @Bean 68 | EventHandler eventHandler() { 69 | return new EventHandler() { 70 | @Override 71 | void onEvent(TestEvent event) { 72 | goodHandlerInvoked.set(true) 73 | } 74 | } 75 | } 76 | } 77 | 78 | 79 | } -------------------------------------------------------------------------------- /src/test/groovy/io/codearte/duramen/RetryableSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen 2 | 3 | import java.util.concurrent.TimeUnit 4 | import java.util.concurrent.atomic.AtomicInteger 5 | 6 | import io.codearte.duramen.annotation.EnableDuramen 7 | import io.codearte.duramen.config.DuramenConfiguration 8 | import io.codearte.duramen.datastore.Datastore 9 | import io.codearte.duramen.datastore.InMemory 10 | import io.codearte.duramen.handler.EventHandler 11 | import spock.lang.Specification 12 | import test.codearte.duramen.RetryableEventProducer 13 | import test.codearte.duramen.TestRetryableEvent 14 | 15 | import org.springframework.context.annotation.AnnotationConfigApplicationContext 16 | import org.springframework.context.annotation.Bean 17 | import org.springframework.context.annotation.ComponentScan 18 | import org.springframework.context.annotation.Configuration 19 | 20 | import static org.awaitility.Awaitility.await 21 | import static org.hamcrest.core.IsEqual.equalTo 22 | 23 | /** 24 | * @author Jakub Kubrynski 25 | */ 26 | class RetryableSpec extends Specification { 27 | 28 | static AtomicInteger goodHandlerInvoked 29 | 30 | void setup() { 31 | TestUtil.cleanupDatastore() 32 | // it's set to 1 due to https://github.com/awaitility/awaitility/issues/99 33 | goodHandlerInvoked = new AtomicInteger(1) 34 | } 35 | 36 | def "should retry event processing"() { 37 | given: 38 | def context = new AnnotationConfigApplicationContext(SampleConfiguration, MultipleRetryConfiguration) 39 | def eventProducer = context.getBean(RetryableEventProducer) 40 | when: 41 | eventProducer.produce() 42 | then: 43 | await().atMost(5, TimeUnit.SECONDS).untilAtomic(goodHandlerInvoked, equalTo(2)) 44 | } 45 | 46 | def "should retry single event after failing in two handlers"() { 47 | given: 48 | def context = new AnnotationConfigApplicationContext(SampleConfiguration, MultipleRetryManyHandlersConfiguration) 49 | def eventProducer = context.getBean(RetryableEventProducer) 50 | when: 51 | eventProducer.produce() 52 | then: 53 | await().atMost(5, TimeUnit.SECONDS).untilAtomic(goodHandlerInvoked, equalTo(3)) 54 | } 55 | 56 | def "should retry only limited number of times"() { 57 | given: 58 | def context = new AnnotationConfigApplicationContext(SampleConfiguration, SingleRetryConfiguration) 59 | def eventProducer = context.getBean(RetryableEventProducer) 60 | when: 61 | eventProducer.produce() 62 | then: 63 | await().atMost(5, TimeUnit.SECONDS).untilAtomic(goodHandlerInvoked, equalTo(1)) 64 | } 65 | 66 | def "should not retry incorrect exception"() { 67 | given: 68 | def context = new AnnotationConfigApplicationContext(SampleConfiguration, SingleRetryConfigurationWithIncorrectException) 69 | def eventProducer = context.getBean(RetryableEventProducer) 70 | when: 71 | eventProducer.produce() 72 | then: 73 | await().atMost(5, TimeUnit.SECONDS).untilAtomic(goodHandlerInvoked, equalTo(1)) 74 | } 75 | 76 | @ComponentScan(basePackages = "test.codearte.duramen") 77 | @EnableDuramen 78 | static class SampleConfiguration { 79 | 80 | @Bean 81 | Datastore datastore() { 82 | return new InMemory() 83 | } 84 | } 85 | 86 | @Configuration 87 | static class MultipleRetryConfiguration { 88 | @Bean 89 | DuramenConfiguration duramenConfiguration() { 90 | return DuramenConfiguration.builder().retryDelayInSeconds(1).build() 91 | } 92 | 93 | @Bean 94 | EventHandler eventHandler() { 95 | return new RetryableEventHandler(1) 96 | } 97 | } 98 | 99 | @Configuration 100 | static class MultipleRetryManyHandlersConfiguration { 101 | @Bean 102 | DuramenConfiguration duramenConfiguration() { 103 | return DuramenConfiguration.builder().retryDelayInSeconds(1).build() 104 | } 105 | 106 | @Bean 107 | EventHandler eventHandlerA() { 108 | return new RetryableEventHandler(1) 109 | } 110 | 111 | @Bean 112 | EventHandler eventHandlerB() { 113 | return new RetryableEventHandler(1) 114 | } 115 | } 116 | 117 | 118 | @Configuration 119 | static class SingleRetryConfiguration { 120 | 121 | @Bean 122 | DuramenConfiguration duramenConfiguration() { 123 | return DuramenConfiguration.builder().retryDelayInSeconds(1).retryCount(1).build() 124 | } 125 | 126 | @Bean 127 | EventHandler eventHandler() { 128 | return new RetryableEventHandler(2) 129 | } 130 | } 131 | 132 | @Configuration 133 | static class SingleRetryConfigurationWithIncorrectException { 134 | 135 | @Bean 136 | DuramenConfiguration duramenConfiguration() { 137 | return DuramenConfiguration.builder().retryDelayInSeconds(1).retryCount(2) 138 | .retryableExceptions(NullPointerException.class).build() 139 | } 140 | 141 | @Bean 142 | EventHandler eventHandler() { 143 | return new RetryableEventHandler(1) 144 | } 145 | } 146 | 147 | static class RetryableEventHandler implements EventHandler { 148 | 149 | int counter = 0 150 | int max = 0 151 | 152 | RetryableEventHandler(int max) { 153 | this.max = max 154 | } 155 | 156 | @Override 157 | void onEvent(TestRetryableEvent event) { 158 | if (counter < max) { 159 | counter++ 160 | throw new RuntimeException() 161 | } 162 | goodHandlerInvoked.incrementAndGet() 163 | } 164 | } 165 | 166 | 167 | } -------------------------------------------------------------------------------- /src/test/groovy/io/codearte/duramen/TestUtil.groovy: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen 2 | 3 | import io.codearte.duramen.datastore.FileData 4 | 5 | import java.nio.file.Files 6 | import java.nio.file.Paths 7 | 8 | /** 9 | * @author Jakub Kubrynski 10 | */ 11 | class TestUtil { 12 | static void cleanupDatastore() { 13 | if (new File(FileData.DEFAULT_FILENAME).exists()) { 14 | Files.delete(Paths.get(FileData.DEFAULT_FILENAME)) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/groovy/io/codearte/duramen/datastore/DatastoreSpecIT.groovy: -------------------------------------------------------------------------------- 1 | package io.codearte.duramen.datastore 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | /** 7 | * Created by jkubrynski@gmail.com / 2014-05-20 8 | */ 9 | class DatastoreSpecIT extends Specification { 10 | 11 | private static final String FILENAME = "duramen.test" 12 | 13 | @Unroll 14 | def "should persist event in datastore"() { 15 | given: 16 | def eventBytes = "Test".getBytes() 17 | when: 18 | def eventId = datastore.saveEvent(eventBytes) 19 | then: 20 | eventId 21 | def eventsMap = datastore.getStoredEvents() 22 | eventsMap.containsKey(eventId) 23 | eventsMap.get(eventId) == eventBytes 24 | datastore.size() == 1 25 | datastore.deleteEvent(eventId) 26 | datastore.size() == 0 27 | where: 28 | datastore << [new InMemory(), new FileData(FILENAME, 1024, 4096)] 29 | } 30 | 31 | def cleanupSpec() { 32 | new File(FILENAME).delete() 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/test/codearte/duramen/EventConsumer.java: -------------------------------------------------------------------------------- 1 | package test.codearte.duramen; 2 | 3 | import io.codearte.duramen.handler.EventHandler; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Created by jkubrynski@gmail.com / 2014-02-10 8 | */ 9 | @Component 10 | public class EventConsumer implements EventHandler { 11 | 12 | @Override 13 | public void onEvent(TestEvent event) { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/test/codearte/duramen/EventProducer.java: -------------------------------------------------------------------------------- 1 | package test.codearte.duramen; 2 | 3 | import io.codearte.duramen.EventBus; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Created by jkubrynski@gmail.com / 2014-02-10 9 | */ 10 | @Component 11 | public class EventProducer { 12 | 13 | private final EventBus consumer; 14 | 15 | @Autowired 16 | public EventProducer(EventBus consumer) { 17 | this.consumer = consumer; 18 | } 19 | 20 | public void produce() { 21 | TestEvent event = new TestEvent("Test string"); 22 | consumer.publish(event); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/test/codearte/duramen/RetryableEventProducer.java: -------------------------------------------------------------------------------- 1 | package test.codearte.duramen; 2 | 3 | import io.codearte.duramen.EventBus; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Created by jkubrynski@gmail.com / 2014-02-10 9 | */ 10 | @Component 11 | public class RetryableEventProducer { 12 | 13 | private final EventBus consumer; 14 | 15 | @Autowired 16 | public RetryableEventProducer(EventBus consumer) { 17 | this.consumer = consumer; 18 | } 19 | 20 | public void produce() { 21 | TestRetryableEvent event = new TestRetryableEvent("Test string"); 22 | consumer.publish(event); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/test/codearte/duramen/TestEvent.java: -------------------------------------------------------------------------------- 1 | package test.codearte.duramen; 2 | 3 | import io.codearte.duramen.event.Event; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by jkubrynski@gmail.com / 2014-02-10 9 | */ 10 | public class TestEvent implements Event { 11 | 12 | private String test; 13 | 14 | private Date date1; 15 | 16 | private TestEvent() { 17 | // just for persistence 18 | } 19 | 20 | public TestEvent(String test) { 21 | this.test = test; 22 | this.date1 = new Date(); 23 | } 24 | 25 | public String getTest() { 26 | return test; 27 | } 28 | 29 | public void setTest(String test) { 30 | this.test = test; 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/java/test/codearte/duramen/TestRetryableEvent.java: -------------------------------------------------------------------------------- 1 | package test.codearte.duramen; 2 | 3 | import io.codearte.duramen.event.RetryableEvent; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by jkubrynski@gmail.com / 2014-02-10 9 | */ 10 | public class TestRetryableEvent implements RetryableEvent { 11 | 12 | private String test; 13 | 14 | private Date date1; 15 | 16 | private TestRetryableEvent() { 17 | // just for persistence 18 | } 19 | 20 | public TestRetryableEvent(String test) { 21 | this.test = test; 22 | this.date1 = new Date(); 23 | } 24 | 25 | public String getTest() { 26 | return test; 27 | } 28 | 29 | public Date getDate1() { 30 | return date1; 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} ${LEVEL:-%6p} [%-9t] %-42logger{39} : %m%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------