├── .gitignore ├── LICENSE-2.0.txt ├── README.md ├── cluster-chain ├── build.sh └── docker-compose.yml ├── innoq-hands-on-event └── readme.pdf ├── prometheus ├── download.sh └── prometheus.yml ├── reactive-java-chain ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── Dockerfile ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── ac │ │ │ └── simons │ │ │ └── reactive │ │ │ └── chains │ │ │ ├── Application.java │ │ │ ├── Block.java │ │ │ ├── Chain.java │ │ │ ├── HashUtils.java │ │ │ └── Transaction.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── ac │ └── simons │ └── reactive │ └── chains │ ├── ChainTest.java │ └── EncodeBenchmark.java ├── reactive-kotlin-chain ├── .gitignore ├── Dockerfile ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── kotlin │ │ └── ac │ │ │ └── simons │ │ │ └── reactive │ │ │ └── chains │ │ │ ├── Application.kt │ │ │ ├── Chain.kt │ │ │ ├── Events.kt │ │ │ ├── Hashing.kt │ │ │ ├── JacksonModules.kt │ │ │ ├── NodeRegistry.kt │ │ │ └── SSDP.kt │ └── resources │ │ └── application.properties │ └── test │ └── kotlin │ └── ac │ └── simons │ └── reactive │ └── chains │ ├── ChainTest.kt │ ├── IntegrationTests.kt │ └── JacksonModulesTests.kt └── springio2018 ├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── ac │ │ └── simons │ │ └── springio2018 │ │ └── Demo.java └── resources │ └── application.properties ├── misc └── springio.png └── talk └── micrometer_new-insights-into-your-spring-boot-application.adoc /.gitignore: -------------------------------------------------------------------------------- 1 | prometheus/* 2 | !prometheus/prometheus.yml 3 | !prometheus/download.sh 4 | .idea 5 | .DS_Store -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blockchain playground 2 | 3 | Most of the ideas here came from an internal [INNOQ event](https://www.innoq.com/de/articles/2018/05/innoq-blockchain-event-2018/). The original description or task can be found [here](innoq-hands-on-event/readme.pdf). Thanks to [Mark](https://github.com/mjansing) and [Robert](https://github.com/mrreynolds). -------------------------------------------------------------------------------- /cluster-chain/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | (cd ../reactive-java-chain && ./mvnw clean verify) 3 | (cd ../reactive-kotlin-chain && ./gradlew clean build) 4 | 5 | docker-compose build -------------------------------------------------------------------------------- /cluster-chain/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | reactive-java-chain: 4 | image: msimons/reactive-java-chain 5 | build: 6 | context: ../reactive-java-chain 7 | ports: 8 | - 8080 9 | reactive-kotlin-chain: 10 | image: msimons/reactive-kotlin-chain 11 | build: 12 | context: ../reactive-kotlin-chain 13 | ports: 14 | - 8090 15 | lb: 16 | image: dockercloud/haproxy 17 | links: 18 | - reactive-java-chain 19 | - reactive-kotlin-chain 20 | volumes: 21 | - /var/run/docker.sock:/var/run/docker.sock 22 | ports: 23 | - 9000:80 -------------------------------------------------------------------------------- /innoq-hands-on-event/readme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-simons/blockchain-playground/b9897dfc14f060fece173f49b1da358b50276f6c/innoq-hands-on-event/readme.pdf -------------------------------------------------------------------------------- /prometheus/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget -qO- https://github.com/prometheus/prometheus/releases/download/v2.2.1/prometheus-2.2.1.`uname | tr '[:upper:]' '[:lower:]'`-amd64.tar.gz | tar xzk --strip-components 1 -C . 2>/dev/null -------------------------------------------------------------------------------- /prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | # A scrape configuration containing exactly one endpoint to scrape: 6 | # Here it's Prometheus itself. 7 | scrape_configs: 8 | - job_name: 'reactive-java-chain' 9 | metrics_path: '/actuator/prometheus' 10 | static_configs: 11 | - targets: ['localhost:8080'] 12 | - job_name: 'reactive-kotlin-chain' 13 | metrics_path: '/actuator/prometheus' 14 | static_configs: 15 | - targets: ['localhost:8090'] 16 | -------------------------------------------------------------------------------- /reactive-java-chain/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /reactive-java-chain/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-simons/blockchain-playground/b9897dfc14f060fece173f49b1da358b50276f6c/reactive-java-chain/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /reactive-java-chain/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip 2 | -------------------------------------------------------------------------------- /reactive-java-chain/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:10.0.1-jre-slim 2 | COPY target/reactive-java-chain-0.0.1-SNAPSHOT.jar /usr/local/reactive-chains/reactive-java-chain.jar 3 | WORKDIR /usr/local/reactive-chains 4 | EXPOSE 8080 5 | CMD ["java", "-jar", "reactive-java-chain.jar"] -------------------------------------------------------------------------------- /reactive-java-chain/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 | -------------------------------------------------------------------------------- /reactive-java-chain/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 | -------------------------------------------------------------------------------- /reactive-java-chain/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | ac.simons 7 | reactive-java-chain 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | reactive-java-chain 12 | Demo Project for Micrometer based on the topic of a blockchain 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.4.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 10 25 | 1.2.0 26 | 1.20 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-webflux 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-actuator 37 | 38 | 39 | io.micrometer 40 | micrometer-registry-prometheus 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-devtools 45 | runtime 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-configuration-processor 50 | true 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-test 55 | test 56 | 57 | 58 | junit 59 | junit 60 | 61 | 62 | 63 | 64 | org.junit.jupiter 65 | junit-jupiter-api 66 | test 67 | 68 | 69 | io.projectreactor 70 | reactor-test 71 | test 72 | 73 | 74 | org.openjdk.jmh 75 | jmh-core 76 | ${jmh.version} 77 | test 78 | 79 | 80 | org.openjdk.jmh 81 | jmh-generator-annprocess 82 | ${jmh.version} 83 | test 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-maven-plugin 92 | 93 | 94 | maven-surefire-plugin 95 | 96 | 97 | org.junit.platform 98 | junit-platform-surefire-provider 99 | ${junit-platform.version} 100 | 101 | 102 | org.junit.jupiter 103 | junit-jupiter-engine 104 | ${junit-jupiter.version} 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /reactive-java-chain/src/main/java/ac/simons/reactive/chains/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | import static org.springframework.http.HttpStatus.CREATED; 19 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 20 | import static org.springframework.web.reactive.function.server.RequestPredicates.POST; 21 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 22 | import static org.springframework.web.reactive.function.server.ServerResponse.created; 23 | import static org.springframework.web.reactive.function.server.ServerResponse.ok; 24 | import static org.springframework.web.reactive.function.server.ServerResponse.status; 25 | 26 | import java.util.Map; 27 | 28 | import io.micrometer.core.instrument.FunctionCounter; 29 | import io.micrometer.core.instrument.Gauge; 30 | import io.micrometer.core.instrument.MeterRegistry; 31 | import org.springframework.beans.factory.annotation.Value; 32 | import org.springframework.boot.SpringApplication; 33 | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; 34 | import org.springframework.boot.autoconfigure.SpringBootApplication; 35 | import org.springframework.context.annotation.Bean; 36 | import org.springframework.web.reactive.function.server.RouterFunction; 37 | import org.springframework.web.util.UriComponentsBuilder; 38 | 39 | import reactor.core.publisher.Mono; 40 | 41 | @SpringBootApplication 42 | public class Application { 43 | 44 | @Bean 45 | public MeterRegistryCustomizer commonTagsCustomizer(@Value("${spring.application.name}") final String applicationName) { 46 | return registry -> registry.config().commonTags("application", applicationName); 47 | } 48 | 49 | @Bean 50 | public Chain chain(final MeterRegistry meterRegistry) { 51 | var chain = Chain.defaultChain(); 52 | 53 | FunctionCounter.builder("chain.blocks.computed", chain, Chain::getLength) 54 | .baseUnit("block") 55 | .register(meterRegistry); 56 | 57 | Gauge.builder("chain.transactions.pending", chain, Chain::getNumberOfPendingTransactions) 58 | .baseUnit("transaction") 59 | .register(meterRegistry); 60 | 61 | return chain; 62 | } 63 | 64 | @Bean 65 | RouterFunction router(final Chain chain) { 66 | return route(GET("/mine"), request -> status(CREATED).body(chain.mine(), Block.class)) 67 | .and(route(POST("/transactions"), request -> 68 | request.bodyToMono(String.class) 69 | .flatMap(chain::queue) 70 | .flatMap(p -> created( 71 | UriComponentsBuilder.fromUri(request.uri()) 72 | .pathSegment("{id}").buildAndExpand(Map.of("id", p.getId())).encode(). 73 | toUri()) 74 | .body(Mono.just(p), Transaction.class)))) 75 | .and(route(GET("/blocks"), request -> ok().body( 76 | chain.getBlocks().map(blocks -> Map.of("blocks", blocks, "blockHeight", blocks.size())), Map.class))); 77 | } 78 | 79 | public static void main(String[] args) { 80 | SpringApplication.run(Application.class, args); 81 | } 82 | } -------------------------------------------------------------------------------- /reactive-java-chain/src/main/java/ac/simons/reactive/chains/Block.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | import java.util.List; 19 | 20 | /** 21 | * A block. A building block of a block chain, so to speak. 22 | */ 23 | public class Block { 24 | private final int index; 25 | 26 | private final long timestamp; 27 | 28 | private final long proof; 29 | 30 | private final List transactions; 31 | 32 | private final String previousBlockHash; 33 | 34 | public Block(int index, long timestamp, long proof, List transactions, String previousBlockHash) { 35 | this.index = index; 36 | this.timestamp = timestamp; 37 | this.proof = proof; 38 | this.transactions = transactions; 39 | this.previousBlockHash = previousBlockHash; 40 | } 41 | 42 | public Block newCandidateOf(final long newProof) { 43 | return new Block(index, timestamp, newProof, transactions, previousBlockHash); 44 | } 45 | 46 | public int getIndex() { 47 | return index; 48 | } 49 | 50 | public long getTimestamp() { 51 | return timestamp; 52 | } 53 | 54 | public long getProof() { 55 | return proof; 56 | } 57 | 58 | public List getTransactions() { 59 | return transactions; 60 | } 61 | 62 | public String getPreviousBlockHash() { 63 | return previousBlockHash; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /reactive-java-chain/src/main/java/ac/simons/reactive/chains/Chain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | import static java.util.stream.Collectors.toList; 19 | 20 | import java.math.BigInteger; 21 | import java.security.MessageDigest; 22 | import java.security.NoSuchAlgorithmException; 23 | import java.time.Clock; 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.Comparator; 27 | import java.util.List; 28 | import java.util.Optional; 29 | import java.util.Queue; 30 | import java.util.UUID; 31 | import java.util.concurrent.ConcurrentLinkedQueue; 32 | import java.util.concurrent.PriorityBlockingQueue; 33 | import java.util.function.Consumer; 34 | import java.util.function.Function; 35 | import java.util.function.Supplier; 36 | import java.util.stream.Stream; 37 | 38 | import com.fasterxml.jackson.core.JsonProcessingException; 39 | import com.fasterxml.jackson.databind.ObjectMapper; 40 | import io.micrometer.core.instrument.Counter; 41 | import io.micrometer.core.instrument.Metrics; 42 | import io.micrometer.core.instrument.Timer; 43 | import reactor.core.publisher.Flux; 44 | import reactor.core.publisher.Mono; 45 | import reactor.core.scheduler.Schedulers; 46 | 47 | public class Chain { 48 | 49 | /** 50 | * A supplier containing the genesis block. 51 | */ 52 | static Supplier DEFAULT_GENESIS_BLOCK = () -> new Block(1, 0, 1917336, List.of(new Transaction("b3c973e2-db05-4eb5-9668-3e81c7389a6d", 0, "I am Heribert Innoq")), "0"); 53 | 54 | /** 55 | * Digester wrapping a MessageDigest, which is not thread safe. 56 | */ 57 | private static final Function DIGEST = bytes -> { 58 | try { 59 | return MessageDigest.getInstance("SHA-256").digest(bytes); 60 | } catch (NoSuchAlgorithmException e) { 61 | throw new RuntimeException(e); 62 | } 63 | }; 64 | 65 | /** 66 | * Creates a base64 string from a byte array. 67 | */ 68 | private static final Function ENCODE = HashUtils.ENCODE_WITH_GUAVA_ALGORITHM; 69 | 70 | /** 71 | * A function to generate JSON from a block. 72 | */ 73 | private final Function blockToJson; 74 | 75 | /** 76 | * Can be injected to compute blocks in different timezones. 77 | */ 78 | private final Clock clock = Clock.systemUTC(); 79 | 80 | private final Queue> pendingBlocks = new ConcurrentLinkedQueue<>(); 81 | 82 | /** 83 | * The actual chain. 84 | */ 85 | private final List blocks = Collections.synchronizedList(new ArrayList<>()); 86 | 87 | /** 88 | * A queue with pending transactions. 89 | */ 90 | private final Queue pendingTransactions = 91 | new PriorityBlockingQueue<>(64, Comparator.comparingLong(Transaction::getTimestamp)); 92 | 93 | /** 94 | * A meter timing the computation of hashes. 95 | */ 96 | private final Timer hashTimer = Metrics.timer("chain.hashes"); 97 | 98 | public static Chain defaultChain() { 99 | final ObjectMapper objectMapper = new ObjectMapper(); 100 | return new Chain(DEFAULT_GENESIS_BLOCK.get(), block -> { 101 | try { 102 | return objectMapper.writeValueAsBytes(block); 103 | } catch (JsonProcessingException e) { 104 | throw new RuntimeException(e); 105 | } 106 | }); 107 | } 108 | 109 | private Chain(Block genesisBlock, final Function blockToJson) { 110 | this.blocks.add(genesisBlock); 111 | this.blockToJson = blockToJson; 112 | } 113 | 114 | public int getLength() { 115 | return this.blocks.size(); 116 | } 117 | 118 | public int getNumberOfPendingTransactions() { 119 | return this.pendingTransactions.size(); 120 | } 121 | 122 | public Mono queue(final String payload) { 123 | return Mono.fromSupplier(() -> { 124 | var pendingTransaction = new Transaction(UUID.randomUUID().toString(), clock.millis(), payload); 125 | pendingTransactions.add(pendingTransaction); 126 | return pendingTransaction; 127 | }); 128 | } 129 | 130 | public Mono mine() { 131 | final Function toTemplate = previousBlock -> 132 | new Block(previousBlock.getIndex() + 1, clock.millis(), -1, selectTransactions(5), hash(previousBlock)); 133 | 134 | final Function> toNextBlock = template -> Flux.fromStream(Stream.iterate(0L, i -> i + 1)) 135 | .parallel().runOn(Schedulers.parallel()) 136 | .map(proof -> template.newCandidateOf(proof)) 137 | .filter(newCandidate -> hash(newCandidate).startsWith("000000")) 138 | .sequential() 139 | .next(); 140 | 141 | final Supplier> latestBlock = () -> Mono.just(blocks.get(this.blocks.size() - 1)); 142 | 143 | synchronized (pendingBlocks) { 144 | // Check first if there's a pending block, otherwise use the latest 145 | var miner = Optional.ofNullable(pendingBlocks.poll()).orElseGet(latestBlock) 146 | .map(toTemplate) 147 | .flatMap(toNextBlock) 148 | .doOnSuccess(blocks::add) 149 | // This is paramount. The mono gets replayed on each subscription 150 | .cache(); 151 | // Add it to the pending blocks in any case. 152 | pendingBlocks.add(miner); 153 | return miner; 154 | } 155 | } 156 | 157 | public Mono> getBlocks() { 158 | return Mono.just(Collections.unmodifiableList(this.blocks)); 159 | } 160 | 161 | List selectTransactions(final int maxNumberOfTransactions) { 162 | return Stream.iterate(1, i -> i + 1) 163 | .limit(maxNumberOfTransactions) 164 | .map(i -> pendingTransactions.poll()) 165 | .takeWhile(t -> t != null).collect(toList()); 166 | } 167 | 168 | String hash(final Block block) { 169 | return hashTimer.record(() -> 170 | blockToJson 171 | .andThen(DIGEST) 172 | .andThen(ENCODE) 173 | .apply(block)); 174 | } 175 | } -------------------------------------------------------------------------------- /reactive-java-chain/src/main/java/ac/simons/reactive/chains/HashUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | import java.math.BigInteger; 19 | import java.util.function.Function; 20 | 21 | public final class HashUtils { 22 | private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); 23 | 24 | static final Function ENCODE_BY_FORMAT = bytes -> String.format("%064x", new BigInteger(1, bytes)); 25 | 26 | static final Function ENCODE_WITH_GUAVA_ALGORITHM = bytes -> { 27 | final StringBuilder sb = new StringBuilder(2 * bytes.length); 28 | for (byte b : bytes) { 29 | sb.append(HEX_DIGITS[(b >> 4) & 0xf]).append(HEX_DIGITS[b & 0xf]); 30 | } 31 | return sb.toString(); 32 | }; 33 | 34 | static final Function ENCODE_WITH_TO_HEX_STRING = bytes -> { 35 | final StringBuilder rv = new StringBuilder(); 36 | for (byte b : bytes) { 37 | rv.append(Integer.toHexString((b & 0xff) + 0x100).substring(1)); 38 | } 39 | return rv.toString(); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /reactive-java-chain/src/main/java/ac/simons/reactive/chains/Transaction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | /** 19 | * An arbitrary, untyped transaction which can be contained in the block of a chain. 20 | */ 21 | public class Transaction { 22 | private final String id; 23 | 24 | private final long timestamp; 25 | 26 | private final String payload; 27 | 28 | public Transaction(String id, long timestamp, String payload) { 29 | this.id = id; 30 | this.timestamp = timestamp; 31 | this.payload = payload; 32 | } 33 | 34 | public String getId() { 35 | return id; 36 | } 37 | 38 | public long getTimestamp() { 39 | return timestamp; 40 | } 41 | 42 | public String getPayload() { 43 | return payload; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /reactive-java-chain/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include = * 2 | 3 | spring.application.name = reactive-java-chain 4 | -------------------------------------------------------------------------------- /reactive-java-chain/src/test/java/ac/simons/reactive/chains/ChainTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import java.util.stream.Collectors; 21 | import java.util.stream.Stream; 22 | 23 | import org.junit.jupiter.api.Test; 24 | 25 | import reactor.core.publisher.Flux; 26 | 27 | public class ChainTest { 28 | @Test 29 | public void hashingShouldWork() { 30 | var chain = Chain.defaultChain(); 31 | var genesisBlock = Chain.DEFAULT_GENESIS_BLOCK.get(); 32 | 33 | assertThat(chain.hash(genesisBlock)) 34 | .isEqualTo("000000b642b67d8bea7cffed1ec990719a3f7837de5ef0f8ede36537e91cdc0e"); 35 | } 36 | 37 | @Test 38 | public void selectTransactionsShouldWork() { 39 | var chain = Chain.defaultChain(); 40 | 41 | Flux.concat( 42 | Stream.of("a", "b", "c").map(chain::queue).collect(Collectors.toList()) 43 | ).collectList().subscribe(allTransactions -> { 44 | assertThat(chain.selectTransactions(2)).hasSize(2); 45 | assertThat(chain.selectTransactions(2)).hasSize(1); 46 | assertThat(chain.selectTransactions(2)).hasSize(0); 47 | }); 48 | } 49 | } -------------------------------------------------------------------------------- /reactive-java-chain/src/test/java/ac/simons/reactive/chains/EncodeBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains; 17 | 18 | import static java.nio.charset.StandardCharsets.UTF_8; 19 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 20 | import static org.openjdk.jmh.annotations.Mode.AverageTime; 21 | import static org.openjdk.jmh.annotations.Mode.Throughput; 22 | 23 | import org.openjdk.jmh.annotations.Benchmark; 24 | import org.openjdk.jmh.annotations.BenchmarkMode; 25 | import org.openjdk.jmh.annotations.Fork; 26 | import org.openjdk.jmh.annotations.OutputTimeUnit; 27 | import org.openjdk.jmh.annotations.Scope; 28 | import org.openjdk.jmh.annotations.State; 29 | import org.openjdk.jmh.runner.Runner; 30 | import org.openjdk.jmh.runner.options.Options; 31 | import org.openjdk.jmh.runner.options.OptionsBuilder; 32 | 33 | /** 34 | * For interpreting the result, read Performance measurement with JMH – Java Microbenchmark Harness 35 | * by Kevin. 36 | * 37 | *
"Mode.AverageTime a lower score is preferred, while using Mode.Throughput a higher value points to better performance."
38 | */ 39 | @BenchmarkMode(Throughput) 40 | @OutputTimeUnit(NANOSECONDS) 41 | @Fork(3) 42 | public class EncodeBenchmark { 43 | public static void main(String[] args) throws Exception { 44 | final Options opt = new OptionsBuilder() 45 | .include(EncodeBenchmark.class.getSimpleName()) 46 | .build(); 47 | new Runner(opt).run(); 48 | } 49 | 50 | @State(Scope.Benchmark) 51 | public static class BytesState { 52 | public byte[] value = "000000b642b67d8bea7cffed1ec990719a3f7837de5ef0f8ede36537e91cdc0e".getBytes(UTF_8); 53 | } 54 | 55 | @Benchmark 56 | public String encodeByFormat(BytesState state) { 57 | return HashUtils.ENCODE_BY_FORMAT.apply(state.value); 58 | } 59 | 60 | @Benchmark 61 | public String encodeWithGuavaAlgorith(BytesState state) { 62 | return HashUtils.ENCODE_WITH_GUAVA_ALGORITHM.apply(state.value); 63 | } 64 | 65 | @Benchmark 66 | public String encodeWithToHexString(BytesState state) { 67 | return HashUtils.ENCODE_WITH_TO_HEX_STRING.apply(state.value); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | /out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /build/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ -------------------------------------------------------------------------------- /reactive-kotlin-chain/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:10.0.1-jre-slim 2 | COPY build/libs/reactive-kotlin-chain-0.0.1-SNAPSHOT.jar /usr/local/reactive-chains/reactive-kotlin-chain.jar 3 | WORKDIR /usr/local/reactive-chains 4 | EXPOSE 8090 5 | CMD ["java", "-jar", "reactive-kotlin-chain.jar"] -------------------------------------------------------------------------------- /reactive-kotlin-chain/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.2.61' 4 | springBootVersion = '2.0.4.RELEASE' 5 | } 6 | repositories { 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 12 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 13 | } 14 | } 15 | 16 | apply plugin: 'kotlin' 17 | apply plugin: 'kotlin-spring' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'idea' 20 | apply plugin: 'org.springframework.boot' 21 | apply plugin: 'io.spring.dependency-management' 22 | 23 | group = 'ac.simons' 24 | version = '0.0.1-SNAPSHOT' 25 | sourceCompatibility = 1.8 26 | compileKotlin { 27 | kotlinOptions { 28 | freeCompilerArgs = ["-Xjsr305=strict"] 29 | jvmTarget = "1.8" 30 | } 31 | } 32 | compileTestKotlin { 33 | kotlinOptions { 34 | freeCompilerArgs = ["-Xjsr305=strict"] 35 | jvmTarget = "1.8" 36 | } 37 | } 38 | 39 | test { 40 | useJUnitPlatform() 41 | } 42 | 43 | idea { 44 | module { 45 | outputDir file('build/classes/main') 46 | testOutputDir file('build/classes/test') 47 | } 48 | } 49 | 50 | repositories { 51 | mavenCentral() 52 | } 53 | 54 | dependencies { 55 | compile('org.springframework.boot:spring-boot-starter-actuator') 56 | compile('org.springframework.boot:spring-boot-starter-webflux') 57 | compile('io.micrometer:micrometer-registry-prometheus') 58 | compile('com.fasterxml.jackson.module:jackson-module-kotlin') 59 | compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 60 | compile("org.jetbrains.kotlin:kotlin-reflect") 61 | 62 | compileOnly('org.springframework.boot:spring-boot-configuration-processor') 63 | 64 | testCompile('org.springframework.boot:spring-boot-starter-test') { 65 | exclude module: 'junit' 66 | } 67 | testImplementation('org.junit.jupiter:junit-jupiter-api') 68 | testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine') 69 | testCompile('io.projectreactor:reactor-test') 70 | } 71 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-simons/blockchain-playground/b9897dfc14f060fece173f49b1da358b50276f6c/reactive-kotlin-chain/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /reactive-kotlin-chain/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 25 12:11:42 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip 7 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'reactive-kotlin-chain' 2 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/Application.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import io.micrometer.core.instrument.FunctionCounter 19 | import io.micrometer.core.instrument.Gauge 20 | import io.micrometer.core.instrument.MeterRegistry 21 | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer 22 | import org.springframework.boot.autoconfigure.SpringBootApplication 23 | import org.springframework.boot.autoconfigure.web.ServerProperties 24 | import org.springframework.boot.context.event.ApplicationReadyEvent 25 | import org.springframework.boot.runApplication 26 | import org.springframework.boot.web.codec.CodecCustomizer 27 | import org.springframework.context.ApplicationListener 28 | import org.springframework.context.support.beans 29 | import org.springframework.core.env.Environment 30 | import org.springframework.http.HttpStatus.CREATED 31 | import org.springframework.http.MediaType.APPLICATION_STREAM_JSON 32 | import org.springframework.http.codec.json.Jackson2JsonDecoder 33 | import org.springframework.web.reactive.function.server.ServerResponse.* 34 | import org.springframework.web.reactive.function.server.body 35 | import org.springframework.web.reactive.function.server.bodyToMono 36 | import org.springframework.web.reactive.function.server.router 37 | import org.springframework.web.util.UriComponentsBuilder 38 | import reactor.core.publisher.Flux 39 | import reactor.core.publisher.Mono 40 | import reactor.core.scheduler.Schedulers 41 | import java.net.DatagramPacket 42 | import java.net.InetAddress 43 | import java.time.Duration 44 | import java.util.function.ToDoubleFunction 45 | import java.util.logging.Logger 46 | 47 | @SpringBootApplication 48 | class Application 49 | 50 | fun beans() = beans { 51 | 52 | // Customize metrics by adding the applications name 53 | bean { 54 | MeterRegistryCustomizer { registry -> 55 | registry.config().commonTags("application", ref().getProperty("spring.application.name", "unknown")) 56 | } 57 | } 58 | 59 | // Some JSON customization 60 | bean() 61 | bean { 62 | CodecCustomizer { customizer -> 63 | customizer.customCodecs().decoder(Jackson2JsonDecoder(ref())) 64 | } 65 | } 66 | 67 | bean("nodeRegistery") { NodeRegistry(ref()) } 68 | 69 | bean("chain") { 70 | Chain().also { 71 | val meterRegistry = ref() 72 | 73 | FunctionCounter.builder("chain.blocks.computed", it, { it.getLength().toDouble() }) 74 | .baseUnit("block") 75 | .register(meterRegistry) 76 | 77 | Gauge.builder("chain.transactions.pending", it, { it.getNumberOfPendingTransactions().toDouble() }) 78 | .baseUnit("transaction") 79 | .register(meterRegistry) 80 | } 81 | } 82 | 83 | bean("eventPublisher") { EventPublisher() } 84 | 85 | bean { 86 | ApplicationListener { 87 | val logger = Logger.getLogger(Application::class.qualifiedName!!) 88 | 89 | val nodeRegistry = ref() 90 | 91 | // Listen for events on other nodes 92 | ref().events().filter { it is NewNodeEvent } 93 | .flatMap { 94 | nodeRegistry.listenTo((it as NewNodeEvent).data) 95 | } 96 | .subscribe { 97 | when (it) { 98 | is NewBlockEvent -> logger.fine { "New block on other node" } 99 | is NewTransactionEvent -> ref().queue(it.data) 100 | is NewNodeEvent -> logger.fine { "New node on other node "} 101 | } 102 | } 103 | 104 | val serverProperties = ref() 105 | val address = serverProperties.address ?: InetAddress.getLocalHost() 106 | val socket = openMulticastSocket(address) 107 | 108 | with(ref()) { 109 | // Publish local port and address of this instance on the network and also 110 | Flux.interval(Duration.ofSeconds(10)) 111 | .map { createSSDPAlivePacket(address, serverProperties.port) } 112 | .subscribe { socket.send(it) } 113 | 114 | // register with nodes that are announced against this node 115 | Flux.generate { emitter -> 116 | val dp = DatagramPacket(ByteArray(8192), 8129) 117 | socket.receive(dp) 118 | emitter.next(dp) 119 | }.subscribeOn(Schedulers.elastic()) 120 | .flatMap { readSSDPAlivePacket(it) } 121 | .filter { it.isNotBlank() } 122 | .flatMap { nodeRegistry.register(it) } 123 | .doOnNext{ ref().publish(it) } 124 | .subscribe { logger.info { "Registered new node ${it}" } } 125 | } 126 | } 127 | } 128 | 129 | // Routing and control flow 130 | bean { 131 | val eventPublisher = ref() 132 | 133 | router { 134 | with(ref()) { 135 | GET("/", { ok().body(getStatus()) }) 136 | GET("/mine", { 137 | status(CREATED).body(mine().doOnNext(eventPublisher::publish)) 138 | }) 139 | POST("/transactions", { request -> 140 | request.bodyToMono() 141 | .flatMap { queue(it) } 142 | .doOnNext(eventPublisher::publish) 143 | .flatMap { 144 | created(UriComponentsBuilder.fromUri(request.uri()) 145 | .pathSegment("{id}") 146 | .buildAndExpand(mapOf("id" to it.id)).encode().toUri() 147 | ).body(Mono.just(it)) 148 | } 149 | }) 150 | GET("/blocks", { 151 | ok().body(getBlocks().map { mapOf("blocks" to it, "blockHeight" to it.size) }) 152 | }) 153 | } 154 | 155 | with(ref()) { 156 | POST("/nodes/register", { request -> 157 | request.bodyToMono() 158 | .flatMap { register(it) } 159 | .doOnNext(eventPublisher::publish) 160 | .flatMap { ok().body(Mono.just(it)) } 161 | }) 162 | } 163 | 164 | with(eventPublisher) { 165 | GET("/events", { 166 | ok().contentType(APPLICATION_STREAM_JSON).body(events()) 167 | }) 168 | } 169 | } 170 | } 171 | } 172 | 173 | fun main(args: Array) { 174 | runApplication(*args) { 175 | addInitializers(beans()) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/Chain.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper 19 | import io.micrometer.core.instrument.Metrics 20 | import reactor.core.publisher.Mono 21 | import reactor.core.publisher.toFlux 22 | import reactor.core.scheduler.Schedulers 23 | import java.time.Clock 24 | import java.util.Queue 25 | import java.util.UUID 26 | import java.util.concurrent.ConcurrentLinkedQueue 27 | import java.util.concurrent.PriorityBlockingQueue 28 | import java.util.function.Supplier 29 | import java.util.logging.Logger 30 | 31 | /** 32 | * A transaction with a payload. 33 | */ 34 | data class Transaction(val id: String, val timestamp: Long, val payload: String) 35 | 36 | /** 37 | * A block inside the chain having a list of transactions. 38 | */ 39 | data class Block( 40 | val index: Long, 41 | val timestamp: Long, 42 | val proof: Long, 43 | val transactions: List, 44 | val previousBlockHash: String 45 | ) 46 | 47 | /** 48 | * This chains status 49 | */ 50 | data class Status(val nodeId: String, val currentBlockHeight: Int) 51 | 52 | /** 53 | * The genesis block supplier. 54 | */ 55 | fun genesisBlock() = 56 | Block(1, 0, 1917336, listOf(Transaction("b3c973e2-db05-4eb5-9668-3e81c7389a6d", 0, "I am Heribert Innoq")), "0") 57 | 58 | class Chain( 59 | genesisBlock: Block = genesisBlock(), 60 | private val clock: Clock = Clock.systemUTC(), 61 | private val blockToJson: (block: Block) -> ByteArray 62 | ) { 63 | 64 | constructor() : this(blockToJson = { 65 | objectMapper.writeValueAsBytes(it) 66 | }) 67 | 68 | companion object { 69 | val objectMapper = ObjectMapper() 70 | val log = Logger.getLogger(Chain::class.qualifiedName!!) 71 | } 72 | 73 | /** 74 | * This chains id. 75 | */ 76 | val nodeId = UUID.randomUUID().toString() 77 | 78 | /** 79 | * Blocks that are currently being mined. 80 | */ 81 | private val pendingBlocks = ConcurrentLinkedQueue>() 82 | 83 | /** 84 | * The actual chain. 85 | */ 86 | private val blocks = mutableListOf(genesisBlock) 87 | 88 | private val pendingTransactions: Queue = PriorityBlockingQueue(64, compareBy { it.timestamp }) 89 | 90 | private val hashTimer = Metrics.timer("chain.hashes") 91 | 92 | fun getLength() = this.blocks.size 93 | 94 | fun getNumberOfPendingTransactions() = this.pendingTransactions.size 95 | 96 | fun queue(payload: String) = Mono.fromSupplier { 97 | val pendingTransaction = Transaction(UUID.randomUUID().toString(), clock.millis(), payload) 98 | pendingTransactions.add(pendingTransaction) 99 | pendingTransaction 100 | } 101 | 102 | fun queue(transaction: Transaction) { 103 | if(!hasTransaction(transaction)) { 104 | pendingTransactions += transaction 105 | log.info { "Added transaction " + transaction.id } 106 | } 107 | } 108 | 109 | fun mine(): Mono { 110 | val toTemplate = { it: Block -> 111 | it.copy( 112 | index = it.index + 1, 113 | timestamp = clock.millis(), 114 | transactions = selectTransactions(5), 115 | previousBlockHash = hash(it) 116 | ) 117 | } 118 | 119 | val toNextBlock = { template: Block -> 120 | generateSequence(0L) { it + 1 }.toFlux() 121 | .parallel().runOn(Schedulers.parallel()) 122 | .map { newProof -> template.copy(proof = newProof) } 123 | .filter { hash(it).startsWith("000000") } 124 | .sequential() 125 | .next() 126 | } 127 | 128 | synchronized(pendingBlocks) { 129 | // Check first if there's a pending block, otherwise use the latest 130 | val miner = (pendingBlocks.poll() ?: Mono.just(blocks.last())) 131 | .map(toTemplate) 132 | .flatMap(toNextBlock) 133 | .doOnSuccess {blocks += it} 134 | // This is paramount. The mono gets replayed on each subscription 135 | .cache() 136 | // Add it to the pending blocks in any case. 137 | pendingBlocks.add(miner) 138 | return miner 139 | } 140 | } 141 | 142 | fun getBlocks() = Mono.just(this.blocks.toList()) 143 | 144 | fun getStatus() = Mono.just(Status(this.nodeId, this.blocks.size)) 145 | 146 | internal fun selectTransactions(maxNumberOfTransactions: Int) = (1..maxNumberOfTransactions) 147 | .map { pendingTransactions.poll() } 148 | .takeWhile { it != null } 149 | .toList() 150 | 151 | internal fun hash(block: Block) = hashTimer.record(Supplier { 152 | blockToJson(block) 153 | .run(::digest) 154 | .run(::encode) 155 | }) 156 | 157 | internal fun hasTransaction(transaction: Transaction) = 158 | this.pendingTransactions.find { it.id == transaction.id } != null 159 | } -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/Events.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import org.springframework.web.reactive.function.client.WebClient 19 | import reactor.core.publisher.EmitterProcessor 20 | import reactor.core.publisher.Flux 21 | import reactor.core.publisher.FluxSink 22 | import reactor.core.publisher.FluxSink.OverflowStrategy 23 | import java.util.concurrent.atomic.AtomicInteger 24 | 25 | /** 26 | * Sealed classes allow only a restricted set of subtypes which must be defined in "this" file. 27 | */ 28 | sealed class Event { 29 | abstract val id: Int 30 | abstract val data: D 31 | } 32 | 33 | data class NewBlockEvent(override val id: Int, override val data: Block) : Event() 34 | 35 | data class NewTransactionEvent(override val id: Int, override val data: Transaction) : Event() 36 | 37 | data class NewNodeEvent(override val id: Int, override val data: Node) : Event() 38 | 39 | class EventPublisher() { 40 | private val idGenerator = AtomicInteger() 41 | private val eventStream = EmitterProcessor.create>(false) 42 | private val eventSink = eventStream.sink(OverflowStrategy.LATEST) 43 | 44 | fun publish(data: Any) { 45 | val nextId = idGenerator.incrementAndGet() 46 | val event = when(data) { 47 | is Block -> NewBlockEvent(nextId, data) 48 | is Transaction -> NewTransactionEvent(nextId, data) 49 | is Node -> NewNodeEvent(nextId, data) 50 | else -> throw IllegalArgumentException() 51 | } 52 | eventSink.next(event) 53 | } 54 | 55 | fun events(): Flux> = eventStream 56 | } -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/Hashing.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import java.security.MessageDigest 19 | 20 | /** 21 | * Digests a byte array. MessageDigest is not thread safe, so just get a new one. 22 | */ 23 | fun digest(bytes: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(bytes) 24 | 25 | private val HEX_DIGITS = "0123456789abcdef".toCharArray() 26 | 27 | /** 28 | * Encodes a byte array as hex. 29 | */ 30 | fun encode(bytes: ByteArray): String = bytes.fold("", {str, b -> str + HEX_DIGITS[ (b.toInt() shr 4) and 0xf] + HEX_DIGITS[b.toInt() and 0xf]}) -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/JacksonModules.kt: -------------------------------------------------------------------------------- 1 | package ac.simons.reactive.chains 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.fasterxml.jackson.annotation.JsonSubTypes 5 | import com.fasterxml.jackson.annotation.JsonSubTypes.Type 6 | import com.fasterxml.jackson.annotation.JsonTypeInfo 7 | import com.fasterxml.jackson.databind.module.SimpleModule 8 | 9 | class EventModule() : SimpleModule() { 10 | init { 11 | setMixInAnnotation(Event::class.java, EventMixIn::class.java) 12 | } 13 | 14 | @JsonTypeInfo( 15 | use = JsonTypeInfo.Id.NAME, 16 | include = JsonTypeInfo.As.PROPERTY, 17 | property = "event", 18 | visible = true 19 | ) 20 | @JsonSubTypes(value = [ 21 | Type(value = NewBlockEvent::class, name = "new_block"), 22 | Type(value = NewTransactionEvent::class, name = "new_transaction"), 23 | Type(value = NewNodeEvent::class, name = "new_node") 24 | ]) 25 | @JsonIgnoreProperties(ignoreUnknown = true) 26 | class EventMixIn 27 | } -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/NodeRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import org.springframework.web.reactive.function.client.WebClient 19 | import org.springframework.web.reactive.function.client.bodyToFlux 20 | import org.springframework.web.reactive.function.client.bodyToMono 21 | import reactor.core.publisher.Flux 22 | import reactor.core.publisher.Mono 23 | import java.util.logging.Logger 24 | 25 | /** 26 | * Representation of a node. 27 | */ 28 | data class Node(val id: String, val host: String) 29 | 30 | /** 31 | * Used for retrieving events from a node 32 | */ 33 | class NodeClient(base: WebClient) : WebClient by base { 34 | var active = false 35 | 36 | fun retrieveEvents() : Flux> { 37 | active = true 38 | return get().uri("/events").retrieve().bodyToFlux>() 39 | } 40 | } 41 | 42 | class NodeRegistry( 43 | private val webClientBuilder: WebClient.Builder = WebClient.builder() 44 | ) { 45 | private val nodes = mutableSetOf() 46 | 47 | private val nodeClients = mutableMapOf() 48 | 49 | companion object { 50 | val log = Logger.getLogger(NodeRegistry::class.qualifiedName!!) 51 | } 52 | 53 | /** 54 | * Registers the given host to the registry and creates a webclient for it. 55 | */ 56 | fun register(host: String) = if (nodes.find { it.host == host } != null) Mono.empty() else 57 | Mono.just(webClientBuilder.baseUrl(host).build()) 58 | .flatMap { client -> 59 | client.get().retrieve().bodyToMono() 60 | .map { Node(it.nodeId, host) } 61 | .doOnSuccess { 62 | nodes += it 63 | nodeClients += it.id to NodeClient(client) 64 | } 65 | }.doOnError { Mono.empty() } 66 | 67 | fun listenTo(node: Node) = nodeClients[node.id] 68 | ?.takeUnless { it.active } 69 | ?.let { it.retrieveEvents().doOnError { unregister(node) } } ?: Flux.empty() 70 | 71 | internal fun unregister(node: Node): Flux { 72 | nodeClients -= node.id 73 | nodes -= node 74 | log.info{ "Removed node ${node}" } 75 | return Flux.empty() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/kotlin/ac/simons/reactive/chains/SSDP.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import reactor.core.publisher.Mono 19 | import java.net.* 20 | import java.time.Duration 21 | 22 | private const val NT = "urn:blockchain-playground:node" 23 | 24 | private val SSDP_MULTICAST_ADDRESS = InetAddress.getByName("239.255.255.250") 25 | private const val SSDP_PORT = 1900 26 | 27 | fun openMulticastSocket(address: InetAddress): MulticastSocket { 28 | val socket = MulticastSocket(SSDP_PORT) 29 | socket.reuseAddress = true 30 | socket.soTimeout = Duration.ofMinutes(1).toMillis().toInt() 31 | socket.joinGroup(InetSocketAddress(SSDP_MULTICAST_ADDRESS, SSDP_PORT), NetworkInterface.getByInetAddress(address)) 32 | return socket 33 | } 34 | 35 | private fun Chain.usn() = "uuid:${nodeId}::${NT}" 36 | 37 | fun Chain.createSSDPAlivePacket(address: InetAddress, port: Int) = """ 38 | NOTIFY * HTTP/1.1 39 | LOCATION: http://${address.hostAddress}:${port} 40 | NTS: ssdp:alive 41 | NT: ${NT} 42 | USN: ${usn()} 43 | HOST: 239.255.255.250:1900 44 | 45 | """.trimIndent().toByteArray().let { DatagramPacket(it, it.size, SSDP_MULTICAST_ADDRESS, SSDP_PORT) } 46 | 47 | fun Chain.readSSDPAlivePacket(dp: DatagramPacket): Mono { 48 | val ssdpRequest = String(dp.data) 49 | .lines().drop(1) 50 | .map { it.split(": ") } 51 | .filter { it.size == 2 } 52 | .associateBy({ it[0] }, { it[1] }) 53 | return if (ssdpRequest["NT"].equals(NT, true) && ssdpRequest.containsKey("LOCATION") && ssdpRequest["USN"] != usn()) { 54 | Mono.just(ssdpRequest["LOCATION"]!!) 55 | } else Mono.empty() 56 | } -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include = * 2 | 3 | spring.application.name = reactive-kotlin-chain 4 | 5 | server.port = 8090 6 | 7 | logging.level.ac.simons.reactive.chains.Application = debug 8 | -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/test/kotlin/ac/simons/reactive/chains/ChainTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.junit.jupiter.api.Test 20 | import reactor.core.publisher.Flux 21 | import reactor.core.publisher.Mono 22 | 23 | class ChainTest { 24 | @Test 25 | fun `hashing should work`() { 26 | val chain = Chain() 27 | val genesisBlock = genesisBlock() 28 | 29 | assertThat(chain.hash(genesisBlock)) 30 | .isEqualTo("000000b642b67d8bea7cffed1ec990719a3f7837de5ef0f8ede36537e91cdc0e") 31 | } 32 | 33 | @Test 34 | fun `select transactions should work`() { 35 | with(Chain()) { 36 | Flux.concat> { 37 | queue("a") 38 | queue("b") 39 | queue("c") 40 | }.collectList().subscribe { 41 | (2 downTo 0).forEach { 42 | assertThat(selectTransactions(2)).hasSize(it) 43 | } 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/test/kotlin/ac/simons/reactive/chains/IntegrationTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import org.junit.jupiter.api.Test 19 | import org.junit.jupiter.api.extension.ExtendWith 20 | import org.springframework.beans.factory.annotation.Autowired 21 | import org.springframework.boot.test.context.SpringBootTest 22 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT 23 | import org.springframework.context.ApplicationContextInitializer 24 | import org.springframework.context.support.GenericApplicationContext 25 | import org.springframework.http.MediaType 26 | import org.springframework.test.context.TestPropertySource 27 | import org.springframework.test.context.junit.jupiter.SpringExtension 28 | import org.springframework.test.web.reactive.server.WebTestClient 29 | 30 | @ExtendWith(SpringExtension::class) 31 | @SpringBootTest(webEnvironment = RANDOM_PORT) 32 | @TestPropertySource(properties = ["context.initializer.classes = ac.simons.reactive.chains.BeansInitializer"]) 33 | class IntegrationTests ( 34 | @Autowired val webClient: WebTestClient 35 | ) { 36 | @Test 37 | fun `Root should return node id and block height`() { 38 | this.webClient.get().uri("/").accept(MediaType.APPLICATION_JSON) 39 | .exchange() 40 | .expectStatus().isOk() 41 | .expectBody() 42 | .jsonPath("$.nodeId").isNotEmpty 43 | .jsonPath("$.currentBlockHeight").isNumber 44 | 45 | } 46 | } 47 | 48 | /** 49 | * Needed to make the nice functional bean definition work with @SpringBootTest. Compare to the ideas 50 | * discussed here: #8115. 51 | */ 52 | internal class BeansInitializer : ApplicationContextInitializer { 53 | override fun initialize(context: GenericApplicationContext) = 54 | beans().initialize(context) 55 | } -------------------------------------------------------------------------------- /reactive-kotlin-chain/src/test/kotlin/ac/simons/reactive/chains/JacksonModulesTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.reactive.chains 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper 19 | import com.fasterxml.jackson.module.kotlin.KotlinModule 20 | import com.fasterxml.jackson.module.kotlin.readValue 21 | import org.assertj.core.api.Assertions.assertThat 22 | import org.junit.jupiter.api.DisplayName 23 | import org.junit.jupiter.api.Nested 24 | import org.junit.jupiter.api.Test 25 | import org.junit.jupiter.api.fail 26 | import org.slf4j.LoggerFactory 27 | 28 | @DisplayName("Jackson modules") 29 | class JacksonModulesTests { 30 | 31 | private val log = LoggerFactory.getLogger(JacksonModulesTests::class.java) 32 | 33 | private val objectMapper = ObjectMapper() 34 | 35 | init { 36 | objectMapper.registerModules(KotlinModule(), EventModule()) 37 | } 38 | 39 | @Nested 40 | @DisplayName("Event") 41 | inner class EventModuleTest { 42 | @Test 43 | fun `Should serialize and deserialize to and from correct event type`() { 44 | val sourceEvent = NewBlockEvent(1, genesisBlock()) 45 | val serializedEvent = objectMapper.writeValueAsString(sourceEvent) 46 | 47 | log.debug("Serialized event to '{}'", serializedEvent) 48 | 49 | val deserializedEvent : Event<*> = objectMapper.readValue(serializedEvent) 50 | 51 | when(deserializedEvent) { 52 | is NewBlockEvent -> assertThat(deserializedEvent).isEqualTo(sourceEvent) 53 | else -> fail { "Unexpected event type: " + deserializedEvent::class } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /springio2018/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | /out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /build/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ -------------------------------------------------------------------------------- /springio2018/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.4.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | plugins { 14 | id "org.asciidoctor.convert" version "1.5.6" 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'idea' 20 | apply plugin: 'org.springframework.boot' 21 | apply plugin: 'io.spring.dependency-management' 22 | 23 | group = 'ac.simons.springio2018' 24 | version = '0.0.1-SNAPSHOT' 25 | sourceCompatibility = 10 26 | 27 | idea { 28 | module { 29 | outputDir file('build/classes/main') 30 | testOutputDir file('build/classes/test') 31 | } 32 | } 33 | 34 | repositories { 35 | mavenCentral() 36 | } 37 | 38 | dependencies { 39 | compile('io.micrometer:micrometer-core') 40 | compile 'io.dropwizard.metrics:metrics-core:4.0.3' 41 | } 42 | 43 | import org.asciidoctor.gradle.AsciidoctorTask 44 | 45 | tasks.withType(AsciidoctorTask) { docTask -> 46 | attributes 'sourceDir': "$projectDir/src/" 47 | 48 | sourceDir = new File("src/talk") 49 | outputDir = new File(project.buildDir, "talk") 50 | resources { 51 | from(sourceDir) { 52 | include '*.png' 53 | } 54 | } 55 | } 56 | 57 | task generateHTML (type: AsciidoctorTask) { 58 | backends = ['html5'] 59 | } 60 | 61 | defaultTasks "generateHTML" 62 | -------------------------------------------------------------------------------- /springio2018/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-simons/blockchain-playground/b9897dfc14f060fece173f49b1da358b50276f6c/springio2018/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /springio2018/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 14 15:42:05 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip 7 | -------------------------------------------------------------------------------- /springio2018/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /springio2018/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /springio2018/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | -------------------------------------------------------------------------------- /springio2018/src/main/java/ac/simons/springio2018/Demo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 michael-simons.eu. 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 | * http://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 | package ac.simons.springio2018; 17 | 18 | import io.micrometer.core.instrument.Clock; 19 | import io.micrometer.core.instrument.Counter; 20 | import io.micrometer.core.instrument.Gauge; 21 | import io.micrometer.core.instrument.MeterRegistry; 22 | import io.micrometer.core.instrument.Metrics; 23 | import io.micrometer.core.instrument.Timer; 24 | import io.micrometer.core.instrument.dropwizard.DropwizardConfig; 25 | import io.micrometer.core.instrument.dropwizard.DropwizardMeterRegistry; 26 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 27 | import io.micrometer.core.instrument.util.HierarchicalNameMapper; 28 | 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | import java.util.concurrent.ThreadLocalRandom; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.function.Supplier; 34 | import java.util.stream.Stream; 35 | 36 | import com.codahale.metrics.ConsoleReporter; 37 | import com.codahale.metrics.MetricRegistry; 38 | 39 | public class Demo { 40 | public static void main(String... args) { 41 | Metrics.addRegistry(new SimpleMeterRegistry()); 42 | 43 | // We can use Dropwizard Console Reporter and Dropwizard Registries as well 44 | var dropwizardsMetricRegistry = new MetricRegistry(); 45 | Metrics.addRegistry(consoleLoggingRegistry(dropwizardsMetricRegistry)); 46 | var consoleReporter = consoleReporter(dropwizardsMetricRegistry); 47 | 48 | var meterRegistry = Metrics.globalRegistry; 49 | 50 | // Create a counter 51 | var ehemCounter = meterRegistry.counter("presentation.filler.words", "word", "ehem"); 52 | ehemCounter.increment(); 53 | 54 | // Or via the builder 55 | var sooooCounter = Counter.builder("presentation.filler.words") 56 | .tag("word", "soooo…") 57 | .register(meterRegistry); 58 | sooooCounter.increment(2); 59 | 60 | System.out.println(ehemCounter.count()); 61 | System.out.println(sooooCounter.count()); 62 | 63 | // A timer 64 | var slideTimer = Timer.builder("presentation.slide.timer") 65 | .description("This is a timer.") 66 | .tags( 67 | "conference", "Spring I/O", 68 | "place", "Barcelona" 69 | ) 70 | .register(meterRegistry); 71 | 72 | Supplier slideSupplier = () -> { 73 | try { 74 | Thread.sleep((long) (1_000 * ThreadLocalRandom.current().nextDouble())); 75 | } catch (InterruptedException e) { 76 | e.printStackTrace(); 77 | } 78 | return "Next slide"; 79 | }; 80 | 81 | Stream.iterate(1, i -> i <= 3, i -> i + 1).forEach(i -> 82 | System.out.println(slideTimer.record(slideSupplier)) 83 | ); 84 | 85 | System.out.println(slideTimer.max(TimeUnit.SECONDS)); 86 | System.out.println(slideTimer.count()); 87 | 88 | // And last but not least the Gauge 89 | var audience = meterRegistry.gauge("presentation.audience", new ArrayList(), List::size); 90 | 91 | // What is audience? 92 | // It's the actual list 93 | audience.add("Mark"); 94 | audience.add("Oliver"); 95 | 96 | System.out.println(meterRegistry.find("presentation.audience").gauge().value()); 97 | 98 | audience.add("Alice"); 99 | audience.add("Tina"); 100 | audience.remove("Mark"); 101 | 102 | System.out.println(meterRegistry.find("presentation.audience").gauge().value()); 103 | 104 | // One hardly interacts with a gauge direclty, but it's possible 105 | Gauge usedMemory = Gauge.builder("jvm.memory.used", Runtime.getRuntime(), r -> r.totalMemory() - r.freeMemory()) 106 | .baseUnit("bytes") 107 | .tag("host", "chronos") 108 | .tag("region", "my-desk") 109 | .register(meterRegistry); 110 | 111 | System.out.println(usedMemory.value()); 112 | consoleReporter.report(); 113 | } 114 | 115 | static MeterRegistry consoleLoggingRegistry(MetricRegistry dropwizardRegistry) { 116 | DropwizardConfig consoleConfig = new DropwizardConfig() { 117 | 118 | @Override 119 | public String prefix() { 120 | return "console"; 121 | } 122 | 123 | @Override 124 | public String get(String key) { 125 | return null; 126 | } 127 | }; 128 | 129 | return new DropwizardMeterRegistry(consoleConfig, dropwizardRegistry, HierarchicalNameMapper.DEFAULT, 130 | Clock.SYSTEM) { 131 | @Override 132 | protected Double nullGaugeValue() { 133 | return null; 134 | } 135 | }; 136 | } 137 | 138 | static ConsoleReporter consoleReporter(MetricRegistry dropwizardRegistry) { 139 | ConsoleReporter reporter = ConsoleReporter.forRegistry(dropwizardRegistry) 140 | .convertRatesTo(TimeUnit.SECONDS) 141 | .convertDurationsTo(TimeUnit.MILLISECONDS) 142 | .build(); 143 | return reporter; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /springio2018/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-simons/blockchain-playground/b9897dfc14f060fece173f49b1da358b50276f6c/springio2018/src/main/resources/application.properties -------------------------------------------------------------------------------- /springio2018/src/misc/springio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-simons/blockchain-playground/b9897dfc14f060fece173f49b1da358b50276f6c/springio2018/src/misc/springio.png -------------------------------------------------------------------------------- /springio2018/src/talk/micrometer_new-insights-into-your-spring-boot-application.adoc: -------------------------------------------------------------------------------- 1 | = Micrometer: New insights into your Spring Boot application 2 | Michael Simons 3 | :doctype: article 4 | :lang: de 5 | :listing-caption: Listing 6 | :source-highlighter: coderay 7 | :icons: font 8 | :sectlink: true 9 | :sectanchors: true 10 | :xrefstyle: short 11 | :tabsize: 4 12 | :toc: left 13 | :toclevels: 5 14 | 15 | == Michael Simons 16 | 17 | I'm working as a Senior Consultant at INNOQ, Germany. My first Spring project was around 2009, I have been using Spring Boot right from the start. I like writing a lot and I somehow became the author of the bestselling German book about https://www.amazon.de/Spring-Boot-Moderne-Softwareentwicklung-mit/dp/3864905257/[Spring 5 and Spring Boot 2] on Amazon last month. 18 | 19 | I do rant occasionally on https://twitter.com/@rotnroll666[Twitter] about stuff. 20 | 21 | == Ways to observe a system 22 | 23 | Today we'll speak a bit about one of three ways one can observce a system. Among them are 24 | 25 | * Logging 26 | * Tracing 27 | * and metrics 28 | 29 | While logging is usually about events happening in a system and tracing can mean a lot of things, metrics deal with measuring things. 30 | 31 | Usually you trace a request, a call stack or routes throughout your system. While doing so, you can measure the time a single request or a downsampled among of request takes to go through the system. 32 | 33 | Metrics however deals with aggregates of measurements, over system and over time. That means, you can answer the question how long requests took on average in the last minutes throughout your whole system and not how long one single request got stuck in one of your subsystems and that's where Micrometer comes into play. 34 | 35 | == Micrometer 36 | 37 | https://micrometer.io[Micrometer] is a new product or project by Pivotal. It is all about collecting measurements and aggregating them into metrics. Micrometers elevator pitch is "Like SLF4J but for metrics". Now most of us know that SLF4J is a logging facade. You can connect various logging systems to it and use various appenders with it. That's one aspect of Micrometer, too. It's a vendor neutral facade for various monitoring systems and provides an API to collect metrics for you. 38 | 39 | Furthermore, and that's the next thing in common with SLF4: It provides a nice, clean way of retrieving meters to measure things, a gobal factory method, much like all the logging system do. 40 | 41 | == How to get Micrometer into Spring Boot? 42 | 43 | === How to get Micrometer into Spring Boot? 44 | 45 | The only thing you have to do is adding the Spring Boot Actuator Starter into your application like this 46 | 47 | [source,xml] 48 | [[spring-boot-starter-actuator]] 49 | .pom.xml 50 | ---- 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | ---- 56 | 57 | In a good, old Spring Boot 1 application, this gives us the `/metrics` endpoint. 58 | 59 | After putting some load onto the application, for example with Apache Bench `ab -n 12 -c 4 -s 120 http://localhost:8080/mine`, the Metrics-Endpoint answers something like this 60 | 61 | [source,json] 62 | [[metrics-boot-1]] 63 | .Metrics in Spring Boot 1 64 | ---- 65 | { 66 | "mem": 664619, 67 | "mem.free": 327742, 68 | "processors": 8, 69 | "instance.uptime": 602975, 70 | "uptime": 606611, 71 | "systemload.average": 3.19677734375, 72 | "heap.committed": 596480, 73 | "heap.init": 262144, 74 | "heap.used": 268737, 75 | "heap": 3728384, 76 | "nonheap.committed": 69472, 77 | "nonheap.init": 2496, 78 | "nonheap.used": 68142, 79 | "nonheap": 0, 80 | "threads.peak": 70, 81 | "threads.daemon": 27, 82 | "threads.totalStarted": 101, 83 | "threads": 29, 84 | "classes": 9472, 85 | "classes.loaded": 9472, 86 | "classes.unloaded": 0, 87 | "gc.ps_scavenge.count": 7, 88 | "gc.ps_scavenge.time": 74, 89 | "gc.ps_marksweep.count": 2, 90 | "gc.ps_marksweep.time": 66, 91 | "httpsessions.max": -1, 92 | "httpsessions.active": 0, 93 | "datasource.primary.active": 0, 94 | "datasource.primary.usage": 0.0, 95 | "gauge.response.metrics": 2.0, 96 | "gauge.response.motd": 3.0, 97 | "gauge.response.star-star.favicon.ico": 8.0, 98 | "counter.status.200.star-star.favicon.ico": 1, 99 | "counter.status.200.metrics": 4, 100 | "counter.status.200.mine": 1008 101 | } 102 | ---- 103 | 104 | You can drill down one metric with an call like 105 | 106 | [source,bash] 107 | [[drill-down-metrics-boot-1]] 108 | .Drilling down into a metric with Boot 1 109 | ---- 110 | curl http://localhost:8080/metrics/counter.status.200.metrics 111 | ---- 112 | 113 | and get some more detailed information. 114 | 115 | === That changes a lot with Spring Boot 2 116 | 117 | The simple application - which represents a naive blockchain implementation we created at our last INNOQ internal event - can be easily upgrade. Just bump the version from 1.5.x to 2.0.x. 118 | 119 | Your whole application or service along with the management endpoint is now using Micrometer. Let's have a look. 120 | 121 | Now, try the `/metrics` endpoint again and end in a 404 error. 122 | 123 | Spring Boot Actuator has some new concepts in Spring Boot 2 as we might have already heard here in beautiful Barcelona. Endpoints are no longer sensitive or not and there is no explicit security for them in place. 124 | 125 | Instead there's a concept of having them enabled and exposed. All endpoints except for the `/shutdown` endpoint are enabled by default, none but `/health` and `/info` are exposed. 126 | 127 | For this demo we expose all of them with 128 | 129 | [source,properties] 130 | [[expose-all-endpoints]] 131 | .Exposing all Spring Boot 2 management endpoints 132 | ---- 133 | management.endpoints.web.exposure.include = * 134 | ---- 135 | 136 | Furthermore the Management-Endpoints are now all prefixed with `/actuator`, so after we take this into account we can retrieve our metrics with `curl localhost:8080/actuator/metrics`. 137 | 138 | === What happened to the nice format? 😳 139 | 140 | [source,json] 141 | [[metrics-boot-2]] 142 | .Metrics in Spring Boot 2 143 | ---- 144 | { 145 | "names": [ 146 | "jvm.buffer.memory.used", 147 | "jvm.memory.used", 148 | "jvm.gc.memory.allocated", 149 | "jvm.memory.committed", 150 | "jdbc.connections.min", 151 | "tomcat.sessions.created", 152 | "tomcat.sessions.expired", 153 | "hikaricp.connections.usage", 154 | "tomcat.global.request.max", 155 | "tomcat.global.error", 156 | "http.server.requests", 157 | "jvm.gc.max.data.size", 158 | "logback.events", 159 | "system.cpu.count", 160 | "jvm.memory.max", 161 | "jdbc.connections.active", 162 | "jvm.buffer.total.capacity", 163 | "jvm.buffer.count", 164 | "process.files.max", 165 | "jvm.threads.daemon", 166 | "hikaricp.connections", 167 | "process.start.time", 168 | "hikaricp.connections.active", 169 | "tomcat.global.sent", 170 | "hikaricp.connections.creation.percentile", 171 | "tomcat.sessions.active.max", 172 | "tomcat.threads.config.max", 173 | "jvm.gc.live.data.size", 174 | "process.files.open", 175 | "process.cpu.usage", 176 | "hikaricp.connections.acquire", 177 | "hikaricp.connections.timeout", 178 | "tomcat.servlet.request", 179 | "jvm.gc.pause", 180 | "hikaricp.connections.idle", 181 | "process.uptime", 182 | "tomcat.global.received", 183 | "system.load.average.1m", 184 | "tomcat.cache.hit", 185 | "hikaricp.connections.pending", 186 | "hikaricp.connections.acquire.percentile", 187 | "tomcat.servlet.error", 188 | "tomcat.servlet.request.max", 189 | "hikaricp.connections.usage.percentile", 190 | "jdbc.connections.max", 191 | "tomcat.cache.access", 192 | "tomcat.threads.busy", 193 | "tomcat.sessions.active.current", 194 | "system.cpu.usage", 195 | "jvm.threads.live", 196 | "jvm.classes.loaded", 197 | "jvm.classes.unloaded", 198 | "jvm.threads.peak", 199 | "tomcat.threads.current", 200 | "tomcat.global.request", 201 | "hikaricp.connections.creation", 202 | "jvm.gc.memory.promoted", 203 | "tomcat.sessions.rejected", 204 | "tomcat.sessions.alive.max" 205 | ] 206 | } 207 | ---- 208 | 209 | Compared to the Actuator 1 metrics endpoint, you'll only get a list of names and not a single, current value of any kind. 210 | 211 | === Now we have „real“ drill down… 212 | 213 | You know have to use one of the names to drill down into a metric, for example use `http.server.requests` to get your system-metric of how many request hammered your service. A curl call like `curl localhost:8080/actuator/metrics/http.server.requests` gets you: 214 | 215 | [source,json] 216 | [[drill-down-metrics-boot-2a]] 217 | .Drill down result of one metric 218 | ---- 219 | { 220 | "name": "http.server.requests", 221 | "measurements": [ 222 | { 223 | "statistic": "COUNT", 224 | "value": 509.0 225 | }, 226 | { 227 | "statistic": "TOTAL_TIME", 228 | "value": 21.655494492999996 229 | }, 230 | { 231 | "statistic": "MAX", 232 | "value": 0.012536956 233 | } 234 | ], 235 | "availableTags": [ 236 | { 237 | "tag": "exception", 238 | "values": [ 239 | "None" 240 | ] 241 | }, 242 | { 243 | "tag": "method", 244 | "values": [ 245 | "GET" 246 | ] 247 | }, 248 | { 249 | "tag": "uri", 250 | "values": [ 251 | "/mine", 252 | "/actuator/metrics/{requiredMetricName}", 253 | "/**/favicon.ico", 254 | "/actuator/flyway", 255 | "/actuator/metrics" 256 | ] 257 | }, 258 | { 259 | "tag": "status", 260 | "values": [ 261 | "404", 262 | "200" 263 | ] 264 | } 265 | ] 266 | } 267 | ---- 268 | 269 | Now this gives use at least some information back, for example the total count of things, a total time and a max value. The metric we retrieved is a timer, containing all timed measurements, their total used time and the maximum number of measurements across a base unit. 270 | 271 | === Dimensions all the way 272 | 273 | Drilling down further is possible as well and is realized with a tag, giving name and value like this: `curl localhost:8080/actuator/metrics/http.server.requests\?tag\=status:200 ` 274 | 275 | The result gives us a sneak peek into the things in Micrometer: 276 | 277 | [source,json] 278 | [[drill-down-metrics-boot-2b]] 279 | .Drill down result of one metric, along one dimension 280 | ---- 281 | { 282 | "name": "http.server.requests", 283 | "measurements": [ 284 | { 285 | "statistic": "COUNT", 286 | "value": 511.0 287 | }, 288 | { 289 | "statistic": "TOTAL_TIME", 290 | "value": 21.664119738999997 291 | }, 292 | { 293 | "statistic": "MAX", 294 | "value": 0.0 295 | } 296 | ], 297 | "availableTags": [ 298 | { 299 | "tag": "exception", 300 | "values": [ 301 | "None" 302 | ] 303 | }, 304 | { 305 | "tag": "method", 306 | "values": [ 307 | "GET" 308 | ] 309 | }, 310 | { 311 | "tag": "uri", 312 | "values": [ 313 | "/motd", 314 | "/actuator/metrics/{requiredMetricName}", 315 | "/**/favicon.ico", 316 | "/actuator/flyway", 317 | "/actuator/metrics" 318 | ] 319 | } 320 | ] 321 | } 322 | ---- 323 | 324 | We can drill down along as many dimension as we want. As long as there are more dimensions, we get a list of available tags together with the result. Drilling down along multiple dimensions is done by repeating a tag like this: `curl localhost:8080/actuator/metrics/http.server.requests\?tag\=status:200\&tag\=uri:/mine`, the result being similar to Spring Boot 1 metrics. 325 | 326 | [source,json] 327 | [[drill-down-metrics-boot-2c]] 328 | .Drill down result of one metric, along several dimensions 329 | ---- 330 | { 331 | "name": "http.server.requests", 332 | "measurements": [ 333 | { 334 | "statistic": "COUNT", 335 | "value": 500.0 336 | }, 337 | { 338 | "statistic": "TOTAL_TIME", 339 | "value": 21.543507905 340 | }, 341 | { 342 | "statistic": "MAX", 343 | "value": 0.0 344 | } 345 | ], 346 | "availableTags": [ 347 | { 348 | "tag": "exception", 349 | "values": [ 350 | "None" 351 | ] 352 | }, 353 | { 354 | "tag": "method", 355 | "values": [ 356 | "GET" 357 | ] 358 | } 359 | ] 360 | } 361 | ---- 362 | 363 | == Core concepts 364 | 365 | Before looking into the API in some more detail, I want to present some of the core concepts of Micrometer. Credit, where Credit is due: Some of the ideas coming up in the next slide are not new, many of them notably found in http://metrics.dropwizard.io[Dropwizard Metrics]. 366 | 367 | === Core concepts 368 | 369 | The core concepts of Micrometer are 370 | 371 | * A sense of dimensionality 372 | * Multiple registries 373 | * The idea of a meter with different characteristics 374 | * A SPI for registry-implementations for different monitoring systems 375 | 376 | ==== Dimensionality 377 | 378 | Probably one of the most important aspects of Micrometer and its meters is dimensionality. 379 | 380 | All metrics in Spring Boot 1 or for what it's worth in Dropwizard are hierarchical in nature. To be more precise, they have been mono-hierarchical and thus forming a *taxonomy*. 381 | 382 | The word taxonomy has a well set meaning in the world of biology and can be best represented like this: 383 | 384 | 1. We have counter 385 | 2. A status 386 | 3. A concrete statuts 387 | 4. And a concrete method 388 | 389 | for the metric how often a specific method has been called. That works okish. 390 | 391 | Things get a bit hairy though when dealing with multiple hosts. Add it on top. And then you might want to add a region. 392 | 393 | And even a specific vendors cloud. If you want to query that metric with a pattern in your favorite dashboard application, you get screwed until you can upgrade all instances to have this metric. Meaning: You'll be flying blind for a time. 394 | 395 | ==== From taxonomy to folksonomy 396 | 397 | A folksonomy is a classification system based on tags. The term folksonomy arose in 2004, when blogging and tag clouds where a thing. 398 | 399 | Wikipedia highlights the following advantages among others 400 | 401 | * The vocabulary of a folksonomy is a reflection of the users vocabulary itself 402 | * Folksonomies are flexible in a way that a user can add or remove tags at will 403 | * And folksonomies are multidimensional by nature, one thing can have several and any combination of tags assigned 404 | 405 | Tags are what Micrometer actually uses. They form a dimension of a metric, independent of the metrics type. 406 | 407 | As such we can directly address those things in the previous slide, like adding the instance, the region or even the cloud as tags to a metric without having to change the classification system. 408 | 409 | ==== Creating tags 410 | 411 | Tags can be added as global, common tags or on a given metric instance itself, like on this timer 412 | 413 | [source,java] 414 | [[metrics-on-a-timer]] 415 | .Metrics on a timer 416 | ---- 417 | Timer.builder("presentation.slide.timer") 418 | .description("This is a timer.") 419 | .tags( 420 | "conference", "Spring I/O", 421 | "place", "Barcelona" 422 | ) 423 | .register(meterRegistry); 424 | ---- 425 | 426 | or on a Guage 427 | 428 | [source,java] 429 | [[metrics-on-a-gauge]] 430 | .Metrics on a timer 431 | ---- 432 | Gauge.builder("jvm.memory.used", Runtime.getRuntime(), r -> r.totalMemory() - r.freeMemory()) 433 | .tag("host", "chronos") 434 | .tag("region", "my-desk") 435 | .register(meterRegistry); 436 | ---- 437 | 438 | Those tags or dimensions than can be used to query this dimensional data in a monitoring system that supports dimensions or as show in the slides before. 439 | 440 | === Registries 441 | 442 | A registry is Micrometers interface for collecting sets of measurements. None of the meters we're gonna see in a minute will actually measure things without being added to a registry. Actually, they cannot even be created without one. 443 | 444 | Furthermore, there's an implementation of a registry for every supported monitoring system. 445 | 446 | Let's have a closer look. 447 | 448 | ==== Registries 449 | 450 | ===== Simple registry 451 | 452 | We have the simple registry: 453 | 454 | [source,java] 455 | [[simple-registry]] 456 | .Getting a simple registry 457 | ---- 458 | MeterRegistry registry = new SimpleMeterRegistry(); 459 | ---- 460 | 461 | Use this if you only want to use some metrics and look at them while your service is running. A simple registry holds the latest value of each registered meter in memory and doesn't export it anywhere. The simple registry is the default registry you get when you add Spring Boot actuator to a Spring Boot application. 462 | 463 | If you add only one concrete implementation to your service like `micrometer-registry-atlas`, then you'll get that concrete instance, in this case `AtlasMeterRegistry` and so on. 464 | 465 | ===== Composite registry 466 | 467 | A composite registry holds one or more registries and each of them can be of a different type. 468 | 469 | When you instantiate a composite registry like in the following listing, it's actually a registry that boils down to noop operations only. 470 | 471 | [source,java] 472 | [[composite-registry]] 473 | .Getting and using a composite registry 474 | ---- 475 | CompositeMeterRegistry composite = new CompositeMeterRegistry(); 476 | 477 | Counter counter = composite.counter("counter"); 478 | counter.increment(); // noop 479 | 480 | SimpleMeterRegistry simple = new SimpleMeterRegistry(); 481 | composite.add(simple); 482 | counter.increment(); // now stuff happens 483 | ---- 484 | 485 | The composite registry is an important building block for the 486 | 487 | ===== Global registry 488 | 489 | The global registry is actually a static attribute of the `Metrics` utility class and acts pretty much like global logger factories in SLF4J, Log4j and others. 490 | 491 | You just can reference the instance like 492 | 493 | [source,java] 494 | [[global-registry]] 495 | .Referencing the global registry 496 | ---- 497 | MeterRegistry registry = Metrics.globalRegistry; 498 | ---- 499 | 500 | The global registry is special. It is by default an empty, composite registry. 501 | 502 | The global registry can be however the source of metrics everywhere. And that's where Micrometers catchy phase "like SLF4J but for metrics" makes a lot more sense than only in regards having multiple implementors of their SPI. 503 | 504 | So given the information on the previous slide it should be clear, that every meter registered with it does nothing as long as you don't add something to it. We'll see this later in the demo. 505 | 506 | Good thing, though: Spring boot adds its own registry, wether its a simple, a concrete implementation of also a composite, to the global registry. 507 | 508 | That way, you can use Micrometer meters without any annotation :) 509 | 510 | If you don't want Spring Boot to push it's registry to the global one, use the following setting to disable this: 511 | 512 | [source,properties] 513 | [[dont-use-global-reg]] 514 | .Disable usage of global registry 515 | ---- 516 | management.metrics.use-global-registry=false 517 | ---- 518 | 519 | === Metrics: Measurement of meters over time 520 | 521 | Now that we have a registry to store metrics, we got create some and register them. A metric is a measurement of meters over time. So what kind of different meters are there to be measured? 522 | 523 | ==== Counter 524 | 525 | Let's start with the most basic one. A counter. That thing on the picture is actually called a "tally counter." It's not quite accurate, as the tally counter can be reset to zero whereas Micrometers counter only goes upwards. Use a counter only if you only want to count things. If you're timing computation anyway, use the <>. 526 | 527 | ==== Gauges 528 | 529 | A gauge reassembles a classical instrument the most. It display a value at a given point and usually a Gauge has an upper limit (So, don't use a Gauge when you have none, have a look at <> instead). 530 | 531 | Also, Gauges are usually sampled and not set by you. That means, a gauge observes values and you hardly interact with a gauge on your own. 532 | 533 | ==== Timer 534 | 535 | A timer is used to measure short or medium durations and also frequencies of events. All timer report the total time recorded and also the count of recordings. 536 | 537 | Timer can record block as code in various forms. Either throughout suppliers, callables and runnables. 538 | 539 | There is an AspectJ-implementation for a `@Timed` annotation. The `@Timed` annotion is not recognized out of the box by boot. This says a lot about the goals of Micrometer in regards of doing stuff only with annotations I think. 540 | 541 | === Demo 542 | 543 | Let's finish this section with a short demo of the core concepts. 544 | 545 | == What's next? 546 | 547 | Now that you know the basic building blocks of Micrometer, we can evaluate what's next. Let's see if we can group metrics themselves 548 | 549 | === Three different kind of metrics 550 | 551 | We have 3 different levels of detail and knowledge 552 | 553 | * System metrics 554 | * Application metrics 555 | * Domain metrics (or KPIs) 556 | 557 | ==== System metrics 558 | 559 | We start with the system metrics: System metrics are very low level. CPU and memory usage as well as thread count etc. are part of system metrics. 560 | 561 | Micrometer provides you - either standalone or automatically - with the following http://micrometer.io/docs/ref/jvm[system metrics]: 562 | 563 | [source,java] 564 | [[system-metrics]] 565 | .Various system metrics 566 | ---- 567 | new ClassLoaderMetrics().bindTo(registry); 568 | new JvmMemoryMetrics().bindTo(registry); 569 | new JvmGcMetrics().bindTo(registry); 570 | new ProcessorMetrics().bindTo(registry); 571 | new JvmThreadMetrics().bindTo(registry); 572 | ---- 573 | 574 | ==== Application metrics with Spring Boot 575 | 576 | Application metrics reside on the next level. Among them are usually the number of HTTP requests, inbound as well as outbound. Cache hits and misses, datasource usage or the number of exceptions. 577 | 578 | With Spring Boot, Micrometer is setup in a way that you get all the metrics above and at the following - depending on your class path - as well: 579 | 580 | Micrometer gives you most of them together with Spring Boot. Those are 581 | 582 | * Spring MVC 583 | * Spring WebFlux 584 | * RestTemplate 585 | * Spring Integration 586 | * Rabbit MQ 587 | 588 | At this point you'll get a deep insight into your application already. 589 | 590 | ==== Domain metrics (or KPIs) 591 | 592 | This is where stuff get's really interesting. Domain metrics or Key Performance indicators are metrics like "how many products did I sell the last hour" or "how many customer did I loose due to a bad checkout experience" or "How many pending customers are there?" 593 | 594 | === Demo 595 | 596 | This demo shows the system and application metrics you'll get with Micrometer and Spring Boot 2. After, we will add your own domain metrics into the mix and in such a way that it won't bother us in tests etc. 597 | 598 | == Monitoring 599 | 600 | Monitoring happens on a different level than collecting metrics. One or the other makes no sense without the other. 601 | 602 | Micrometers next similarity with a logging facade it's the possibility connect it to a lot of different monitoring systems. Together with Spring Boot this is done automatically for you by adding a dependency. 603 | 604 | Micrometer also has a concept of adapting the names of meters for you to the needs of different systems. 605 | 606 | Questions that have to be answered are the following: 607 | 608 | === Where to store this? 609 | 610 | Micrometer does not select a monitoring system for you but postprocesses the metrics in a way for you to make them compatible with a lot of systems. 611 | 612 | ==== Supported dimensional monitoring systems 613 | 614 | There are a lot of dimensional monitoring systems that support tags one way or the other out of the box. 615 | 616 | ==== Supported hierarchical monitoring systems 617 | 618 | For some of the older systems, especially like JMX, Micrometer still makes use of Dropwizards JMX exporter. 619 | 620 | === How to store this? 621 | 622 | This at the point of writing a simple to answer questions. How to store this relates in this case to the way data gets into the monitoring system: Either through pushes to it or by polling the application. 623 | 624 | ==== Push model 625 | 626 | Most monitoring systems use the push model. 627 | 628 | If you add such a registry to your application, there's usually a new configurational property to configure URLs, ports and stuff of your monitoring system. 629 | 630 | ==== Poll model 631 | 632 | Right now, only Prometheus, which is used quit heavily those days, polls the application and systems being monitored. 633 | 634 | By adding the Prometheus registry `micrometer-registry-prometheus` to your project, you'll get a new management endpoint, `/actuator/prometheus`, that can be configured like this 635 | 636 | [source,yaml] 637 | [[PrometheusConfig]] 638 | .prometheus.yml 639 | ---- 640 | scrape_configs: 641 | - job_name: 'reactive-java-chain' 642 | metrics_path: '/actuator/prometheus' 643 | static_configs: 644 | - targets: ['localhost:8080'] 645 | - job_name: 'reactive-kotlin-chain' 646 | metrics_path: '/actuator/prometheus' 647 | static_configs: 648 | - targets: ['localhost:8090'] 649 | ---- 650 | 651 | === Demo 652 | 653 | In this last demo I'm gonna show you exactly this configuration and how one can make use of multidimensional metrics coming from Spring Boot with Micrometer and going into Prometheus. 654 | 655 | == Closing notes 656 | 657 | As we have seen throughout the demo, Micrometer is not bound to Spring Boot 2 at all. It is certainly more fun to use it together, but not necessary. 658 | 659 | There's even a legacy integration project for Spring Boot 1 which can easily be added: 660 | 661 | [source,xml] 662 | [[micrometer-spring-boot-1]] 663 | .Adding Micrometer to Spring Boot 1 664 | ---- 665 | 666 | io.micrometer 667 | micrometer-spring-legacy 668 | ${micrometer.version} 669 | 670 | ---- 671 | 672 | The version has to be added to your legacy Spring Boot service, its not part of dependency management. 673 | 674 | == Resources 675 | 676 | Thank you for your time, I hope you got some new insights in my talk. I have the demo at https://github.com/michael-simons/blockchain-playground[Github] which contains the complete talk and my manuscript as well. Slides are in my https://speakerdeck.com/michaelsimons[Speakerdeck]. 677 | 678 | If you can read German or want to learn it, get a copy of my book, too :) --------------------------------------------------------------------------------