├── .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 :)
--------------------------------------------------------------------------------