From 0dbc1da4d9824e589337f3f7ce0cf5bbda0dd13b Mon Sep 17 00:00:00 2001 From: Mike Slinn Date: Mon, 20 Mar 2017 11:19:55 -0700 Subject: [PATCH] Reorganized most of the sample code and began rewriting the User Guide Time to see if Travis builds this thing Time to see if Travis builds this thing #2 Forgot this file Moved and renamed Java serialization started but not complete Scala serialization works for ActorExample Runs but with errors shown as comments Scala serialization works for CommunicationExample Removed redundant message classes Scala serialization works for ConditionalExample Made RichVectorTime, which serializes and deserializes easily Made RichVectorTime, which serializes and deserializes easily Made RichVectorTime, which serializes and deserializes easily Made RichVersioned, which serializes and deserializes easily Deleted redundant message types Hived serialization example from user-guide Added list of serialization packages bit by bit Sphinx output looks good --- deploy.enc | Bin 1680 -> 0 bytes examples/adapter/spark/.travis.yml | 37 + examples/adapter/spark/README.md | 6 + examples/adapter/spark/build.sbt | 88 +++ .../adapter/spark/project/build.properties | 1 + .../src/main/resources/application.conf | 8 +- .../spark}/src/main/resources/log4j2.xml | 0 .../spark/src/main/scala/com/Util.scala | 33 + .../spark/SparkBatchAdapterExample.scala | 3 + .../spark/SparkStreamAdapterExample.scala | 2 + examples/adapter/stream/.travis.yml | 37 + examples/adapter/stream/README.md | 6 + examples/adapter/stream/build.sbt | 76 ++ .../adapter/stream/project/build.properties | 1 + .../src/main/resources/application.conf | 12 + .../example/stream/DurableEventLogs.scala | 0 .../stream/DurableEventProcessorExample.scala | 6 + .../stream/DurableEventSourceExample.scala | 16 +- .../stream/DurableEventWriterExample.scala | 2 + .../stream/ProgressSourceExample.scala | 0 .../stream/src/main/scala/sapi/Util.scala | 19 + examples/adapter/vertx/.travis.yml | 37 + examples/adapter/vertx/README.md | 6 + examples/adapter/vertx/build.sbt | 86 +++ .../adapter/vertx/project/build.properties | 1 + .../docs/vertx/japi/Documentation.java | 0 .../vertx/japi/DiskStorageProvider.java | 0 .../example/vertx/japi/Event.java | 0 .../vertx/japi/VertxAdapterExample.java | 1 - .../src/main/resources/application.conf | 0 .../vertx}/src/main/resources/log4j2.xml | 0 .../docs/vertx/Documentation.scala | 17 +- .../example/vertx/VertxAdapterExample.scala | 5 +- .../example/vertx/package.scala | 0 .../bin/ordermgnt | 0 .../bin/ordermgnt-location | 0 .../bin/querydb | 0 .../example/japi/ordermgnt/Order.java | 0 .../example/japi/ordermgnt/OrderActor.java | 0 .../example/japi/ordermgnt/OrderExample.java | 0 .../example/japi/ordermgnt/OrderId.java | 0 .../example/japi/ordermgnt/OrderManager.java | 0 .../japi/ordermgnt/OrderSerializer.java | 0 .../example/japi/ordermgnt/OrderView.java | 0 .../example/japi/querydb/Emitter.java | 0 .../example/japi/querydb/Writer.java | 0 .../example/japi/querydb/WriterApp.java | 0 .../src/main/resources/log4j2.xml | 0 .../main/resources/ordermgnt/location-A.conf | 0 .../main/resources/ordermgnt/location-B.conf | 0 .../main/resources/ordermgnt/location-C.conf | 0 .../main/resources/ordermgnt/location-D.conf | 0 .../main/resources/ordermgnt/location-E.conf | 0 .../main/resources/ordermgnt/location-F.conf | 0 .../main/resources/querydb/application.conf | 0 .../example/ordermgnt/Order.scala | 0 .../example/ordermgnt/OrderActor.scala | 0 .../example/ordermgnt/OrderExample.scala | 0 .../example/ordermgnt/OrderManager.scala | 0 .../example/ordermgnt/OrderView.scala | 0 .../example/ordermgnt/package.scala | 0 .../example/querydb/Emitter.scala | 0 .../example/querydb/Writer.scala | 0 .../example/querydb/WriterApp.scala | 0 examples/serialization/.travis.yml | 37 + examples/serialization/README.md | 8 + examples/serialization/build.sbt | 99 +++ .../serialization/project/build.properties | 1 + examples/serialization/project/plugins.sbt | 12 + examples/serialization/project/scalapb.sbt | 3 + .../src/main/java}/japi/ActorExample.java | 87 +-- .../main/java}/japi/CommunicationExample.java | 15 +- .../main/java}/japi/ConcurrentExample.java | 11 +- .../main/java/japi/ConditionalExample.java | 112 +++ .../src/main/java}/japi/CrdtExample.java | 24 +- .../src/main/java}/japi/DocUtils.java | 6 +- .../src/main/java/japi/Messages.java | 71 ++ .../src/main/java/japi/ResolveExample.java | 139 ++++ .../src/main/java}/japi/TrackingExample.java | 10 +- .../src/main/java/japi/ViewExample.java | 87 +++ .../serialization/src/main/protobuf/api.proto | 19 + .../src/main/resources/application.conf | 54 ++ .../src/main/scala/sapi/ActorExample.scala | 124 +++ .../scala/sapi/ExampleScalaSerializer.scala | 86 +++ ...InteractiveResolutionScalaSerializer.scala | 88 +++ .../sapi/InteractiveResolveExample.scala | 74 ++ .../src/main/scala/sapi/Messages.scala | 39 + .../src/main/scala/sapi/Util.scala | 33 + examples/serialization/src/test/java/.gitkeep | 0 .../src/test/resources/application.conf | 52 ++ .../src/test/scala/TestSerialization.scala | 47 ++ examples/user-guide/.travis.yml | 37 + examples/user-guide/README.md | 8 + examples/user-guide/bin/docs | 8 + examples/user-guide/build.sbt | 86 +++ examples/user-guide/project/build.properties | 1 + examples/user-guide/project/plugins.sbt | 12 + .../src/main/java/japi/ActorExample.java | 146 ++++ .../main/java/japi/CommunicationExample.java | 105 +++ .../src/main/java/japi/ConcurrentExample.java | 59 ++ .../main/java}/japi/ConditionalExample.java | 59 +- .../src/main/java/japi/CrdtExample.java | 87 +++ .../src/main/java/japi/DocUtils.java | 31 + .../src/main/java/japi/Messages.java | 75 ++ .../src/main/java}/japi/ResolveExample.java | 45 +- .../src/main/java/japi/TrackingExample.java | 57 ++ .../src/main/java}/japi/ViewExample.java | 12 +- .../src/main/resources/application.conf | 12 + .../src/main/scala/sapi/ActorExample.scala | 126 ++++ .../scala/sapi/CommunicationExample.scala | 98 +++ .../main/scala/sapi/ConcurrentExample.scala | 55 ++ .../main/scala/sapi/ConditionalExample.scala | 93 +++ .../main/scala/sapi/EventsourcedViews.scala | 50 ++ .../sapi/InteractiveResolveExample.scala | 64 ++ .../src/main/scala/sapi/Messages.scala | 41 + .../src/main/scala/sapi/ResolveExample.scala | 53 ++ .../src/main/scala/sapi/TrackingExample.scala | 61 ++ .../user-guide/src/main/scala/sapi/Util.scala | 33 + examples/user-guide/src/test/java/.gitkeep | 0 .../src/test/resources/application.conf | 12 + examples/user-guide/src/test/scala/.gitkeep | 0 project/ProjectSettings.scala | 5 +- project/build.properties | 2 +- src/sphinx/_ext/includecode.pyc | Bin 0 -> 4534 bytes src/sphinx/_ext/tabbedcode.pyc | Bin 0 -> 2769 bytes src/sphinx/adapters/spark.rst | 45 +- src/sphinx/adapters/stream.rst | 128 +++- src/sphinx/adapters/vertx.rst | 191 +++-- src/sphinx/code/UserGuideDoc.scala | 426 ----------- src/sphinx/developers.rst | 3 +- src/sphinx/reference/event-sourcing.rst | 452 ++++++++--- src/sphinx/run | 3 + src/sphinx/user-guide.rst | 705 ++++++++++++++---- 133 files changed, 4335 insertions(+), 995 deletions(-) delete mode 100644 deploy.enc create mode 100644 examples/adapter/spark/.travis.yml create mode 100644 examples/adapter/spark/README.md create mode 100644 examples/adapter/spark/build.sbt create mode 100644 examples/adapter/spark/project/build.properties rename {eventuate-example-spark => examples/adapter/spark}/src/main/resources/application.conf (62%) rename {eventuate-example-spark => examples/adapter/spark}/src/main/resources/log4j2.xml (100%) create mode 100644 examples/adapter/spark/src/main/scala/com/Util.scala rename {eventuate-example-spark => examples/adapter/spark}/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala (98%) rename {eventuate-example-spark => examples/adapter/spark}/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala (99%) create mode 100644 examples/adapter/stream/.travis.yml create mode 100644 examples/adapter/stream/README.md create mode 100644 examples/adapter/stream/build.sbt create mode 100644 examples/adapter/stream/project/build.properties create mode 100644 examples/adapter/stream/src/main/resources/application.conf rename {eventuate-example-stream => examples/adapter/stream}/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala (100%) rename {eventuate-example-stream => examples/adapter/stream}/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala (97%) rename {eventuate-example-stream => examples/adapter/stream}/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala (67%) rename {eventuate-example-stream => examples/adapter/stream}/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala (97%) rename {eventuate-example-stream => examples/adapter/stream}/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala (100%) create mode 100644 examples/adapter/stream/src/main/scala/sapi/Util.scala create mode 100644 examples/adapter/vertx/.travis.yml create mode 100644 examples/adapter/vertx/README.md create mode 100644 examples/adapter/vertx/build.sbt create mode 100644 examples/adapter/vertx/project/build.properties rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java (100%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/java/com/rbmhtechnology/example/vertx/japi/DiskStorageProvider.java (100%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/java/com/rbmhtechnology/example/vertx/japi/Event.java (100%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java (99%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/resources/application.conf (100%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/resources/log4j2.xml (100%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala (90%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala (99%) rename {eventuate-example-vertx => examples/adapter/vertx}/src/main/scala/com/rbmhtechnology/example/vertx/package.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/bin/ordermgnt (100%) mode change 100755 => 100644 rename {eventuate-examples => examples/old/interactive-confict-resolution}/bin/ordermgnt-location (100%) mode change 100755 => 100644 rename {eventuate-examples => examples/old/interactive-confict-resolution}/bin/querydb (100%) mode change 100755 => 100644 rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/Order.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderActor.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderExample.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderId.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderManager.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderSerializer.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderView.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/querydb/Emitter.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/querydb/Writer.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/java/com/rbmhtechnology/example/japi/querydb/WriterApp.java (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/log4j2.xml (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/ordermgnt/location-A.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/ordermgnt/location-B.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/ordermgnt/location-C.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/ordermgnt/location-D.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/ordermgnt/location-E.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/ordermgnt/location-F.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/resources/querydb/application.conf (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/ordermgnt/Order.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderActor.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderExample.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderManager.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderView.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/ordermgnt/package.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/querydb/Emitter.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/querydb/Writer.scala (100%) rename {eventuate-examples => examples/old/interactive-confict-resolution}/src/main/scala/com/rbmhtechnology/example/querydb/WriterApp.scala (100%) create mode 100644 examples/serialization/.travis.yml create mode 100644 examples/serialization/README.md create mode 100644 examples/serialization/build.sbt create mode 100644 examples/serialization/project/build.properties create mode 100644 examples/serialization/project/plugins.sbt create mode 100644 examples/serialization/project/scalapb.sbt rename {src/sphinx/code/userguide => examples/serialization/src/main/java}/japi/ActorExample.java (69%) rename {src/sphinx/code/userguide => examples/serialization/src/main/java}/japi/CommunicationExample.java (90%) rename {src/sphinx/code/userguide => examples/serialization/src/main/java}/japi/ConcurrentExample.java (91%) create mode 100644 examples/serialization/src/main/java/japi/ConditionalExample.java rename {src/sphinx/code/userguide => examples/serialization/src/main/java}/japi/CrdtExample.java (86%) rename {src/sphinx/code/userguide => examples/serialization/src/main/java}/japi/DocUtils.java (95%) create mode 100644 examples/serialization/src/main/java/japi/Messages.java create mode 100644 examples/serialization/src/main/java/japi/ResolveExample.java rename {src/sphinx/code/userguide => examples/serialization/src/main/java}/japi/TrackingExample.java (89%) create mode 100644 examples/serialization/src/main/java/japi/ViewExample.java create mode 100644 examples/serialization/src/main/protobuf/api.proto create mode 100644 examples/serialization/src/main/resources/application.conf create mode 100644 examples/serialization/src/main/scala/sapi/ActorExample.scala create mode 100644 examples/serialization/src/main/scala/sapi/ExampleScalaSerializer.scala create mode 100644 examples/serialization/src/main/scala/sapi/InteractiveResolutionScalaSerializer.scala create mode 100644 examples/serialization/src/main/scala/sapi/InteractiveResolveExample.scala create mode 100644 examples/serialization/src/main/scala/sapi/Messages.scala create mode 100644 examples/serialization/src/main/scala/sapi/Util.scala create mode 100644 examples/serialization/src/test/java/.gitkeep create mode 100644 examples/serialization/src/test/resources/application.conf create mode 100644 examples/serialization/src/test/scala/TestSerialization.scala create mode 100644 examples/user-guide/.travis.yml create mode 100644 examples/user-guide/README.md create mode 100644 examples/user-guide/bin/docs create mode 100644 examples/user-guide/build.sbt create mode 100644 examples/user-guide/project/build.properties create mode 100644 examples/user-guide/project/plugins.sbt create mode 100644 examples/user-guide/src/main/java/japi/ActorExample.java create mode 100644 examples/user-guide/src/main/java/japi/CommunicationExample.java create mode 100644 examples/user-guide/src/main/java/japi/ConcurrentExample.java rename {src/sphinx/code/userguide => examples/user-guide/src/main/java}/japi/ConditionalExample.java (67%) create mode 100644 examples/user-guide/src/main/java/japi/CrdtExample.java create mode 100644 examples/user-guide/src/main/java/japi/DocUtils.java create mode 100644 examples/user-guide/src/main/java/japi/Messages.java rename {src/sphinx/code/userguide => examples/user-guide/src/main/java}/japi/ResolveExample.java (76%) create mode 100644 examples/user-guide/src/main/java/japi/TrackingExample.java rename {src/sphinx/code/userguide => examples/user-guide/src/main/java}/japi/ViewExample.java (95%) create mode 100644 examples/user-guide/src/main/resources/application.conf create mode 100644 examples/user-guide/src/main/scala/sapi/ActorExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/CommunicationExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/ConcurrentExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/ConditionalExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/EventsourcedViews.scala create mode 100644 examples/user-guide/src/main/scala/sapi/InteractiveResolveExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/Messages.scala create mode 100644 examples/user-guide/src/main/scala/sapi/ResolveExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/TrackingExample.scala create mode 100644 examples/user-guide/src/main/scala/sapi/Util.scala create mode 100644 examples/user-guide/src/test/java/.gitkeep create mode 100644 examples/user-guide/src/test/resources/application.conf create mode 100644 examples/user-guide/src/test/scala/.gitkeep create mode 100644 src/sphinx/_ext/includecode.pyc create mode 100644 src/sphinx/_ext/tabbedcode.pyc delete mode 100644 src/sphinx/code/UserGuideDoc.scala create mode 100644 src/sphinx/run diff --git a/deploy.enc b/deploy.enc deleted file mode 100644 index 72297e798c90bf221a9552327be3e468eeae1568..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1680 zcmV;B25N!1VG zJv@7>mcxL3^nD6=mE@6B4Q^1?CJar8lu@jF#Ta&fW~yW$0XGMA2+qPvUl|3b9W{^E-CVUSSo|As)$^uu|v!43#sY!$ZR~GB(6R_`{;Mk<-`%5zkILDpUBZ#=ODHKk?nmzG_HX)blyjCrb>0Qde1&3&BJ*lKsispq3fc!#W zvyR2HE8m_Z$^2ZICqN@7+f^?RPPT=2sKtm!#Td5And|DTfnc70RL&Otu z~;9xfS>dt?ruU%<5U^0XB#{{iOCPO{~L~v%z&sY$;(Z70&b9 zJX?-#pb+ivbx#xeLdsj#Gs)sj!(&AlZMs4-4Yk?2$>n}IgNSnV87mrD;+GEiA|Gw|>zXDf8c z`>ZzSe71Gt{&4ch-)yvRzhT;j4A1J4;n^#1|y8$Awx4 zy&ssU$Z^;+iH*_8X&MT7Ad(scrxIwIy*wmKTU!!-saHabmRZWqe#@W_$jSy!xv_kq z_K62n-L@w56aEk|95~a{pCI)JbRM48lB3rX_>m`vfes>uIv=XoJHp16qoYuCzGGD) zvw{GpQY}04&T2KXYqGcyOSa!mktmJfSGV~|ix=G0?WZqQxOf~_V80A+xK~bx%e2uF zV)kb>?&P2rP$)le0DHrX$OdUra6!p|!qxSCXM)Q;hL8%g*sp4iXYX;lp=CJHlkWl- zd7^g}d#1gX4PuGKkrGnq+u47M(?muByzX#F-y=k#)=P<(Rzr@uUJ{KCm67_UHJ~k_ z1Oh-5WNT$g51Jt*j|1@cjEC0R;;0$2XY=?|5Ut}s>SH1ZmZ1OY9a)}Hq;-{bB!vky z3{S(rH_`_19}seUDKnHdy;`InR)A5pv$k|+84b_aMKgZ+iG;>egO}!_poJ{WF%6_x zI7I|4+eWi`YbMe)pqZXC4e7z4-HT;{VqgjE40!>RxkY$h#N~3Ttny3v2VDS&G8)q| zRUwXm_g|yH1t(ESy0!BgQgAbZMoqj6$0Qu;Jh3Dz+10G@3WgAk9#OF3=V*6TPf2Wx a#!5(Ya}ULC<>@^!wszf&Tq#K7Dlm<#&O6!w diff --git a/examples/adapter/spark/.travis.yml b/examples/adapter/spark/.travis.yml new file mode 100644 index 00000000..8cb86ac0 --- /dev/null +++ b/examples/adapter/spark/.travis.yml @@ -0,0 +1,37 @@ +jdk: + - oraclejdk8 + +branches: + only: + - newSerializeDocs + +sudo: + false +language: + - scala +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot/ +before_install: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ./deploy.prep; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then eval `ssh-agent`; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ssh-add deploy; fi +before_script: + - pip install --user sphinx + - pip install --user sphinx_rtd_theme +script: + - .travis/compile-all.sh $TRAVIS_SCALA_VERSION + - .travis/test-core.sh $TRAVIS_SCALA_VERSION + - .travis/test-leveldb.sh $TRAVIS_SCALA_VERSION + - .travis/test-crdt.sh $TRAVIS_SCALA_VERSION + - .travis/test-stream.sh $TRAVIS_SCALA_VERSION + - .travis/test-spark.sh $TRAVIS_SCALA_VERSION + - .travis/test-vertx.sh $TRAVIS_SCALA_VERSION + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm +after_success: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ]; then .travis/publish-all.sh $TRAVIS_SCALA_VERSION; fi +scala: + - 2.11.8 + - 2.12.0 diff --git a/examples/adapter/spark/README.md b/examples/adapter/spark/README.md new file mode 100644 index 00000000..66ed29eb --- /dev/null +++ b/examples/adapter/spark/README.md @@ -0,0 +1,6 @@ +# Eventuate Apache Spark Adapter Example + +[![Build Status](https://travis-ci.org/mslinn/eventuate.svg?branch=newSerializeDocs)](https://travis-ci.org/mslinn/eventuate) + +This project runs the Scala sample code for the Eventuate +[Apache Spark Adapter](http://rbmhtechnology.github.io/eventuate/adapters/vertx.html). diff --git a/examples/adapter/spark/build.sbt b/examples/adapter/spark/build.sbt new file mode 100644 index 00000000..5e03edf4 --- /dev/null +++ b/examples/adapter/spark/build.sbt @@ -0,0 +1,88 @@ +organization := "com.micronautics" + +name := "eventuate-spark-adapter-example" + +version := "0.2.0" + +scalaVersion := "2.11.8" // Apache Spark does not support Scala 2.12 yet + +scalacOptions ++= Seq( + "-deprecation", + "-encoding", "UTF-8", + "-feature", + "-target:jvm-1.8", + "-unchecked", + "-Ywarn-adapted-args", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Ywarn-unused", + "-Ywarn-value-discard", + "-Xfuture", + "-Xlint" +) + +scalacOptions in (Compile, doc) ++= baseDirectory.map { + (bd: File) => Seq[String]( + "-sourcepath", bd.getAbsolutePath, + "-doc-source-url", "https://github.com/mslinn/eventuate/tree/master€{FILE_PATH}.scala" + ) +}.value + +javacOptions ++= Seq( + "-Xlint:deprecation", + "-Xlint:unchecked", + "-source", "1.8", + "-target", "1.8", + "-g:vars" +) + +resolvers += "Eventuate Releases" at "https://dl.bintray.com/rbmhtechnology/maven" + +val akkaVer = "2.4.17" // Specify the Akka version so we get the latest +val evVer = "0.8.1" +val log4jVer = "2.5" +val sparkVer = "1.6.1" + +libraryDependencies ++= Seq( + //"com.rbmhtechnology" %% "eventuate-adapter-stream" % evVer withSources(), + //"com.rbmhtechnology" %% "eventuate-adapter-vertx" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-adapter-spark" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-core" % evVer withSources(), + //"com.rbmhtechnology" %% "eventuate-crdt" % evVer withSources(), + //"com.rbmhtechnology" %% "eventuate-log-cassandra" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-log-leveldb" % evVer withSources(), + "com.typesafe.akka" %% "akka-actor" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-protobuf" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-remote" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-slf4j" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-stream" % akkaVer withSources(), + "org.apache.logging.log4j" % "log4j-core" % log4jVer withSources(), + "org.apache.spark" %% "spark-core" % sparkVer withSources(), + "org.apache.spark" %% "spark-sql" % sparkVer withSources(), + "org.apache.spark" %% "spark-streaming" % sparkVer withSources(), + "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8" withSources(), + // + "org.scalatest" %% "scalatest" % "3.0.1" % Test withSources(), + "junit" % "junit" % "4.12" % Test +) + +parallelExecution in Test := false +fork in Test := true +fork in Runtime := true + +logLevel := Level.Warn + +// Only show warnings and errors on the screen for compilations. +// This applies to both test:compile and compile and is Info by default +logLevel in compile := Level.Warn + +// Level.INFO is needed to see detailed output when running tests +logLevel in test := Level.Info + +// define the statements initially evaluated when entering 'console', 'console-quick', but not 'console-project' +initialCommands in console := """import akka.actor._ + |import com.rbmhtechnology.eventuate.EventsourcedActor + |import scala.util._ + |""".stripMargin + +cancelable := true diff --git a/examples/adapter/spark/project/build.properties b/examples/adapter/spark/project/build.properties new file mode 100644 index 00000000..27e88aa1 --- /dev/null +++ b/examples/adapter/spark/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.13 diff --git a/eventuate-example-spark/src/main/resources/application.conf b/examples/adapter/spark/src/main/resources/application.conf similarity index 62% rename from eventuate-example-spark/src/main/resources/application.conf rename to examples/adapter/spark/src/main/resources/application.conf index c2adc83f..e3ad9b0f 100644 --- a/eventuate-example-spark/src/main/resources/application.conf +++ b/examples/adapter/spark/src/main/resources/application.conf @@ -1,6 +1,12 @@ akka { actor.provider = "akka.remote.RemoteActorRefProvider" + // See http://doc.akka.io/docs/akka/current/scala/logging.html#slf4j-scala + event-handlers = ["akka.event.slf4j.Slf4jLogger"] + logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" + loglevel = "ERROR" + stdout-loglevel = INFO + remote { enabled-transports = ["akka.remote.netty.tcp"] netty.tcp { @@ -8,8 +14,6 @@ akka { port = 2552 } } - - loglevel = "ERROR" } eventuate.log.write-batch-size = 3 diff --git a/eventuate-example-spark/src/main/resources/log4j2.xml b/examples/adapter/spark/src/main/resources/log4j2.xml similarity index 100% rename from eventuate-example-spark/src/main/resources/log4j2.xml rename to examples/adapter/spark/src/main/resources/log4j2.xml diff --git a/examples/adapter/spark/src/main/scala/com/Util.scala b/examples/adapter/spark/src/main/scala/com/Util.scala new file mode 100644 index 00000000..9e97c39f --- /dev/null +++ b/examples/adapter/spark/src/main/scala/com/Util.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +import akka.actor.ActorSystem + +object Util { + // Pause for messages to be displayed before shutting down Akka + def pauseThenStop(seconds: Int = 1)(implicit system: ActorSystem): Unit = { + import scala.concurrent.duration._ + import system.dispatcher + import scala.language.postfixOps + system.scheduler.scheduleOnce(seconds seconds) { + system.terminate() + () + } + () + } +} diff --git a/eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala b/examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala similarity index 98% rename from eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala rename to examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala index 0fff7eb5..816e0151 100644 --- a/eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala +++ b/examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala @@ -112,4 +112,7 @@ object SparkBatchAdapterExample extends App { sparkContext.stop() system.terminate() + + sapi.Util.pauseThenStop() + System.exit(0) } diff --git a/eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala b/examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala similarity index 99% rename from eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala rename to examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala index fb327f72..be036efa 100644 --- a/eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala +++ b/examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala @@ -75,6 +75,8 @@ object SparkStreamAdapterExample extends App { case "exit" => sparkStreamingContext.stop(stopSparkContext = true) system.terminate() + () + case line => writer.write(Seq(line)) prompt() diff --git a/examples/adapter/stream/.travis.yml b/examples/adapter/stream/.travis.yml new file mode 100644 index 00000000..8cb86ac0 --- /dev/null +++ b/examples/adapter/stream/.travis.yml @@ -0,0 +1,37 @@ +jdk: + - oraclejdk8 + +branches: + only: + - newSerializeDocs + +sudo: + false +language: + - scala +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot/ +before_install: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ./deploy.prep; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then eval `ssh-agent`; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ssh-add deploy; fi +before_script: + - pip install --user sphinx + - pip install --user sphinx_rtd_theme +script: + - .travis/compile-all.sh $TRAVIS_SCALA_VERSION + - .travis/test-core.sh $TRAVIS_SCALA_VERSION + - .travis/test-leveldb.sh $TRAVIS_SCALA_VERSION + - .travis/test-crdt.sh $TRAVIS_SCALA_VERSION + - .travis/test-stream.sh $TRAVIS_SCALA_VERSION + - .travis/test-spark.sh $TRAVIS_SCALA_VERSION + - .travis/test-vertx.sh $TRAVIS_SCALA_VERSION + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm +after_success: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ]; then .travis/publish-all.sh $TRAVIS_SCALA_VERSION; fi +scala: + - 2.11.8 + - 2.12.0 diff --git a/examples/adapter/stream/README.md b/examples/adapter/stream/README.md new file mode 100644 index 00000000..bc978cab --- /dev/null +++ b/examples/adapter/stream/README.md @@ -0,0 +1,6 @@ +# Eventuate Streaming Adapter Example + +[![Build Status](https://travis-ci.org/mslinn/eventuate.svg?branch=newSerializeDocs)](https://travis-ci.org/mslinn/eventuate) + +This project runs the Scala sample code for the Eventuate +[Streaming Adapter](http://rbmhtechnology.github.io/eventuate/adapters/stream.html#event-source). diff --git a/examples/adapter/stream/build.sbt b/examples/adapter/stream/build.sbt new file mode 100644 index 00000000..6b61f73d --- /dev/null +++ b/examples/adapter/stream/build.sbt @@ -0,0 +1,76 @@ +organization := "com.micronautics" + +name := "eventuate-stream-adapter-example" + +version := "0.2.0" + +scalaVersion := "2.12.1" + +scalacOptions ++= Seq( + "-deprecation", + "-encoding", "UTF-8", + "-feature", + "-target:jvm-1.8", + "-unchecked", + "-Ywarn-adapted-args", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Ywarn-unused", + "-Ywarn-value-discard", + "-Xfuture", + "-Xlint" +) + +scalacOptions in (Compile, doc) ++= baseDirectory.map { + (bd: File) => Seq[String]( + "-sourcepath", bd.getAbsolutePath, + "-doc-source-url", "https://github.com/mslinn/eventuate/tree/master€{FILE_PATH}.scala" + ) +}.value + +javacOptions ++= Seq( + "-Xlint:deprecation", + "-Xlint:unchecked", + "-source", "1.8", + "-target", "1.8", + "-g:vars" +) + +resolvers += "Eventuate Releases" at "https://dl.bintray.com/rbmhtechnology/maven" + +val evVer = "0.8.1" + +libraryDependencies ++= Seq( + "com.rbmhtechnology" %% "eventuate-adapter-stream" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-adapter-vertx" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-adapter-spark" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-core" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-crdt" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-log-cassandra" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-log-leveldb" % evVer withSources(), + "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8" withSources(), + // + "org.scalatest" %% "scalatest" % "3.0.1" % Test withSources(), + "junit" % "junit" % "4.12" % Test +) + +parallelExecution in Test := false +fork in Test := true +fork in Runtime := true + +logLevel := Level.Warn + +// Only show warnings and errors on the screen for compilations. +// This applies to both test:compile and compile and is Info by default +logLevel in compile := Level.Warn + +// Level.INFO is needed to see detailed output when running tests +logLevel in test := Level.Info + +// define the statements initially evaluated when entering 'console', 'console-quick', but not 'console-project' +initialCommands in console := """import akka.actor._ + |import com.rbmhtechnology.eventuate.EventsourcedActor + |import scala.util._ + |""".stripMargin + +cancelable := true diff --git a/examples/adapter/stream/project/build.properties b/examples/adapter/stream/project/build.properties new file mode 100644 index 00000000..27e88aa1 --- /dev/null +++ b/examples/adapter/stream/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.13 diff --git a/examples/adapter/stream/src/main/resources/application.conf b/examples/adapter/stream/src/main/resources/application.conf new file mode 100644 index 00000000..b9a3b029 --- /dev/null +++ b/examples/adapter/stream/src/main/resources/application.conf @@ -0,0 +1,12 @@ +akka { + log-dead-letters = off + + log-dead-letters-during-shutdown = off + + persistence { + journal { + plugin = "akka.persistence.journal.leveldb" + leveldb.native = off + } + } +} diff --git a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala similarity index 100% rename from eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala rename to examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala diff --git a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala similarity index 97% rename from eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala rename to examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala index ae63cfa8..0aa7ef0f 100644 --- a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala +++ b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala @@ -69,6 +69,8 @@ object DurableEventProcessorExample1 extends App with DurableEventLogs { // (b-1,2) // (c-1,3) //# + + sapi.Util.pauseThenStop() } object DurableEventProcessorExample2 extends App with DurableEventLogs { @@ -118,6 +120,8 @@ object DurableEventProcessorExample2 extends App with DurableEventLogs { graph.run() //# + + sapi.Util.pauseThenStop() } object DurableEventProcessorExample3 extends App with DurableEventLogs { @@ -132,4 +136,6 @@ object DurableEventProcessorExample3 extends App with DurableEventLogs { .map(event => (event.payload, event.localSequenceNr)) .runForeach(println) //# + + sapi.Util.pauseThenStop() } diff --git a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala similarity index 67% rename from eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala rename to examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala index 4b21cd56..88a06c0b 100644 --- a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala +++ b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala @@ -17,16 +17,24 @@ package com.rbmhtechnology.example.stream //# durable-event-source-1 +import akka.actor.ActorRef import akka.stream.scaladsl.Source +import com.rbmhtechnology.eventuate.DurableEvent import com.rbmhtechnology.eventuate.adapter.stream.DurableEventSource //# -object DurableEventSourceExample extends DurableEventLogs { +object DurableEventSourceExample extends App with DurableEventLogs { //# durable-event-source-1 - val source1 = Source.fromGraph(DurableEventSource(logA)) + val source1: Source[DurableEvent, ActorRef] = + Source.fromGraph(DurableEventSource(logA)) //# //# durable-event-source-2 - val source2 = Source.fromGraph(DurableEventSource( - logA, fromSequenceNr = 12414, aggregateId = Some("user-17"))) + val source2: Source[DurableEvent, ActorRef] = + Source.fromGraph(DurableEventSource( + logA, + fromSequenceNr = 12414, aggregateId = Some("user-17")) + ) //# + + sapi.Util.pauseThenStop() } diff --git a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala similarity index 97% rename from eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala rename to examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala index 73b13c4c..8636a667 100644 --- a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala +++ b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala @@ -37,4 +37,6 @@ object DurableEventWriterExample extends App with DurableEventLogs { // (b,2) // (c,3) //# + + sapi.Util.pauseThenStop() } diff --git a/eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala b/examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala similarity index 100% rename from eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala rename to examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala diff --git a/examples/adapter/stream/src/main/scala/sapi/Util.scala b/examples/adapter/stream/src/main/scala/sapi/Util.scala new file mode 100644 index 00000000..86469190 --- /dev/null +++ b/examples/adapter/stream/src/main/scala/sapi/Util.scala @@ -0,0 +1,19 @@ +package sapi + +import akka.actor.ActorSystem + +/** + * @author mslinn */ +object Util { + // Pause for messages to be displayed before shutting down Akka + def pauseThenStop(seconds: Int = 1)(implicit system: ActorSystem): Unit = { + import system.dispatcher + import scala.concurrent.duration._ + import scala.language.postfixOps + system.scheduler.scheduleOnce(seconds seconds) { + system.terminate() + () + } + () + } +} diff --git a/examples/adapter/vertx/.travis.yml b/examples/adapter/vertx/.travis.yml new file mode 100644 index 00000000..8cb86ac0 --- /dev/null +++ b/examples/adapter/vertx/.travis.yml @@ -0,0 +1,37 @@ +jdk: + - oraclejdk8 + +branches: + only: + - newSerializeDocs + +sudo: + false +language: + - scala +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot/ +before_install: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ./deploy.prep; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then eval `ssh-agent`; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ssh-add deploy; fi +before_script: + - pip install --user sphinx + - pip install --user sphinx_rtd_theme +script: + - .travis/compile-all.sh $TRAVIS_SCALA_VERSION + - .travis/test-core.sh $TRAVIS_SCALA_VERSION + - .travis/test-leveldb.sh $TRAVIS_SCALA_VERSION + - .travis/test-crdt.sh $TRAVIS_SCALA_VERSION + - .travis/test-stream.sh $TRAVIS_SCALA_VERSION + - .travis/test-spark.sh $TRAVIS_SCALA_VERSION + - .travis/test-vertx.sh $TRAVIS_SCALA_VERSION + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm +after_success: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ]; then .travis/publish-all.sh $TRAVIS_SCALA_VERSION; fi +scala: + - 2.11.8 + - 2.12.0 diff --git a/examples/adapter/vertx/README.md b/examples/adapter/vertx/README.md new file mode 100644 index 00000000..640021e8 --- /dev/null +++ b/examples/adapter/vertx/README.md @@ -0,0 +1,6 @@ +# Eventuate Vertx Adapter Example + +[![Build Status](https://travis-ci.org/mslinn/eventuate.svg?branch=newSerializeDocs)](https://travis-ci.org/mslinn/eventuate) + +This project runs the Scala sample code for the Eventuate +[Vertx Adapter](http://rbmhtechnology.github.io/eventuate/adapters/vertx.html). diff --git a/examples/adapter/vertx/build.sbt b/examples/adapter/vertx/build.sbt new file mode 100644 index 00000000..42c50fd9 --- /dev/null +++ b/examples/adapter/vertx/build.sbt @@ -0,0 +1,86 @@ +organization := "com.micronautics" + +name := "eventuate-vertx-adapter-example" + +version := "0.2.0" + +scalaVersion := "2.12.1" + +scalacOptions ++= Seq( + "-deprecation", + "-encoding", "UTF-8", + "-feature", + "-target:jvm-1.8", + "-unchecked", + "-Ywarn-adapted-args", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Ywarn-unused", + "-Ywarn-value-discard", + "-Xfuture", + "-Xlint" +) + +scalacOptions in (Compile, doc) ++= baseDirectory.map { + (bd: File) => Seq[String]( + "-sourcepath", bd.getAbsolutePath, + "-doc-source-url", "https://github.com/mslinn/eventuate/tree/master€{FILE_PATH}.scala" + ) +}.value + +javacOptions ++= Seq( + "-Xlint:deprecation", + "-Xlint:unchecked", + "-source", "1.8", + "-target", "1.8", + "-g:vars" +) + +resolvers += "Eventuate Releases" at "https://dl.bintray.com/rbmhtechnology/maven" + +val akkaVer = "2.4.17" // Specify the Akka version so we get the latest +val evVer = "0.8.1" +val vrtxVer = "3.4.1" + +libraryDependencies ++= Seq( + // "com.rbmhtechnology" %% "eventuate-adapter-stream" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-adapter-vertx" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-adapter-spark" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-core" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-crdt" % evVer withSources(), + // "com.rbmhtechnology" %% "eventuate-log-cassandra" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-log-leveldb" % evVer withSources(), + "com.typesafe.akka" %% "akka-actor" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-protobuf" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-remote" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-slf4j" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-stream" % akkaVer withSources(), + "io.javaslang" % "javaslang" % "2.0.5" withSources(), + "io.vertx" % "vertx-core" % vrtxVer withSources(), + "io.vertx" % "vertx-rx-java" % vrtxVer withSources(), + "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8" withSources(), + // + "org.scalatest" %% "scalatest" % "3.0.1" % Test withSources(), + "junit" % "junit" % "4.12" % Test +) + +parallelExecution in Test := false +fork in Test := true +fork in Runtime := true + +logLevel := Level.Warn + +// Only show warnings and errors on the screen for compilations. +// This applies to both test:compile and compile and is Info by default +logLevel in compile := Level.Warn + +// Level.INFO is needed to see detailed output when running tests +logLevel in test := Level.Info + +// define the statements initially evaluated when entering 'console', 'console-quick', but not 'console-project' +initialCommands in console := """import akka.actor._ + |import com.rbmhtechnology.eventuate.EventsourcedActor + |import scala.util._ + |""".stripMargin + +cancelable := true diff --git a/examples/adapter/vertx/project/build.properties b/examples/adapter/vertx/project/build.properties new file mode 100644 index 00000000..27e88aa1 --- /dev/null +++ b/examples/adapter/vertx/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.13 diff --git a/eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java b/examples/adapter/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java similarity index 100% rename from eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java rename to examples/adapter/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java diff --git a/eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/DiskStorageProvider.java b/examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/DiskStorageProvider.java similarity index 100% rename from eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/DiskStorageProvider.java rename to examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/DiskStorageProvider.java diff --git a/eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/Event.java b/examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/Event.java similarity index 100% rename from eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/Event.java rename to examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/Event.java diff --git a/eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java b/examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java similarity index 99% rename from eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java rename to examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java index f7cdfacf..025d9976 100644 --- a/eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java +++ b/examples/adapter/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java @@ -54,7 +54,6 @@ import static scala.compat.java8.JFunction.proc; public class VertxAdapterExample { - private static class Endpoints { static final String PROCESSOR = "eb-address:logA-processor"; static final String PUBLISH_RECEIVER = "eb-address:logB-publish-receiver"; diff --git a/eventuate-example-vertx/src/main/resources/application.conf b/examples/adapter/vertx/src/main/resources/application.conf similarity index 100% rename from eventuate-example-vertx/src/main/resources/application.conf rename to examples/adapter/vertx/src/main/resources/application.conf diff --git a/eventuate-example-vertx/src/main/resources/log4j2.xml b/examples/adapter/vertx/src/main/resources/log4j2.xml similarity index 100% rename from eventuate-example-vertx/src/main/resources/log4j2.xml rename to examples/adapter/vertx/src/main/resources/log4j2.xml diff --git a/eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala b/examples/adapter/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala similarity index 90% rename from eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala rename to examples/adapter/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala index 8241e405..350a5173 100644 --- a/eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala +++ b/examples/adapter/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala @@ -16,12 +16,13 @@ package com.rbmhtechnology.docs.vertx -import com.rbmhtechnology.eventuate.adapter.vertx.{ Confirmation, ProcessingResult } -import com.rbmhtechnology.eventuate.adapter.vertx.api.{ Batch, EventMetadata, StorageProvider } +import akka.actor.ActorRef +import com.rbmhtechnology.eventuate.adapter.vertx.{Confirmation, ProcessingResult} +import com.rbmhtechnology.eventuate.adapter.vertx.api.{Batch, EventMetadata, StorageProvider} import io.vertx.core.AsyncResult import scala.concurrent.duration._ -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.{ExecutionContext, Future} object Documentation { @@ -44,17 +45,17 @@ object Documentation { import io.vertx.core.{ Handler, Vertx } implicit val actorSystem = ActorSystem("system") - val vertx = Vertx.vertx() + val vertx: Vertx = Vertx.vertx() val endpoint = new ReplicationEndpoint( id = "endpoint", logNames = Set("sourceLog", "destinationLog"), logFactory = logId => LeveldbEventLog.props(logId), connections = Set.empty) - val sourceLog = endpoint.logs("sourceLog") - val destinationLog = endpoint.logs("destinationLog") + val sourceLog: ActorRef = endpoint.logs("sourceLog") + val destinationLog: ActorRef = endpoint.logs("destinationLog") - val config = VertxAdapterConfig() + val config: VertxAdapterConfig = VertxAdapterConfig() .addProducer( EventProducer.fromLog(sourceLog) .publishTo { case _ => "address-1" } @@ -64,7 +65,7 @@ object Documentation { .writeTo(destinationLog) .as("log-producer")) - val adapter = VertxAdapter(config, vertx, storageProvider)(actorSystem) + val adapter: VertxAdapter = VertxAdapter(config, vertx, storageProvider)(actorSystem) // receive events from sourceLog... vertx.eventBus().consumer[Event]("address-1").handler(new Handler[Message[Event]] { diff --git a/eventuate-example-vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala b/examples/adapter/vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala similarity index 99% rename from eventuate-example-vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala rename to examples/adapter/vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala index 5114b6ff..991fc92e 100644 --- a/eventuate-example-vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala +++ b/examples/adapter/vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala @@ -52,15 +52,14 @@ object Endpoints { case class Event(id: String) object VertxAdapterExample extends App { - import ExampleVertxExtensions._ implicit val timeout = Timeout(5.minutes) implicit val system = ActorSystem(ReplicationConnection.DefaultRemoteSystemName) - val vertx = Vertx.vertx() - import system.dispatcher + val vertx = Vertx.vertx() + val endpoint = new ReplicationEndpoint(id = "id1", logNames = Set(LogNames.logA, LogNames.logB), logFactory = logId => LeveldbEventLog.props(logId), connections = Set()) diff --git a/eventuate-example-vertx/src/main/scala/com/rbmhtechnology/example/vertx/package.scala b/examples/adapter/vertx/src/main/scala/com/rbmhtechnology/example/vertx/package.scala similarity index 100% rename from eventuate-example-vertx/src/main/scala/com/rbmhtechnology/example/vertx/package.scala rename to examples/adapter/vertx/src/main/scala/com/rbmhtechnology/example/vertx/package.scala diff --git a/eventuate-examples/bin/ordermgnt b/examples/old/interactive-confict-resolution/bin/ordermgnt old mode 100755 new mode 100644 similarity index 100% rename from eventuate-examples/bin/ordermgnt rename to examples/old/interactive-confict-resolution/bin/ordermgnt diff --git a/eventuate-examples/bin/ordermgnt-location b/examples/old/interactive-confict-resolution/bin/ordermgnt-location old mode 100755 new mode 100644 similarity index 100% rename from eventuate-examples/bin/ordermgnt-location rename to examples/old/interactive-confict-resolution/bin/ordermgnt-location diff --git a/eventuate-examples/bin/querydb b/examples/old/interactive-confict-resolution/bin/querydb old mode 100755 new mode 100644 similarity index 100% rename from eventuate-examples/bin/querydb rename to examples/old/interactive-confict-resolution/bin/querydb diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/Order.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/Order.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/Order.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/Order.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderActor.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderActor.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderActor.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderActor.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderExample.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderExample.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderExample.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderExample.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderId.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderId.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderId.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderId.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderManager.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderManager.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderManager.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderManager.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderSerializer.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderSerializer.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderSerializer.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderSerializer.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderView.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderView.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderView.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/ordermgnt/OrderView.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/querydb/Emitter.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/querydb/Emitter.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/querydb/Emitter.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/querydb/Emitter.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/querydb/Writer.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/querydb/Writer.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/querydb/Writer.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/querydb/Writer.java diff --git a/eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/querydb/WriterApp.java b/examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/querydb/WriterApp.java similarity index 100% rename from eventuate-examples/src/main/java/com/rbmhtechnology/example/japi/querydb/WriterApp.java rename to examples/old/interactive-confict-resolution/src/main/java/com/rbmhtechnology/example/japi/querydb/WriterApp.java diff --git a/eventuate-examples/src/main/resources/log4j2.xml b/examples/old/interactive-confict-resolution/src/main/resources/log4j2.xml similarity index 100% rename from eventuate-examples/src/main/resources/log4j2.xml rename to examples/old/interactive-confict-resolution/src/main/resources/log4j2.xml diff --git a/eventuate-examples/src/main/resources/ordermgnt/location-A.conf b/examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-A.conf similarity index 100% rename from eventuate-examples/src/main/resources/ordermgnt/location-A.conf rename to examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-A.conf diff --git a/eventuate-examples/src/main/resources/ordermgnt/location-B.conf b/examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-B.conf similarity index 100% rename from eventuate-examples/src/main/resources/ordermgnt/location-B.conf rename to examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-B.conf diff --git a/eventuate-examples/src/main/resources/ordermgnt/location-C.conf b/examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-C.conf similarity index 100% rename from eventuate-examples/src/main/resources/ordermgnt/location-C.conf rename to examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-C.conf diff --git a/eventuate-examples/src/main/resources/ordermgnt/location-D.conf b/examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-D.conf similarity index 100% rename from eventuate-examples/src/main/resources/ordermgnt/location-D.conf rename to examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-D.conf diff --git a/eventuate-examples/src/main/resources/ordermgnt/location-E.conf b/examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-E.conf similarity index 100% rename from eventuate-examples/src/main/resources/ordermgnt/location-E.conf rename to examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-E.conf diff --git a/eventuate-examples/src/main/resources/ordermgnt/location-F.conf b/examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-F.conf similarity index 100% rename from eventuate-examples/src/main/resources/ordermgnt/location-F.conf rename to examples/old/interactive-confict-resolution/src/main/resources/ordermgnt/location-F.conf diff --git a/eventuate-examples/src/main/resources/querydb/application.conf b/examples/old/interactive-confict-resolution/src/main/resources/querydb/application.conf similarity index 100% rename from eventuate-examples/src/main/resources/querydb/application.conf rename to examples/old/interactive-confict-resolution/src/main/resources/querydb/application.conf diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/Order.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/Order.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/Order.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/Order.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderActor.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderActor.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderActor.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderActor.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderExample.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderExample.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderExample.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderExample.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderManager.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderManager.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderManager.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderManager.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderView.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderView.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderView.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/OrderView.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/package.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/package.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/ordermgnt/package.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/ordermgnt/package.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/Emitter.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/querydb/Emitter.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/Emitter.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/querydb/Emitter.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/Writer.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/querydb/Writer.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/Writer.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/querydb/Writer.scala diff --git a/eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/WriterApp.scala b/examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/querydb/WriterApp.scala similarity index 100% rename from eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/WriterApp.scala rename to examples/old/interactive-confict-resolution/src/main/scala/com/rbmhtechnology/example/querydb/WriterApp.scala diff --git a/examples/serialization/.travis.yml b/examples/serialization/.travis.yml new file mode 100644 index 00000000..8cb86ac0 --- /dev/null +++ b/examples/serialization/.travis.yml @@ -0,0 +1,37 @@ +jdk: + - oraclejdk8 + +branches: + only: + - newSerializeDocs + +sudo: + false +language: + - scala +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot/ +before_install: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ./deploy.prep; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then eval `ssh-agent`; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ssh-add deploy; fi +before_script: + - pip install --user sphinx + - pip install --user sphinx_rtd_theme +script: + - .travis/compile-all.sh $TRAVIS_SCALA_VERSION + - .travis/test-core.sh $TRAVIS_SCALA_VERSION + - .travis/test-leveldb.sh $TRAVIS_SCALA_VERSION + - .travis/test-crdt.sh $TRAVIS_SCALA_VERSION + - .travis/test-stream.sh $TRAVIS_SCALA_VERSION + - .travis/test-spark.sh $TRAVIS_SCALA_VERSION + - .travis/test-vertx.sh $TRAVIS_SCALA_VERSION + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm +after_success: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ]; then .travis/publish-all.sh $TRAVIS_SCALA_VERSION; fi +scala: + - 2.11.8 + - 2.12.0 diff --git a/examples/serialization/README.md b/examples/serialization/README.md new file mode 100644 index 00000000..a1f3aba5 --- /dev/null +++ b/examples/serialization/README.md @@ -0,0 +1,8 @@ +# Eventuate Serialization + +[![Build Status](https://travis-ci.org/mslinn/eventuate.svg?branch=newDocs)](https://travis-ci.org/mslinn/eventuate) + +This project runs the Scala sample code for the Eventuate +[Custom Serialization](http://rbmhtechnology.github.io/eventuate/reference/event-sourcing.html#custom-serialization) +documentation. This code is provided as a +[standalone SBT project](https://github.com/RBMHTechnology/eventuate/blob/master/examples/serialization/). diff --git a/examples/serialization/build.sbt b/examples/serialization/build.sbt new file mode 100644 index 00000000..fbecc027 --- /dev/null +++ b/examples/serialization/build.sbt @@ -0,0 +1,99 @@ +import com.trueaccord.scalapb.compiler.Version.scalapbVersion + +organization := "com.micronautics" + +name := "eventuate-serialization-example" + +version := "0.2.0" + +scalaVersion := "2.12.1" + +scalacOptions ++= Seq( + "-deprecation", + "-encoding", "UTF-8", + "-feature", + "-target:jvm-1.8", + "-unchecked", + "-Ywarn-adapted-args", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Ywarn-unused", + "-Ywarn-value-discard", + "-Xfuture", + "-Xlint" +) + +scalacOptions in (Compile, doc) ++= baseDirectory.map { + (bd: File) => Seq[String]( + "-sourcepath", bd.getAbsolutePath, + "-doc-source-url", "https://github.com/mslinn/eventuate-user-guide/tree/master€{FILE_PATH}.scala" + ) +}.value + +javacOptions ++= Seq( + "-Xlint:deprecation", + "-Xlint:unchecked", + "-source", "1.8", + "-target", "1.8", + "-g:vars" +) + +resolvers += "Eventuate Releases" at "https://dl.bintray.com/rbmhtechnology/maven" + +val akkaVer = "2.4.17" // Specify the Akka version so we get the latest +val evVer = "0.8.1" + +libraryDependencies ++= Seq( + "com.trueaccord.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf", + // + "com.rbmhtechnology" %% "eventuate-core" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-crdt" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-log-leveldb" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-log-cassandra" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-adapter-stream" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-adapter-vertx" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-adapter-spark" % evVer withSources(), + "com.typesafe.akka" %% "akka-actor" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-protobuf" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-remote" % akkaVer withSources(), + "com.typesafe.akka" %% "akka-stream" % akkaVer withSources(), + "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8" withSources(), + // + "org.scalatest" %% "scalatest" % "3.0.1" % Test withSources(), + "junit" % "junit" % "4.12" % Test +) + +PB.targets in Compile := Seq( + scalapb.gen() -> (sourceManaged in Compile).value +) + +discoveredMainClasses in Compile += "japi.ActorExample" +discoveredMainClasses in Compile += "japi.CommunicationExample" +discoveredMainClasses in Compile += "japi.ConcurrentExample" +discoveredMainClasses in Compile += "japi.CrdtExample" +discoveredMainClasses in Compile += "japi.ResolveExample" +discoveredMainClasses in Compile += "japi.TrackingExample" +discoveredMainClasses in Compile += "japi.ViewExample" + +parallelExecution in Test := false +fork in Test := true +fork in Runtime := true + +logLevel := Level.Warn + +// Only show warnings and errors on the screen for compilations. +// This applies to both test:compile and compile and is Info by default +logLevel in compile := Level.Warn + +// Level.INFO is needed to see detailed output when running tests +logLevel in test := Level.Info + +// define the statements initially evaluated when entering 'console', 'console-quick', but not 'console-project' +initialCommands in console := """import akka.actor._ + |import com.rbmhtechnology.eventuate.EventsourcedActor + |import scala.util._ + |""".stripMargin + +cancelable := true + +sublimeTransitive := true diff --git a/examples/serialization/project/build.properties b/examples/serialization/project/build.properties new file mode 100644 index 00000000..27e88aa1 --- /dev/null +++ b/examples/serialization/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.13 diff --git a/examples/serialization/project/plugins.sbt b/examples/serialization/project/plugins.sbt new file mode 100644 index 00000000..9a317f57 --- /dev/null +++ b/examples/serialization/project/plugins.sbt @@ -0,0 +1,12 @@ +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") + +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") + +// See https://github.com/orrsella/sbt-sublime +addSbtPlugin("com.orrsella" % "sbt-sublime" % "1.1.1") + +// See https://github.com/jrudolph/sbt-dependency-graph +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.0") + +// See https://github.com/rtimush/sbt-updates +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0") diff --git a/examples/serialization/project/scalapb.sbt b/examples/serialization/project/scalapb.sbt new file mode 100644 index 00000000..d9519930 --- /dev/null +++ b/examples/serialization/project/scalapb.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.6") + +libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.6.0-pre2" diff --git a/src/sphinx/code/userguide/japi/ActorExample.java b/examples/serialization/src/main/java/japi/ActorExample.java similarity index 69% rename from src/sphinx/code/userguide/japi/ActorExample.java rename to examples/serialization/src/main/java/japi/ActorExample.java index 0d5b5f0b..495541ad 100644 --- a/src/sphinx/code/userguide/japi/ActorExample.java +++ b/examples/serialization/src/main/java/japi/ActorExample.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package userguide.japi; +package japi; //#event-sourced-actor import akka.actor.ActorRef; @@ -37,7 +37,7 @@ import static java.util.stream.Stream.of; class ExampleActor extends AbstractEventsourcedActor { - + private Messages msgs = new Messages(); // outer class reference private final Optional aggregateId; private Collection currentState = Collections.emptyList(); @@ -47,15 +47,15 @@ public ExampleActor(String id, Optional aggregateId, ActorRef eventLog) this.aggregateId = aggregateId; setOnCommand(ReceiveBuilder - .match(Print.class, cmd -> printState(id, currentState)) - .match(Append.class, cmd -> persist(new Appended(cmd.entry), ResultHandler.on( - evt -> sender().tell(new AppendSuccess(evt.entry), self()), - err -> sender().tell(new AppendFailure(err), self()) + .match(Messages.PrintCommand.class, cmd -> printState(id, currentState)) + .match(Messages.AppendCommand.class, cmd -> persist(msgs.new AppendedEvent(cmd.entry), ResultHandler.on( + evt -> sender().tell(msgs.new AppendSuccessCommandReply(evt.entry), self()), + err -> sender().tell(msgs.new AppendFailureCommandReply(err), self()) ))) .build()); setOnEvent(ReceiveBuilder - .match(Appended.class, evt -> currentState = append(currentState, evt.entry)) + .match(Messages.AppendedEvent.class, evt -> currentState = append(currentState, evt.entry)) .build()); } @@ -73,51 +73,14 @@ private Collection append(Collection collection, T el) { return concat(collection.stream(), of(el)).collect(toList()); } } - -// Commands -class Print { -} - -class Append { - public final String entry; - - public Append(String entry) { - this.entry = entry; - } -} - -// Command replies -class AppendSuccess { - public final String entry; - - public AppendSuccess(String entry) { - this.entry = entry; - } -} - -class AppendFailure { - public final Throwable cause; - - public AppendFailure(Throwable cause) { - this.cause = cause; - } -} - -// Events -class Appended { - public final String entry; - - public Appended(String entry) { - this.entry = entry; - } -} //# public class ActorExample { - - public static void main() throws InterruptedException { + public static void main(String[] args) throws InterruptedException { //#create-one-instance + Messages msgs = new Messages(); // outer class reference + final ActorSystem system = // ... //# ActorSystem.create(ReplicationConnection.DefaultRemoteSystemName()); @@ -132,28 +95,28 @@ public static void main() throws InterruptedException { final ActorRef ea1 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("1", Optional.of("a"), eventLog))); - ea1.tell(new Append("a"), noSender()); - ea1.tell(new Append("b"), noSender()); + ea1.tell(msgs.new AppendCommand("a"), noSender()); + ea1.tell(msgs.new AppendCommand("b"), noSender()); //# //#print-one-instance - ea1.tell(new Print(), noSender()); + ea1.tell(msgs.new PrintCommand(), noSender()); //# //#create-two-instances final ActorRef b2 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("2", Optional.of("b"), eventLog))); final ActorRef c3 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("3", Optional.of("c"), eventLog))); - b2.tell(new Append("a"), noSender()); - b2.tell(new Append("b"), noSender()); + b2.tell(msgs.new AppendCommand("a"), noSender()); + b2.tell(msgs.new AppendCommand("b"), noSender()); - c3.tell(new Append("x"), noSender()); - c3.tell(new Append("y"), noSender()); + c3.tell(msgs.new AppendCommand("x"), noSender()); + c3.tell(msgs.new AppendCommand("y"), noSender()); //# //#print-two-instances - b2.tell(new Print(), noSender()); - c3.tell(new Print(), noSender()); + b2.tell(msgs.new PrintCommand(), noSender()); + c3.tell(msgs.new PrintCommand(), noSender()); //# //#create-replica-instances @@ -163,21 +126,21 @@ public static void main() throws InterruptedException { // created at location 2 final ActorRef d5 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("5", Optional.of("d"), eventLog))); - d4.tell(new Append("a"), noSender()); + d4.tell(msgs.new AppendCommand("a"), noSender()); //# Thread.sleep(1000); - d4.tell(new Print(), noSender()); - d5.tell(new Print(), noSender()); + d4.tell(msgs.new PrintCommand(), noSender()); + d5.tell(msgs.new PrintCommand(), noSender()); //#send-another-append - d5.tell(new Append("b"), noSender()); + d5.tell(msgs.new AppendCommand("b"), noSender()); //# Thread.sleep(1000); - d4.tell(new Print(), noSender()); - d5.tell(new Print(), noSender()); + d4.tell(msgs.new PrintCommand(), noSender()); + d5.tell(msgs.new PrintCommand(), noSender()); } } diff --git a/src/sphinx/code/userguide/japi/CommunicationExample.java b/examples/serialization/src/main/java/japi/CommunicationExample.java similarity index 90% rename from src/sphinx/code/userguide/japi/CommunicationExample.java rename to examples/serialization/src/main/java/japi/CommunicationExample.java index f5d2b98a..608e53f2 100644 --- a/src/sphinx/code/userguide/japi/CommunicationExample.java +++ b/examples/serialization/src/main/java/japi/CommunicationExample.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package userguide.japi; - //#event-driven-communication + +package japi; + import akka.actor.ActorRef; import akka.actor.ActorSystem; import akka.actor.Props; @@ -27,11 +28,9 @@ //# public class CommunicationExample { - //#event-driven-communication class PingActor extends AbstractEventsourcedActor { - public PingActor(String id, ActorRef eventLog, ActorRef completion) { super(id, eventLog); @@ -47,7 +46,6 @@ public PingActor(String id, ActorRef eventLog, ActorRef completion) { } class PongActor extends AbstractEventsourcedActor { - public PongActor(String id, ActorRef eventLog) { super(id, eventLog); @@ -74,14 +72,15 @@ public Pong(Integer num) { } //# - public void main() { + public static void main(String[] args) { final ActorSystem system = ActorSystem.create("system"); final ActorRef eventLog = null; //#event-driven-communication - final ActorRef pingActor = system.actorOf(Props.create(PingActor.class, () -> new PingActor("ping", eventLog, system.deadLetters()))); - final ActorRef pongActor = system.actorOf(Props.create(PongActor.class, () -> new PongActor("pong", eventLog))); + CommunicationExample ce = new CommunicationExample(); + final ActorRef pingActor = system.actorOf(Props.create(PingActor.class, () -> ce.new PingActor("ping", eventLog, system.deadLetters()))); + final ActorRef pongActor = system.actorOf(Props.create(PongActor.class, () -> ce.new PongActor("pong", eventLog))); pingActor.tell("serve", noSender()); //# diff --git a/src/sphinx/code/userguide/japi/ConcurrentExample.java b/examples/serialization/src/main/java/japi/ConcurrentExample.java similarity index 91% rename from src/sphinx/code/userguide/japi/ConcurrentExample.java rename to examples/serialization/src/main/java/japi/ConcurrentExample.java index b4c7ea40..e274ddcd 100644 --- a/src/sphinx/code/userguide/japi/ConcurrentExample.java +++ b/examples/serialization/src/main/java/japi/ConcurrentExample.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package userguide.japi; +package japi; -import static userguide.japi.DocUtils.append; +import static japi.DocUtils.append; //#detecting-concurrent-update import akka.actor.ActorRef; @@ -26,14 +26,13 @@ import java.util.Collection; import java.util.Collections; -//# +//# public class ConcurrentExample { //#detecting-concurrent-update class ExampleActor extends AbstractEventsourcedActor { - private Collection currentState = Collections.emptyList(); private VectorTime updateTimestamp = VectorTime.Zero(); @@ -41,7 +40,7 @@ public ExampleActor(String id, ActorRef eventLog) { super(id, eventLog); setOnEvent(ReceiveBuilder - .match(Appended.class, evt -> { + .match(Messages.AppendedEvent.class, evt -> { if (updateTimestamp.lt(lastVectorTimestamp())) { // regular update currentState = append(currentState, evt.entry); @@ -55,4 +54,6 @@ public ExampleActor(String id, ActorRef eventLog) { } } //# + + // TODO Make this into an executable example } diff --git a/examples/serialization/src/main/java/japi/ConditionalExample.java b/examples/serialization/src/main/java/japi/ConditionalExample.java new file mode 100644 index 00000000..89ba576a --- /dev/null +++ b/examples/serialization/src/main/java/japi/ConditionalExample.java @@ -0,0 +1,112 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package japi; + +import static japi.DocUtils.append; +import japi.ViewExample.GetAppendCountCommandReply; + +//#conditional-requests +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.japi.pf.ReceiveBuilder; +import akka.pattern.Patterns; +import akka.util.Timeout; +import com.rbmhtechnology.eventuate.*; +import scala.concurrent.ExecutionContextExecutor; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static scala.compat.java8.JFunction.func; +import static scala.compat.java8.JFunction.proc; +//# + +public class ConditionalExample { + //#conditional-requests + + class ExampleActor extends AbstractEventsourcedActor { + private final String id; + private Collection currentState = Collections.emptyList(); + private Messages msgs = new Messages(); + + public ExampleActor(String id, ActorRef eventLog) { + super(id, eventLog); + this.id = id; + + setOnCommand(ReceiveBuilder + .match(Messages.AppendCommand.class, cmd -> persist(msgs.new AppendedEvent(cmd.entry), ResultHandler.onSuccess( + evt -> sender().tell(msgs.new AppendSuccessWithTimestampCommandReply(evt.entry, lastVectorTimestamp()), self()) + ))) + // ... + .build()); + + setOnEvent(ReceiveBuilder + .match(Messages.AppendedEvent.class, evt -> currentState = append(currentState, evt.entry)) + .build()); + } + + @Override + public Optional getAggregateId() { + return Optional.of(id); + } + } + + // Eventsourced-View + class ExampleView extends AbstractEventsourcedView { + // AbstractEventsourcedView has ConditionalRequests mixed-in by default + + public ExampleView(String id, ActorRef eventLog) { + super(id, eventLog); + + // ... + } + } + //# + public static void main(String[] args) { + final ActorSystem system = ActorSystem.create(""); + final ActorRef eventLog = null; + final ExecutionContextExecutor dispatcher = system.dispatcher(); + + //#conditional-requests + + ConditionalExample ce = new ConditionalExample(); + Messages msgs = new Messages(); + final ActorRef ea = system.actorOf(Props.create(ExampleActor.class, () -> ce.new ExampleActor("ea", eventLog))); + final ActorRef ev = system.actorOf(Props.create(ExampleView.class, () -> ce.new ExampleView("ev", eventLog))); + + final Timeout timeout = Timeout.apply(5, TimeUnit.SECONDS); + + Patterns.ask(ea, msgs.new AppendCommand("a"), timeout) + .flatMap(func(m -> + Patterns.ask(ev, + new ConditionalRequest(((Messages.AppendSuccessWithTimestampCommandReply) m).updateTimestamp, ce.new GetAppendCount()), timeout) + ) + , dispatcher) + .onComplete(proc(result -> { + if (result.isSuccess()) { + System.out.println("append count = " + ((GetAppendCountCommandReply) result.get()).count); + } + }), dispatcher); + + //# + } + + class GetAppendCount {} +} diff --git a/src/sphinx/code/userguide/japi/CrdtExample.java b/examples/serialization/src/main/java/japi/CrdtExample.java similarity index 86% rename from src/sphinx/code/userguide/japi/CrdtExample.java rename to examples/serialization/src/main/java/japi/CrdtExample.java index 0cac42b5..5d60c447 100644 --- a/src/sphinx/code/userguide/japi/CrdtExample.java +++ b/examples/serialization/src/main/java/japi/CrdtExample.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package userguide.japi; +package japi; import akka.actor.ActorRef; import akka.actor.ActorSystem; @@ -23,19 +23,16 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +// TODO turn this into a runnable example public class CrdtExample { - //#or-set-service - /** - * Java API of a replicated [[ORSet]] CRDT service. - * - * @param serviceId Unique id of this service. - * @param log Event log. - * @param system Actor system. - * @tparam A [[ORSet]] entry type. - */ + /** Java API of a replicated [[ORSet]] CRDT service. */ class ORSetService extends CRDTService, Set> { + /** @param serviceId Unique id of this service. + * @param log Event log. + * @param system Actor system. + * @tparam A [[ORSet]] entry type. */ ORSetService(String serviceId, ActorRef log, ActorSystem system) { super(serviceId, log, system); @@ -53,7 +50,6 @@ public CompletionStage> remove(String id, A entry) { //# abstract class CRDTService { - private final String serviceId; private final ActorRef log; private final ActorSystem system; @@ -68,12 +64,10 @@ public CompletionStage op(String id, Object op) { return CompletableFuture.completedFuture(null); } - public void start() { - } + public void start() {} } - interface ORSet { - } + interface ORSet {} private class AddOp { final Object entry; diff --git a/src/sphinx/code/userguide/japi/DocUtils.java b/examples/serialization/src/main/java/japi/DocUtils.java similarity index 95% rename from src/sphinx/code/userguide/japi/DocUtils.java rename to examples/serialization/src/main/java/japi/DocUtils.java index 127b00f8..99ca0191 100644 --- a/src/sphinx/code/userguide/japi/DocUtils.java +++ b/examples/serialization/src/main/java/japi/DocUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package userguide.japi; +package japi; import java.util.Collection; @@ -23,9 +23,7 @@ import static java.util.stream.Stream.of; public final class DocUtils { - - private DocUtils() { - } + private DocUtils() {} public static Collection append(Collection collection, T el) { return concat(collection.stream(), of(el)).collect(toList()); diff --git a/examples/serialization/src/main/java/japi/Messages.java b/examples/serialization/src/main/java/japi/Messages.java new file mode 100644 index 00000000..8f939ab6 --- /dev/null +++ b/examples/serialization/src/main/java/japi/Messages.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#event-sourced-actor +package japi; + +import com.rbmhtechnology.eventuate.VectorTime; + +/** Outer class allows many inner classes to be defined in one file */ +public class Messages { + public class PrintCommand {} + + public class AppendCommand { + public final String entry; + + public AppendCommand(String entry) { + this.entry = entry; + } + } + + public class AppendSuccessCommandReply { + public final String entry; + + public AppendSuccessCommandReply(String entry) { + this.entry = entry; + } + } + + public class AppendFailureCommandReply { + public final Throwable cause; + + public AppendFailureCommandReply(Throwable cause) { + this.cause = cause; + } + } + + public class AppendedEvent { + public final String entry; + + public AppendedEvent(String entry) { + this.entry = entry; + } + } +//# +//#conditional-requests + public class AppendSuccessWithTimestampCommandReply { + public final String entry; + public final VectorTime updateTimestamp; + + public AppendSuccessWithTimestampCommandReply(String entry, VectorTime updateTimestamp) { + this.entry = entry; + this.updateTimestamp = updateTimestamp; + } + } +//# +//#event-sourced-actor +} +//# diff --git a/examples/serialization/src/main/java/japi/ResolveExample.java b/examples/serialization/src/main/java/japi/ResolveExample.java new file mode 100644 index 00000000..f815c6f9 --- /dev/null +++ b/examples/serialization/src/main/java/japi/ResolveExample.java @@ -0,0 +1,139 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#automated-conflict-resolution + +package japi; + +import akka.actor.ActorRef; +import akka.japi.pf.ReceiveBuilder; +import com.rbmhtechnology.eventuate.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Stream; + +import static japi.DocUtils.append; + +// TODO turn this into a runnable example + +//# + +public class ResolveExample { + public static class Auto { + //#automated-conflict-resolution + class ExampleActor extends AbstractEventsourcedActor { + + private ConcurrentVersions, String> versionedState = + ConcurrentVersionsTree.create(Collections.emptyList(), DocUtils::append); + + public ExampleActor(String id, ActorRef eventLog) { + super(id, eventLog); + + setOnEvent(ReceiveBuilder + .match(Messages.AppendedEvent.class, evt -> { + versionedState = versionedState.update(evt.entry, lastVectorTimestamp(), lastSystemTimestamp(), lastEmitterId()); + + if (versionedState.conflict()) { + final Stream>> conflictingVersions = versionedState.getAll().stream() + .sorted((v1, v2) -> { + if (v1.systemTimestamp() == v2.systemTimestamp()) { + return v1.creator().compareTo(v2.creator()); + } + return v1.systemTimestamp() > v2.systemTimestamp() ? -1 : 1; + }); + + final VectorTime winnerTimestamp = conflictingVersions.findFirst().get().vectorTimestamp(); + versionedState = versionedState.resolve(winnerTimestamp); + } + }) + .build()); + } + } + //# + } + + public static class Interactive { + //#interactive-conflict-resolution + + class ExampleActor extends AbstractEventsourcedActor { + + private ConcurrentVersions, String> versionedState = + ConcurrentVersionsTree.create(Collections.emptyList(), (s, a) -> append(s, a)); + + public ExampleActor(String id, ActorRef eventLog) { + super(id, eventLog); + + setOnCommand(ReceiveBuilder + .match(AppendCommand.class, cmd -> versionedState.conflict(), + cmd -> sender().tell(new AppendRejectedCommandReply(cmd.entry, versionedState.getAll()), self()) + ) + .match(AppendCommand.class, cmd -> { + // .... + }) + .match(ResolveCommand.class, cmd -> persist(new ResolvedCommandReply(cmd.selectedTimestamp), ResultHandler.on( + evt -> { /* reply to sender omitted ... */ }, + err -> { /* reply to sender omitted ... */ } + ))) + .build()); + + setOnEvent(ReceiveBuilder + .match(Messages.AppendedEvent.class, evt -> + versionedState = versionedState.update(evt.entry, lastVectorTimestamp(), lastSystemTimestamp(), lastEmitterId()) + ) + .match(ResolvedCommandReply.class, evt -> + versionedState = versionedState.resolve(evt.selectedTimestamp, lastVectorTimestamp(), lastSystemTimestamp()) + ) + .build()); + } + } + + class AppendCommand { + public final String entry; + + public AppendCommand(String entry) { + this.entry = entry; + } + } + + class AppendRejectedCommandReply { + public final String entry; + public final Collection>> conflictingVersions; + + public AppendRejectedCommandReply(String entry, Collection>> conflictingVersions) { + this.entry = entry; + this.conflictingVersions = conflictingVersions; + } + } + + class ResolveCommand { + public final VectorTime selectedTimestamp; + + public ResolveCommand(VectorTime selectedTimestamp) { + this.selectedTimestamp = selectedTimestamp; + } + } + + class ResolvedCommandReply { + public final VectorTime selectedTimestamp; + + public ResolvedCommandReply(VectorTime selectedTimestamp) { + this.selectedTimestamp = selectedTimestamp; + } + } + } + //# +} diff --git a/src/sphinx/code/userguide/japi/TrackingExample.java b/examples/serialization/src/main/java/japi/TrackingExample.java similarity index 89% rename from src/sphinx/code/userguide/japi/TrackingExample.java rename to examples/serialization/src/main/java/japi/TrackingExample.java index a14356b2..fd4ed070 100644 --- a/src/sphinx/code/userguide/japi/TrackingExample.java +++ b/examples/serialization/src/main/java/japi/TrackingExample.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package userguide.japi; - -import static userguide.japi.DocUtils.append; +package japi; //#tracking-conflicting-versions @@ -28,20 +26,20 @@ import java.util.Collections; //# +// TODO turn this into a runnable example public class TrackingExample { - //#tracking-conflicting-versions class ExampleActor extends AbstractEventsourcedActor { private ConcurrentVersions, String> versionedState = - ConcurrentVersionsTree.create(Collections.emptyList(), (s, a) -> append(s, a)); + ConcurrentVersionsTree.create(Collections.emptyList(), DocUtils::append); public ExampleActor(String id, ActorRef eventLog) { super(id, eventLog); setOnEvent(ReceiveBuilder - .match(Appended.class, evt -> { + .match(Messages.AppendedEvent.class, evt -> { versionedState = versionedState.update(evt.entry, lastVectorTimestamp(), lastSystemTimestamp(), lastEmitterId()); if (versionedState.conflict()) { diff --git a/examples/serialization/src/main/java/japi/ViewExample.java b/examples/serialization/src/main/java/japi/ViewExample.java new file mode 100644 index 00000000..1eb182bb --- /dev/null +++ b/examples/serialization/src/main/java/japi/ViewExample.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#event-sourced-view + +package japi; + +import akka.actor.ActorRef; +import akka.japi.pf.ReceiveBuilder; +import com.rbmhtechnology.eventuate.AbstractEventsourcedView; +import com.rbmhtechnology.eventuate.VectorTime; +//# + +// TODO turn this into a runnable example +public class ViewExample { + //#event-sourced-view + + class ExampleView extends AbstractEventsourcedView { + + private Long appendCount = 0L; + private Long resolveCount = 0L; + + public ExampleView(String id, ActorRef eventLog) { + super(id, eventLog); + + setOnCommand(ReceiveBuilder + .match(GetAppendCountCommand.class, cmd -> sender().tell(new GetAppendCountCommandReply(appendCount), self())) + .match(GetResolveCountCommand.class, cmd -> sender().tell(new GetResolveCountCommandReply(resolveCount), self())) + .build()); + + setOnEvent(ReceiveBuilder + .match(AppendedEvent.class, evt -> appendCount += 1) + .match(ResolvedEvent.class, evt -> resolveCount += 1) + .build()); + } + } + + class GetAppendCountCommand { } + + class GetResolveCountCommand { } + + class GetAppendCountCommandReply { + public final Long count; + + public GetAppendCountCommandReply(Long count) { + this.count = count; + } + } + + class GetResolveCountCommandReply { + public final Long count; + + public GetResolveCountCommandReply(Long count) { + this.count = count; + } + } + + class AppendedEvent { + public final String entry; + + public AppendedEvent(String entry) { + this.entry = entry; + } + } + + class ResolvedEvent { + public final VectorTime selectedTimestamp; + + public ResolvedEvent(VectorTime selectedTimestamp) { + this.selectedTimestamp = selectedTimestamp; + } + } + //# +} diff --git a/examples/serialization/src/main/protobuf/api.proto b/examples/serialization/src/main/protobuf/api.proto new file mode 100644 index 00000000..c04ddce6 --- /dev/null +++ b/examples/serialization/src/main/protobuf/api.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +// From scalapb-runtime +import "scalapb/scalapb.proto"; +import "google/protobuf/wrappers.proto"; +//import "CommonFormats.proto"; +import "com.rbmhtechnology.eventuate.serializer.CommonFormats"; + +package api; + +// VectorTimeFormat is defined in eventuate-core/src/main/protobuf/CommonFormats.proto +message ResolvedEvent { + option (scalapb.message).extends = "api.Event"; + optional VectorTimeFormat selectedTimestamp = 6; +} + +// note that api comes from the name of this proto file +// creates api.Event +// Generated code is placed in target/ diff --git a/examples/serialization/src/main/resources/application.conf b/examples/serialization/src/main/resources/application.conf new file mode 100644 index 00000000..baae9f91 --- /dev/null +++ b/examples/serialization/src/main/resources/application.conf @@ -0,0 +1,54 @@ +akka { + actor { + // See https://rbmhtechnology.github.io/eventuate/reference/event-sourcing.html#custom-serialization + serializers { + // See http://doc.akka.io/docs/akka/2.4/scala/serialization.html + + // WHY? [akka.serialization.Serialization(akka://location)] Using the default Java serializer for class [sapi.AppendedEvent] + // which is not recommended because of performance implications. Use another serializer or disable this + // warning using the setting 'akka.actor.warn-about-java-serializer-usage' + + exampleScalaSerializer = "sapi.ExampleScalaSerializer" + interactiveResoultionScalaSerializer = "sapi.InteractiveResolutionScalaSerializer" + + // See http://doc.akka.io/docs/akka/current/java/serialization.html (not implemented) + //exampleJavaSerializer = "japi.ExampleJavaSerializer" + //interactiveResoultionJavaSerializer = "japi.InteractiveResolutionJavaSerializer" + } + + serialization-bindings { + "sapi.PrintCommand" = exampleScalaSerializer + "sapi.AppendCommand" = exampleScalaSerializer + "sapi.AppendSuccessCommandReply" = exampleScalaSerializer + "sapi.AppendFailureCommandReply" = exampleScalaSerializer + "sapi.AppendedEvent" = exampleScalaSerializer + + "sapi.InteractiveResolveExample$Append" = interactiveResoultionScalaSerializer + "sapi.InteractiveResolveExample$AppendRejected" = interactiveResoultionScalaSerializer + "sapi.InteractiveResolveExample$Resolve" = interactiveResoultionScalaSerializer + "sapi.InteractiveResolveExample$Resolved" = interactiveResoultionScalaSerializer + +// "japi.ActorExample.PrintCommand" = exampleJavaSerializer +// "japi.ActorExample.AppendCommand" = exampleJavaSerializer +// "japi.ActorExample.AppendSuccessWithTimestampCommandReply" = exampleJavaSerializer +// "japi.ActorExample.AppendFailureCommandReply" = exampleJavaSerializer +// "japi.ActorExample.AppendedEvent" = exampleJavaSerializer + +// "japi.InteractiveResolveExample.AppendCommand" = interactiveResoultionJavaSerializer +// "japi.InteractiveResolveExample.AppendRejectedCommandReply" = interactiveResoultionJavaSerializer +// "japi.InteractiveResolveExample.ResolveCommand" = interactiveResoultionJavaSerializer +// "japi.InteractiveResolveExample.ResolvedCommandReply" = interactiveResoultionJavaSerializer + } + } + + log-dead-letters = off + + log-dead-letters-during-shutdown = off + + persistence { + journal { + plugin = "akka.persistence.journal.leveldb" + leveldb.native = off + } + } +} diff --git a/examples/serialization/src/main/scala/sapi/ActorExample.scala b/examples/serialization/src/main/scala/sapi/ActorExample.scala new file mode 100644 index 00000000..cbf014dd --- /dev/null +++ b/examples/serialization/src/main/scala/sapi/ActorExample.scala @@ -0,0 +1,124 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +object ActorExample extends App { + //#event-sourced-actor + import akka.actor._ + import com.rbmhtechnology.eventuate.EventsourcedActor + import scala.util._ + + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + import scala.collection.mutable + private val currentState: mutable.ArrayBuffer[String] = mutable.ArrayBuffer.empty + + override def onCommand: PartialFunction[Any, Unit] = { + case PrintCommand => + println(s"[id = $id, aggregate id = ${ aggregateId.getOrElse("")}] ${ currentState.mkString(",") }") + + case AppendCommand(entry) => persist(AppendedEvent(entry)) { + case Success(_) => sender() ! AppendSuccessCommandReply(entry) + case Failure(err) => sender() ! AppendFailureCommandReply(err) + } + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => currentState :+ entry + } + } + //# + + //#create-one-instance + import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog + import com.rbmhtechnology.eventuate.ReplicationConnection._ + + // `DefaultRemoteSystemName` is defined as "location" in the + // [[ReplicationConnection]] object, so the ActorSystem name + // is "location" + implicit val system: ActorSystem = ActorSystem(DefaultRemoteSystemName) + //# + + //#create-one-instance + + // Wrap a new instance of a `LeveldbEventLog` configuration object with + // log id "qt-1" into an Actor. + // This creates a log directory called `target/log-qt-1/` + val eventLog: ActorRef = system.actorOf(LeveldbEventLog.props("qt-1")) + //# + + //#create-one-instance + + // Create a new instance of ExampleActor with id=="1" and aggregateId==Some("a"); + // also provide the eventLog [[ActorRef]] to the actorOf [[Actor]] factory + val ea1 = system.actorOf(Props(new ExampleActor("1", Some("a"), eventLog))) + + ea1 ! AppendCommand("a") + ea1 ! AppendCommand("b") + //# + + //#print-one-instance + ea1 ! PrintCommand + //# + + //#create-two-instances + val b2: ActorRef = + system.actorOf(Props(new ExampleActor("2", Some("b"), eventLog))) + val c3: ActorRef = + system.actorOf(Props(new ExampleActor("3", Some("c"), eventLog))) + + b2 ! AppendCommand("a") + b2 ! AppendCommand("b") + + c3 ! AppendCommand("x") + c3 ! AppendCommand("y") + //# + + //#print-two-instances + b2 ! PrintCommand + c3 ! PrintCommand + //# + + //#create-replica-instances + // created at location 1 + val d4 = system.actorOf(Props(new ExampleActor("4", Some("d"), eventLog))) + + // created at location 2 + val d5 = system.actorOf(Props(new ExampleActor("5", Some("d"), eventLog))) + + d4 ! AppendCommand("a") + //# + + Thread.sleep(1000) + + d4 ! PrintCommand // fixme why is this not referenced in user-guide.rst? + d5 ! PrintCommand + + //#send-another-append + d5 ! AppendCommand("b") + //# + + Thread.sleep(1000) + + d4 ! PrintCommand + d5 ! PrintCommand + + Util.pauseThenStop() +} diff --git a/examples/serialization/src/main/scala/sapi/ExampleScalaSerializer.scala b/examples/serialization/src/main/scala/sapi/ExampleScalaSerializer.scala new file mode 100644 index 00000000..6c778622 --- /dev/null +++ b/examples/serialization/src/main/scala/sapi/ExampleScalaSerializer.scala @@ -0,0 +1,86 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#scala-serializer +package sapi + +import akka.serialization._ + +object ExampleScalaSerializer { + val UTF_8: String = java.nio.charset.StandardCharsets.UTF_8.name + + /* Case is significant for pattern matching to work. In case clauses, a term that begins with a + * lowercase letter is assumed to be the name of a new variable that will hold an extracted value. + * To refer to a previously defined variable, enclose it in back ticks. Conversely, a term that begins + * with an uppercase letter is assumed to be a type name. */ + val AppendManifest: String = classOf[AppendCommand].getName // "sapi.AppendCommand" + val AppendedManifest: String = classOf[AppendedEvent].getName // "sapi.AppendedEvent" + val AppendFailureManifest: String = classOf[AppendFailureCommandReply].getName // "sapi.AppendFailureCommandReply" + val AppendSuccessManifest: String = classOf[AppendSuccessCommandReply].getName // "sapi.AppendSuccessCommandReply" + val PrintManifest: String = PrintCommand.getClass.getName // "sapi.PrintCommand$" note the $ at the end +} + +class ExampleScalaSerializer extends SerializerWithStringManifest { + import ExampleScalaSerializer._ + + /** Unique identifier for your Serializer. + * 0 - 16 is reserved by Akka itself */ + def identifier: Int = 93478411 + + /** The manifest (type hint) that will be provided in the fromBinary method. + * Use `""` if a manifest is not needed. */ + def manifest(obj: AnyRef): String = + obj match { + case _: AppendCommand => AppendManifest + case _: AppendedEvent => AppendedManifest + case _: AppendFailureCommandReply => AppendFailureManifest + case _: AppendSuccessCommandReply => AppendSuccessManifest + case PrintCommand => PrintManifest + } + + /** Serializes the given object to an Array[Byte] */ + def toBinary(obj: AnyRef): Array[Byte] = + obj match { + case AppendCommand(name) => name.getBytes(UTF_8) + case AppendFailureCommandReply(name) => name.getMessage.getBytes(UTF_8) + case AppendSuccessCommandReply(name) => name.getBytes(UTF_8) + case AppendedEvent(name) => name.getBytes(UTF_8) + case PrintCommand => "".getBytes(UTF_8) // case objects have no payload + } + + /** Deserializes the given Array[Byte] using the type hint (`manifest`) */ + def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = + manifest match { + case AppendManifest => + AppendCommand(new String(bytes, UTF_8)) + + case AppendedManifest => + AppendCommand(new String(bytes, UTF_8)) + + case AppendFailureManifest => + import scala.util.control.NoStackTrace + AppendFailureCommandReply(new Throwable(new String(bytes, UTF_8)) with NoStackTrace) + + case AppendSuccessManifest => + AppendSuccessCommandReply(new String(bytes, UTF_8)) + + case PrintManifest => + PrintCommand + + case _ => throw new java.io.NotSerializableException + } +} +//# diff --git a/examples/serialization/src/main/scala/sapi/InteractiveResolutionScalaSerializer.scala b/examples/serialization/src/main/scala/sapi/InteractiveResolutionScalaSerializer.scala new file mode 100644 index 00000000..78b10f56 --- /dev/null +++ b/examples/serialization/src/main/scala/sapi/InteractiveResolutionScalaSerializer.scala @@ -0,0 +1,88 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#scala-serializer +package sapi + +import akka.serialization._ + +object InteractiveResolutionScalaSerializer { + val UTF_8: String = java.nio.charset.StandardCharsets.UTF_8.name + + /* Case is significant for pattern matching to work. In case clauses, a term that begins with a + * lowercase letter is assumed to be the name of a new variable that will hold an extracted value. + * To refer to a previously defined variable, enclose it in back ticks. Conversely, a term that begins + * with an uppercase letter is assumed to be a type name. */ + val AppendManifest: String = classOf[InteractiveResolveExample.AppendCommand].getName // "sapi.InteractiveResolveExample$Append" + val AppendRejectedManifest: String = classOf[InteractiveResolveExample.AppendRejected].getName // "sapi.InteractiveResolveExample$AppendRejected" + val ResolveManifest: String = classOf[InteractiveResolveExample.ResolveCommand].getName // "sapi.InteractiveResolveExample$Resolve" + val ResolvedManifest: String = classOf[InteractiveResolveExample.ResolvedEvent].getName // "sapi.InteractiveResolveExample$Resolved" +} + +class InteractiveResolutionScalaSerializer extends SerializerWithStringManifest { + import InteractiveResolutionScalaSerializer._ + + /** Unique identifier for your Serializer. + * 0 - 16 is reserved by Akka itself */ + def identifier: Int = 1381719666 + + /** The manifest (type hint) that will be provided in the fromBinary method. + * Use `""` if a manifest is not needed. */ + def manifest(obj: AnyRef): String = + obj match { + case _: InteractiveResolveExample.AppendCommand => AppendManifest + case _: InteractiveResolveExample.AppendRejected => AppendRejectedManifest + case _: InteractiveResolveExample.ResolveCommand => ResolveManifest + case _: InteractiveResolveExample.ResolvedEvent => ResolvedManifest + } + + /** Serializes the given object to an Array[Byte] */ + def toBinary(obj: AnyRef): Array[Byte] = + obj match { + case InteractiveResolveExample.AppendCommand(entry) => + entry.getBytes(UTF_8) + + case appendRejected: InteractiveResolveExample.AppendRejected => + appendRejected.serialize + + case InteractiveResolveExample.ResolveCommand(vectorTime) => + vectorTime.serialize + + case InteractiveResolveExample.ResolvedEvent(vectorTime) => + vectorTime.serialize + + case _ => throw new java.io.NotSerializableException + } + + /** Deserializes the given Array[Byte] using the type hint (`manifest`) */ + def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = + manifest match { + case AppendManifest => + InteractiveResolveExample.AppendCommand(new String(bytes, UTF_8)) + + case AppendRejectedManifest => + InteractiveResolveExample.AppendRejected.deserialize(bytes) + + case ResolveManifest => + InteractiveResolveExample.ResolveCommand(RichVectorTime.deserialize(bytes)) + + case ResolvedManifest => + InteractiveResolveExample.ResolvedEvent(RichVectorTime.deserialize(bytes)) + + case _ => throw new java.io.NotSerializableException + } +} +//# diff --git a/examples/serialization/src/main/scala/sapi/InteractiveResolveExample.scala b/examples/serialization/src/main/scala/sapi/InteractiveResolveExample.scala new file mode 100644 index 00000000..4e8de6c6 --- /dev/null +++ b/examples/serialization/src/main/scala/sapi/InteractiveResolveExample.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +import org.apache.commons.lang3.SerializationUtils + +object InteractiveResolveExample extends App { + import akka.actor._ + import scala.util._ + import com.rbmhtechnology.eventuate._ + import scala.collection.immutable.Seq + + //#interactive-conflict-resolution + sealed trait SIRECommand + + case class AppendCommand(entry: String) extends SIRECommand + + sealed trait SIRECommandReply + object AppendRejected { + def deserialize(byteArray: Array[Byte]): AppendRejected = SerializationUtils.deserialize(byteArray) + } + + case class AppendRejected(entry: String, conflictingVersions: Seq[Versioned[Vector[String]]]) extends SIRECommandReply { + def serialize: Array[Byte] = SerializationUtils.serialize(this.asInstanceOf[Serializable]) + } + + case class ResolveCommand(selectedTimestamp: VectorTime) extends SIRECommand + + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + private var versionedState: ConcurrentVersions[Vector[String], String] = + ConcurrentVersions(Vector.empty, (s, a) => s :+ a) + + override def onCommand: PartialFunction[Any, Unit] = { + case AppendCommand(entry) if versionedState.conflict => + sender() ! AppendRejected(entry, versionedState.all) + + case AppendCommand(entry) => + // ... + + case ResolveCommand(selectedTimestamp) => persist(ResolvedEvent(selectedTimestamp)) { + case Success(event) => // reply to sender omitted ... + case Failure(ex) => // reply to sender omitted ... + } + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => + versionedState = versionedState + .update(entry, lastVectorTimestamp, lastSystemTimestamp, lastEmitterId) + + case ResolvedEvent(selectedTimestamp) => + versionedState = versionedState.resolve(selectedTimestamp, lastVectorTimestamp) + } + } + //# +} diff --git a/examples/serialization/src/main/scala/sapi/Messages.scala b/examples/serialization/src/main/scala/sapi/Messages.scala new file mode 100644 index 00000000..016f3f46 --- /dev/null +++ b/examples/serialization/src/main/scala/sapi/Messages.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +//#event-sourced-actor + +// Commands +sealed trait SCommand +case object PrintCommand extends SCommand +case class AppendCommand(entry: String) extends SCommand + +// Command replies +sealed trait SCommandReply +case class AppendFailureCommandReply(cause: Throwable) extends SCommandReply +case class AppendSuccessCommandReply(entry: String) extends SCommandReply + +// Events +sealed trait SEvent +case class AppendedEvent(entry: String) extends SEvent +//# + +import com.rbmhtechnology.eventuate.VectorTime +//#interactive-conflict-resolution +case class ResolvedEvent(selectedTimestamp: VectorTime) extends SEvent +//# diff --git a/examples/serialization/src/main/scala/sapi/Util.scala b/examples/serialization/src/main/scala/sapi/Util.scala new file mode 100644 index 00000000..9e97c39f --- /dev/null +++ b/examples/serialization/src/main/scala/sapi/Util.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +import akka.actor.ActorSystem + +object Util { + // Pause for messages to be displayed before shutting down Akka + def pauseThenStop(seconds: Int = 1)(implicit system: ActorSystem): Unit = { + import scala.concurrent.duration._ + import system.dispatcher + import scala.language.postfixOps + system.scheduler.scheduleOnce(seconds seconds) { + system.terminate() + () + } + () + } +} diff --git a/examples/serialization/src/test/java/.gitkeep b/examples/serialization/src/test/java/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/serialization/src/test/resources/application.conf b/examples/serialization/src/test/resources/application.conf new file mode 100644 index 00000000..e03734bb --- /dev/null +++ b/examples/serialization/src/test/resources/application.conf @@ -0,0 +1,52 @@ +akka { + actor { + // See http://doc.akka.io/docs/akka/2.4/scala/serialization.html + serialize-creators = on + serialize-messages = on + + // See https://rbmhtechnology.github.io/eventuate/reference/event-sourcing.html#custom-serialization + serializers { + // See http://doc.akka.io/docs/akka/2.4/scala/serialization.html + exampleScalaSerializer = "sapi.ExampleScalaSerializer" + interactiveResoultionScalaSerializer = "sapi.InteractiveResolutionScalaSerializer" + + // See http://doc.akka.io/docs/akka/current/java/serialization.html (not implemented) + //exampleJavaSerializer = "japi.ExampleJavaSerializer" + //interactiveResoultionJavaSerializer = "japi.InteractiveResolutionJavaSerializer" + } + + serialization-bindings { + "sapi.PrintCommand" = exampleScalaSerializer + "sapi.AppendCommand" = exampleScalaSerializer + "sapi.AppendSuccessCommandReply" = exampleScalaSerializer + "sapi.AppendFailureCommandReply" = exampleScalaSerializer + "sapi.AppendedEvent" = exampleScalaSerializer + + "sapi.InteractiveResolveExample$Append" = interactiveResoultionScalaSerializer + "sapi.InteractiveResolveExample$AppendRejected" = interactiveResoultionScalaSerializer + "sapi.InteractiveResolveExample$Resolve" = interactiveResoultionScalaSerializer + "sapi.InteractiveResolveExample$Resolved" = interactiveResoultionScalaSerializer + +// "japi.Messages.PrintCommandCommand" = exampleJavaSerializer +// "japi.Messages.AppendCommandCommand" = exampleJavaSerializer +// "japi.Messages.AppendSuccessWithTimestampCommandReplyCommandReply" = exampleJavaSerializer +// "japi.Messages.AppendFailureCommandReplyCommandReply" = exampleJavaSerializer +// "japi.Messages.AppendedEventEvent" = exampleJavaSerializer + +// "japi.InteractiveResolveExample.AppendCommand" = interactiveResoultionJavaSerializer +// "japi.InteractiveResolveExample.AppendRejectedCommandReply" = interactiveResoultionJavaSerializer +// "japi.InteractiveResolveExample.ResolveCommand" = interactiveResoultionJavaSerializer +// "japi.InteractiveResolveExample.ResolvedCommandReply" = interactiveResoultionJavaSerializer } + } + + log-dead-letters = off + + log-dead-letters-during-shutdown = off + + persistence { + journal { + plugin = "akka.persistence.journal.leveldb" + leveldb.native = off + } + } +} diff --git a/examples/serialization/src/test/scala/TestSerialization.scala b/examples/serialization/src/test/scala/TestSerialization.scala new file mode 100644 index 00000000..cd5d11c8 --- /dev/null +++ b/examples/serialization/src/test/scala/TestSerialization.scala @@ -0,0 +1,47 @@ +import com.rbmhtechnology.eventuate.{VectorTime, Versioned} +import org.apache.commons.lang3.SerializationUtils +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest._ +import org.scalatest.Matchers._ + +@RunWith(classOf[JUnitRunner]) +class TestSerialization extends WordSpec { + "VectorTime" should { + val desired = VectorTime("a" -> 1L, "b" -> 2L, "c" -> 3L) + + "manually serialize and deserialize" in { + val byteArray: Array[Byte] = SerializationUtils.serialize(desired.value.asInstanceOf[Serializable]) + val map: Map[String, Long] = SerializationUtils.deserialize(byteArray) + val actual = VectorTime(map.toSeq: _*) + actual === desired + } + + "implicitly serialize and deserialize" in { + import sapi._ + val byteArray: Array[Byte] = desired.serialize + val actual: VectorTime = RichVectorTime.deserialize(byteArray) + actual === desired + } + } + + "Versioned" should { + val vectorTime = VectorTime("a" -> 1L, "b" -> 2L, "c" -> 3L) + val systemTimeStamp = System.currentTimeMillis + val creator = "The Software God" + val desired = Versioned("value", vectorTime, systemTimeStamp, creator) + + "manually serialize and deserialize" in { + val byteArray: Array[Byte] = SerializationUtils.serialize(desired.asInstanceOf[Serializable]) + val actual: Versioned[String] = SerializationUtils.deserialize(byteArray) + actual === desired + } + + "implicitly serialize and deserialize" in { + import sapi._ + val byteArray: Array[Byte] = desired.serialize + val actual: Versioned[String] = RichVersioned.deserialize(byteArray) + actual === desired + } + } +} diff --git a/examples/user-guide/.travis.yml b/examples/user-guide/.travis.yml new file mode 100644 index 00000000..8cb86ac0 --- /dev/null +++ b/examples/user-guide/.travis.yml @@ -0,0 +1,37 @@ +jdk: + - oraclejdk8 + +branches: + only: + - newSerializeDocs + +sudo: + false +language: + - scala +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot/ +before_install: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ./deploy.prep; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then eval `ssh-agent`; fi + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then ssh-add deploy; fi +before_script: + - pip install --user sphinx + - pip install --user sphinx_rtd_theme +script: + - .travis/compile-all.sh $TRAVIS_SCALA_VERSION + - .travis/test-core.sh $TRAVIS_SCALA_VERSION + - .travis/test-leveldb.sh $TRAVIS_SCALA_VERSION + - .travis/test-crdt.sh $TRAVIS_SCALA_VERSION + - .travis/test-stream.sh $TRAVIS_SCALA_VERSION + - .travis/test-spark.sh $TRAVIS_SCALA_VERSION + - .travis/test-vertx.sh $TRAVIS_SCALA_VERSION + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm +after_success: + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ]; then .travis/publish-all.sh $TRAVIS_SCALA_VERSION; fi +scala: + - 2.11.8 + - 2.12.0 diff --git a/examples/user-guide/README.md b/examples/user-guide/README.md new file mode 100644 index 00000000..57075134 --- /dev/null +++ b/examples/user-guide/README.md @@ -0,0 +1,8 @@ +# Eventuate User Guide + +[![Build Status](https://travis-ci.org/mslinn/eventuate.svg?branch=newSerialDocs)](https://travis-ci.org/mslinn/eventuate) + +This project runs the Scala sample code for the Eventuate +[User Guide](http://rbmhtechnology.github.io/eventuate/user-guide.html#event-sourced-actors). +This code is provided as a +[standalone SBT project](https://github.com/RBMHTechnology/eventuate/blob/master/examples/user-guide/). diff --git a/examples/user-guide/bin/docs b/examples/user-guide/bin/docs new file mode 100644 index 00000000..3e840022 --- /dev/null +++ b/examples/user-guide/bin/docs @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$DIR/.." + +rm -rf target/html +sphinx-build -b html ../../src/sphinx/ target/html/ &> sphinx.log + diff --git a/examples/user-guide/build.sbt b/examples/user-guide/build.sbt new file mode 100644 index 00000000..44c97533 --- /dev/null +++ b/examples/user-guide/build.sbt @@ -0,0 +1,86 @@ +organization := "com.micronautics" + +name := "eventuate-user-guide" + +version := "0.2.0" + +scalaVersion := "2.12.1" + +scalacOptions ++= Seq( + "-deprecation", + "-encoding", "UTF-8", + "-feature", + "-target:jvm-1.8", + "-unchecked", + "-Ywarn-adapted-args", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Ywarn-unused", + "-Ywarn-value-discard", + "-Xfuture", + "-Xlint" +) + +scalacOptions in (Compile, doc) ++= baseDirectory.map { + (bd: File) => Seq[String]( + "-sourcepath", bd.getAbsolutePath, + "-doc-source-url", "https://github.com/mslinn/eventuate/tree/master€{FILE_PATH}.scala" + ) +}.value + +javacOptions ++= Seq( + "-Xlint:deprecation", + "-Xlint:unchecked", + "-source", "1.8", + "-target", "1.8", + "-g:vars" +) + +resolvers += "Eventuate Releases" at "https://dl.bintray.com/rbmhtechnology/maven" + +val evVer = "0.8.1" + +libraryDependencies ++= Seq( + "com.rbmhtechnology" %% "eventuate-core" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-crdt" % evVer withSources(), + "com.rbmhtechnology" %% "eventuate-log-leveldb" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-log-cassandra" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-adapter-stream" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-adapter-vertx" % evVer withSources(), +// "com.rbmhtechnology" %% "eventuate-adapter-spark" % evVer withSources(), + "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8" withSources(), + // + "org.scalatest" %% "scalatest" % "3.0.1" % Test withSources(), + "junit" % "junit" % "4.12" % Test +) + +discoveredMainClasses in Compile += "japi.ActorExample" +discoveredMainClasses in Compile += "japi.CommunicationExample" +discoveredMainClasses in Compile += "japi.ConcurrentExample" +discoveredMainClasses in Compile += "japi.CrdtExample" +discoveredMainClasses in Compile += "japi.ResolveExample" +discoveredMainClasses in Compile += "japi.TrackingExample" +discoveredMainClasses in Compile += "japi.ViewExample" + +parallelExecution in Test := false +fork in Test := true +fork in Runtime := true + +logLevel := Level.Warn + +// Only show warnings and errors on the screen for compilations. +// This applies to both test:compile and compile and is Info by default +logLevel in compile := Level.Warn + +// Level.INFO is needed to see detailed output when running tests +logLevel in test := Level.Info + +// define the statements initially evaluated when entering 'console', 'console-quick', but not 'console-project' +initialCommands in console := """import akka.actor._ + |import com.rbmhtechnology.eventuate.EventsourcedActor + |import scala.util._ + |""".stripMargin + +cancelable := true + +sublimeTransitive := true diff --git a/examples/user-guide/project/build.properties b/examples/user-guide/project/build.properties new file mode 100644 index 00000000..27e88aa1 --- /dev/null +++ b/examples/user-guide/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.13 diff --git a/examples/user-guide/project/plugins.sbt b/examples/user-guide/project/plugins.sbt new file mode 100644 index 00000000..9a317f57 --- /dev/null +++ b/examples/user-guide/project/plugins.sbt @@ -0,0 +1,12 @@ +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") + +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") + +// See https://github.com/orrsella/sbt-sublime +addSbtPlugin("com.orrsella" % "sbt-sublime" % "1.1.1") + +// See https://github.com/jrudolph/sbt-dependency-graph +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.0") + +// See https://github.com/rtimush/sbt-updates +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0") diff --git a/examples/user-guide/src/main/java/japi/ActorExample.java b/examples/user-guide/src/main/java/japi/ActorExample.java new file mode 100644 index 00000000..495541ad --- /dev/null +++ b/examples/user-guide/src/main/java/japi/ActorExample.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package japi; + +//#event-sourced-actor +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.japi.pf.ReceiveBuilder; +import com.rbmhtechnology.eventuate.AbstractEventsourcedActor; +import com.rbmhtechnology.eventuate.ReplicationConnection; +import com.rbmhtechnology.eventuate.ResultHandler; +import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import static akka.actor.ActorRef.noSender; +import static java.lang.System.out; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Stream.concat; +import static java.util.stream.Stream.of; + +class ExampleActor extends AbstractEventsourcedActor { + private Messages msgs = new Messages(); // outer class reference + private final Optional aggregateId; + + private Collection currentState = Collections.emptyList(); + + public ExampleActor(String id, Optional aggregateId, ActorRef eventLog) { + super(id, eventLog); + this.aggregateId = aggregateId; + + setOnCommand(ReceiveBuilder + .match(Messages.PrintCommand.class, cmd -> printState(id, currentState)) + .match(Messages.AppendCommand.class, cmd -> persist(msgs.new AppendedEvent(cmd.entry), ResultHandler.on( + evt -> sender().tell(msgs.new AppendSuccessCommandReply(evt.entry), self()), + err -> sender().tell(msgs.new AppendFailureCommandReply(err), self()) + ))) + .build()); + + setOnEvent(ReceiveBuilder + .match(Messages.AppendedEvent.class, evt -> currentState = append(currentState, evt.entry)) + .build()); + } + + @Override + public Optional getAggregateId() { + return aggregateId; + } + + private void printState(String id, Collection currentState) { + out.println(String.format("[id = %s, aggregate id = %s] %s", id, getAggregateId().orElseGet(() -> "undefined"), + String.join(",", currentState))); + } + + private Collection append(Collection collection, T el) { + return concat(collection.stream(), of(el)).collect(toList()); + } +} +//# + +public class ActorExample { + public static void main(String[] args) throws InterruptedException { + + //#create-one-instance + Messages msgs = new Messages(); // outer class reference + + final ActorSystem system = // ... + //# + ActorSystem.create(ReplicationConnection.DefaultRemoteSystemName()); + + //#create-one-instance + final ActorRef eventLog = // ... + //# + system.actorOf(LeveldbEventLog.props("qt-1", "", false)); + + + //#create-one-instance + + final ActorRef ea1 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("1", Optional.of("a"), eventLog))); + + ea1.tell(msgs.new AppendCommand("a"), noSender()); + ea1.tell(msgs.new AppendCommand("b"), noSender()); + //# + + //#print-one-instance + ea1.tell(msgs.new PrintCommand(), noSender()); + //# + + //#create-two-instances + final ActorRef b2 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("2", Optional.of("b"), eventLog))); + final ActorRef c3 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("3", Optional.of("c"), eventLog))); + + b2.tell(msgs.new AppendCommand("a"), noSender()); + b2.tell(msgs.new AppendCommand("b"), noSender()); + + c3.tell(msgs.new AppendCommand("x"), noSender()); + c3.tell(msgs.new AppendCommand("y"), noSender()); + //# + + //#print-two-instances + b2.tell(msgs.new PrintCommand(), noSender()); + c3.tell(msgs.new PrintCommand(), noSender()); + //# + + //#create-replica-instances + // created at location 1 + final ActorRef d4 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("4", Optional.of("d"), eventLog))); + + // created at location 2 + final ActorRef d5 = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("5", Optional.of("d"), eventLog))); + + d4.tell(msgs.new AppendCommand("a"), noSender()); + //# + + Thread.sleep(1000); + + d4.tell(msgs.new PrintCommand(), noSender()); + d5.tell(msgs.new PrintCommand(), noSender()); + + //#send-another-append + d5.tell(msgs.new AppendCommand("b"), noSender()); + //# + + Thread.sleep(1000); + + d4.tell(msgs.new PrintCommand(), noSender()); + d5.tell(msgs.new PrintCommand(), noSender()); + } +} diff --git a/examples/user-guide/src/main/java/japi/CommunicationExample.java b/examples/user-guide/src/main/java/japi/CommunicationExample.java new file mode 100644 index 00000000..a4796e7a --- /dev/null +++ b/examples/user-guide/src/main/java/japi/CommunicationExample.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#event-driven-communication + +package japi; + +//#event-driven-communication1 +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.japi.pf.ReceiveBuilder; +import com.rbmhtechnology.eventuate.AbstractEventsourcedActor; +import com.rbmhtechnology.eventuate.ResultHandler; +import static akka.actor.ActorRef.noSender; + +//# + +public class CommunicationExample { + //#ping-actor + + class PingActor extends AbstractEventsourcedActor { + public PingActor(String id, ActorRef eventLog, ActorRef completion) { + super(id, eventLog); + + setOnCommand(ReceiveBuilder + .matchEquals("serve", cmd -> persist(new PingEvent(1), ResultHandler.none())) + .build()); + + setOnEvent(ReceiveBuilder + .match(PongEvent.class, evt -> evt.num == 10 && !recovering(), evt -> completion.tell("done", self())) + .match(PongEvent.class, evt -> persistOnEvent(new PingEvent(evt.num + 1))) + .build()); + } + } + //# + //#pong-actor + + class PongActor extends AbstractEventsourcedActor { + public PongActor(String id, ActorRef eventLog) { + super(id, eventLog); + + setOnEvent(ReceiveBuilder + .match(PingEvent.class, evt -> persistOnEvent(new PongEvent(evt.num))) + .build()); + } + } + + //#ping-pong-events + + class PingEvent { + public final Integer num; + + public PingEvent(Integer num) { + this.num = num; + } + } + + class PongEvent { + public final Integer num; + + public PongEvent(Integer num) { + this.num = num; + } + } + //# + + public static void main(String[] args) { + //#event-driven-communication1 + final ActorSystem system = ActorSystem.create("system"); + final ActorRef eventLog = null; + //# + //#event-driven-communication2 + + CommunicationExample ce = new CommunicationExample(); + final ActorRef pingActor = system.actorOf( + Props.create( + PingActor.class, + () -> ce.new PingActor("ping", eventLog, system.deadLetters()) + ) + ); + final ActorRef pongActor = system.actorOf( + Props.create( + PongActor.class, + () -> ce.new PongActor("pong", eventLog) + ) + ); + + pingActor.tell("serve", noSender()); + //# + } +} diff --git a/examples/user-guide/src/main/java/japi/ConcurrentExample.java b/examples/user-guide/src/main/java/japi/ConcurrentExample.java new file mode 100644 index 00000000..e274ddcd --- /dev/null +++ b/examples/user-guide/src/main/java/japi/ConcurrentExample.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package japi; + +import static japi.DocUtils.append; + +//#detecting-concurrent-update +import akka.actor.ActorRef; +import akka.japi.pf.ReceiveBuilder; +import com.rbmhtechnology.eventuate.AbstractEventsourcedActor; +import com.rbmhtechnology.eventuate.VectorTime; + +import java.util.Collection; +import java.util.Collections; + +//# +public class ConcurrentExample { + + //#detecting-concurrent-update + + class ExampleActor extends AbstractEventsourcedActor { + private Collection currentState = Collections.emptyList(); + private VectorTime updateTimestamp = VectorTime.Zero(); + + public ExampleActor(String id, ActorRef eventLog) { + super(id, eventLog); + + setOnEvent(ReceiveBuilder + .match(Messages.AppendedEvent.class, evt -> { + if (updateTimestamp.lt(lastVectorTimestamp())) { + // regular update + currentState = append(currentState, evt.entry); + updateTimestamp = lastVectorTimestamp(); + } else if (updateTimestamp.conc(lastVectorTimestamp())) { + // concurrent update + // TODO: track conflicting versions + } + }) + .build()); + } + } + //# + + // TODO Make this into an executable example +} diff --git a/src/sphinx/code/userguide/japi/ConditionalExample.java b/examples/user-guide/src/main/java/japi/ConditionalExample.java similarity index 67% rename from src/sphinx/code/userguide/japi/ConditionalExample.java rename to examples/user-guide/src/main/java/japi/ConditionalExample.java index df000602..9970767e 100644 --- a/src/sphinx/code/userguide/japi/ConditionalExample.java +++ b/examples/user-guide/src/main/java/japi/ConditionalExample.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package userguide.japi; +package japi; -import static userguide.japi.DocUtils.append; -import userguide.japi.ViewExample.GetAppendCountReply; +import static japi.DocUtils.append; +import japi.ViewExample.GetAppendCountReply; //#conditional-requests import akka.actor.ActorRef; @@ -28,9 +28,6 @@ import akka.util.Timeout; import com.rbmhtechnology.eventuate.*; import scala.concurrent.ExecutionContextExecutor; -import scala.concurrent.Future; -import scala.reflect.ClassTag; -import scala.reflect.ClassTag$; import java.util.Collection; import java.util.Collections; @@ -42,27 +39,26 @@ //# public class ConditionalExample { - //#conditional-requests class ExampleActor extends AbstractEventsourcedActor { - private final String id; private Collection currentState = Collections.emptyList(); + private Messages msgs = new Messages(); public ExampleActor(String id, ActorRef eventLog) { super(id, eventLog); this.id = id; setOnCommand(ReceiveBuilder - .match(Append.class, cmd -> persist(new Appended(cmd.entry), ResultHandler.onSuccess( - evt -> sender().tell(new AppendSuccess(evt.entry, lastVectorTimestamp()), self()) + .match(Messages.AppendCommand.class, cmd -> persist(msgs.new AppendedEvent(cmd.entry), ResultHandler.onSuccess( + evt -> sender().tell(msgs.new AppendSuccessWithTimestampCommandReply(evt.entry, lastVectorTimestamp()), self()) ))) // ... .build()); setOnEvent(ReceiveBuilder - .match(Appended.class, evt -> currentState = append(currentState, evt.entry)) + .match(Messages.AppendedEvent.class, evt -> currentState = append(currentState, evt.entry)) .build()); } @@ -72,29 +68,8 @@ public Optional getAggregateId() { } } - // Command - public class Append { - public final String entry; - - public Append(String entry) { - this.entry = entry; - } - } - - // Command reply - public class AppendSuccess { - public final String entry; - public final VectorTime updateTimestamp; - - public AppendSuccess(String entry, VectorTime updateTimestamp) { - this.entry = entry; - this.updateTimestamp = updateTimestamp; - } - } - // Eventsourced-View class ExampleView extends AbstractEventsourcedView { - // AbstractEventsourcedView has ConditionalRequests mixed-in by default public ExampleView(String id, ActorRef eventLog) { @@ -104,21 +79,26 @@ public ExampleView(String id, ActorRef eventLog) { } } //# - public void main() { - + public static void main(String[] args) { final ActorSystem system = ActorSystem.create(""); final ActorRef eventLog = null; final ExecutionContextExecutor dispatcher = system.dispatcher(); //#conditional-requests - final ActorRef ea = system.actorOf(Props.create(ExampleActor.class, () -> new ExampleActor("ea", eventLog))); - final ActorRef ev = system.actorOf(Props.create(ExampleView.class, () -> new ExampleView("ev", eventLog))); + ConditionalExample ce = new ConditionalExample(); + Messages msgs = new Messages(); + final ActorRef ea = system.actorOf(Props.create(ExampleActor.class, () -> ce.new ExampleActor("ea", eventLog))); + final ActorRef ev = system.actorOf(Props.create(ExampleView.class, () -> ce.new ExampleView("ev", eventLog))); final Timeout timeout = Timeout.apply(5, TimeUnit.SECONDS); - Patterns.ask(ea, new Append("a"), timeout) - .flatMap(func(m -> Patterns.ask(ev, new ConditionalRequest(((AppendSuccess) m).updateTimestamp, new GetAppendCount()), timeout)), dispatcher) + Patterns.ask(ea, msgs.new AppendCommand("a"), timeout) + .flatMap(func(m -> + Patterns.ask(ev, + new ConditionalRequest(((Messages.AppendSuccessWithTimestampCommandReply) m).updateTimestamp, ce.new GetAppendCountCommand()), timeout) + ) + , dispatcher) .onComplete(proc(result -> { if (result.isSuccess()) { System.out.println("append count = " + ((GetAppendCountReply) result.get()).count); @@ -128,6 +108,5 @@ public void main() { //# } - class GetAppendCount { - } + class GetAppendCountCommand {} } diff --git a/examples/user-guide/src/main/java/japi/CrdtExample.java b/examples/user-guide/src/main/java/japi/CrdtExample.java new file mode 100644 index 00000000..5d60c447 --- /dev/null +++ b/examples/user-guide/src/main/java/japi/CrdtExample.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package japi; + +import akka.actor.ActorRef; +import akka.actor.ActorSystem; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +// TODO turn this into a runnable example +public class CrdtExample { + //#or-set-service + /** Java API of a replicated [[ORSet]] CRDT service. */ + class ORSetService extends CRDTService, Set> { + + /** @param serviceId Unique id of this service. + * @param log Event log. + * @param system Actor system. + * @tparam A [[ORSet]] entry type. */ + ORSetService(String serviceId, ActorRef log, ActorSystem system) { + super(serviceId, log, system); + + start(); + } + + public CompletionStage> add(String id, A entry) { + return op(id, new AddOp(entry)); + } + + public CompletionStage> remove(String id, A entry) { + return op(id, new RemoveOp(entry)); + } + } + //# + + abstract class CRDTService { + private final String serviceId; + private final ActorRef log; + private final ActorSystem system; + + protected CRDTService(String serviceId, ActorRef log, ActorSystem system) { + this.serviceId = serviceId; + this.log = log; + this.system = system; + } + + public CompletionStage op(String id, Object op) { + return CompletableFuture.completedFuture(null); + } + + public void start() {} + } + + interface ORSet {} + + private class AddOp { + final Object entry; + + private AddOp(Object entry) { + this.entry = entry; + } + } + + private class RemoveOp { + final Object entry; + + private RemoveOp(Object entry) { + this.entry = entry; + } + } +} diff --git a/examples/user-guide/src/main/java/japi/DocUtils.java b/examples/user-guide/src/main/java/japi/DocUtils.java new file mode 100644 index 00000000..99ca0191 --- /dev/null +++ b/examples/user-guide/src/main/java/japi/DocUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package japi; + +import java.util.Collection; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Stream.concat; +import static java.util.stream.Stream.of; + +public final class DocUtils { + private DocUtils() {} + + public static Collection append(Collection collection, T el) { + return concat(collection.stream(), of(el)).collect(toList()); + } +} diff --git a/examples/user-guide/src/main/java/japi/Messages.java b/examples/user-guide/src/main/java/japi/Messages.java new file mode 100644 index 00000000..4b8bf21d --- /dev/null +++ b/examples/user-guide/src/main/java/japi/Messages.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#event-sourced-actor +package japi; + +import com.rbmhtechnology.eventuate.VectorTime; + +/** Outer class allows many inner classes to be defined in one file */ +public class Messages { + // Commands + public class PrintCommand {} + + public class AppendCommand { + public final String entry; + + public AppendCommand(String entry) { + this.entry = entry; + } + } + + // Command replies + public class AppendSuccessCommandReply { + public final String entry; + + public AppendSuccessCommandReply(String entry) { + this.entry = entry; + } + } + + public class AppendFailureCommandReply { + public final Throwable cause; + + public AppendFailureCommandReply(Throwable cause) { + this.cause = cause; + } + } + + // Events + public class AppendedEvent { + public final String entry; + + public AppendedEvent(String entry) { + this.entry = entry; + } + } +//# +//#conditional-requests + // Command reply + public class AppendSuccessWithTimestampCommandReply { + public final String entry; + public final VectorTime updateTimestamp; + + public AppendSuccessWithTimestampCommandReply(String entry, VectorTime updateTimestamp) { + this.entry = entry; + this.updateTimestamp = updateTimestamp; + } + } +//# +//#event-sourced-actor +} +//# diff --git a/src/sphinx/code/userguide/japi/ResolveExample.java b/examples/user-guide/src/main/java/japi/ResolveExample.java similarity index 76% rename from src/sphinx/code/userguide/japi/ResolveExample.java rename to examples/user-guide/src/main/java/japi/ResolveExample.java index 6f925aff..d0cecd7f 100644 --- a/src/sphinx/code/userguide/japi/ResolveExample.java +++ b/examples/user-guide/src/main/java/japi/ResolveExample.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package userguide.japi; - //#automated-conflict-resolution +package japi; + import akka.actor.ActorRef; import akka.japi.pf.ReceiveBuilder; import com.rbmhtechnology.eventuate.*; @@ -26,26 +26,25 @@ import java.util.Collections; import java.util.stream.Stream; -import static userguide.japi.DocUtils.append; +import static japi.DocUtils.append; + +// TODO turn this into a runnable example //# public class ResolveExample { - public static class Auto { - //#automated-conflict-resolution - class ExampleActor extends AbstractEventsourcedActor { private ConcurrentVersions, String> versionedState = - ConcurrentVersionsTree.create(Collections.emptyList(), (s, a) -> append(s, a)); + ConcurrentVersionsTree.create(Collections.emptyList(), DocUtils::append); public ExampleActor(String id, ActorRef eventLog) { super(id, eventLog); setOnEvent(ReceiveBuilder - .match(Appended.class, evt -> { + .match(Messages.AppendedEvent.class, evt -> { versionedState = versionedState.update(evt.entry, lastVectorTimestamp(), lastSystemTimestamp(), lastEmitterId()); if (versionedState.conflict()) { @@ -68,11 +67,9 @@ public ExampleActor(String id, ActorRef eventLog) { } public static class Interactive { - //#interactive-conflict-resolution class ExampleActor extends AbstractEventsourcedActor { - private ConcurrentVersions, String> versionedState = ConcurrentVersionsTree.create(Collections.emptyList(), (s, a) -> append(s, a)); @@ -80,23 +77,23 @@ public ExampleActor(String id, ActorRef eventLog) { super(id, eventLog); setOnCommand(ReceiveBuilder - .match(Append.class, cmd -> versionedState.conflict(), - cmd -> sender().tell(new AppendRejected(cmd.entry, versionedState.getAll()), self()) + .match(AppendCommand.class, cmd -> versionedState.conflict(), + cmd -> sender().tell(new AppendRejectedCommandReply(cmd.entry, versionedState.getAll()), self()) ) - .match(Append.class, cmd -> { + .match(AppendCommand.class, cmd -> { // .... }) - .match(Resolve.class, cmd -> persist(new Resolved(cmd.selectedTimestamp), ResultHandler.on( + .match(ResolveCommand.class, cmd -> persist(new ResolvedCommandReply(cmd.selectedTimestamp), ResultHandler.on( evt -> { /* reply to sender omitted ... */ }, err -> { /* reply to sender omitted ... */ } ))) .build()); setOnEvent(ReceiveBuilder - .match(Appended.class, evt -> + .match(Messages.AppendedEvent.class, evt -> versionedState = versionedState.update(evt.entry, lastVectorTimestamp(), lastSystemTimestamp(), lastEmitterId()) ) - .match(Resolved.class, evt -> + .match(ResolvedCommandReply.class, evt -> versionedState = versionedState.resolve(evt.selectedTimestamp, lastVectorTimestamp(), lastSystemTimestamp()) ) .build()); @@ -104,39 +101,39 @@ public ExampleActor(String id, ActorRef eventLog) { } // Command - class Append { + class AppendCommand { public final String entry; - public Append(String entry) { + public AppendCommand(String entry) { this.entry = entry; } } // Command reply - class AppendRejected { + class AppendRejectedCommandReply { public final String entry; public final Collection>> conflictingVersions; - public AppendRejected(String entry, Collection>> conflictingVersions) { + public AppendRejectedCommandReply(String entry, Collection>> conflictingVersions) { this.entry = entry; this.conflictingVersions = conflictingVersions; } } // Command - class Resolve { + class ResolveCommand { public final VectorTime selectedTimestamp; - public Resolve(VectorTime selectedTimestamp) { + public ResolveCommand(VectorTime selectedTimestamp) { this.selectedTimestamp = selectedTimestamp; } } // Command reply - class Resolved { + class ResolvedCommandReply { public final VectorTime selectedTimestamp; - public Resolved(VectorTime selectedTimestamp) { + public ResolvedCommandReply(VectorTime selectedTimestamp) { this.selectedTimestamp = selectedTimestamp; } } diff --git a/examples/user-guide/src/main/java/japi/TrackingExample.java b/examples/user-guide/src/main/java/japi/TrackingExample.java new file mode 100644 index 00000000..fd4ed070 --- /dev/null +++ b/examples/user-guide/src/main/java/japi/TrackingExample.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package japi; + +//#tracking-conflicting-versions + +import akka.actor.ActorRef; +import akka.japi.pf.ReceiveBuilder; +import com.rbmhtechnology.eventuate.*; + +import java.util.Collection; +import java.util.Collections; +//# + +// TODO turn this into a runnable example +public class TrackingExample { + //#tracking-conflicting-versions + + class ExampleActor extends AbstractEventsourcedActor { + + private ConcurrentVersions, String> versionedState = + ConcurrentVersionsTree.create(Collections.emptyList(), DocUtils::append); + + public ExampleActor(String id, ActorRef eventLog) { + super(id, eventLog); + + setOnEvent(ReceiveBuilder + .match(Messages.AppendedEvent.class, evt -> { + versionedState = versionedState.update(evt.entry, lastVectorTimestamp(), lastSystemTimestamp(), lastEmitterId()); + + if (versionedState.conflict()) { + final Collection>> all = versionedState.getAll(); + // TODO: resolve conflicting versions + } else { + final Collection currentState = versionedState.getAll().get(0).value(); + // ... + } + }) + .build()); + } + } + //# +} diff --git a/src/sphinx/code/userguide/japi/ViewExample.java b/examples/user-guide/src/main/java/japi/ViewExample.java similarity index 95% rename from src/sphinx/code/userguide/japi/ViewExample.java rename to examples/user-guide/src/main/java/japi/ViewExample.java index 6fd78533..2da67561 100644 --- a/src/sphinx/code/userguide/japi/ViewExample.java +++ b/examples/user-guide/src/main/java/japi/ViewExample.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package userguide.japi; - //#event-sourced-view +package japi; + import akka.actor.ActorRef; import akka.japi.pf.ReceiveBuilder; import com.rbmhtechnology.eventuate.AbstractEventsourcedView; import com.rbmhtechnology.eventuate.VectorTime; //# +// TODO turn this into a runnable example public class ViewExample { - //#event-sourced-view class ExampleView extends AbstractEventsourcedView { @@ -49,11 +49,9 @@ public ExampleView(String id, ActorRef eventLog) { } // Commands - class GetAppendCount { - } + class GetAppendCount { } - class GetResolveCount { - } + class GetResolveCount { } // Command replies class GetAppendCountReply { diff --git a/examples/user-guide/src/main/resources/application.conf b/examples/user-guide/src/main/resources/application.conf new file mode 100644 index 00000000..b9a3b029 --- /dev/null +++ b/examples/user-guide/src/main/resources/application.conf @@ -0,0 +1,12 @@ +akka { + log-dead-letters = off + + log-dead-letters-during-shutdown = off + + persistence { + journal { + plugin = "akka.persistence.journal.leveldb" + leveldb.native = off + } + } +} diff --git a/examples/user-guide/src/main/scala/sapi/ActorExample.scala b/examples/user-guide/src/main/scala/sapi/ActorExample.scala new file mode 100644 index 00000000..73767d37 --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/ActorExample.scala @@ -0,0 +1,126 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +object ActorExample extends App { + //#event-sourced-actor + import akka.actor._ + import com.rbmhtechnology.eventuate.EventsourcedActor + import scala.util._ + + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + import scala.collection.mutable + private val currentState: mutable.ArrayBuffer[String] = mutable.ArrayBuffer.empty + + override def onCommand: PartialFunction[Any, Unit] = { + case PrintCommand => + println(s"[id = $id, aggregate id = ${ aggregateId.getOrElse("")}] ${ currentState.mkString(",") }") + + case AppendCommand(entry) => persist(AppendedEvent(entry)) { + case Success(_) => sender() ! AppendSuccessCommandReply(entry) + case Failure(err) => sender() ! AppendFailureCommandReply(err) + } + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => + currentState :+ entry + () + } + } + //# + + //#create-one-instance + import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog + import com.rbmhtechnology.eventuate.ReplicationConnection._ + + // `DefaultRemoteSystemName` is defined as "location" in the + // [[ReplicationConnection]] object, so the ActorSystem name + // is "location" + implicit val system: ActorSystem = ActorSystem(DefaultRemoteSystemName) + //# + + //#create-one-instance + + // Wrap a new instance of a `LeveldbEventLog` configuration object with + // log id "qt-1" into an Actor. + // This creates a log directory called `target/log-qt-1/` + val eventLog: ActorRef = system.actorOf(LeveldbEventLog.props("qt-1")) + //# + + //#create-one-instance + + // Create a new instance of ExampleActor with id=="1" and aggregateId==Some("a"); + // also provide the eventLog [[ActorRef]] to the actorOf [[Actor]] factory + val ea1 = system.actorOf(Props(new ExampleActor("1", Some("a"), eventLog))) + + ea1 ! AppendCommand("a") + ea1 ! AppendCommand("b") + //# + + //#print-one-instance + ea1 ! PrintCommand + //# + + //#create-two-instances + val b2: ActorRef = + system.actorOf(Props(new ExampleActor("2", Some("b"), eventLog))) + val c3: ActorRef = + system.actorOf(Props(new ExampleActor("3", Some("c"), eventLog))) + + b2 ! AppendCommand("a") + b2 ! AppendCommand("b") + + c3 ! AppendCommand("x") + c3 ! AppendCommand("y") + //# + + //#print-two-instances + b2 ! PrintCommand + c3 ! PrintCommand + //# + + //#create-replica-instances + // created at location 1 + val d4 = system.actorOf(Props(new ExampleActor("4", Some("d"), eventLog))) + + // created at location 2 + val d5 = system.actorOf(Props(new ExampleActor("5", Some("d"), eventLog))) + + d4 ! AppendCommand("a") + //# + + Thread.sleep(1000) + + d4 ! PrintCommand // fixme why is this not referenced in user-guide.rst? + d5 ! PrintCommand + + //#send-another-append + d5 ! AppendCommand("b") + //# + + Thread.sleep(1000) + + d4 ! PrintCommand + d5 ! PrintCommand + + Util.pauseThenStop() +} diff --git a/examples/user-guide/src/main/scala/sapi/CommunicationExample.scala b/examples/user-guide/src/main/scala/sapi/CommunicationExample.scala new file mode 100644 index 00000000..f1dff17a --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/CommunicationExample.scala @@ -0,0 +1,98 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +//#ping-pong-events +sealed trait SPEvent +case class PingEvent(num: Int) extends SPEvent +case class PongEvent(num: Int) extends SPEvent +//# + +object CommunicationExample extends App { + //#event-driven-communication1 + import akka.actor._ + import com.rbmhtechnology.eventuate.{EventsourcedActor, PersistOnEvent} + import com.rbmhtechnology.eventuate.EventsourcedView.Handler + import com.rbmhtechnology.eventuate.ReplicationConnection.DefaultRemoteSystemName + import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog + + val system: ActorSystem = ActorSystem(DefaultRemoteSystemName) + val eventLog: ActorRef = system.actorOf(LeveldbEventLog.props("qt-1")) + //# + //#ping-actor + + class PingActor( + val id: String, + val eventLog: ActorRef, + completion: ActorRef + ) extends EventsourcedActor with PersistOnEvent { + override def onCommand: PartialFunction[Any, Unit] = { + case "serve" => + persist(PingEvent(1))(Handler.empty) + + case x => + Console.err.println(s"Error: PingActor.onCommand did not expect a $x message") + } + + override def onEvent: PartialFunction[Any, Unit] = { + case PongEvent(10) if !recovering => + completion ! "done" + + case PongEvent(i) => + persistOnEvent(PingEvent(i + 1)) + + case x => + // Error: PongActor.onEvent did not expect a Append(a) message + // Error: PingActor.onEvent did not expect a PingEvent(6222) message + // Error: PongActor.onEvent did not expect a PongEvent(6222) message + Console.err.println(s"Error: PingActor.onEvent did not expect a $x message") + } + } + //# + //#pong-actor + + class PongActor( + val id: String, + val eventLog: ActorRef + ) extends EventsourcedActor with PersistOnEvent { + override def onCommand: PartialFunction[Any, Unit] = { + case _ => + } + + override def onEvent: PartialFunction[Any, Unit] = { + case PingEvent(i) => + persistOnEvent(PongEvent(i)) + + case x => + // leftovers from running ActorExample: + // Error: PingActor.onEvent did not expect a Append(x) message + Console.err.println(s"Error: PongActor.onEvent did not expect a $x message") + } + } + //# + //#event-driven-communication2 + + val pingActor: ActorRef = system.actorOf( + Props(new PingActor("ping", eventLog, system.deadLetters)) + ) + val pongActor: ActorRef = system.actorOf( + Props(new PongActor("pong", eventLog)) + ) + + pingActor ! "serve" + //# +} diff --git a/examples/user-guide/src/main/scala/sapi/ConcurrentExample.scala b/examples/user-guide/src/main/scala/sapi/ConcurrentExample.scala new file mode 100644 index 00000000..c91cadd2 --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/ConcurrentExample.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +/** Referenced `TrackingExample`. */ +object ConcurrentExample extends App { + //#detecting-concurrent-update + import akka.actor._ + import com.rbmhtechnology.eventuate.{EventsourcedActor, VectorTime} + + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + private var currentState: Vector[String] = Vector.empty + // This is the first time we see a VectorTime instance: + private var updateTimestamp: VectorTime = VectorTime() + + def onCommand: PartialFunction[Any, Unit] = { + case _ => // ... + //# + + // todo add code here to make this example more realistic + + //#detecting-concurrent-update + } + + def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry2) => + if (updateTimestamp < lastVectorTimestamp) { // regular update + currentState = currentState :+ entry2 + updateTimestamp = lastVectorTimestamp + } else if (updateTimestamp conc lastVectorTimestamp) { + // concurrent update + // TODO: track conflicting versions + } + } + } + //# +} diff --git a/examples/user-guide/src/main/scala/sapi/ConditionalExample.scala b/examples/user-guide/src/main/scala/sapi/ConditionalExample.scala new file mode 100644 index 00000000..9a5554e4 --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/ConditionalExample.scala @@ -0,0 +1,93 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +object ConditionalExample extends App { + import akka.actor._ + //#conditional-requests + import akka.pattern.ask + import akka.util.Timeout + import com.rbmhtechnology.eventuate._ + import EventsourcedViews._ + import scala.concurrent.duration._ + import scala.util._ + + //# + import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog + + implicit val system: ActorSystem = ActorSystem(ReplicationConnection.DefaultRemoteSystemName) + + // This will create a directory called `target/log-qt-2/` to contain the log files + val eventLog: ActorRef = system.actorOf(LeveldbEventLog.props("qt-2")) + + //#conditional-requests + class ExampleActor( + override val id: String, + override val eventLog: ActorRef + ) extends EventsourcedActor { + private var currentState: Vector[String] = Vector.empty + override val aggregateId = Some(id) + + override def onCommand: PartialFunction[Any, Unit] = { + case AppendCommand(entry) => persist(AppendedEvent(entry)) { + case Success(_) => + sender() ! AppendSuccessWithTimestampCommandReply(entry, lastVectorTimestamp) + + case Failure(_) => + // ... + } + // ... + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => currentState = currentState :+ entry + } + } + + class ExampleView(override val id: String, override val eventLog: ActorRef) + extends EventsourcedView with ConditionalRequests { + // ... + //# + private var appendCount: Long = 0L + private var resolveCount: Long = 0L + + override def onCommand: PartialFunction[Any, Unit] = { + case GetAppendCount => sender() ! GetAppendCountReply(appendCount) + case GetResolveCount => sender() ! GetResolveCountReply(resolveCount) + } + + override def onEvent: PartialFunction[Any, Unit] = { + case _: AppendedEvent => appendCount += 1L + case _: Resolved => resolveCount += 1L + } + //#conditional-requests + } + + val ea: ActorRef = system.actorOf(Props(new ExampleActor("ea", eventLog))) + val ev: ActorRef = system.actorOf(Props(new ExampleView("ev", eventLog))) + + import system.dispatcher + implicit val timeout = Timeout(5.seconds) + + for { + AppendSuccessWithTimestampCommandReply(_, timestamp) <- ea ? AppendCommand("a") + GetAppendCountReply(count) <- ev ? ConditionalRequest(timestamp, GetAppendCount) + } println(s"append count = $count") + //# + + Util.pauseThenStop() +} diff --git a/examples/user-guide/src/main/scala/sapi/EventsourcedViews.scala b/examples/user-guide/src/main/scala/sapi/EventsourcedViews.scala new file mode 100644 index 00000000..d05388d0 --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/EventsourcedViews.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +/** Used by [[ConditionalExample]] */ +object EventsourcedViews { + //#event-sourced-view + import akka.actor.ActorRef + import com.rbmhtechnology.eventuate.{EventsourcedView, VectorTime} + + sealed trait EventSourcedMsg + + case class Resolved(selectedTimestamp: VectorTime) extends EventSourcedMsg + + case object GetAppendCount extends EventSourcedMsg + case class GetAppendCountReply(count: Long) extends EventSourcedMsg + + case object GetResolveCount extends EventSourcedMsg + case class GetResolveCountReply(count: Long) extends EventSourcedMsg + + class ExampleView(override val id: String, override val eventLog: ActorRef) extends EventsourcedView { + private var appendCount: Long = 0L + private var resolveCount: Long = 0L + + override def onCommand: PartialFunction[Any, Unit] = { + case GetAppendCount => sender() ! GetAppendCountReply(appendCount) + case GetResolveCount => sender() ! GetResolveCountReply(resolveCount) + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(_) => appendCount += 1L + case Resolved(_) => resolveCount += 1L + } + } + //# +} diff --git a/examples/user-guide/src/main/scala/sapi/InteractiveResolveExample.scala b/examples/user-guide/src/main/scala/sapi/InteractiveResolveExample.scala new file mode 100644 index 00000000..a4d68289 --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/InteractiveResolveExample.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +object InteractiveResolveExample extends App { + import akka.actor._ + import scala.util._ + import com.rbmhtechnology.eventuate._ + import scala.collection.immutable.Seq + + //#interactive-conflict-resolution + sealed trait IreAlgebra + + case class Append(entry: String) extends IreAlgebra + case class AppendRejected(entry: String, conflictingVersions: Seq[Versioned[Vector[String]]]) extends IreAlgebra + case class Resolve(selectedTimestamp: VectorTime) extends IreAlgebra + case class Resolved(selectedTimestamp: VectorTime) extends IreAlgebra + + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + private var versionedState: ConcurrentVersions[Vector[String], String] = + ConcurrentVersions(Vector.empty, (s, a) => s :+ a) + + override def onCommand: PartialFunction[Any, Unit] = { + case Append(entry) if versionedState.conflict => + sender() ! AppendRejected(entry, versionedState.all) + + case Append(entry) => + // ... + + case Resolve(selectedTimestamp) => persist(Resolved(selectedTimestamp)) { + case Success(event) => // reply to sender omitted ... + case Failure(ex) => // reply to sender omitted ... + } + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => + versionedState = versionedState + .update(entry, lastVectorTimestamp, lastSystemTimestamp, lastEmitterId) + + case Resolved(selectedTimestamp) => + versionedState = versionedState.resolve(selectedTimestamp, lastVectorTimestamp) + } + } + //# +} diff --git a/examples/user-guide/src/main/scala/sapi/Messages.scala b/examples/user-guide/src/main/scala/sapi/Messages.scala new file mode 100644 index 00000000..a239f27c --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/Messages.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +//#event-sourced-actor +// Commands +sealed trait SCommand +case object PrintCommand extends SCommand +case class AppendCommand(entry: String) extends SCommand + +// Command replies +sealed trait SCommandReply +case class AppendFailureCommandReply(cause: Throwable) extends SCommandReply +case class AppendSuccessCommandReply(entry: String) extends SCommandReply + +// Event +sealed trait SEvent +case class AppendedEvent(entry: String) extends SEvent +//# + +import com.rbmhtechnology.eventuate.VectorTime +//#conditional-requests +case class AppendSuccessWithTimestampCommandReply( + entry: String, + updateTimestamp: VectorTime +) extends SCommandReply +//# diff --git a/examples/user-guide/src/main/scala/sapi/ResolveExample.scala b/examples/user-guide/src/main/scala/sapi/ResolveExample.scala new file mode 100644 index 00000000..ae29528e --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/ResolveExample.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +object ResolveExample extends App { + import akka.actor._ + import com.rbmhtechnology.eventuate._ + + //#automated-conflict-resolution + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + private var versionedState: ConcurrentVersions[Vector[String], String] = + ConcurrentVersions(Vector.empty, (s, a) => s :+ a) + + override def onCommand: PartialFunction[Any, Unit] = { + // ... + //# + case _ => + //#automated-conflict-resolution + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => + versionedState = versionedState.update(entry, lastVectorTimestamp, lastSystemTimestamp, lastEmitterId) + if (versionedState.conflict) { + val conflictingVersions = versionedState.all.sortWith { (v1, v2) => + if (v1.systemTimestamp == v2.systemTimestamp) v1.creator < v2.creator + else v1.systemTimestamp > v2.systemTimestamp + } + val winnerTimestamp: VectorTime = conflictingVersions.head.vectorTimestamp + versionedState = versionedState.resolve(winnerTimestamp) + } + } + } + //# +} diff --git a/examples/user-guide/src/main/scala/sapi/TrackingExample.scala b/examples/user-guide/src/main/scala/sapi/TrackingExample.scala new file mode 100644 index 00000000..8976bb2d --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/TrackingExample.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +object TrackingExample extends App { + import akka.actor._ + + //#tracking-conflicting-versions + import com.rbmhtechnology.eventuate.{ConcurrentVersions, EventsourcedActor, Versioned} + import scala.collection.immutable.Seq + + class ExampleActor( + override val id: String, + override val aggregateId: Option[String], + override val eventLog: ActorRef + ) extends EventsourcedActor { + // notice this is different: + private var versionedState: ConcurrentVersions[Vector[String], String] = + ConcurrentVersions(Vector.empty, (s, a) => s :+ a) + + override def onCommand: PartialFunction[Any, Unit] = { + case _ => // ... + //# + + // todo write something here to make this more realistic + + //#tracking-conflicting-versions + } + + override def onEvent: PartialFunction[Any, Unit] = { + case AppendedEvent(entry) => + versionedState = versionedState.update(entry, lastVectorTimestamp) + if (versionedState.conflict) { + val conflictingVersions: Seq[Versioned[Vector[String]]] = + versionedState.all + val conflictExists = conflictingVersions.size==1 + if (conflictExists) { + // TODO: resolve conflicting versions + } + } else { + val currentState: Vector[String] = versionedState.all.head.value + // happily process causal event + } + } + } + //# +} diff --git a/examples/user-guide/src/main/scala/sapi/Util.scala b/examples/user-guide/src/main/scala/sapi/Util.scala new file mode 100644 index 00000000..9e97c39f --- /dev/null +++ b/examples/user-guide/src/main/scala/sapi/Util.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2015 - 2017 Red Bull Media House GmbH and Mike Slinn - all rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sapi + +import akka.actor.ActorSystem + +object Util { + // Pause for messages to be displayed before shutting down Akka + def pauseThenStop(seconds: Int = 1)(implicit system: ActorSystem): Unit = { + import scala.concurrent.duration._ + import system.dispatcher + import scala.language.postfixOps + system.scheduler.scheduleOnce(seconds seconds) { + system.terminate() + () + } + () + } +} diff --git a/examples/user-guide/src/test/java/.gitkeep b/examples/user-guide/src/test/java/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/user-guide/src/test/resources/application.conf b/examples/user-guide/src/test/resources/application.conf new file mode 100644 index 00000000..b9a3b029 --- /dev/null +++ b/examples/user-guide/src/test/resources/application.conf @@ -0,0 +1,12 @@ +akka { + log-dead-letters = off + + log-dead-letters-during-shutdown = off + + persistence { + journal { + plugin = "akka.persistence.journal.leveldb" + leveldb.native = off + } + } +} diff --git a/examples/user-guide/src/test/scala/.gitkeep b/examples/user-guide/src/test/scala/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/project/ProjectSettings.scala b/project/ProjectSettings.scala index e8ba0666..b02878ec 100644 --- a/project/ProjectSettings.scala +++ b/project/ProjectSettings.scala @@ -177,6 +177,9 @@ object ProjectSettings { git.remoteRepo := "git@github.com:RBMHTechnology/eventuate.git", site.addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), "latest/api"), unmanagedSourceDirectories in Test += baseDirectory.value / "src" / "sphinx"/ "code", + unmanagedSourceDirectories in Test += baseDirectory.value / "examples" / "user-guide"/ "src" / "main" / "java", + unmanagedSourceDirectories in Test += baseDirectory.value / "examples" / "user-guide"/ "src" / "main" / "scala", + unmanagedSourceDirectories in Test += baseDirectory.value / "examples" / "user-guide"/ "src" / "main" / "resources", scalacOptions in (Compile, doc) := List("-skip-packages", "akka") ) @@ -208,4 +211,4 @@ object ProjectSettings { connectInput in run := true ) } -} \ No newline at end of file +} diff --git a/project/build.properties b/project/build.properties index 35c88bab..27e88aa1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.12 +sbt.version=0.13.13 diff --git a/src/sphinx/_ext/includecode.pyc b/src/sphinx/_ext/includecode.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26bb8d89a00007760b5add31ead277197034f4ac GIT binary patch literal 4534 zcmb_gOK%*<5w4kikh@wwM2e3UDQQSa&Ps7eSxOW{vGbDbKnNfMOBk{#Ig`;$?{b#2 zGwbPIi3Da(3VaBVQ~p7K{Er~loP5YVry$5Bhuo8VRXt0t93v-DoMBgWb#>KO)zvks z{A;24i+_LhX-MTy1%H2nNBPhu2gnsnW1U zhc!!ADXG(Njt=K6U87{4h7CGw(6C8|O>pa^8q|a8Ir=rxYogB%FHkhk#9@oAIOsHB zQT!kLJn1+XnkrtSz#LiXBB;JHexgoP?!P$o(_pCVR1H&U7Af6vc^&I{6z3{5@k?cz zprR?I%>|H;(=ZuF>LHfAfy>V@ryt{`V3eQ${}tS>vVm|m0wQn(oH>dyHBW;Eqj8@{ zO^T{?pS`M;UUd&TbyoQducn8ea$_&>LhS9SlVCVXl-J9%A$YwQ<5#gc@-*9?rP_;B z5)WgeqLM#SVH_k_Jshgk=snRIGg>hoGIMz8G#-tVG4mKFLE0Y&eHF6ERXl9ZLp=H` zfL0I<@axdTkuiepPAHXI=A< zclrorDc7J$or;P;Q)6BF%aTMG1})~~zhe%vVDlX9JL*ZN{bBzk*LxaT%>-Xa-=JZ|MLxTkk-*}*ywg;x)G8DD?z`p(e29FQl7X; z#o`Pmd4)X&P;0_m7ZqSG)8H0+S*+4E3t6;58vw$A33kaFdj7^%%%VEEM8ze+mP?A9 z8x8|7{8NP|a73-%6j@QywHqpobX82k>F*|5b&p|5CM~`v0mBfTxffAX%5inl=?U-^-%? zUX81kJ#IM1i?k7Wc)nKIxNxCF?eqDv$=#OXb$MDN?$GFYE!v^UmXt(WL3UYoWNO7_ znrzeQBANH75CC!X7l>=q7SC|qF?VTjkL_a9uFF2IGeNo8rs4`kyY&89bsc)=JABDr6aI8}y4_vIE5-itshxjj^h zYbs9rBJc?=JX|vsyo}sxCXYj0L7=DCnp|Kvb1%(|*UQFf+tAKTQF?aziiS)0q z(2PM-bFOP>?ueIc|^qW~dh_r)|s(ZCFi8;^*g^v{yO zSEo}YeV`Voy)n*MW zG=%J!2C0BanXc@IO#^Elq(niOrMl}O z-{C0+ploQn*O|fYb&lx0ug%*TeY3GzHR6}?FbDbiZJpPpw)^fL8rsWgn>A{NV00-;jP zM~w|ZRS3q(45aL|7=RG*r%4Ai$xHtfqx}&+PvUH>_g`iC@xFQq!^Sw^Gb5dc`+9^r zaMTTW>bSjqR*`$Vy8_pJVGPmJebC+Pj4fnFU0$VEBsp2%1i*&^}HtAh$`Jt1rg*ri=XZ`p1>m9(v^NIhz5(L73tB zua1!6dK84JE7t+99H-$CjsrAKRm{mvE@i;PEH>L5IbkXnw!9cSqcr$tanX zW?o+br=e*q!|>remy literal 0 HcmV?d00001 diff --git a/src/sphinx/_ext/tabbedcode.pyc b/src/sphinx/_ext/tabbedcode.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88f907639af74602bd26439ed77edae36d2058ef GIT binary patch literal 2769 zcmb_eOK&4Z5Uw7-V#k4fuqX@5swGy!2Vx_^2?1IWKs=TlCXoPNM&p@wW}NZNXu2m( zq~sL-ApZo3JMdMFV@HejfE34FuIfj<`m5@+{@NS-^!FdXMzsF4@cRLHf3#Ec4*pZJYZqEOYyESLyCvO?6KA3(H=YV0%UL!&SoY)EMlW=X(BuL^7YRk{Rf|;k-~P@ z2l*zgOytsY(@b?RmE*i^#2tuHQC`l|%-Fa1j97@UV;os)+4P;Mg>{cHvvp&meUm*l z{56LC0K_0H1PkhM;L|o|k7MnSe?0Pd+vhki(D5%^V7{)n-nW88_h+yEDM!``|S zU5KNAw-%0M26k+jg_rDHs8^{=^sIKS$SsFyO@`Zo3b!(_%0}MgR}(X*yM@ulIOKe0 zAXh{SK^KE_p+xrRM$xq**P=z6u9S!#U3rYLr;84Q8aou9?eg@k(4)o9S_7~3WevE; zmQw|EvgISoehgd>G|ED2jkUYr8|WNx!bby^AA#_4<$@%%0beCD?pVj7K%PWFGwHKR zf$;jmaj473%My3t7`b5_2YI+Oj!|u0=!_eJ%Z+s=4$82y#xbbZW`bfq$IOWHB_@&W zxiHRLG){17jIc~X>`QQy5(ShNwVkYr>TF`p;cyL8cQ{)`6I&)}elZE4X>zAx%1b1i zsH*d)_>mGLHBgV#+v=g(Q_s|hW0GhBi6saeXDSGmMOR%v%N=#!I9uF zam&np%*nt5!cBGyd%eBd$@Uh<6nYilrBXtV#|0iopc{t)KzrvErP3=pW!v`K7wf90U@=o+Ul`-t`2~}hHk2l zOF(e=8JGr703jD30I|Uy18*w>Hf@m^pX=8BTBDX8wNC#*nx|-`+o^*&W>B*6h+D4X ze-F4^HqW-S3rRM5ZjPtSe}*mo0f^MC{woh%8o1Re9Sj3_;$5RmV7PbKjcR#I( eQTs*nD10Rmb2wc!lu#ddUDfs8ebR6DhyMc2?S`rV literal 0 HcmV?d00001 diff --git a/src/sphinx/adapters/spark.rst b/src/sphinx/adapters/spark.rst index 211c3e26..56774118 100644 --- a/src/sphinx/adapters/spark.rst +++ b/src/sphinx/adapters/spark.rst @@ -3,7 +3,8 @@ Spark adapter ------------- -The Eventuate Spark adapter allows applications to consume events from event logs and to process them in `Apache Spark`_. Writing processed events back to event logs is not possible yet but will be supported in future versions. +The Eventuate Spark adapter allows applications to consume events from event logs and to process them in `Apache Spark`_. +Writing processed events back to event logs is not possible yet but will be supported in future versions. .. note:: The Spark adapter is only available for Scala 2.11 at the moment (see :ref:`download`). @@ -14,21 +15,27 @@ The Eventuate Spark adapter allows applications to consume events from event log Batch processing ~~~~~~~~~~~~~~~~ -`SparkBatchAdapter`_ supports event batch processing from event logs with a :ref:`cassandra-storage-backend`. The batch adapter internally uses the `Spark Cassandra Connector`_ for exposing an event log as `Spark RDD`_ of `DurableEvent`_\ s: +`SparkBatchAdapter`_ supports event batch processing from event logs with a :ref:`cassandra-storage-backend`. +The batch adapter internally uses the `Spark Cassandra Connector`_ for exposing an event log as `Spark RDD`_ of `DurableEvent`_\ s: -.. includecode:: ../../../eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala +.. includecode:: ../../../examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala :snippet: spark-batch-adapter -A `SparkBatchAdapter`_ is instantiated with a ``SparkContext``, configured for connecting to a Cassandra storage backend, and a :ref:`event-serialization` configuration (if any). The ``eventBatch`` method exposes an event log with given ``logId`` as ``RDD[DurableEvent]``, optionally starting from a custom sequence number. +A `SparkBatchAdapter`_ is instantiated with a ``SparkContext``, configured for connecting to a Cassandra storage backend, +and a :ref:`event-serialization` configuration (if any). +The ``eventBatch`` method exposes an event log with given ``logId`` as ``RDD[DurableEvent]``, optionally starting from a custom sequence number. -Event logs can span several partitions in a Cassandra cluster and the batch adapter reads from these partitions concurrently. Hence, events in the resulting RDD are ordered per partition. Applications that require a total order by ``localSequenceNr`` can sort the resulting RDD: +Event logs can span several partitions in a Cassandra cluster and the batch adapter reads from these partitions concurrently. +Hence, events in the resulting RDD are ordered per partition. +Applications that require a total order by ``localSequenceNr`` can sort the resulting RDD: -.. includecode:: ../../../eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala +.. includecode:: ../../../examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala :snippet: spark-batch-sorting -Exposing `Spark DataFrames`_ directly is not possible yet but will be supported in future versions. In the meantime, applications should convert RDDs to DataFrames or Datasets as shown in the following example: +Exposing `Spark DataFrames`_ directly is not possible yet but will be supported in future versions. +In the meantime, applications should convert RDDs to DataFrames or Datasets as shown in the following example: -.. includecode:: ../../../eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala +.. includecode:: ../../../examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala :snippet: spark-batch-dataframe .. hint:: @@ -36,17 +43,23 @@ Exposing `Spark DataFrames`_ directly is not possible yet but will be supported Stream processing ~~~~~~~~~~~~~~~~~ +`SparkStreamAdapter`_ supports event stream processing from event logs with any storage backend. +The stream adapter connects to the `ReplicationEndpoint`_\ [#]_ of an event log for exposing it as `Spark DStream`_ of `DurableEvent`_\ s: -`SparkStreamAdapter`_ supports event stream processing from event logs with any storage backend. The stream adapter connects to the `ReplicationEndpoint`_\ [#]_ of an event log for exposing it as `Spark DStream`_ of `DurableEvent`_\ s: - -.. includecode:: ../../../eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala +.. includecode:: ../../../examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala :snippet: spark-stream-adapter -A `SparkStreamAdapter`_ is instantiated with a Spark ``StreamingContext`` and a :ref:`event-serialization` configuration (if any). The ``eventStream`` method exposes an event log with given ``logName`` as ``DStream[DurableEvent]``. The stream is updated by interacting with the event log's replication endpoint at given ``host`` and ``port``. +A `SparkStreamAdapter`_ is instantiated with a Spark ``StreamingContext`` and a :ref:`event-serialization` configuration (if any). +The ``eventStream`` method exposes an event log with given ``logName`` as ``DStream[DurableEvent]``. +The stream is updated by interacting with the event log's replication endpoint at given ``host`` and ``port``. -The stream starts from the given ``fromSequenceNr`` and is updated with both, replayed events and newly written events. The storage level of events in Spark can be set with the ``storageLevel`` parameter. Applications that want to enforce event processing in strict event log storage order should repartition the stream with ``.repartition(1)``, as shown in the example. +The stream starts from the given ``fromSequenceNr`` and is updated with both, replayed events and newly written events. +The storage level of events in Spark can be set with the ``storageLevel`` parameter. +Applications that want to enforce event processing in strict event log storage order should repartition the stream with ``.repartition(1)``, as shown in the example. -For persisting the stream processing progress, an application should store the last processed sequence number at a custom place. When the application is restarted, the stored sequence number should be used as argument to the ``eventStream`` call. Later versions will additionally support internal storage of event processing progresses. +For persisting the stream processing progress, an application should store the last processed sequence number at a custom place. +When the application is restarted, the stored sequence number should be used as argument to the ``eventStream`` call. +Later versions will additionally support internal storage of event processing progresses. .. hint:: The full example source code is in `SparkStreamAdapterExample.scala`_ @@ -60,7 +73,7 @@ For persisting the stream processing progress, an application should store the l .. _ReplicationEndpoint: ../latest/api/index.html#com.rbmhtechnology.eventuate.ReplicationEndpoint .. _SparkBatchAdapter: ../latest/api/index.html#com.rbmhtechnology.eventuate.adapter.spark.SparkBatchAdapter .. _SparkStreamAdapter: ../latest/api/index.html#com.rbmhtechnology.eventuate.adapter.spark.SparkStreamAdapter -.. _SparkBatchAdapterExample.scala: https://github.com/RBMHTechnology/eventuate/blob/master/eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala -.. _SparkStreamAdapterExample.scala: https://github.com/RBMHTechnology/eventuate/blob/master/eventuate-example-spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala +.. _SparkBatchAdapterExample.scala: https://github.com/RBMHTechnology/eventuate/blob/master/examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkBatchAdapterExample.scala +.. _SparkStreamAdapterExample.scala: https://github.com/RBMHTechnology/eventuate/blob/master/examples/adapter/spark/src/main/scala/com/rbmhtechnology/example/spark/SparkStreamAdapterExample.scala .. [#] See also :ref:`replication-endpoints` in the reference documentation. diff --git a/src/sphinx/adapters/stream.rst b/src/sphinx/adapters/stream.rst index d5c06af2..3e648ce5 100644 --- a/src/sphinx/adapters/stream.rst +++ b/src/sphinx/adapters/stream.rst @@ -3,111 +3,159 @@ Akka Streams adapter -------------------- -This adapter provides an `Akka Streams`_ interface for Eventuate :ref:`event-logs`. It allows applications to consume event streams from event logs, write event streams to event logs, build idempotent event stream processing networks and exchange events with other systems that provide a `Reactive Streams`_ API. +This adapter provides an `Akka Streams`_ interface for Eventuate :ref:`event-logs`. +It allows applications to consume event streams from event logs, write event streams to event logs, build idempotent +event stream processing networks and exchange events with other systems that provide a `Reactive Streams`_ API. -The examples in the following subsections depend on the event log references ``logA``, ``logB`` and ``logC``. Here, they reference isolated :ref:`local-event-log`\ s with a :ref:`leveldb-storage-backend`: +The examples in the following subsections depend on the event log references ``logA``, ``logB`` and ``logC``. +Here, they reference isolated :ref:`local-event-log`\ s with a :ref:`leveldb-storage-backend`: -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventLogs.scala :snippet: durable-event-logs -We could also have used the local references of :ref:`replicated-event-log`\ s but this had no influence on the examples. Obtaining the local references of replicated event logs is explained in section :ref:`replication-endpoints`. +The above is provided in the ``examples/adapter/stream/`` SBT project, in the ``DurableEventLogs.scala`` file within +the ``src/main/scala/com/rbmhtechnology/example/stream/`` directory. + +We could also have used the local references of :ref:`replicated-event-log`\ s but this would have no influence on the examples. +Obtaining the local references of replicated event logs is explained in section :ref:`replication-endpoints`. .. _event-source: Event source ~~~~~~~~~~~~ +An event source can be created with DurableEventSource_ from an event log reference. +The result is a ``Graph[SourceShape[DurableEvent], ActorRef]`` which can be used with the Akka Streams `Scala DSL`_ or `Java DSL`_. +Here, the Scala DSL is used to create a ``Source[DurableEvent, ActorRef]`` from ``logA``: -An event source can be created with DurableEventSource_ from an event log reference. The result is a ``Graph[SourceShape[DurableEvent], ActorRef]`` which can be used with the Akka Streams `Scala DSL`_ or `Java DSL`_. Here, the Scala DSL is used to create a ``Source[DurableEvent, ActorRef]`` from ``logA``: - -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala :snippet: durable-event-source-1 -A ``DurableEventSource`` does not only emit events that already exist in an event log but also those events that have been written to the event log after the source has been materialized. A DurableEvent_ contains the application-specific event (``payload`` field) and its metadata (all other fields). +A ``DurableEventSource`` does not only emit events that already exist in an event log but also those events that have +been written to the event log after the source has been materialized. +A DurableEvent_ contains the application-specific event (``payload`` field) and its metadata (all other fields). -To create a source that emits elements from a given sequence number and/or for a given aggregate id only, the parameters ``fromSequenceNr`` and ``aggregateId`` should be used, respectively. If ``aggregateId`` is ``None`` events with any aggregate id, defined or not, are emitted (see also :ref:`event-routing`). +To create a source that emits elements from a given sequence number and/or for a given aggregate id only, +the parameters ``fromSequenceNr`` and ``aggregateId`` should be used, respectively. +If ``aggregateId`` is ``None`` events with any aggregate id, defined or not, are emitted (see also :ref:`event-routing`). -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventSourceExample.scala :snippet: durable-event-source-2 .. note:: - ``DurableEventSource`` emits events in local storage order. Local storage order is consistent with the *potential causality* of events which is tracked with :ref:`vector-clocks`. Find more details in section :ref:`event-logs`. Emission in local storage order also means that the order is repeatable across source materializations. + ``DurableEventSource`` emits events in local storage order. + Local storage order is consistent with the *potential causality* of events which is tracked with :ref:`vector-clocks`. + Find more details in section :ref:`event-logs`. + Emission in local storage order also means that the order is repeatable across source materializations. .. _event-writer: Event writer ~~~~~~~~~~~~ +An event writer is a stream stage that writes input events to an event log in stream order. +It can be created with DurableEventWriter_ from a unique writer id and an event log reference. +The result is a ``Graph[FlowShape[DurableEvent, DurableEvent], NotUsed]`` that emits the written events with updated metadata. -An event writer is a stream stage that writes input events to an event log in stream order. It can be created with DurableEventWriter_ from a unique writer id and an event log reference. The result is a ``Graph[FlowShape[DurableEvent, DurableEvent], NotUsed]`` that emits the written events with updated metadata. +The following example converts a stream of ``String``\ s to a stream of ``DurableEvent``\ s and writes that stream to ``logA``. +It then extracts ``payload`` and ``localSequenceNr`` from the written events and prints the results to ``stdout``: -The following example converts a stream of ``String``\ s to a stream of ``DurableEvent``\ s and writes that stream to ``logA``. It then extracts ``payload`` and ``localSequenceNr`` from the written events and prints the results to ``stdout``: - -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventWriterExample.scala :snippet: durable-event-writer -The writer sets the ``emitterId`` of the input events to ``writerId``. The ``processId``, ``localLogId``, ``localSequenceNr`` and ``systemTimestamp`` are set by the event log. The event log also updates the local time of ``vectorTimestamp``. All other ``DurableEvent`` fields are written to the event log without modification. +The writer sets the ``emitterId`` of the input events to ``writerId``. +The ``processId``, ``localLogId``, ``localSequenceNr`` and ``systemTimestamp`` are set by the event log. +The event log also updates the local time of ``vectorTimestamp``. +All other ``DurableEvent`` fields are written to the event log without modification. -Input events are batched if they are produced faster than they can be written. The maximum batch size can be configured with ``eventuate.log.write-batch-size``. On write failure, the writer fails the stream. +Input events are batched if they are produced faster than they can be written. +The maximum batch size can be configured with ``eventuate.log.write-batch-size``. +On write failure, the writer fails the stream. .. _event-processor: Event processor ~~~~~~~~~~~~~~~ +An event processor is a stream stage that expects input events from one or more source event logs, processes these events +with application-defined *processing logic* and writes the processed events to a target event log. +An event processor can be a *stateful processor* or a *stateless processor* and can be created with DurableEventProcessor_. -An event processor is a stream stage that expects input events from one or more source event logs, processes these events with application-defined *processing logic* and writes the processed events to a target event log. An event processor can be a *stateful processor* or a *stateless processor* and can be created with DurableEventProcessor_. - -Stateful processors apply processing logic of type ``(S, DurableEvent) => (S, Seq[O])`` to input events where ``S`` is the type of the processing state and ``O`` is the type of the processed **payload**. Stateless processors apply processing logic of type ``DurableEvent => Seq[O]`` to input events. +Stateful processors apply processing logic of type ``(S, DurableEvent) => (S, Seq[O])`` to input events where ``S`` is +the type of the processing state and ``O`` is the type of the processed **payload**. Stateless processors apply processing +logic of type ``DurableEvent => Seq[O]`` to input events. -Processing logic can filter, transform and/or split input events. To filter an input event from the event stream, an empty sequence should be returned. To transform an input event into one output event a sequence of length 1 should be returned. To split an input event into multiple output events a sequence of corresponding length should be returned. +Processing logic can filter, transform and/or split input events. +To filter an input event from the event stream, an empty sequence should be returned. +To transform an input event into one output event a sequence of length 1 should be returned. +To split an input event into multiple output events a sequence of corresponding length should be returned. .. note:: - Application-defined processing logic can read the payload and metadata from the input event but can only return updated payloads. This makes metadata update a processor-internal concern, ensuring that event processing to the target log works correctly and is idempotent. + Application-defined processing logic can read the payload and metadata from the input event but can only return updated payloads. + This makes metadata update a processor-internal concern, ensuring that event processing to the target log works correctly and is idempotent. -The following example is a stateless processor that consumes events from ``logA`` and writes the processing results to ``logB``. The processing logic filters an input event if the payload equals ``a``, it appends the source sequence number if the payload equals ``b`` and duplicates the input event if the payload equals ``c``. Events with other payloads remain unchanged: +The following example is a stateless processor that consumes events from ``logA`` and writes the processing results to ``logB``. +The processing logic filters an input event if the payload equals ``a``, it appends the source sequence number if the +payload equals ``b`` and duplicates the input event if the payload equals ``c``. +Events with other payloads remain unchanged: -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala :snippet: durable-event-processor-stateless -The example assumes that ``logA`` still contains the events that have been written by the event writer in the previous section. The next example uses a stateful processor that counts the number of events with a ``b`` payload and appends that number to all events. The processor consumes events from ``logA`` and writes the processing results to ``logC``: +The example assumes that ``logA`` still contains the events that have been written by the event writer in the previous section. +The next example uses a stateful processor that counts the number of events with a ``b`` payload and appends that number to all events. +The processor consumes events from ``logA`` and writes the processing results to ``logC``: -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala :snippet: durable-event-processor-stateful .. note:: - When running the examples a second time or more often, no events will be written to ``logB`` and ``logC`` because the processors will detect previously processed events as duplicates and discard them. This makes event processing **idempotent** i.e. it can be re-started after failures without generating duplicates in the target event logs. + When running the examples a second time or more often, no events will be written to ``logB`` and ``logC`` because the + processors will detect previously processed events as duplicates and discard them. + This makes event processing **idempotent** i.e. it can be re-started after failures without generating duplicates in the target event logs. -Input events are batched if they are produced faster than they can be processed. The maximum batch size can be configured with ``eventuate.log.write-batch-size``. On write failure, a processor fails the stream. +Input events are batched if they are produced faster than they can be processed. +The maximum batch size can be configured with ``eventuate.log.write-batch-size``. +On write failure, a processor fails the stream. Consuming from a shared source ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In the above example, both processors use their own ``DurableEventSource`` to read from ``logA``. +A better alternative is to use a single source and broadcast the events to both processors which reduces the read load on ``logA``: -In the above example, both processors use their own ``DurableEventSource`` to read from ``logA``. A better alternative is to use a single source and broadcast the events to both processors which reduces the read load on ``logA``: - -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala :snippet: durable-event-processor-shared-source .. _consume-multiple-sources: Consuming from multiple sources ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +An event processor may also consume events from multiple sources. +In the following example, the processor consumes the merged stream from ``logA`` and ``logB`` and writes the processing results to ``logC``: -An event processor may also consume events from multiple sources. In the following example, the processor consumes the merged stream from ``logA`` and ``logB`` and writes the processing results to ``logC``: - -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/DurableEventProcessorExample.scala :snippet: durable-event-processor-multiple-sources .. note:: - The example assumes that ``logA`` and ``logB`` are independent i.e. have no causal relationship. A plain stream ``merge`` is sufficient in this case. If these two logs had a causal relationship (e.g. after having processed events from ``logA`` into ``logB``) a plain stream ``merge`` may generate a stream that is not consistent with *potential causality*. + The example assumes that ``logA`` and ``logB`` are independent i.e. have no causal relationship. + A plain stream ``merge`` is sufficient in this case. + If these two logs had a causal relationship (e.g. after having processed events from ``logA`` into ``logB``) + a plain stream ``merge`` may generate a stream that is not consistent with *potential causality*. - Processing such a stream may generate ``vectorTimestamps`` that indicate concurrency of otherwise causally related events. This is acceptable for some applications but many others require stream merges that preserve causality. We will therefore soon provide a `causal stream merge stage`_. + Processing such a stream may generate ``vectorTimestamps`` that indicate concurrency of otherwise causally related events. + This is acceptable for some applications but many others require stream merges that preserve causality. + We will therefore soon provide a `causal stream merge stage`_. Event processing progress tracking ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +An event processor does not only write processed events to a target event log but also writes the latest source log +sequence number to that log for tracking processing progress. +When composing an event processing stream, an application should first read processing progresses from target logs in +order to initialize ``DurableEventSource``\ s with appropriate ``fromSequenceNr``\ s. +Eventuate provides ProgressSource_ for reading the processing progress for a given source from a target log. -An event processor does not only write processed events to a target event log but also writes the latest source log sequence number to that log for tracking processing progress. When composing an event processing stream, an application should first read processing progresses from target logs in order to initialize ``DurableEventSource``\ s with appropriate ``fromSequenceNr``\ s. Eventuate provides ProgressSource_ for reading the processing progress for a given source from a target log. - -In :ref:`consume-multiple-sources`, for example, the processing progress for ``logA`` and ``logB`` is stored at ``logC``. The following example creates two sources, ``sourceA`` and ``sourceB``, that first read the progress for values for ``logA`` and ``logB`` from ``logC``, respectively, and then create the actual ``DurableEventSource``\ s with an appropriate ``fromSequenceNr``: +In :ref:`consume-multiple-sources`, for example, the processing progress for ``logA`` and ``logB`` is stored at ``logC``. +The following example creates two sources, ``sourceA`` and ``sourceB``, that first read the progress for values for +``logA`` and ``logB`` from ``logC``, respectively, and then create the actual ``DurableEventSource``\ s with an appropriate ``fromSequenceNr``: -.. includecode:: ../../../eventuate-example-stream/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala +.. includecode:: ../../../examples/adapter/stream/src/main/scala/com/rbmhtechnology/example/stream/ProgressSourceExample.scala :snippet: progress-source .. _Akka Streams: http://doc.akka.io/docs/akka/2.4/scala/stream/index.html diff --git a/src/sphinx/adapters/vertx.rst b/src/sphinx/adapters/vertx.rst index 46344379..67caaa44 100644 --- a/src/sphinx/adapters/vertx.rst +++ b/src/sphinx/adapters/vertx.rst @@ -2,102 +2,125 @@ Vert.x adapter -------------- +The Eventuate Vert.x adapter allows applications using a `Vert.x`_ instance to interact with event logs. +Events can both be consumed from and produced to an event log by a Vert.x instance. -The Eventuate Vert.x adapter allows applications using a `Vert.x`_ instance to interact with event logs. Events can both be consumed from and produced to an event log by a Vert.x instance. - -Event exchange is performed over the Vert.x `event bus`_. Events delivered to a Vert.x instance are either published to all subscribers or sent to a single subscriber on the event bus. Events received from a Vert.x instance are persisted to an event log by consuming events from a particular endpoint on the event bus. +Event exchange is performed over the Vert.x `event bus`_. +Events delivered to a Vert.x instance are either published to all subscribers or sent to a single subscriber on the event bus. +Events received from a Vert.x instance are persisted to an event log by consuming events from a particular endpoint on the event bus. Event producers ~~~~~~~~~~~~~~~ - -The Vert.x adapter exchanges events with a Vert.x instance by using so called *event producers*. An event producer consumes events from a given source and produces the same events to a specified destination. Both sources and destinations can either be an event bus endpoint or an even log. +The Vert.x adapter exchanges events with a Vert.x instance by using so called *event producers*. +An event producer consumes events from a given source and produces the same events to a specified destination. +Both sources and destinations can either be an event bus endpoint or an even log. The Vert.x adapter supports two kinds of event producers: - **Vert.x event producers** consume events from an event log and publish or send the events to a configurable event bus endpoint. - **Log event producers** consume events from a given event bus endpoint and persist the events in an event log. -An event producer establishes an unidirectional connection between exactly one event log and one or multiple event bus endpoints. Event producers are instantiated by using the ``EventProducer`` API. The configuration of a producer consists of: +An event producer establishes an unidirectional connection between exactly one event log and one or multiple event bus endpoints. +Event producers are instantiated by using the ``EventProducer`` API. +The configuration of a producer consists of: - an event source, - an event destination and - a unique id. -*Vert.x producers* are created by using the ``EventProducer.fromLog`` method. Applications define the source log, the delivery method and an arbitrary amount of event bus endpoints, the events will be delivered to. +*Vert.x producers* are created by using the ``EventProducer.fromLog`` method. +Applications define the source log, the delivery method and an arbitrary amount of event bus endpoints, the events will be delivered to. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: vertx-event-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: vertx-event-producer -*Log producers* are created by using the ``EventProducer.fromEndpoints`` method. Multiple event bus endpoints can be defined, which are used to consume events from the event bus and persist the same events to the given event log. +*Log producers* are created by using the ``EventProducer.fromEndpoints`` method. +Multiple event bus endpoints can be defined, which are used to consume events from the event bus and persist the same events to the given event log. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: log-event-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: log-event-producer -An event log must be supplied as an ``ActorRef`` which is usually obtained from a `ReplicationEndpoint`_\ [#]_. Event producers are implementation-agnostic in respect to event logs - any event log implementation may be used in combination with a producer. +An event log must be supplied as an ``ActorRef`` which is usually obtained from a `ReplicationEndpoint`_\ [#]_. +Event producers are implementation-agnostic in respect to event logs - any event log implementation may be used in combination with a producer. .. note:: - The id of a producer must be unique and should be stable over time. It is used as the primary key to store meta information about the producer. + The id of a producer must be unique and should be stable over time. + It is used as the primary key to store meta information about the producer. .. hint:: - Event producers are covered in more detail in the sections `Vert.x Publish Event Producer`_ and `Vert.x Point-to-Point Event Producer`_. Log producers are covered in the section `Log Event Producer`_. + Event producers are covered in more detail in the sections `Vert.x Publish Event Producer`_ and `Vert.x Point-to-Point Event Producer`_. + Log producers are covered in the section `Log Event Producer`_. Event processing ~~~~~~~~~~~~~~~~ -Applications process events sent from Vert.x producers by registering event bus handlers at the configured endpoints on the event bus. An event bus endpoint is a simple address represented as a ``String``, which can follow any addressing scheme. Vert.x producers deliver events on the event bus as instances of an event bus `message`_. Event bus handlers access the underlying event by obtaining the body of a message. +Applications process events sent from Vert.x producers by registering event bus handlers at the configured endpoints on the event bus. +An event bus endpoint is a simple address represented as a ``String``, which can follow any addressing scheme. +Vert.x producers deliver events on the event bus as instances of an event bus `message`_. Event bus handlers access the underlying event by obtaining the body of a message. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: event-processing-vertx-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: event-processing-vertx-producer -For events to be written to an event log, applications send events to the specified endpoints configured for a log producer. The log producer consumes all events from these endpoints and persists the events to the configured event log. +For events to be written to an event log, applications send events to the specified endpoints configured for a log producer. +The log producer consumes all events from these endpoints and persists the events to the configured event log. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: event-processing-log-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: event-processing-log-producer .. note:: - Event processing in event handlers should be performed idempotent because a Vert.x producer may deliver the same event multiple times under certain conditions. Events may be redelivered after a restart of a producer if it was not able to successfully persist its read progress on shutdown (or crash). + Event processing in event handlers should be performed idempotent because a Vert.x producer may deliver the same event + multiple times under certain conditions. + Events may be redelivered after a restart of a producer if it was not able to successfully persist its read progress on shutdown (or crash). Adapter usage ~~~~~~~~~~~~~ -Event producers are managed by a ``VertxAdapter``. Applications can connect to multiple event logs by instantiating event producers and supplying them to the ``VertxAdapterConfig``. +Event producers are managed by a ``VertxAdapter``. +Applications can connect to multiple event logs by instantiating event producers and supplying them to the ``VertxAdapterConfig``. -The ``VertxAdatperConfig`` is passed to the ``VertxAdapter`` together with an ``ActorSystem`` and the ``Vert.x`` instance the adapter will connect to. The adapter is also supplied with a ``StorageProvider`` which is used to persist the read progress of the individual event producers. +The ``VertxAdatperConfig`` is passed to the ``VertxAdapter`` together with an ``ActorSystem`` and the ``Vert.x`` instance +the adapter will connect to. +The adapter is also supplied with a ``StorageProvider`` which is used to persist the read progress of the individual event producers. Applications invoke the ``start`` method of the system to initialize the registered event producers and start event exchange with the ``Vert.x`` instance. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: adapter-example - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: adapter-example .. warning:: - The ``start`` method should only be called after all handlers on the event bus have been registered. Failing to do so may lead to loss of events because a producer might try to deliver events to an event bus endpoint which has not yet an event handler assigned to it. + The ``start`` method should only be called after all handlers on the event bus have been registered. + Failing to do so may lead to loss of events because a producer might try to deliver events to an event bus endpoint + which has not yet an event handler assigned to it. The following sections contain a detailed description of the different kinds of event producers. Vert.x publish event producer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A *Publish Event Producer* publishes events from an event log to *multiple* subscribers on the event bus. Events are delivered to specific endpoints defined in the configuration of the producer. A producer can route events to different event bus endpoints based on the content of the event. Event routing is enabled by supplying a partial function which maps events to event bus endpoints. If the partial function is not defined at the event, the event will not be processed. +A *Publish Event Producer* publishes events from an event log to *multiple* subscribers on the event bus. +Events are delivered to specific endpoints defined in the configuration of the producer. +A producer can route events to different event bus endpoints based on the content of the event. +Event routing is enabled by supplying a partial function which maps events to event bus endpoints. +If the partial function is not defined at the event, the event will not be processed. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: vertx-publish-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: vertx-publish-producer Event publishing is performed with *At-Most-Once* delivery semantics, so no guarantees about the successful delivery of events can be made. @@ -105,118 +128,148 @@ Event publishing is performed with *At-Most-Once* delivery semantics, so no guar Applications consume events by registering an event handler at the configured endpoints on the event bus. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: event-processing-vertx-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: event-processing-vertx-producer -Read progress from the source event log is tracked by persisting the ``localSequenceNr`` of the latest sent event to the ``StorageProvider`` supplied to the ``VertxAdapter``. After publishing one or multiple events the read progress is persisted. The producer continues publishing events from the latest known ``localSequenceNr`` once the it is started. +Read progress from the source event log is tracked by persisting the ``localSequenceNr`` of the latest sent event to +the ``StorageProvider`` supplied to the ``VertxAdapter``. +After publishing one or multiple events the read progress is persisted. +The producer continues publishing events from the latest known ``localSequenceNr`` once the it is started. Vert.x point-to-point event producer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A *Point-to-Point Event Producer* sends an event to a *single* subscriber on the event bus. If a single subscriber is registered for an endpoint all events are delivered to this subscriber. If multiple subscribers are registered for the same endpoint, events are delivered alternately to only one of those subscribers using a non-strict round-robin algorithm. Event routing can be enabled by supplying a partial function. +A *Point-to-Point Event Producer* sends an event to a *single* subscriber on the event bus. +If a single subscriber is registered for an endpoint all events are delivered to this subscriber. +If multiple subscribers are registered for the same endpoint, events are delivered alternately to only one of those +subscribers using a non-strict round-robin algorithm. +Event routing can be enabled by supplying a partial function. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: vertx-ptp-producer-at-most-once - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: vertx-ptp-producer-at-most-once -Point-to-point event producers support both *At-Most-Once* and *At-Least-Once* delivery semantics. If not specified otherwise *At-Most-Once* delivery is chosen. *At-Least-Once* delivery is enabled by configuring the adapter accordingly. +Point-to-point event producers support both *At-Most-Once* and *At-Least-Once* delivery semantics. +If not specified otherwise *At-Most-Once* delivery is chosen. +*At-Least-Once* delivery is enabled by configuring the adapter accordingly. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: vertx-ptp-producer-at-least-once - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: vertx-ptp-producer-at-least-once Events sent by a point-to-point event producer are received by registering an event handler on the event bus. -Using *At-Least-Once* delivery semantics, every event must be confirmed by the receiver. Unconfirmed events are redelivered until a confirmation was received by the adapter. Event handlers confirm event delivery by replying to the event bus message with a ``Confirmation``. +Using *At-Least-Once* delivery semantics, every event must be confirmed by the receiver. +Unconfirmed events are redelivered until a confirmation was received by the adapter. +Event handlers confirm event delivery by replying to the event bus message with a ``Confirmation``. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: vertx-ptp-producer-handler - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: vertx-ptp-producer-handler Event confirmations are persisted on a per-event basis or in batches of configurable size. - **Per-event confirmations**: - Using per-event confirmations, every confirmation received by the adapter is persisted to the source event log. Confirmation events are not delivered to any event bus handlers but will increase the size of the source event log. With this confirmation mode events will not be redelivered once an event confirmation has been received. + Using per-event confirmations, every confirmation received by the adapter is persisted to the source event log. + Confirmation events are not delivered to any event bus handlers but will increase the size of the source event log. + With this confirmation mode events will not be redelivered once an event confirmation has been received. - **Batch event confirmations**: - Using batch confirmations, events are delivered in batches where the next batch is only delivered once all events of the previous batch have been confirmed. Batches containing events which have not been confirmed are redelivered as a whole, resulting in redelivery of all events of the same batch. This approach leads to modest storage requirements as no individual per-event confirmation information has to be tracked. Using this confirmation mode, events may be redelivered multiple times even though a confirmation has already been received. + Using batch confirmations, events are delivered in batches where the next batch is only delivered once all events of + the previous batch have been confirmed. Batches containing events which have not been confirmed are redelivered as a whole, + resulting in redelivery of all events of the same batch. + This approach leads to modest storage requirements as no individual per-event confirmation information has to be tracked. + Using this confirmation mode, events may be redelivered multiple times even though a confirmation has already been received. Log event producer ~~~~~~~~~~~~~~~~~~ -A *Log Event Producer* consumes events from multiple event bus endpoints and persists these events to a single event log. Every persisted event creates a write confirmation which is returned to the sender of the event, containing the result of the write operation. +A *Log Event Producer* consumes events from multiple event bus endpoints and persists these events to a single event log. +Every persisted event creates a write confirmation which is returned to the sender of the event, containing the result of the write operation. -Log event producers can be configured with an optional event filter. This filter is applied to events sent to the producer. +Log event producers can be configured with an optional event filter. +This filter is applied to events sent to the producer. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: log-event-multiple-producer - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: log-event-multiple-producer -Events accepted by the filter are written to the configured destination log. Events rejected by the filter are dropped and a corresponding write result is returned to the sender. +Events accepted by the filter are written to the configured destination log. +Events rejected by the filter are dropped and a corresponding write result is returned to the sender. -Applications persist events by sending them to the endpoint configured for the producer. The result of the write operation is returned as a response message. +Applications persist events by sending them to the endpoint configured for the producer. +The result of the write operation is returned as a response message. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: log-producer-handler - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: log-producer-handler .. note:: - A single endpoint can only be configured once as the source for an log event producer. This ensures that write confirmations can reliably be returned to the source endpoint. Configuring the same source endpoint for multiple producers will lead to a configuration error. + A single endpoint can only be configured once as the source for an log event producer. + This ensures that write confirmations can reliably be returned to the source endpoint. + Configuring the same source endpoint for multiple producers will lead to a configuration error. Message codecs ~~~~~~~~~~~~~~ -All messages transmitted over the event bus must provide a Vert.x `message codec`_. The event bus uses this message codec to serialize and deserialize the body of an event bus message. +All messages transmitted over the event bus must provide a Vert.x `message codec`_. +The event bus uses this message codec to serialize and deserialize the body of an event bus message. -Events sent or received by the Vert.x adapter may not have an instance of a ``MessageCodec`` defined, since they usually originate from an external system. To ease the integration of external events into a Vert.x application, the adapter offers a generic message codec for types serializable by the ``ActorSystem`` provided to the ``VertxAdapter``. All events persisted to an event log are serializable by the ``ActorSystem``, hence the generic message codec can be used for those objects. +Events sent or received by the Vert.x adapter may not have an instance of a ``MessageCodec`` defined, since they usually +originate from an external system. +To ease the integration of external events into a Vert.x application, the adapter offers a generic message codec for +types serializable by the ``ActorSystem`` provided to the ``VertxAdapter``. +All events persisted to an event log are serializable by the ``ActorSystem``, hence the generic message codec can be used for those objects. The generic ``MessageCodec`` is applied for an object type by registering the type with the ``VertxAdapterConfig``. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: message-codec - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: message-codec -A message codec for the type is created which uses the ``Serializer`` assigned to the type at the ``ActorSystem``. This codec is registered as the default message codec for the type and will subsequently be used to encode and decode all messages of this type on the event bus. +A message codec for the type is created which uses the ``Serializer`` assigned to the type at the ``ActorSystem``. +This codec is registered as the default message codec for the type and will subsequently be used to encode and decode all messages of this type on the event bus. .. note:: - The generic ``MessageCodec`` can also be used for events not stored in an event log if a ``Serializer`` for the event type is configured at the ``ActorSystem``. If no ``Serializer`` for a type is configured the generated ``MessageCodec`` will fail to process instances of the type. + The generic ``MessageCodec`` can also be used for events not stored in an event log if a ``Serializer`` for the event + type is configured at the ``ActorSystem``. + If no ``Serializer`` for a type is configured the generated ``MessageCodec`` will fail to process instances of the type. Event metadata ~~~~~~~~~~~~~~ - -Applications can access the metadata of an event by querying the `headers`_ of an event bus message. The following metadata is available for each event: +Applications can access the metadata of an event by querying the `headers`_ of an event bus message. +The following metadata is available for each event: - the *local log id* of the event, - the *local sequence number* of the event and - the *id of the emitter* that persisted the event. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: event-metadata-from-headers - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: event-metadata-from-headers The Vert.x adapter also offers the ``EventMetadata`` helper, which is instantiated from the message headers and provides the metadata of an event. An ``EventMetadata`` instance is only created if the message originated from a Vert.x producer. .. tabbed-code:: - .. includecode:: ../../../eventuate-example-vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala + .. includecode:: ../../../examples/vertx/src/main/scala/com/rbmhtechnology/docs/vertx/Documentation.scala :snippet: event-metadata-from-helper - .. includecode:: ../../../eventuate-example-vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java + .. includecode:: ../../../examples/vertx/src/main/java/com/rbmhtechnology/docs/vertx/japi/Documentation.java :snippet: event-metadata-from-helper .. hint:: @@ -227,8 +280,8 @@ An ``EventMetadata`` instance is only created if the message originated from a V .. _message codec: http://vertx.io/docs/apidocs/io/vertx/core/eventbus/MessageCodec.html .. _message: http://vertx.io/docs/apidocs/io/vertx/core/eventbus/Message.html .. _headers: http://vertx.io/docs/apidocs/io/vertx/core/eventbus/Message.html#headers-- -.. _VertxAdapterExample.scala: https://github.com/RBMHTechnology/eventuate/blob/master/eventuate-example-vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala -.. _VertxAdapterExample.java: https://github.com/RBMHTechnology/eventuate/blob/master/eventuate-example-vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java +.. _VertxAdapterExample.scala: https://github.com/RBMHTechnology/eventuate/blob/master/examples/vertx/src/main/scala/com/rbmhtechnology/example/vertx/VertxAdapterExample.scala +.. _VertxAdapterExample.java: https://github.com/RBMHTechnology/eventuate/blob/master/examples/vertx/src/main/java/com/rbmhtechnology/example/vertx/japi/VertxAdapterExample.java .. _ReplicationEndpoint: ../latest/api/index.html#com.rbmhtechnology.eventuate.ReplicationEndpoint .. [#] See also :ref:`replication-endpoints` in the reference documentation. diff --git a/src/sphinx/code/UserGuideDoc.scala b/src/sphinx/code/UserGuideDoc.scala deleted file mode 100644 index 7f42562e..00000000 --- a/src/sphinx/code/UserGuideDoc.scala +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright 2015 - 2016 Red Bull Media House GmbH - all rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package doc - -object EventsourcedActors extends App { - //#event-sourced-actor - import scala.util._ - import akka.actor._ - import com.rbmhtechnology.eventuate.EventsourcedActor - - // Commands - case object Print - case class Append(entry: String) - - // Command replies - case class AppendSuccess(entry: String) - case class AppendFailure(cause: Throwable) - - // Event - case class Appended(entry: String) - - class ExampleActor(override val id: String, - override val aggregateId: Option[String], - override val eventLog: ActorRef) extends EventsourcedActor { - - private var currentState: Vector[String] = Vector.empty - - override def onCommand = { - case Print => - println(s"[id = $id, aggregate id = ${aggregateId.getOrElse("")}] ${currentState.mkString(",")}") - case Append(entry) => persist(Appended(entry)) { - case Success(evt) => sender() ! AppendSuccess(entry) - case Failure(err) => sender() ! AppendFailure(err) - } - } - - override def onEvent = { - case Appended(entry) => currentState = currentState :+ entry - } - } - //# - - import com.rbmhtechnology.eventuate.ReplicationConnection._ - import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog - - //#create-one-instance - val system: ActorSystem = // ... - //# - ActorSystem(DefaultRemoteSystemName) - - //#create-one-instance - val eventLog: ActorRef = // ... - //# - system.actorOf(LeveldbEventLog.props("qt-1")) - - //#create-one-instance - - val ea1 = system.actorOf(Props(new ExampleActor("1", Some("a"), eventLog))) - - ea1 ! Append("a") - ea1 ! Append("b") - //# - - //#print-one-instance - ea1 ! Print - //# - - //#create-two-instances - val b2 = system.actorOf(Props(new ExampleActor("2", Some("b"), eventLog))) - val c3 = system.actorOf(Props(new ExampleActor("3", Some("c"), eventLog))) - - b2 ! Append("a") - b2 ! Append("b") - - c3 ! Append("x") - c3 ! Append("y") - //# - - //#print-two-instances - b2 ! Print - c3 ! Print - //# - - //#create-replica-instances - // created at location 1 - val d4 = system.actorOf(Props(new ExampleActor("4", Some("d"), eventLog))) - - // created at location 2 - val d5 = system.actorOf(Props(new ExampleActor("5", Some("d"), eventLog))) - - d4 ! Append("a") - //# - - Thread.sleep(1000) - - d4 ! Print - d5 ! Print - - //#send-another-append - d5 ! Append("b") - //# - - Thread.sleep(1000) - - d4 ! Print - d5 ! Print -} - -object EventsourcedActorsUpdated { - import scala.util._ - import akka.actor._ - - case object Print - case class Append(entry: String) - case class AppendSuccess(entry: String) - case class AppendFailure(cause: Throwable) - case class Appended(entry: String) - - { - //#detecting-concurrent-update - import com.rbmhtechnology.eventuate.EventsourcedActor - import com.rbmhtechnology.eventuate.VectorTime - - class ExampleActor(override val id: String, - override val aggregateId: Option[String], - override val eventLog: ActorRef) extends EventsourcedActor { - - private var currentState: Vector[String] = Vector.empty - private var updateTimestamp: VectorTime = VectorTime() - - override def onCommand = { - // ... - //# - case _ => - //#detecting-concurrent-update - } - - override def onEvent = { - case Appended(entry) => - if (updateTimestamp < lastVectorTimestamp) { - // regular update - currentState = currentState :+ entry - updateTimestamp = lastVectorTimestamp - } else if (updateTimestamp conc lastVectorTimestamp) { - // concurrent update - // TODO: track conflicting versions - } - } - } - //# - } - - { - //#tracking-conflicting-versions - import scala.collection.immutable.Seq - import com.rbmhtechnology.eventuate.{ConcurrentVersions, Versioned} - import com.rbmhtechnology.eventuate.EventsourcedActor - - class ExampleActor(override val id: String, - override val aggregateId: Option[String], - override val eventLog: ActorRef) extends EventsourcedActor { - - private var versionedState: ConcurrentVersions[Vector[String], String] = - ConcurrentVersions(Vector.empty, (s, a) => s :+ a) - - override def onCommand = { - // ... - //# - case _ => - //#tracking-conflicting-versions - } - - override def onEvent = { - case Appended(entry) => - versionedState = versionedState.update(entry, lastVectorTimestamp) - if (versionedState.conflict) { - val conflictingVersions: Seq[Versioned[Vector[String]]] = versionedState.all - // TODO: resolve conflicting versions - } else { - val currentState: Vector[String] = versionedState.all.head.value - // ... - } - } - } - //# - } - - { - import com.rbmhtechnology.eventuate._ - - //#automated-conflict-resolution - class ExampleActor(override val id: String, - override val aggregateId: Option[String], - override val eventLog: ActorRef) extends EventsourcedActor { - - private var versionedState: ConcurrentVersions[Vector[String], String] = - ConcurrentVersions(Vector.empty, (s, a) => s :+ a) - - override def onCommand = { - // ... - //# - case _ => - //#automated-conflict-resolution - } - - override def onEvent = { - case Appended(entry) => - versionedState = versionedState - .update(entry, lastVectorTimestamp, lastSystemTimestamp, lastEmitterId) - if (versionedState.conflict) { - val conflictingVersions = versionedState.all.sortWith { (v1, v2) => - if (v1.systemTimestamp == v2.systemTimestamp) v1.creator < v2.creator - else v1.systemTimestamp > v2.systemTimestamp - } - val winnerTimestamp: VectorTime = conflictingVersions.head.vectorTimestamp - versionedState = versionedState.resolve(winnerTimestamp) - } - } - } - //# - } - - { - import scala.collection.immutable.Seq - import com.rbmhtechnology.eventuate._ - - //#interactive-conflict-resolution - case class Append(entry: String) - case class AppendRejected(entry: String, conflictingVersions: Seq[Versioned[Vector[String]]]) - - case class Resolve(selectedTimestamp: VectorTime) - case class Resolved(selectedTimestamp: VectorTime) - - class ExampleActor(override val id: String, - override val aggregateId: Option[String], - override val eventLog: ActorRef) extends EventsourcedActor { - - private var versionedState: ConcurrentVersions[Vector[String], String] = - ConcurrentVersions(Vector.empty, (s, a) => s :+ a) - - override def onCommand = { - case Append(entry) if versionedState.conflict => - sender() ! AppendRejected(entry, versionedState.all) - case Append(entry) => - // ... - case Resolve(selectedTimestamp) => persist(Resolved(selectedTimestamp)) { - case Success(evt) => // reply to sender omitted ... - case Failure(err) => // reply to sender omitted ... - } - } - - override def onEvent = { - case Appended(entry) => - versionedState = versionedState - .update(entry, lastVectorTimestamp, lastSystemTimestamp, lastEmitterId) - case Resolved(selectedTimestamp) => - versionedState = versionedState.resolve(selectedTimestamp, lastVectorTimestamp) - } - } - //# - } -} - -object EventsourcedViews { - //#event-sourced-view - import akka.actor.ActorRef - import com.rbmhtechnology.eventuate.EventsourcedView - import com.rbmhtechnology.eventuate.VectorTime - - case class Appended(entry: String) - case class Resolved(selectedTimestamp: VectorTime) - - case object GetAppendCount - case class GetAppendCountReply(count: Long) - - case object GetResolveCount - case class GetResolveCountReply(count: Long) - - class ExampleView(override val id: String, override val eventLog: ActorRef) extends EventsourcedView { - private var appendCount: Long = 0L - private var resolveCount: Long = 0L - - override def onCommand = { - case GetAppendCount => sender() ! GetAppendCountReply(appendCount) - case GetResolveCount => sender() ! GetResolveCountReply(resolveCount) - } - - override def onEvent = { - case Appended(_) => appendCount += 1L - case Resolved(_) => resolveCount += 1L - } - } - //# -} - -object ConditionalRequests extends App { - import akka.actor._ - import com.rbmhtechnology.eventuate.ReplicationConnection - import com.rbmhtechnology.eventuate.log.leveldb.LeveldbEventLog - import EventsourcedViews._ - - val system: ActorSystem = ActorSystem(ReplicationConnection.DefaultRemoteSystemName) - val eventLog = system.actorOf(LeveldbEventLog.props("qt-2")) - - //#conditional-requests - import scala.concurrent.duration._ - import scala.util._ - import akka.actor._ - import akka.pattern.ask - import akka.util.Timeout - import com.rbmhtechnology.eventuate._ - - case class Append(entry: String) - case class AppendSuccess(entry: String, updateTimestamp: VectorTime) - - class ExampleActor(override val id: String, - override val eventLog: ActorRef) extends EventsourcedActor { - - private var currentState: Vector[String] = Vector.empty - override val aggregateId = Some(id) - - override def onCommand = { - case Append(entry) => persist(Appended(entry)) { - case Success(evt) => - sender() ! AppendSuccess(entry, lastVectorTimestamp) - // ... - } - // ... - } - - override def onEvent = { - case Appended(entry) => currentState = currentState :+ entry - } - } - - class ExampleView(override val id: String, override val eventLog: ActorRef) - extends EventsourcedView with ConditionalRequests { - // ... - //# - private var appendCount: Long = 0L - private var resolveCount: Long = 0L - - override def onCommand = { - case GetAppendCount => sender() ! GetAppendCountReply(appendCount) - case GetResolveCount => sender() ! GetResolveCountReply(resolveCount) - } - - override def onEvent = { - case Appended(_) => appendCount += 1L - case Resolved(_) => resolveCount += 1L - } - //#conditional-requests - } - - val ea = system.actorOf(Props(new ExampleActor("ea", eventLog))) - val ev = system.actorOf(Props(new ExampleView("ev", eventLog))) - - import system.dispatcher - implicit val timeout = Timeout(5.seconds) - - for { - AppendSuccess(_, timestamp) <- ea ? Append("a") - GetAppendCountReply(count) <- ev ? ConditionalRequest(timestamp, GetAppendCount) - } println(s"append count = $count") - //# -} - -object EventCommunication { - import akka.actor._ - - var system: ActorSystem = _ - var eventLog: ActorRef = _ - - //#event-driven-communication - // some imports omitted ... - import com.rbmhtechnology.eventuate.EventsourcedView.Handler - import com.rbmhtechnology.eventuate.EventsourcedActor - import com.rbmhtechnology.eventuate.PersistOnEvent - - case class Ping(num: Int) - case class Pong(num: Int) - - class PingActor(val id: String, val eventLog: ActorRef, completion: ActorRef) - extends EventsourcedActor with PersistOnEvent { - - override def onCommand = { - case "serve" => persist(Ping(1))(Handler.empty) - } - - override def onEvent = { - case Pong(10) if !recovering => completion ! "done" - case Pong(i) => persistOnEvent(Ping(i + 1)) - } - } - - class PongActor(val id: String, val eventLog: ActorRef) - extends EventsourcedActor with PersistOnEvent { - - override def onCommand = { - case _ => - } - override def onEvent = { - case Ping(i) => persistOnEvent(Pong(i)) - } - } - - val pingActor = system.actorOf(Props(new PingActor("ping", eventLog, system.deadLetters))) - val pongActor = system.actorOf(Props(new PongActor("pong", eventLog))) - - pingActor ! "serve" - //# -} diff --git a/src/sphinx/developers.rst b/src/sphinx/developers.rst index 6d96d602..fd9326d6 100644 --- a/src/sphinx/developers.rst +++ b/src/sphinx/developers.rst @@ -46,7 +46,8 @@ To publish the binaries to the local Maven repository, run:: Documentation ~~~~~~~~~~~~~ -Our documentation is written in reStructuredText_ and located in ``src/sphinx``. Building the documentation requires an installation of Python_ and Sphinx_. +Our documentation is written in reStructuredText_ and located in ``src/sphinx``, except for the User Guide, which is located in `examples/user-guide`. +Building the documentation requires an installation of Python_ and Sphinx_. The following installation instructions are for Mac OS X. If Python isn’t already installed, install Homebrew_ first and then Python with:: diff --git a/src/sphinx/reference/event-sourcing.rst b/src/sphinx/reference/event-sourcing.rst index 44c288cf..13f05f70 100644 --- a/src/sphinx/reference/event-sourcing.rst +++ b/src/sphinx/reference/event-sourcing.rst @@ -3,26 +3,39 @@ Event-sourced actors -------------------- -An introduction to event-sourced actors is already given in sections :ref:`overview`, :ref:`architecture` and the :ref:`user-guide`. Applications use event-sourced actors for writing events to an event log and for maintaining in-memory write models on the command side (C) of a CQRS_ application. Event-sourced actors distinguish command processing from event processing. They must extend the EventsourcedActor_ trait and implement a :ref:`command-handler` and an :ref:`event-handler`. +An introduction to event-sourced actors is already given in sections :ref:`overview`, :ref:`architecture` and the :ref:`user-guide`. +Applications use event-sourced actors for writing events to an event log and for maintaining in-memory write models on +the command side (C) of a CQRS_ application. Event-sourced actors distinguish command processing from event processing. +They must extend the EventsourcedActor_ trait and implement a :ref:`command-handler` and an :ref:`event-handler`. .. _command-handler: Command handler ~~~~~~~~~~~~~~~ -A command handler is partial function of type ``PartialFunction[Any, Unit]`` for which a type alias ``Receive`` exists. It can be defined by implementing ``onCommand``: +A command handler is partial function of type ``PartialFunction[Any, Unit]`` for which a type alias ``Receive`` exists. +It can be defined by implementing ``onCommand``: .. includecode:: ../code/EventSourcingDoc.scala :snippet: command-handler -Messages sent by an application to an event-sourced actor are received by its command handler. Usually, a command handler first validates a command, then derives one or more events from it, persists these events with ``persist`` and replies with the persistence result. The ``persist`` method has the following signature\ [#]_: +Messages sent by an application to an event-sourced actor are received by its command handler. +Usually a command handler first validates a command, then derives one or more events from it, +persists these events with ``persist`` and replies with the persistence result. +The ``persist`` method has the following signature\ [#]_: .. includecode:: ../code/EventSourcingDoc.scala :snippet: persist-signature -The ``persist`` method can be called one ore more times per received command. Calling ``persist`` does not immediately write events to the event log. Instead, events from ``persist`` calls are collected in memory and written to the event log when ``onCommand`` returns. +The ``persist`` method can be called one ore more times per received command. +Calling ``persist`` does not immediately write events to the event log. +Instead, events from ``persist`` calls are collected in memory and written to the event log when ``onCommand`` returns. -Events are written asynchronously to the event-sourced actor’s ``eventLog``. After writing, the ``eventLog`` actor internally replies to the event-sourced actor with a success or failure message which is passed as argument to the persist ``handler``. Before calling the persist ``handler``, the event-sourced actor internally calls the ``onEvent`` handler with the written event if writing was successful and ``onEvent`` is defined at that event. +Events are written asynchronously to the event-sourced actor’s ``eventLog``. +After writing, the ``eventLog`` actor internally replies to the event-sourced actor with a success or failure message +which is passed as argument to the persist ``handler``. +Before calling the persist ``handler``, the event-sourced actor internally calls the ``onEvent`` handler with the +written event if writing was successful and ``onEvent`` is defined at that event. Both, event handler and persist handler are called on a dispatcher thread of the actor. They can therefore safely access internal actor state. The ``sender()`` reference of the original command sender is also preserved, so that a persist handler can reply to the initial command sender. @@ -30,59 +43,84 @@ Both, event handler and persist handler are called on a dispatcher thread of the The ``EventsourcedActor`` trait also defines a ``persistN`` method. Refer to the EventsourcedActor_ API documentation for details. .. note:: - A command handler should not modify persistent actor state i.e. state that is derived from events. + A command handler should not modify persistent actor state i.e. state that is derived from events. .. _state-sync: State synchronization ~~~~~~~~~~~~~~~~~~~~~ -As explained in section :ref:`command-handler`, events are persisted asynchronously. What happens if another command is sent to an event-sourced actor while persistence is in progress? This depends on the value of ``stateSync``, a member of ``EventsourcedActor`` that can be overridden. +As explained in section :ref:`command-handler`, events are persisted asynchronously. +What happens if another command is sent to an event-sourced actor while persistence is in progress? +This depends on the value of ``stateSync``, a member of ``EventsourcedActor`` that can be overridden. .. includecode:: ../../../eventuate-core/src/main/scala/com/rbmhtechnology/eventuate/EventsourcedActor.scala :snippet: state-sync -If ``stateSync`` is ``true`` (default), new commands are stashed_ while persistence is in progress. Consequently, new commands see actor state that is *in sync* with the events in the event log. A consequence is limited write throughput, because :ref:`batching` of write requests is not possible in this case\ [#]_. This setting is recommended for event-sourced actors that must validate commands against current state. +If ``stateSync`` is ``true`` (default), new commands are stashed_ while persistence is in progress. +Consequently, new commands see actor state that is *in sync* with the events in the event log. +A consequence is limited write throughput, because :ref:`batching` of write requests is not possible in this case\ [#]_. +This setting is recommended for event-sourced actors that must validate commands against current state. -If ``stateSync`` is ``false``, new commands are dispatched to ``onCommand`` immediately. Consequently, new commands may see stale actor state. The advantage is significantly higher write throughput as :ref:`batching` of write requests is possible. This setting is recommended for event-sourced actors that don’t need to validate commands against current state. +If ``stateSync`` is ``false``, new commands are dispatched to ``onCommand`` immediately. +Consequently, new commands may see stale actor state. +The advantage is significantly higher write throughput as :ref:`batching` of write requests is possible. +This setting is recommended for event-sourced actors that don’t need to validate commands against current state. -If a sender sends several (update) commands followed by a query to an event-sourced actor that has ``stateSync`` set to ``false``, the query will probably not see the state change from the preceding commands. To achieve read-your-write consistency, the command sender should wait for a reply from the last command before sending the query. The reply must of course be sent from within a ``persist`` handler. +If a sender sends several (update) commands followed by a query to an event-sourced actor that has ``stateSync`` set to ``false``, +the query will probably not see the state change from the preceding commands. +To achieve read-your-write consistency, the command sender should wait for a reply from the last command before sending the query. +The reply must of course be sent from within a ``persist`` handler. .. note:: - State synchronization settings only apply to a single actor instance. Events that are emitted concurrently by other actors and handled by that instance can arrive at any time and modify actor state. Anyway, concurrent events are not relevant for achieving read-your-write consistency and should be handled as described in the :ref:`user-guide`. + State synchronization settings only apply to a single actor instance. + Events that are emitted concurrently by other actors and handled by that instance can arrive at any time and modify actor state. + Anyway, concurrent events are not relevant for achieving read-your-write consistency and should be handled as described in the :ref:`user-guide`. .. _event-handler: Event handler ~~~~~~~~~~~~~ -An event handler is partial function of type ``PartialFunction[Any, Unit]`` for which a type alias ``Receive`` exists. It can be defined by implementing ``onEvent``. An event handler handles persisted events by updating actor state from event details. +An event handler is partial function of type ``PartialFunction[Any, Unit]`` for which a type alias ``Receive`` exists. +It can be defined by implementing ``onEvent``. +An event handler handles persisted events by updating actor state from event details. .. includecode:: ../code/EventSourcingDoc.scala :snippet: event-handler -Event metadata of the last handled event can be obtained with the ``last*`` methods defined by ``EventsourcedActor``. For example, ``lastSequenceNr`` returns the event’s local sequence number, ``lastVectorTimestamp`` returns the event’s vector timestamp. A complete reference is given by the EventsourcedActor_ API documentation. +Event metadata of the last handled event can be obtained with the ``last*`` methods defined by ``EventsourcedActor``. +For example, ``lastSequenceNr`` returns the event’s local sequence number, ``lastVectorTimestamp`` returns the event’s vector timestamp. +A complete reference is given by the EventsourcedActor_ API documentation. .. note:: - An event handler should only update internal actor state without having further side-effects. An exception is :ref:`reliable-delivery` of messages and :ref:`guide-event-driven-communication` with PersistOnEvent_. + An event handler should only update internal actor state without having further side-effects. + An exception is :ref:`reliable-delivery` of messages and :ref:`guide-event-driven-communication` with PersistOnEvent_. .. _ref-event-sourced-views: Event-sourced views ------------------- -An introduction to event-sourced views is already given in sections :ref:`overview`, :ref:`architecture` and the :ref:`user-guide`. Applications use event-sourced views for for maintaining in-memory read models on the query side (Q) of a CQRS_ application. +An introduction to event-sourced views is already given in sections :ref:`overview`, :ref:`architecture` and the :ref:`user-guide`. +Applications use event-sourced views for for maintaining in-memory read models on the query side (Q) of a CQRS_ application. -Like event-sourced actors, event-sourced views distinguish command processing from event processing. They must implement the EventsourcedView_ trait. ``EventsourcedView`` is a functional subset of ``EventsourcedActor`` that cannot ``persist`` events. +Like event-sourced actors, event-sourced views distinguish command processing from event processing. +They must implement the EventsourcedView_ trait. +``EventsourcedView`` is a functional subset of ``EventsourcedActor`` that cannot ``persist`` events. .. _ref-event-sourced-writers: Event-sourced writers --------------------- -An introduction to event-sourced writers is already given in sections :ref:`overview` and :ref:`architecture`. Applications use event-sourced writers for maintaining persistent read models on the query side (Q) of a CQRS_ application. +An introduction to event-sourced writers is already given in sections :ref:`overview` and :ref:`architecture`. +Applications use event-sourced writers for maintaining persistent read models on the query side (Q) of a CQRS_ application. -Like event-sourced views, event-sourced writers can only consume events from an event log but can make incremental batch updates to external, application-defined query databases. A query database can be a relational database, a graph database or whatever is needed by an application. Concrete writers must implement the EventsourcedWriter_ trait. +Like event-sourced views, event-sourced writers can only consume events from an event log but can make incremental batch +updates to external, application-defined query databases. +A query database can be a relational database, a graph database or whatever is needed by an application. +Concrete writers must implement the EventsourcedWriter_ trait. This section outlines how to update a persistent read model in Cassandra_ from events consumed by an event-sourced writer. The relevant events are: @@ -103,9 +141,13 @@ The read model update progress is written to a separate ``PROGRESS`` table with ----+------------- 0 | 3 -The stored sequence number is that of the last successfully processed event. An event is considered as successfully processed if its data have been written to the ``CUSTOMER`` table. Only a single row is needed in the ``PROGRESS`` table to track the update progress for the whole ``CUSTOMER`` table. +The stored sequence number is that of the last successfully processed event. An event is considered as successfully processed +if its data have been written to the ``CUSTOMER`` table. +Only a single row is needed in the ``PROGRESS`` table to track the update progress for the whole ``CUSTOMER`` table. -The event-sourced ``Writer`` in the following example implements ``EventsourcedWriter[Long, Unit]`` (where ``Long`` is the type of the initial read result and ``Unit`` the type of write results). It is initialized with an ``eventLog`` from which it consumes events and a Cassandra ``Session`` for writing event processing results. +The event-sourced ``Writer`` in the following example implements ``EventsourcedWriter[Long, Unit]`` +(where ``Long`` is the type of the initial read result and ``Unit`` the type of write results). +It is initialized with an ``eventLog`` from which it consumes events and a Cassandra ``Session`` for writing event processing results. .. includecode:: ../../../eventuate-examples/src/main/scala/com/rbmhtechnology/example/querydb/Writer.scala :snippet: writer @@ -115,33 +157,59 @@ The event-sourced ``Writer`` in the following example implements ``EventsourcedW On a high level, the example ``Writer`` implements the following behavior: -- During initialization (after start or restart) it asynchronously ``read``\ s the stored update progress from the ``PROGRESS`` table. The read result is passed as argument to ``readSuccess`` and incremented by ``1`` before returning it to the caller. This causes the ``Writer`` to resume event processing from that position in the event log. -- Event are processed in ``onEvent`` by translating them to Cassandra update statements which are added to an in-memory ``batch`` of type ``Vector[BoundStatement]``. The batch is written to Cassandra when Eventuate calls the ``write`` method. -- The ``write`` method asynchronously updates the ``CUSTOMER`` table with the statements contained in ``batch`` and then updates the ``PROGRESS`` table with the sequence number of the last processed event. After having submitted the statements to Cassandra, the batch is cleared for further event processing. Event processing can run concurrently to write operations. -- A ``batch`` that has been updated while a write operation is in progress is written directly after the current write operation successfully completes. If no write operation is in progress, a change to ``batch`` is written immediately. This keeps read model update delays at a minimum and increases batch sizes under increasing load. Batch sizes can be limited with ``replayBatchSize``. - -If a ``write`` (or ``read``) operation fails, the writer is restarted, by default, and resumes event processing from the last stored sequence number + ``1``. This behavior can be changed by overriding ``writeFailure`` (or ``readFailure``) from ``EventsourcedWriter``. +- During initialization (after start or restart) it asynchronously ``read``\ s the stored update progress from the ``PROGRESS`` table. + The read result is passed as argument to ``readSuccess`` and incremented by ``1`` before returning it to the caller. + This causes the ``Writer`` to resume event processing from that position in the event log. +- Event are processed in ``onEvent`` by translating them to Cassandra update statements which are added to an in-memory + ``batch`` of type ``Vector[BoundStatement]``. + The batch is written to Cassandra when Eventuate calls the ``write`` method. +- The ``write`` method asynchronously updates the ``CUSTOMER`` table with the statements contained in ``batch`` and then + updates the ``PROGRESS`` table with the sequence number of the last processed event. + After having submitted the statements to Cassandra, the batch is cleared for further event processing. + Event processing can run concurrently to write operations. +- A ``batch`` that has been updated while a write operation is in progress is written directly after the current write + operation successfully completes. + If no write operation is in progress, a change to ``batch`` is written immediately. + This keeps read model update delays at a minimum and increases batch sizes under increasing load. + Batch sizes can be limited with ``replayBatchSize``. + +If a ``write`` (or ``read``) operation fails, the writer is restarted, by default, and resumes event processing from the +last stored sequence number + ``1``. +This behavior can be changed by overriding ``writeFailure`` (or ``readFailure``) from ``EventsourcedWriter``. .. note:: - The example does not use Cassandra ``BatchStatement``\ s for reasons explained in `this article `_. Atomic writes are not needed because database updates in this example are idempotent and can be re-tried in failure cases. Failure cases where idempotency is relevant are partial updates to the ``CUSTOMER`` table or a failed write to the ``PROGRESS`` table. ``BatchStatement``\ s should only be used when database updates are not idempotent and atomicity is required on database level. - + The example does not use Cassandra ``BatchStatement``\ s for reasons explained in + `this article `_. + Atomic writes are not needed because database updates in this example are idempotent and can be re-tried in failure cases. + Failure cases where idempotency is relevant are partial updates to the ``CUSTOMER`` table or a failed write to the ``PROGRESS`` table. + ``BatchStatement``\ s should only be used when database updates are not idempotent and atomicity is required on database level. + .. _stateful-writers: Stateful writers ~~~~~~~~~~~~~~~~ -The above ``Writer`` implements a stateless writer. Although it accumulates batches while a write operation is in progress, it cannot recover permanent in-memory state from the event log, because event processing only starts from the last stored sequence number. If a writer needs to be stateful, it must return ``None`` from ``readSuccess``. In this case, event replay either starts from scratch or from a previously stored snapshot. A stateful writer should still write the update progress to the ``PROGRESS`` table but exclude events with a sequence number less than or equal to the stored sequence number from contributing to the update ``batch``. +The above ``Writer`` implements a stateless writer. Although it accumulates batches while a write operation is in progress, +it cannot recover permanent in-memory state from the event log, because event processing only starts from the last stored sequence number. +If a writer needs to be stateful, it must return ``None`` from ``readSuccess``. +In this case, event replay either starts from scratch or from a previously stored snapshot. +A stateful writer should still write the update progress to the ``PROGRESS`` table but exclude events with a sequence +number less than or equal to the stored sequence number from contributing to the update ``batch``. .. _ref-event-sourced-processors: Event-sourced processors ------------------------ -An introduction to event-sourced processors is already given in sections :ref:`overview` and :ref:`architecture`. Applications use event-sourced processors to consume events form a source event log, process these events and write the processed events to a target event log. With processors, event logs can be connected to event stream processing pipelines and graphs. +An introduction to event-sourced processors is already given in sections :ref:`overview` and :ref:`architecture`. +Applications use event-sourced processors to consume events form a source event log, process these events and write the processed events to a target event log. +With processors, event logs can be connected to event stream processing pipelines and graphs. -Event-sourced processors are a specialization of :ref:`event-sourced-writers` where the *external database* is a target event log. Concrete stateless processors must implement the EventsourcedProcessor_ trait, stateful processors the StatefulProcessor_ trait (see also :ref:`stateful-writers`). +Event-sourced processors are a specialization of :ref:`event-sourced-writers` where the *external database* is a target event log. +Concrete stateless processors must implement the EventsourcedProcessor_ trait, stateful processors the StatefulProcessor_ trait (see also :ref:`stateful-writers`). -The following example ``Processor`` is an implementation of ``EventsourcedProcessor``. In addition to providing a source ``eventLog``, a concrete processor must also provide a ``targetEventLog``: +The following example ``Processor`` is an implementation of ``EventsourcedProcessor``. +In addition to providing a source ``eventLog``, a concrete processor must also provide a ``targetEventLog``: .. includecode:: ../code/EventSourcingDoc.scala :snippet: processor @@ -151,26 +219,34 @@ The event handler implemented by a processor is ``processEvent``. The type of th .. includecode:: ../../../eventuate-core/src/main/scala/com/rbmhtechnology/eventuate/EventsourcedProcessor.scala :snippet: process -Processed events, to be written to the target event log, are returned by the handler as ``Seq[Any]``. With this handler signature, events from the source log can be +Processed events, to be written to the target event log, are returned by the handler as ``Seq[Any]``. +With this handler signature, events from the source log can be -- excluded from being written to the target log by returning an empty ``Seq`` +- excluded from being written to the target log by returning an empty ``Seq`` - transformed one-to-one by returning a ``Seq`` of size 1 or even - transformed and split by returning a ``Seq`` of size greater than ``1`` .. note:: - ``EventsourcedProcessor`` and ``StatefulProcessor`` internally ensure that writing to the target event log is idempotent. Applications don’t need to take extra care about idempotency. + ``EventsourcedProcessor`` and ``StatefulProcessor`` internally ensure that writing to the target event log is idempotent. + Applications don’t need to take extra care about idempotency. .. _state-recovery: State recovery -------------- -When an event-sourced actor or view is started or re-started, events are replayed to its ``onEvent`` handler so that internal state can be recovered\ [#]_. This is also the case for stateful event-sourced writers and processors. During event replay the ``recovering`` method returns ``true``. Applications can also define a recovery completion handler by overriding ``onRecovery``: +When an event-sourced actor or view is started or re-started, events are replayed to its ``onEvent`` handler so that internal state can be recovered\ [#]_. +This is also the case for stateful event-sourced writers and processors. +During event replay the ``recovering`` method returns ``true``. +Applications can also define a recovery completion handler by overriding ``onRecovery``: .. includecode:: ../code/EventSourcingDoc.scala :snippet: recovery-handler -If replay fails the completion handler is called with a ``Failure`` and the actor will be stopped, regardless of the action taken by the handler. The default recovery completion handler does nothing. Internally each replay request towards the event log is retried a couple of times in order to cope with a temporarily unresponsive event log or its underlying storage backend. The maximum number of retries for a replay request can be configured with: +If replay fails the completion handler is called with a ``Failure`` and the actor will be stopped, regardless of the action taken by the handler. +The default recovery completion handler does nothing. Internally each replay request towards the event log is retried a +couple of times in order to cope with a temporarily unresponsive event log or its underlying storage backend. +The maximum number of retries for a replay request can be configured with: .. includecode:: ../conf/common.conf :snippet: replay-retry-max @@ -180,12 +256,19 @@ Moreover the configuration value ``replay-retry-delay`` is used to determine the .. includecode:: ../conf/common.conf :snippet: replay-retry-delay -At the beginning of event replay, the initiating actor is registered at its event log so that newly written events can be routed to that actor. During replay, the actor internally stashes these newly written events and dispatches them to ``onEvent`` after successful replay. In a similar way, the actor also stashes new commands and dispatches them to ``onCommand`` afterwards. This ensures that new commands never see partially recovered state. When the actor is stopped it is automatically de-registered from its event log. +At the beginning of event replay, the initiating actor is registered at its event log so that newly written events can be routed to that actor. +During replay, the actor internally stashes these newly written events and dispatches them to ``onEvent`` after successful replay. +In a similar way, the actor also stashes new commands and dispatches them to ``onCommand`` afterwards. +This ensures that new commands never see partially recovered state. +When the actor is stopped it is automatically de-registered from its event log. Backpressure ~~~~~~~~~~~~ -Events are replayed in batches. A given batch must have been handled by an event handler before the next batch is replayed. This allows slow event handlers to put backpressure on event replay. The default replay batch size can be configured with: +Events are replayed in batches. +A given batch must have been handled by an event handler before the next batch is replayed. +This allows slow event handlers to put backpressure on event replay. +The default replay batch size can be configured with: .. includecode:: ../conf/common.conf :snippet: replay-batch-size @@ -197,7 +280,9 @@ Event-sourced components can override the configured default value by overriding Recovery using an application-defined log sequence number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In order to keep recovery times small it is almost always sensible to recover using snapshots. However, in some very rare cases an event-sourced actor or view can recover quickly using an application-defined log sequence number. If defined, only events with a sequence number equal to or larger than the given sequence number are replayed. +In order to keep recovery times small it is almost always sensible to recover using snapshots. +However, in some very rare cases an event-sourced actor or view can recover quickly using an application-defined log sequence number. +If defined, only events with a sequence number equal to or larger than the given sequence number are replayed. .. includecode:: ../code/EventSourcingDoc.scala :snippet: replay-from-sequence-nr @@ -207,26 +292,34 @@ In order to keep recovery times small it is almost always sensible to recover us Snapshots --------- -Recovery times increase with the number of events that are replayed to event-sourced components. They can be decreased by starting event replay from a previously saved snapshot of internal state rather than replaying events from scratch. Event-sourced components can save snapshots by calling ``save`` within their command handler: +Recovery times increase with the number of events that are replayed to event-sourced components. +They can be decreased by starting event replay from a previously saved snapshot of internal state rather than replaying events from scratch. +Event-sourced components can save snapshots by calling ``save`` within their command handler: .. includecode:: ../code/EventSourcingDoc.scala :snippet: snapshot-save -Snapshots are saved asynchronously. On completion, a user-defined handler of type ``Try[SnapshotMetadata] => Unit`` is called. Like a ``persist`` handler, a ``save`` handler may also close over actor state and can reply to the command sender using the ``sender()`` reference. +Snapshots are saved asynchronously. On completion, a user-defined handler of type ``Try[SnapshotMetadata] => Unit`` is called. +Like a ``persist`` handler, a ``save`` handler may also close over actor state and can reply to the command sender using the ``sender()`` reference. -An event-sourced actor that is :ref:`tracking-conflicting-versions` of application state can also save ``ConcurrentVersions[A, B]`` instances directly. One can even configure custom serializers for type parameter ``A`` as explained in section :ref:`snapshot-serialization`. +An event-sourced actor that is :ref:`tracking-conflicting-versions` of application state can also save ``ConcurrentVersions[A, B]`` instances directly. +One can even configure custom serializers for type parameter ``A`` as explained in section :ref:`snapshot-serialization`. -During recovery, the latest snapshot saved by an event-sourced component is loaded and can be handled with the ``onSnapshot`` handler. This handler should initialize internal actor state from the loaded snapshot: +During recovery, the latest snapshot saved by an event-sourced component is loaded and can be handled with the ``onSnapshot`` handler. +This handler should initialize internal actor state from the loaded snapshot: .. includecode:: ../code/EventSourcingDoc.scala :snippet: snapshot-load -If ``onSnapshot`` is not defined at the loaded snapshot or not overridden at all, event replay starts from scratch. If ``onSnapshot`` is defined at the loaded snapshot, only events that are not covered by that snapshot will be replayed. +If ``onSnapshot`` is not defined at the loaded snapshot or not overridden at all, event replay starts from scratch. +If ``onSnapshot`` is defined at the loaded snapshot, only events that are not covered by that snapshot will be replayed. -Event-sourced actors that implement ``ConfirmedDelivery`` for :ref:`reliable-delivery` automatically include unconfirmed messages into state snapshots. These are restored on recovery and re-delivered on recovery completion. +Event-sourced actors that implement ``ConfirmedDelivery`` for :ref:`reliable-delivery` automatically include unconfirmed messages into state snapshots. +These are restored on recovery and re-delivered on recovery completion. .. note:: - State objects passed as argument to ``save`` should be *immutable objects*. If this is not the case, the caller is responsible for creating a defensive copy before passing it as argument to ``save``. + State objects passed as argument to ``save`` should be *immutable objects*. + If this is not the case, the caller is responsible for creating a defensive copy before passing it as argument to ``save``. Storage locations ~~~~~~~~~~~~~~~~~ @@ -248,17 +341,23 @@ If this number is exceeded, older snapshots are automatically deleted. Event routing ------------- -An event that is emitted by an event-sourced actor or processor can be routed to other event-sourced components if they share an :ref:`event-log`\ [#]_ . The default event routing rules are: +An event that is emitted by an event-sourced actor or processor can be routed to other event-sourced components if they share an :ref:`event-log`\ [#]_ . +The default event routing rules are: -- If an event-sourced component has an undefined ``aggregateId``, all events are routed to it. It may choose to handle only a subset of them though. -- If an event-sourced component has a defined ``aggregateId``, only events emitted by event-sourced actors or processors with the same ``aggregateId`` are routed to it. +- If an event-sourced component has an undefined ``aggregateId``, all events are routed to it. + It may choose to handle only a subset of them though. +- If an event-sourced component has a defined ``aggregateId``, only events emitted by event-sourced actors or processors + with the same ``aggregateId`` are routed to it. -Routing destinations are defined during emission of an event and are persisted together with the event\ [#]_. This makes routing decisions repeatable during event replay and allows for routing rule changes without affecting past routing decisions. Applications can define additional routing destinations with the ``customDestinationAggregateIds`` parameter of ``persist``: +Routing destinations are defined during emission of an event and are persisted together with the event\ [#]_. +This makes routing decisions repeatable during event replay and allows for routing rule changes without affecting past routing decisions. +Applications can define additional routing destinations with the ``customDestinationAggregateIds`` parameter of ``persist``: .. includecode:: ../code/EventRoutingDoc.scala :snippet: custom-routing -Here, ``ExampleEvent`` is routed to destinations with ``aggregateId``\ s ``Some(“a2”)`` and ``Some(“a3”)`` in addition to the default routing destinations with ``aggregateId``\s ``Some(“a1”)`` and ``None``. +Here, ``ExampleEvent`` is routed to destinations with ``aggregateId``\ s ``Some(“a2”)`` and ``Some(“a3”)`` in addition +to the default routing destinations with ``aggregateId``\s ``Some(“a1”)`` and ``None``. .. _ref-event-driven-communication: @@ -272,24 +371,40 @@ Event-driven communication is one form of :ref:`overview-event-collaboration` an Reliable delivery ----------------- -Reliable, event-based remote communication between event-sourced actors should be done via a :ref:`replicated-event-log`. For reliable communication with other services that cannot connect to a replicated event log, event-sourced actors should use the ConfirmedDelivery_ trait: +Reliable, event-based remote communication between event-sourced actors should be done via a :ref:`replicated-event-log`. +For reliable communication with other services that cannot connect to a replicated event log, event-sourced actors should use the ConfirmedDelivery_ trait: .. includecode:: ../code/ReliableDeliveryDoc.scala :snippet: reliable-delivery -``ConfirmedDelivery`` supports the reliable delivery of messages to destinations by enabling applications to re-deliver messages until delivery is confirmed by destinations. In the example above, the reliable delivery of a message is initiated by sending a ``DeliverCommand`` to ``ExampleActor``. +``ConfirmedDelivery`` supports the reliable delivery of messages to destinations by enabling applications to re-deliver +messages until delivery is confirmed by destinations. +In the example above, the reliable delivery of a message is initiated by sending a ``DeliverCommand`` to ``ExampleActor``. -The handler of the generated ``DeliverEvent`` calls ``deliver`` to deliver a ``ReliableMessage`` to ``destination``. The ``deliveryId`` is an identifier to correlate ``ReliableMessage`` with a ``Confirmation`` message. The ``deliveryId`` can be any application-defined id. Here, the event’s sequence number is used which can be obtained with ``lastSequenceNumber``. +The handler of the generated ``DeliverEvent`` calls ``deliver`` to deliver a ``ReliableMessage`` to ``destination``. +The ``deliveryId`` is an identifier to correlate ``ReliableMessage`` with a ``Confirmation`` message. +The ``deliveryId`` can be any application-defined id. Here, the event’s sequence number is used which can be obtained with ``lastSequenceNumber``. -The ``destination`` confirms the delivery of the message by sending a ``Confirmation`` reply to the event-sourced actor from which it generates a ``ConfirmationEvent``. The actor uses the ``persistConfirmation`` method to persist the confirmation event together with the delivery id. After successful persistence of the confirmation event, the corresponding reliable message is removed from the internal buffer of unconfirmed messages. +The ``destination`` confirms the delivery of the message by sending a ``Confirmation`` reply to the event-sourced actor +from which it generates a ``ConfirmationEvent``. +The actor uses the ``persistConfirmation`` method to persist the confirmation event together with the delivery id. +After successful persistence of the confirmation event, the corresponding reliable message is removed from the internal buffer of unconfirmed messages. -When the actor is re-started, unconfirmed reliable messages are automatically re-delivered to their ``destination``\ s. The example actor additionally schedules ``redeliverUnconfirmed`` calls to periodically re-deliver unconfirmed messages. This is done within the actor’s command handler. +When the actor is re-started, unconfirmed reliable messages are automatically re-delivered to their ``destination``\ s. +The example actor additionally schedules ``redeliverUnconfirmed`` calls to periodically re-deliver unconfirmed messages. +This is done within the actor’s command handler. .. note:: - In the above example a pattern guard is used for idempotent confirmation processing by ensuring that the ``deliveryId`` of the ``Confirmation`` message is still unconfirmed. This pattern may only be applied if the ``stateSync`` member of the ``EventsourcedActor`` is set to ``true``. For further details on ``stateSync`` see section :ref:`state-sync`. + In the above example a pattern guard is used for idempotent confirmation processing by ensuring that the ``deliveryId`` + of the ``Confirmation`` message is still unconfirmed. + This pattern may only be applied if the ``stateSync`` member of the ``EventsourcedActor`` is set to ``true``. + For further details on ``stateSync`` see section :ref:`state-sync`. .. note:: - If a snapshot is taken unconfirmed messages are stored in the snapshot along with the destination ``ActorPath``. That is why the actual ``ActorPath`` of the destination must not change between restarts of the actor, if, for example, the destination actor is within the same application and the application is restarted. That is why the destination actor must be named explicitly instead of having a name generated by the ``ActorSystem``. + If a snapshot is taken unconfirmed messages are stored in the snapshot along with the destination ``ActorPath``. + That is why the actual ``ActorPath`` of the destination must not change between restarts of the actor, if, for example, + the destination actor is within the same application and the application is restarted. + That is why the destination actor must be named explicitly instead of having a name generated by the ``ActorSystem``. .. _ref-conditional-requests: @@ -303,38 +418,72 @@ Conditional requests are covered in the :ref:`conditional-requests` section of t Command stashing ---------------- -``EventsourcedView`` and ``EventsourcedActor`` override ``stash()`` and ``unstashAll()`` of ``akka.actor.Stash`` so that application-specific subclasses can safely stash and unstash commands. Stashing of events is not allowed. Hence, ``stash()`` must only be used in a command handler, using it in an event handler will throw ``StashError``. On the other hand, ``unsatshAll()`` can be used anywhere i.e. in a command handler, persist handler or event handler. The following is a trivial usage example which calls ``stash()`` in the command handler and ``unstashAll()`` in the persist handler: +``EventsourcedView`` and ``EventsourcedActor`` override ``stash()`` and ``unstashAll()`` of ``akka.actor.Stash`` so that +application-specific subclasses can safely stash and unstash commands. Stashing of events is not allowed. +Hence, ``stash()`` must only be used in a command handler, using it in an event handler will throw ``StashError``. +On the other hand, ``unsatshAll()`` can be used anywhere i.e. in a command handler, persist handler or event handler. +The following is a trivial usage example which calls ``stash()`` in the command handler and ``unstashAll()`` in the persist handler: .. includecode:: ../code/EventSourcingDoc.scala :snippet: command-stash -The ``UserManager`` maintains a persistent ``users`` map. User can be added to the map by sending a ``CreateUser`` command and updated by sending and ``UpdateUser`` command. Should these commands arrive in wrong order i.e. ``UpdateUser`` before a corresponding ``CreateUser``, the ``UserManager`` stashes ``UpdateUser`` and unstashes it after having successfully processed another ``CreateUser`` command. +The ``UserManager`` maintains a persistent ``users`` map. User can be added to the map by sending a ``CreateUser`` command +and updated by sending and ``UpdateUser`` command. +Should these commands arrive in wrong order i.e. ``UpdateUser`` before a corresponding ``CreateUser``, the ``UserManager`` +stashes ``UpdateUser`` and unstashes it after having successfully processed another ``CreateUser`` command. -In the above implementation, an ``UpdateUser`` command might be repeatedly stashed and unstashed if the corresponding ``CreateUser`` command is preceded by other unrelated ``CreateUser`` commands. Assuming that out-of-order user commands are rare, the performance impact is limited. Alternatively, one could record stashed user ids in transient actor state and conditionally call ``unstashAll()`` by checking that state. +In the above implementation, an ``UpdateUser`` command might be repeatedly stashed and unstashed if the corresponding +``CreateUser`` command is preceded by other unrelated ``CreateUser`` commands. +Assuming that out-of-order user commands are rare, the performance impact is limited. +Alternatively, one could record stashed user ids in transient actor state and conditionally call ``unstashAll()`` by checking that state. Behavior changes ---------------- -Event-sourced components distinguish command processing from event processing. Consequently, applications should be able to change the behavior of command handlers and event handlers independent of each other, at runtime. Command handling behavior can be changed with ``commandContext.become()`` and ``commandContext.unbecome()``, event handling behavior with ``eventContext.become()`` and ``eventContext.unbecome()`` (for details, see the BehaviorContext_ API docs): +Event-sourced components distinguish command processing from event processing. +Consequently, applications should be able to change the behavior of command handlers and event handlers independent of each other, at runtime. +Command handling behavior can be changed with ``commandContext.become()`` and ``commandContext.unbecome()``, +event handling behavior with ``eventContext.become()`` and ``eventContext.unbecome()`` (for details, see the BehaviorContext_ API docs): .. includecode:: ../code/EventSourcingDoc.scala :snippet: behavior-changes -This works for all event-sourcing abstractions except for ``EventsourcedProcessor``. Its ``eventContext`` does not allow behavior changes as ``EventsourcedProcessor`` implements default ``onEvent`` behavior that should be changed by applications. An attempt to change that behavior will throw an ``UnsupportedOperationException``. Changing an ``EventsourcedProcessor``’s ``processEvent`` behavior is not supported yet. +This works for all event-sourcing abstractions except for ``EventsourcedProcessor``. Its ``eventContext`` does not allow +behavior changes as ``EventsourcedProcessor`` implements default ``onEvent`` behavior that should be changed by applications. +An attempt to change that behavior will throw an ``UnsupportedOperationException``. +Changing an ``EventsourcedProcessor``’s ``processEvent`` behavior is not supported yet. .. note:: - Command and event handling behaviors are managed by internal behavior stacks. Eventuate does **not** include these behavior stacks into :ref:`snapshots` when applications ``save`` actor state. Although the state of an event handling behavior stack can be recovered by replaying events from scratch, that stack is not automatically recovered when a snapshot is loaded. Applications are therefore responsible to restore the required command and event handling behavior from application-specific snapshot details in the ``onSnapshot`` handler. Of course, this is only necessary if the required behavior differs from the default ``onEvent`` and ``onCommand`` behavior. + Command and event handling behaviors are managed by internal behavior stacks. + Eventuate does **not** include these behavior stacks into :ref:`snapshots` when applications ``save`` actor state. + Although the state of an event handling behavior stack can be recovered by replaying events from scratch, + that stack is not automatically recovered when a snapshot is loaded. + Applications are therefore responsible to restore the required command and event handling behavior from application-specific + snapshot details in the ``onSnapshot`` handler. + Of course, this is only necessary if the required behavior differs from the default ``onEvent`` and ``onCommand`` behavior. Failure handling ---------------- -Event-sourced components register themselves at an EventLog_ actor in order to be notified about changes in the event log. Directly after registration, during recovery, they read from the event log in order to recover internal state from past events. After recovery has completed, the event log actor **pushes** newly written events to registered actors so that they can update application state with minimal latency. If a registered actor is restarted, it recovers again from the event log and continues to process push-updates after recovery has completed. +Event-sourced components register themselves at an EventLog_ actor in order to be notified about changes in the event log. +Directly after registration, during recovery, they read from the event log in order to recover internal state from past events. +After recovery has completed, the event log actor **pushes** newly written events to registered actors so that they can update application state with minimal latency. +If a registered actor is restarted, it recovers again from the event log and continues to process push-updates after recovery has completed. -An EventLog_ actor processes write requests from :ref:`ref-event-sourced-actors`, :ref:`ref-event-sourced-processors` and :ref:`replication-endpoints`. If a write succeeds it pushes the written events to registered actors (under consideration of :ref:`event-routing` rules) and handles the next write request. Writing to a storage backend may also fail for several reasons. In the following, it is assumed that writes are made to a remote storage backend such as the :ref:`cassandra-storage-backend`. +An EventLog_ actor processes write requests from :ref:`ref-event-sourced-actors`, :ref:`ref-event-sourced-processors` and :ref:`replication-endpoints`. +If a write succeeds it pushes the written events to registered actors (under consideration of :ref:`event-routing` rules) and handles the next write request. +Writing to a storage backend may also fail for several reasons. +In the following, it is assumed that writes are made to a remote storage backend such as the :ref:`cassandra-storage-backend`. -A write failure reported from a storage backend driver does not necessarily mean that the events have not been written to the storage backend. For example, a write could have been actually applied to the remote storage backend but the ACK message got lost. This usually causes the driver to report a timeout. If an event log actor would simply continue with the next write request, after having informed the event emitter about the failure, the emitter and and other registered actors would erroneously assume that the emitted events do not exist in the event log. However, these events may become visible to newly registered actors that are about to recover or to replication endpoints that read events for replication. +A write failure reported from a storage backend driver does not necessarily mean that the events have not been written to the storage backend. +For example, a write could have been actually applied to the remote storage backend but the ACK message got lost. +This usually causes the driver to report a timeout. +If an event log actor would simply continue with the next write request, after having informed the event emitter about +the failure, the emitter and and other registered actors would erroneously assume that the emitted events do not exist in the event log. +However, these events may become visible to newly registered actors that are about to recover or to replication endpoints that read events for replication. -This would violate the event ordering and consistency guarantees made by Eventuate because some registered actors would see an event stream with missing events. The following describes two options to deal with that situation: +This would violate the event ordering and consistency guarantees made by Eventuate because some registered actors would +see an event stream with missing events. The following describes two options to deal with that situation: #. After a failed write, the event log actor notifies all registered actors to restart themselves so that another recovery phase would find out whether the events have been actually written or not. This is fine if the write failure was actually a lost ACK and the storage backend is immediately available for subsequent reads (neglecting a potentially high read load). If the write failure was because of a longer-lasting problem, such as a longer network partition that disconnects the application from the storage backend, registered actors would fail to recover and would be therefore be unavailable for in-memory reads. @@ -342,44 +491,70 @@ This would violate the event ordering and consistency guarantees made by Eventua In Eventuate, the second approach is taken. Should there be a longer-lasting problem with the storage backend, it may take a longer time for an event log actor to make a decision about the success or failure of a write. During that time, it will reject further writes in order to avoid being overloaded with pending write requests. This is an application of the `circuit breaker`_ design pattern. -Consequently, a write failure reported by an event log actor means that the write was actually **not** applied to the storage backend. This additional guarantee comes at the cost of potentially long write reply delays but allows registered actors to remain available for in-memory reads during storage backend unavailability. It also provides clearer semantics of write failures. +Consequently, a write failure reported by an event log actor means that the write was actually **not** applied to the storage backend. This additional guarantee comes at the cost of potentially long write reply delays but allows registered actors to remain available for in-memory reads during storage backend unavailability. It also provides clearer semantics of write failures. .. _circuit-breaker: Circuit breaker ~~~~~~~~~~~~~~~ -The strategy described above can be implemented by wrapping a CassandraEventLog_ in a CircuitBreaker_ actor. This is the default when creating the log actor for a :ref:`cassandra-storage-backend`. Should the event log actor need to retry a write ``eventuate.log.circuit-breaker.open-after-retries`` times or more, the circuit breaker opens. If open, it rejects all requests by replying with a failure message that contains an EventLogUnavailableException_. If retrying the write finally succeeds, the circuit breaker closes again. The maximum number of write retries can be configured with ``eventuate.log.cassandra.write-retry-max`` and the delay between write retries with ``eventuate.log.write-timeout``. If the maximum number of retries is reached, the event log actor gives up and stops itself which also stops all registered actors. +The strategy described above can be implemented by wrapping a CassandraEventLog_ in a CircuitBreaker_ actor. +This is the default when creating the log actor for a :ref:`cassandra-storage-backend`. +Should the event log actor need to retry a write ``eventuate.log.circuit-breaker.open-after-retries`` times or more, +the circuit breaker opens. +If open, it rejects all requests by replying with a failure message that contains an EventLogUnavailableException_. +If retrying the write finally succeeds, the circuit breaker closes again. +The maximum number of write retries can be configured with ``eventuate.log.cassandra.write-retry-max`` and the delay +between write retries with ``eventuate.log.write-timeout``. +If the maximum number of retries is reached, the event log actor gives up and stops itself which also stops all registered actors. .. _persist-failure-handling: ``persist`` failure handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Asynchronous ``persist`` operations send write requests to an EventLog_ actor. The write reply is passed as argument to the persist handler (see section :ref:`command-handler`). If the persist handler is called with a ``Failure`` one can safely assume that the events have not been written to the storage backend. As already explained above, a consequence of this additional guarantee is that persist handler callbacks may be delayed indefinitely. - -For an ``EventsourcedActor`` with ``stateSync`` set to ``true``, this means that further commands sent to that actor will be stashed until the current write completes. In this case, it is the responsibility of the application not to overload that actor with further commands. For example, an application could use timeouts for command replies and prevent sending further commands to that actor if a timeout occurred. After an application-defined delay, command sending can be resumed. This is comparable to using an application-level circuit breaker. Alternatively, an application could restart an event-sourced actor on command timeout and continue sending new commands to that actor after recovery succeeded. This however may take a while depending on the unavailability duration of the storage backend. - -``EventsourcedActor``\ s with ``stateSync`` set to ``false`` do not stash commands but rather send write requests immediately to the event log actor. If the log actor is busy retrying a write and the :ref:`circuit-breaker` opens, later persist operations will be completed immediately with an ``EventLogUnavailableException`` failure, regardless whether the event-sourced actor has persist operations in progress or not. A persist operation of an ``EventsourcedActor`` with ``stateSync`` set to ``true`` will only be completed with an ``EventLogUnavailableException`` failure if that actor had no persist operation in progress at the time the circuit breaker opened. +Asynchronous ``persist`` operations send write requests to an EventLog_ actor. +The write reply is passed as argument to the persist handler (see section :ref:`command-handler`). +If the persist handler is called with a ``Failure`` one can safely assume that the events have not been written to the storage backend. +As already explained above, a consequence of this additional guarantee is that persist handler callbacks may be delayed indefinitely. + +For an ``EventsourcedActor`` with ``stateSync`` set to ``true``, this means that further commands sent to that actor +will be stashed until the current write completes. In this case, it is the responsibility of the application not to +overload that actor with further commands. For example, an application could use timeouts for command replies and prevent +sending further commands to that actor if a timeout occurred. +After an application-defined delay, command sending can be resumed. +This is comparable to using an application-level circuit breaker. +Alternatively, an application could restart an event-sourced actor on command timeout and continue sending new commands +to that actor after recovery succeeded. This however may take a while depending on the unavailability duration of the storage backend. + +``EventsourcedActor``\ s with ``stateSync`` set to ``false`` do not stash commands but rather send write requests +immediately to the event log actor. If the log actor is busy retrying a write and the :ref:`circuit-breaker` opens, +later persist operations will be completed immediately with an ``EventLogUnavailableException`` failure, +regardless whether the event-sourced actor has persist operations in progress or not. +A persist operation of an ``EventsourcedActor`` with ``stateSync`` set to ``true`` will only be completed with +an ``EventLogUnavailableException`` failure if that actor had no persist operation in progress at the time the circuit breaker opened. .. _persist-on-event-failure-handling: ``persistOnEvent`` failure handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``EventsourcedActor``\ s can also persist events in the :ref:`event-handler` if they additionally extend PersistOnEvent_. An asynchronous ``persistOnEvent`` operation may also fail for reasons explained in :ref:`persist-failure-handling`. If a ``persistOnEvent`` operation fails, the actor is automatically restarted by throwing a ``PersistOnEventException``. +``EventsourcedActor``\ s can also persist events in the :ref:`event-handler` if they additionally extend PersistOnEvent_. +An asynchronous ``persistOnEvent`` operation may also fail for reasons explained in :ref:`persist-failure-handling`. +If a ``persistOnEvent`` operation fails, the actor is automatically restarted by throwing a ``PersistOnEventException``. Recovery failure handling ~~~~~~~~~~~~~~~~~~~~~~~~~ - -As explained in section :ref:`state-recovery`, event-sourced components are stopped if their recovery fails. Applications should either define a custom ``onRecovery`` completion handler to obtain information about recovery failure details or just watch these actors if recovery failure details are not relevant. +As explained in section :ref:`state-recovery`, event-sourced components are stopped if their recovery fails. +Applications should either define a custom ``onRecovery`` completion handler to obtain information about recovery failure +details or just watch these actors if recovery failure details are not relevant. Batch write failure handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Events are written in batches. +When using the :ref:`cassandra-storage-backend`, there’s a *warn threshold* and *fail threshold* for batch sizes. +The default settings in ``cassandra.yaml`` are:: -Events are written in batches. When using the :ref:`cassandra-storage-backend`, there’s a *warn threshold* and *fail threshold* for batch sizes. The default settings in ``cassandra.yaml`` are:: - - # Caution should be taken on increasing the size of this threshold as it can + # Caution should be taken on increasing the size of this threshold as it can # lead to node instability. batch_size_warn_threshold_in_kb: 5 @@ -392,10 +567,12 @@ When the size of an event batch exceeds the *fail threshold*, the batch write fa The corresponding entry in the Cassandra system log is:: - ERROR Batch of prepared statements for [eventuate.log_] is of size 103800, exceeding specified threshold of 51200 by 52600. (see batch_size_fail_threshold_in_kb) + ERROR Batch of prepared statements for [eventuate.log_] is of size 103800, + exceeding specified threshold of 51200 by 52600. (see batch_size_fail_threshold_in_kb) .. note:: - If Eventuate is the only writer to the Cassandra cluster then it is safe to increase these thresholds to higher values as Eventuate only makes single-partition batch writes (see also `CASSANDRA-8825`_). + If Eventuate is the only writer to the Cassandra cluster then it is safe to increase these thresholds to higher + values as Eventuate only makes single-partition batch writes (see also `CASSANDRA-8825`_). If other applications additionally make multi-partition batch writes to the same Cassandra cluster then is recommended to reduce @@ -407,17 +584,20 @@ and .. includecode:: ../conf/common.conf :snippet: index-update-limit -to a smaller value like ``32``, for example, or even smaller. Failed replication writes or index writes are re-tried automatically by Eventuate. Failed ``persist`` operations must be re-tried by the application. +to a smaller value like ``32``, for example, or even smaller. +Failed replication writes or index writes are retried automatically by Eventuate. +Failed ``persist`` operations must be retried by the application. Batch replication failure handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -During replication, events are batch-transferred over the network. The maximum number of events per batch can be configured with: +During replication, events are batch-transferred over the network. +The maximum number of events per batch can be configured with: .. includecode:: ../conf/common.conf :snippet: write-batch-size -The maximum batch size in bytes the transport will accept is limited. If this limit is exceeded, batch transfer will fail. In this case, applications should either increase +The maximum batch size in bytes the transport will accept is limited. +If this limit is exceeded, batch transfer will fail. In this case, applications should either increase .. includecode:: ../conf/common.conf :snippet: maximum-frame-size @@ -425,49 +605,109 @@ The maximum batch size in bytes the transport will accept is limited. If this li or decrease the event batch size. .. note:: - Batch sizes in Eventuate are currently defined in units of events whereas ``maximum-frame-size`` is defined in bytes. This mismatch will be removed in a later release (see also `ticket 166`_). + Batch sizes in Eventuate are currently defined in units of events whereas ``maximum-frame-size`` is defined in bytes. + This mismatch will be removed in a later release (see also `ticket 166`_). -Custom serialization +Custom Serialization -------------------- +Commands and their replies are not events, so they are not persisted. + +All persisted messages need to be serializable. +In addition, for distributed systems with multiple network nodes, +all messages sent to and received from ``EventsourcedActor`` and ``EventsourcedView`` that are sent via +`Akka Remoting `_ must be serializable. +This is true even for messages that are not persisted. + +TODO a diagram would be helpful here, showing each types of message, +with a legend that indicates the messages which must be serializable. +The diagram should correspond to a working code example. +This means the code example needs to contain each type of message. .. _event-serialization: -Custom event serialization +Custom Event Serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Custom serializers for application-defined events can be configured with Akka's `serialization extension`_. For example, an application that wants to use a custom ``MyDomainEventSerializer`` for events of type ``MyDomainEvent`` (both defined in package ``com.example``) should add the following configuration to ``application.conf``: +Custom serializers for application-defined events can be configured with Akka's `serialization extension`_. +For example, an application that wants to use a custom ``MyDomainEventSerializer`` for events of type ``MyDomainEvent`` +(both defined in package ``com.example``) should add the following configuration to ``application.conf``: .. includecode:: ../conf/serializer.conf :snippet: custom-event-serializer -``MyDomainEventSerializer`` must extend Akka’s Serializer_ trait. Please refer to Akka’s `serialization extension`_ documentation for further details. - -Eventuate stores application-defined events as ``payload`` of DurableEvent_\ s. ``DurableEvent`` itself is serialized with DurableEventSerializer_, a `Protocol Buffers`_ based serializer that delegates ``payload`` serialization to a custom serializer. If no custom serializer is configured, Akka’s default serializer is used. +``MyDomainEventSerializer`` must extend Akka’s Serializer_ trait. +Please refer to Akka’s `serialization extension`_ documentation for further details. + +Eventuate stores application-defined events as ``payload`` of DurableEvent_\ s. ``DurableEvent`` itself is serialized with DurableEventSerializer_, +a `Protocol Buffers`_ based serializer that delegates ``payload`` serialization to a custom serializer. +If no custom serializer is configured, Akka’s default serializer is used. + +Serialization Mechanisms +~~~~~~~~~~~~~~~~~~~~~~~~ +You have many choices for serialization mechanisms. +In the following list, if a Java-based serialization project has a Scala wrapper project based on it, +the Java package is shown first, followed by a slash, followed by the Scala wrapper package. +You are free to pick the package that suits your needs best. + +* `Apache Avro`_ +* `Apache SerializationUtils`_ with `java.nio.ByteBuffer`_ +* `Apache Thrift`_ / `Scrooge`_ +* `Google Protocol Buffers`_ / `ScalaPB`_ +* `Pickling`_ +* `Kryo`_ / `Chill`_ + +.. _Apache Avro: https://avro.apache.org/ +.. _Apache SerializationUtils: https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/SerializationUtils.html +.. _java.nio.ByteBuffer: https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html +.. _Apache Thrift: https://thrift.apache.org/ +.. _Scrooge: https://twitter.github.io/scrooge/ +.. _Google Protocol Buffers: https://developers.google.com/protocol-buffers/ +.. _ScalaPB: https://github.com/scalapb/ScalaPB +.. _Pickling: https://github.com/scala/pickling +.. _Kryo: https://github.com/EsotericSoftware/kryo +.. _Chill: https://github.com/twitter/chill + +`A benchmark `_ +that shows the relative performance of most of these packages is available. +ScalaPB provided the overall fastest results, which is nice, because it is merely a Scala wrapper around Google Protocol Buffers. + +.. image:: https://cdn-images-1.medium.com/max/1000/1*I3kOVTMlgmpSEEh_QFCK5w.png + :align: center + +Note that because Eventuate uses Google Protocol Buffers internally, Eventuate's Event-related datatypes such as +``Payload``, ``VectorTime``, ``VectorTimeEntry`` and ``Versioned`` already have serializers defined. + +TODO describe how to use them. .. _snapshot-serialization: Custom snapshot serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Applications can also configure custom serializers for snapshots in the same way as for application-defined events and +replication filters (see sections :ref:`event-serialization` and :ref:`replication-filter-serialization`). -Applications can also configure custom serializers for snapshots in the same way as for application-defined events and replication filters (see sections :ref:`event-serialization` and :ref:`replication-filter-serialization`). - -Custom snapshot serialization also works for state managed with ``ConcurrentVersions[A, B]``. A custom serializer configured for type parameter ``A`` is used whenever a snapshot of type ``ConcurrentVersions[A, B]`` is saved (see also :ref:`tracking-conflicting-versions`). +Custom snapshot serialization also works for state managed with ``ConcurrentVersions[A, B]``. +A custom serializer configured for type parameter ``A`` is used whenever a snapshot of type ``ConcurrentVersions[A, B]`` +is saved (see also :ref:`tracking-conflicting-versions`). -Event-sourced actors that extend ``ConfirmedDelivery`` for :ref:`reliable-delivery` of messages to destinations will also include unconfirmed messages as ``deliveryAttempts`` in a Snapshot_. The ``message`` field of a DeliveryAttempt_ can also be custom-serialized by configuring a serializer. +Event-sourced actors that extend ``ConfirmedDelivery`` for :ref:`reliable-delivery` of messages to destinations will also +include unconfirmed messages as ``deliveryAttempts`` in a Snapshot_. +The ``message`` field of a DeliveryAttempt_ can also be custom-serialized by configuring a serializer. Custom CRDT serialization ~~~~~~~~~~~~~~~~~~~~~~~~~ - -Custom serializers can also be configured for the type parameter ``A`` of ``MVRegister[A]``, ``LWWRegister[A]`` and ``ORSet[A]`` :ref:`commutative-replicated-data-types`. These serializers are used for both persistent CRDT operations and CRDT snapshots. +Custom serializers can also be configured for the type parameter ``A`` of ``MVRegister[A]``, ``LWWRegister[A]`` and ``ORSet[A]`` +:ref:`commutative-replicated-data-types`. +These serializers are used for both persistent CRDT operations and CRDT snapshots. Resolution of serializers when deserializing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When eventuate serializes application-defined events, :ref:`replication-filters` or snapshots it includes the ``identifier`` of the Akka serializer and the class or string based manifest_ when available. When deserializing these application-defined payloads a serializer is selected as follows: +When eventuate serializes application-defined events, :ref:`replication-filters` or snapshots it includes the ``identifier`` +of the Akka serializer and the class or string based manifest_ when available. +When deserializing these application-defined payloads a serializer is selected as follows: - If a class-based manifest is included, the serializer that is configured in the Akka configuration for this class is selected -- In case of a string-based manifest or no manifest the serializer is selected by the included ``identifier`` - +- In case of a string-based manifest or no manifest the serializer is selected by the included ``identifier``. .. [#] The ``customDestinationAggregateIds`` parameter is described in section :ref:`event-routing`. .. [#] Writes from different event-sourced actors that have ``stateSync`` set to ``true`` are still batched, but not the writes from a single event-sourced actor. diff --git a/src/sphinx/run b/src/sphinx/run new file mode 100644 index 00000000..827d4fe1 --- /dev/null +++ b/src/sphinx/run @@ -0,0 +1,3 @@ +#!/bin/bash + +sphinx-build -b html src/sphinx/ target/html/ &> sphinx.log diff --git a/src/sphinx/user-guide.rst b/src/sphinx/user-guide.rst index ad23adea..9749c6e0 100644 --- a/src/sphinx/user-guide.rst +++ b/src/sphinx/user-guide.rst @@ -1,333 +1,746 @@ +.. |br| raw:: html + +
+ .. _user-guide: ---------- User guide ---------- -This is a brief user guide to Eventuate. It is recommended to read sections :ref:`overview` and :ref:`architecture` first. Based on simple examples, you’ll see how to +Welcome to the `Eventuate`_ user guide. -- implement an event-sourced actor -- replicate actor state with event sourcing -- detect concurrent updates to replicated state -- track conflicts from concurrent updates -- resolve conflicts automatically and interactively -- make concurrent updates conflict-free with operation-based CRDTs -- implement an event-sourced view over many event-sourced actors -- achieve causal read consistency across event-sourced actors and views and -- implement event-driven communication between event-sourced actors. +.. _Eventuate: http://rbmhtechnology.github.io/eventuate/ -The user guide only scratches the surface of Eventuate. You can find further details in the :ref:`reference`. +What You Will Learn +------------------- +This user guide presents simple examples that demonstrate how to: + +- Implement an event-sourced actor. +- Replicate actor state with event sourcing. +- Detect concurrent updates to replicated state. +- Track conflicts from concurrent updates. +- Resolve conflicts automatically and interactively. +- Make concurrent updates conflict-free with operation-based CRDTs. +- Implement an event-sourced view over many event-sourced actors. +- Achieve causal read consistency across event-sourced actors and views. +- Implement event-driven communication between event-sourced actors. + +You can find further details in the :ref:`reference` documentation. + +Prerequisites +------------- +Before working through this user guide you should: + +* Be familiar with the Eventuate :ref:`overview` and :ref:`architecture`. +* Have `Java 8 `_ installed. +* Have SBT installed (`Mac `_, + `Windows `_, + `Linux `_). +* Understand how the Akka ActorSystem and Actors work. + The `Akka documentation `_ is excellent. +* Understand how `Akka Persistence `_ works, + because Eventuate is a functional superset of Akka Persistence. +* To work through the Java examples, you should be familiar with `programming Java 8 `_. +* To work through the Scala examples, you should be familiar with programming Scala. + The ultimate reference for Scala is `Programming in Scala `_; + this book takes most people weeks or months of dedicated reading to complete, and years to assimilate. + A faster and better way to learn is via `ScalaCourses.com `_. + Their Introduction to Scala course starts you off with Scala, SBT and various editors, + then moves on to object-oriented programming and introduces functional programming. + The Intermediate Scala course teaches the more advanced Scala necessary and enough Akka so you can work through and + understand the material presented in this user guide. .. _guide-event-sourced-actors: -Event-sourced actors --------------------- +About the Code Examples +----------------------- +Code examples in this document are provided for Scala and Java, in the SBT project provided in the ``examples/user-guide`` +directory of the `Eventuate GitHub project `_. -An event-sourced actor is an actor that captures changes to its internal state as a sequence of events. It *persists* these events to an event log and *replays* them to recover internal state after a crash or a planned re-start. This is the basic idea behind `event sourcing`_: instead of storing current application state, the full history of changes is stored as *immutable facts* and current state is derived from these facts. +The `Scaladoc`_ is currently the API reference for both Scala and Java. +No separate Javadoc exists. +Java programmers may find the Scaladoc somewhat confusing because it requires rudimentary knowledge of Scala in order to interpret it. +Studying the provided working Java programs should help. -Event-sourced actors distinguish between *commands* and *events*. During command processing they usually validate external commands against internal state and, if validation succeeds, write one or more events to their event log. During event processing they consume events they have written and update internal state by handling these events. +The Scala code examples use Scala 2.12, because the Spark adapter is not used. +Most of Eventuate is compatible with Scala 2.12, with the exception of the +`Spark Adapter `_. +Spark does not yet support Scala 2.12, and Scala 2.12 support will be slow in coming to Spark. -.. hint:: - Event-sourced actors can also write new events during event processing. This is covered in section :ref:`guide-event-driven-communication`. +The Java code examples use `Javaslang`_ and Java 8. +Javaslang core is a functional library for Java 8+. +It helps to reduce the amount of code and to increase the robustness of the code itself. + +The database used in the code examples is `LevelDB `_, an open source project by Google, written in C++. +LevelDB is a fast key-value storage the provides ordered mapping from ``Array[Byte]`` keys to ``Array[Byte]`` values. +The Eventuate project uses a `JNI wrapper for LevelDB `_ to implement a +`Scala wrapper `_. -Concrete event-sourced actors must implement the ``EventsourcedActor`` trait. The following ``ExampleActor`` maintains state of type ``Vector[String]`` to which entries can be appended: +Akka configuration values are provided in ``src/main/resources/application.conf``, as per the +`Akka configuration documentation `_: + +.. literalinclude:: ../../examples/user-guide/src/main/resources/application.conf + :language: none + +.. _Javaslang: http://www.javaslang.io/ +.. _Scaladoc: http://rbmhtechnology.github.io/eventuate/latest/api/index.html + +Event-Sourced Actors +-------------------- +An event-sourced actor is an actor that captures changes to its internal state as a sequence of events. +It *persists* these events to an event log and *replays* them to recover internal state after a crash or an orderly restart. +This is the basic idea behind `event sourcing`_: instead of storing current application state, +the full history of changes is stored as *immutable facts* and current state is derived from these facts. + +Event-sourced actors respond to *commands* and *events*. +One or more events are created when an event-sourced actor processes a valid command. +Many organization adopt the conventions that commands are named as imperatives (e.g. ``CreateProduct``), in contrast to events, +which are named as verbs in the past tense (e.g. ``ProductCreated``). + +* **During command processing**, event-sourced actors validate external commands against internal state and, if validation succeeds, + they persist (write) one or more events to their named event log. +* **During event processing**, event-sourced actors replay (read) previously-written events from their named log, + then they update their internal state. Event-sourced actors can also create and write additional events during event processing. + This topic is discussed in greater detail in the :ref:`guide-event-driven-communication` section of the Eventuate :ref:`architecture` document. + +*When writing in Scala:* concrete event-sourced actors mix in the `EventsourcedActor`_ trait and define two methods: +the `onCommand`_ method, which persists valid commands as events, and the `onEvent`_ method, which reads events, +takes any necessary action and modifies internal state. +In the following code, ``ExampleActor`` encapsulates state ``currentState`` of type ``Vector[String]``, to which entries can be appended. + +*When writing in Java:* concrete event-sourced actors extend the `AbstractEventsourcedActor`_ abstract class and define the ``onCommand`` and ``onEvent`` methods, +which have the same responsibilities as the Scala version of the same methods. +In the following code, ``ExampleActor`` encapsulates state ``currentState`` of type ``Collection``, to which entries can be appended. +Note that the Java code is a lot longer than the Scala version. +This is one of the differences between the two languages; not only Scala is much more succinct, it is also more expressive and flexible. + +ActorExample +^^^^^^^^^^^^ +This code example for this section and the next is provided in the accompanying source code for this User Guide, +in the ``ActorExample.scala`` and ``ActorExample.java`` programs. + +First we'll define the messages: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/Messages.scala :snippet: event-sourced-actor - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/Messages.java :snippet: event-sourced-actor -For modifying ``currentState``, applications send ``Append`` commands which are handled by the ``onCommand`` handler. From an ``Append`` command, the handler derives an ``Appended`` event and ``persist``\ s it to the given ``eventLog``. If persistence succeeds, the command sender is informed about successful processing. If persistence fails, the command sender is informed about the failure so it can retry, if needed. +Now the code: -The ``onEvent`` handler updates ``currentState`` from persisted events and is automatically called after a successful ``persist``. If the actor is re-started, either after a crash or during normal application start, persisted events are replayed to ``onEvent`` which recovers internal state before new commands are processed. +.. tabbed-code:: + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala + :snippet: event-sourced-actor + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java + :snippet: event-sourced-actor -``EventsourcedActor`` implementations must define a global unique ``id`` and require an ``eventLog`` actor reference for writing and replaying events. An event-sourced actor may also define an optional ``aggregateId`` which has an impact how events are routed between event-sourced actors. +As shown above, ``EventsourcedActor`` implementations must define a global unique ``id`` and an ``eventLog`` actor reference for writing and replaying events. +An event-sourced actor may also define an optional ``aggregateId``, which affects how events are routed between event-sourced actors. +Section :ref:`event-log` explains how to create ``eventLog`` actor references. -.. hint:: - Section :ref:`event-log` explains how to create ``eventLog`` actor references. +As already mentioned, When an event-sourced actor receives a command, first the actor persists the command as an event, then it modifies its own internal state. +Referring to the above code, here is an example sequence: + +1. An ``Append`` command is received by ``ExampleActor``'s ``onCommand`` command handler. +2. The ``onCommand`` command handler derives an ``Appended`` event and ``persist``\ s it to the ``eventLog`` pointed to + by the ``ActorRef`` that it was passed when it was created. +3. If persistence succeeds, the sender of the command is informed about successful processing. + If persistence fails, the command sender is informed about the failure so it can retry, if appropriate. +4. The ``ExampleActor``'s ``onEvent`` handler is automatically called after a successful ``persist``. +5. The ``onEvent`` handler updates ``currentState``. +6. `EventsourcedActor`_ subclasses that need to persist new events within the `onEvent`_ handler should mix in the + `PersistOnEvent`_ trait and invoke the `persistOn`_ method. + +During normal application startup, or if the actor is restarted, persisted events are replayed via the `onEvent`_ handler, +which recovers internal state. Only then may new commands be processed. + +You can run the above Scala example from IntelliJ IDEA by launching ``sapi.ActorExample``. +Output is: + +.. code-block:: none + + Connected to the target VM, address: '127.0.0.1:55525', transport: 'socket' + [id = 5, aggregate id = d] a,b,a,b + [WARN] [SECURITY][03/19/2017 14:36:21.262] [location-eventuate.log.dispatchers.write-dispatcher-6] + [akka.serialization.Serialization(akka://location)] Using the default Java serializer for class [sapi.ActorExample$Appended] + which is not recommended because of performance implications. Use another serializer or disable this warning using the + setting 'akka.actor.warn-about-java-serializer-usage' + [id = 4, aggregate id = d] a,b,a,b,a + [id = 2, aggregate id = b] a,b,a,b,a,b + [id = 1, aggregate id = a] a,b,a,b,a,b + [id = 3, aggregate id = c] x,y,x,y,x,y + [id = 5, aggregate id = d] a,b,a,b,a,b + [id = 4, aggregate id = d] a,b,a,b,a,b + Disconnected from the target VM, address: '127.0.0.1:55525', transport: 'socket' + + Process finished with exit code 0 + +The above serialization warning is addressed in the `Custom Serialization`_ document. + +To run the Scala version from the command line, move to the ``eventuate-user-guide`` directory and type: + +.. code-block:: none -Creating a single instance -~~~~~~~~~~~~~~~~~~~~~~~~~~ + sbt "runMain sapi.ActorExample" +FIXME: Uncaught error from thread [location-eventuate.log.dispatchers.write-dispatcher-7] shutting down JVM since +'akka.jvm-exit-on-fatal-error' is enabled for ActorSystem[location] +java.lang.UnsatisfiedLinkError: org.fusesource.leveldbjni.internal.NativeOptions.init()V |br| +|br| +See http://stackoverflow.com/questions/19425613/unsatisfiedlinkerror-with-native-library-under-sbt |br| +|br| +I tried setting ``fork in Runtime := true`` but no joy + +You can run the above Java example from IntelliJ IDEA by launching ``japi.ActorExample``. + +To run the Java version from the command line, move to the ``eventuate-user-guide`` directory and type: + +.. code-block:: none + + sbt "runMain japi.ActorExample" + +FIXME: It is clear that none of the Java examples have ever worked. +I addressed the problems that I understood, but more problems remain. + +.. _AbstractEventsourcedActor: http://rbmhtechnology.github.io/eventuate/latest/api/index.html#com.rbmhtechnology.eventuate.AbstractEventsourcedActor +.. _Custom Serialization: http://rbmhtechnology.github.io/eventuate/reference/event-sourcing.html#custom-serialization +.. _EventsourcedActor: http://rbmhtechnology.github.io/eventuate/latest/api/index.html#com.rbmhtechnology.eventuate.EventsourcedActor +.. _onCommand: http://rbmhtechnology.github.io/eventuate/latest/api/index.html#com.rbmhtechnology.eventuate.EventsourcedActor@onCommand:EventsourcedView.this.Receive +.. _onEvent: http://rbmhtechnology.github.io/eventuate/latest/api/index.html#com.rbmhtechnology.eventuate.EventsourcedActor@onEvent:EventsourcedView.this.Receive +.. _persistOn: http://rbmhtechnology.github.io/eventuate/latest/api/index.html#com.rbmhtechnology.eventuate.PersistOnEvent@persistOnEvent[A](event:A,customDestinationAggregateIds:Set[String]):Unit +.. _persistOnEvent: http://rbmhtechnology.github.io/eventuate/latest/api/com/rbmhtechnology/eventuate/PersistOnEvent.html + +Working With a Single Instance of an EventsourcedActor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In the following, a single instance of ``ExampleActor`` is created and two ``Append`` commands are sent to it: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala :snippet: create-one-instance - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java :snippet: create-one-instance -Sending a ``Print`` command +Send a ``Print`` command to the ``ExampleActor`` instance like this: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala :snippet: print-one-instance - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java :snippet: print-one-instance -should print:: +The output should be: + +.. code-block:: none [id = 1, aggregate id = a] a,b -When the application is re-started, persisted events are replayed to ``onEvent`` which recovers ``currentState``. Sending another ``Print`` command should print again:: +When the application is restarted, persisted events are replayed via the ``onEvent`` handler, which recovers ``currentState``. +Sending another ``Print`` command should again print: - [id = 1, aggregate id = a] a,b +.. code-block:: none -.. note:: - In the following sections, several instances of ``ExampleActor`` are created. It is assumed that they share a :ref:`replicated-event-log` and are running at different *locations*. + [id = 1, aggregate id = a] a,b - A shared event log is a pre-requisite for event-sourced actors to consume each other’s events. However, sharing an event log doesn’t necessarily mean broadcast communication between all actors on the same log. It is the ``aggreagteId`` that determines which actors consume each other’s events. +Shared Event Logs +^^^^^^^^^^^^^^^^^ +In the following sections, several instances of ``ExampleActor`` are created. +They share a :ref:`replicated-event-log`, event though they are running at different *locations*. -Creating two isolated instances -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A shared event log is a prerequisite for event-sourced actors to be able to read each other’s events. +However, sharing an event log does not necessarily mean all events are consumed by every actor that accesses the same log. +The ``aggreagteId`` determines which events actors consume from other actors; +``aggreagteId`` acts as a filtering mechanism, because actors that define a ``aggreagteId`` property only receive events +from other actors with the same ``aggreagteId`` value. -When creating two instances of ``ExampleActor`` with different ``aggregateId``\ s, they are isolated from each other, by default, and do not consume each other’s events: +Isolated EventsourcedActor Instances +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``EventsourcedActor`` instances with different ``aggregateId`` values are isolated from each other, +which means they do not consume each other’s events: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala :snippet: create-two-instances - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java :snippet: create-two-instances -Sending two ``Print`` commands +Here is an example of sending ``Print`` commands to isolated actors: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala :snippet: print-two-instances - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java :snippet: print-two-instances -should print:: +should display: + +.. code-block:: none [id = 2, aggregate id = b] a,b [id = 3, aggregate id = c] x,y -Creating two replica instances -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When creating two ``ExampleActor`` instances with the same ``aggregateId``, they consume each other’s events [#]_. +Replicated EventsourcedActor Instances +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``EventsourcedActor`` instances with the same ``aggregateId`` are replicants, which means they consume each other’s events [#]_. .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala :snippet: create-replica-instances - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java :snippet: create-replica-instances -Here, ``d4`` processes an ``Append`` command and persists an ``Appended`` event. Both, ``d4`` and ``d5``, consume that event and update their internal state. After waiting a bit for convergence, sending a ``Print`` command to both actors should print:: +In the code above, there are two ``EventsourcedActor`` subclass instances: ``d4`` and ``d5``. +``d4`` processes an ``Append`` command and persists an ``Appended`` event. +Next, both ``d4`` and ``d5`` consume that event and update their internal state. +After a short time, the values in the actors distributed throughout the network converge to a single value. +Once this has happened, sending a ``Print`` command to both actors should display the same value: + +.. code-block:: none [id = 4, aggregate id = d] a [id = 5, aggregate id = d] a -After both replicas have converged, another ``Append`` is sent to ``d5``. +FIXME The ActorExample.scala code does not match the above description. See "fixme why is this not referenced in user-guide.rst?" + +After both replicas have converged, another ``Append`` is sent to ``d5``: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ActorExample.scala :snippet: send-another-append - .. includecode:: code/userguide/japi/ActorExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ActorExample.java :snippet: send-another-append -Again both actors consume the event and sending another ``Print`` command should print:: +Again both actors consume the event and after a waiting a short time for the values to converge, +sending another ``Print`` command should display: + +.. code-block:: none [id = 4, aggregate id = d] a,b [id = 5, aggregate id = d] a,b .. warning:: - As you have probably recognized, replica convergence in this example can only be achieved if the second ``Append`` command is sent after both actors have processed the ``Appended`` event from the first ``Append`` command. + As you have probably recognized, replica convergence in this example can only be achieved if the second ``Append`` + command is sent after both actors have processed the ``Appended`` event from the first ``Append`` command. - In other words, the first ``Appended`` event must *happen before* the second one. Only in this case, these two events can have a causal relationship. Since events are guaranteed to be delivered in potential causal order to all replicas, they can converge to the same state. + In other words, the first ``Appended`` event must *happen before* the second one. + Only in this case can these two events can have a causal relationship. + Since the network required for Eventuate guarantees that events will be delivered in potential causal order, + the ``EventsourcedActor`` replicas are able to converge to the same state. - When concurrent updates are made to both replicas, the corresponding ``Appended`` events are not causally related and can be delivered in any order to both replicas. This may cause replicas to diverge because *append* operations do not commute. The following sections give examples how to detect and handle concurrent updates. + When concurrent updates are made to both replicas, the corresponding ``Appended`` events are not causally related and + may be delivered in any order to both replicas. + This may cause replicas to diverge because *append* operations do not commute. -Detecting concurrent updates ----------------------------- + The following sections give examples how to detect and handle concurrent updates. -Eventuate tracks *happened-before* relationships (= potential causality) of events with :ref:`vector-clocks`. Why is that needed at all? Let’s assume that an event-sourced actor emits an event ``e1`` for changing internal state and later receives an event ``e2`` from a replica instance. If the replica instance emits ``e2`` after having processed ``e1``, the actor can apply ``e2`` as regular update. If the replica instance emits ``e2`` before having received ``e1``, the actor receives a concurrent, potentially conflicting event. +Detecting Concurrent Updates +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Eventuate tracks *happened-before* relationships of events (events that are potentially causally related) with :ref:`vector-clocks`. +Lets consider for a moment why this is important. Assume that an event-sourced actor emits an event ``e1`` for changing internal state +and later receives an event ``e2`` from a replica instance. If the replica instance emits ``e2`` after having processed ``e1``, +the actor can apply ``e2`` as regular update. However, if the replica instance emits ``e2`` before having received ``e1``, +the actor has received a concurrent, potentially conflicting event. -How can the actor determine if ``e2`` is a regular i.e. causally related or concurrent update? It can do so by comparing the vector timestamps of ``e1`` and ``e2``, where ``t1`` is the vector timestamp of ``e1`` and ``t2`` the vector timestamp of ``e2``. If events ``e1`` and ``e2`` are concurrent then ``t1 conc t2`` evaluates to ``true``. Otherwise, they are causally related and ``t1 < t2`` evaluates to ``true`` (because ``e1`` *happened-before* ``e2``). +The actor determines if ``e2`` is a regular (causally related) or a concurrent update by comparing the vector timestamps. +Let ``t1`` be the vector timestamp of ``e1`` and ``t2`` be the vector timestamp of ``e2``. +If events ``e1`` and ``e2`` are concurrent then ``t1 conc t2`` evaluates ``true``. +Otherwise, they are causally related and ``t1 < t2`` evaluates ``true`` because ``e1`` *happened-before* ``e2``. -The vector timestamp of an event can be obtained with ``lastVectorTimestamp`` during event processing. Vector timestamps can be attached as *update timestamp* to current state and compared with the vector timestamp of a new event in order to determine whether the new event is causally related to the previous state update or not\ [#]_: +The vector timestamp of an event can be obtained from the ``lastVectorTimestamp`` property during event processing. +Vector timestamps can be attached as *update timestamp* to current state and compared with the vector timestamp of a +new event in order to determine whether the new event is causally related to the previous state update or not\ [#]_: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ConcurrentExample.scala :snippet: detecting-concurrent-update - .. includecode:: code/userguide/japi/ConcurrentExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ConcurrentExample.java :snippet: detecting-concurrent-update -Attaching update timestamps to current state and comparing them with vector timestamps of new events can be easily abstracted over so that applications don’t have to deal with these low level details, as shown in the next section. +The Java code +(`japi.ConcurrentExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code from the command line by typing: + +.. code-block:: none + + sbt "runMain sapi.ConcurrentExample" .. _tracking-conflicting-versions: -Tracking conflicting versions ------------------------------ +Identifying Conflicting Versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This section demonstrates that update timestamps can be stored in an actor's current state and compared with +the vector timestamps of new events. +We will also see that this process can be abstracted, so that applications do not need to deal with these low level details. -If state update operations from concurrent events do not commute, conflicting versions of actor state arise that must be tracked and resolved. This can be done with Eventuate’s ``ConcurrentVersions[S, A]`` abstraction and an application-defined *update function* of type ``(S, A) => S`` where ``S`` is the type of actor state and ``A`` the update type. In our example, the ``ConcurrentVersions`` type is ``ConcurrentVersions[Vector[String], String]`` and the update function ``(s, a) => s :+ a``: +Since state update operations from concurrent events do not commute, conflicting versions of actor state must be identified and resolved. +This can be done with Eventuate’s +`ConcurrentVersions `_ +trait and an application-defined *update function* of type ``(S, A) => S``, where ``S`` is the type of actor state and ``A`` is the update type. +In the following example, the ``ConcurrentVersions`` type is ``ConcurrentVersions[Vector[String], String]`` and the update function is ``(s, a) => s :+ a``: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/TrackingExample.scala :snippet: tracking-conflicting-versions - .. includecode:: code/userguide/japi/TrackingExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/TrackingExample.java :snippet: tracking-conflicting-versions -Internally, ``ConcurrentVersions`` maintains versions of actor state in a tree structure where each concurrent ``update`` creates a new branch. The shape of the tree is determined solely by the vector timestamps of the corresponding update events. +Internally, ``ConcurrentVersions`` maintains versions of actor state in a tree structure where each concurrent ``versionedState.update`` creates a new branch. +The shape of the tree is determined solely by the vector timestamps of the corresponding ``versionedState.update`` events. -An event’s vector timestamp is passed as ``lastVectorTimestamp`` argument to ``update``. The ``update`` method internally creates a new version by applying the update function ``(s, a) => s :+ a`` to the closest predecessor version and the actual update value (``entry``). The ``lastVectorTimestamp`` is attached as update timestamp to the newly created version. +An event’s vector timestamp is passed as ``lastVectorTimestamp`` argument to ``update``. +The ``update`` method internally creates a new version by applying the update function ``(s, a) => s :+ a`` to the +closest predecessor version and the actual update value (``entry``). +The ``lastVectorTimestamp`` is attached as update timestamp to the newly created version. -Concurrent versions of actor state and their update timestamp can be obtained with ``all`` which is a sequence of type ``Seq[Versioned[Vector[String]]]`` in our example. The Versioned_ data type represents a particular version of actor state and its update timestamp (= ``vectorTimestamp`` field). +In this example, concurrent versions of actor state and their update timestamp can be obtained with ``versionedState.all`` which is a sequence of type +``Seq[Versioned[Vector[String]]]``. +The Versioned_ data type represents a particular version of actor state and its update timestamp (= ``vectorTimestamp`` field). -If ``all`` contains only a single element, there is no conflict and the element represents the current, conflict-free actor state. If the sequence contains two or more elements, there is a conflict where the elements represent conflicting versions of actor states. They can be resolved either automatically or interactively. +If ``all`` contains only a single element, there is no conflict and the element represents the current, conflict-free actor state. +If the sequence contains two or more elements, there is a conflict where the elements represent conflicting versions of actor states. +They can be resolved either automatically or interactively. -.. note:: - Only concurrent updates to replicas with the same ``aggregateId`` may conflict. Concurrent updates to actors with different ``aggregateId`` do not conflict (unless an application does custom :ref:`event-routing`). +The Java code +(`japi.TrackingExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code from the command line by typing: + +.. code-block:: none + + sbt "runMain sapi.TrackingExample" - Also, if the data type of actor state is designed in a way that update operations commute, concurrent updates can be made conflict-free. This is discussed in section :ref:`commutative-replicated-data-types`. +Resolving Conflicting Versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Only concurrent updates to replicas with the same ``aggregateId`` may conflict. +Concurrent updates to actors with different ``aggregateId`` do not conflict, unless an application performs custom :ref:`event-routing`. -Resolving conflicting versions ------------------------------- +Also, if the data type of actor state is designed in a way that update operations commute, concurrent updates can be made conflict-free. +This is discussed in the :ref:`commutative-replicated-data-types` section of this User Guide. .. _automated-conflict-resolution: -Automated conflict resolution +Automated Conflict Resolution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following is a simple example of automated conflict resolution: if a conflict has been detected, the version with the higher wall clock timestamp is selected to be the winner. In case of equal wall clock timestamps, the version with the lower emitter id is selected. The wall clock timestamp can be obtained with ``lastSystemTimestamp`` during event handling, the emitter id with ``lastEmitterId``. The emitter id is the ``id`` of the ``EventsourcedActor`` that emitted the event. +The following is a simple example of automated conflict resolution: +if a conflict has been detected, the version with the higher wall clock timestamp is selected to be the winner. +In case of equal wall clock timestamps, the version with the lower emitter id is selected. +The emitter id is the ``id`` of the ``EventsourcedActor`` that emitted the event. +The wall clock timestamp can be obtained from the actor's ``lastSystemTimestamp`` property, +and the emitter id can be obtained from the actor's ``lastEmitterId`` property. .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ResolveExample.scala :snippet: automated-conflict-resolution - .. includecode:: code/userguide/japi/ResolveExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ResolveExample.java :snippet: automated-conflict-resolution -Here, conflicting versions are sorted by descending wall clock timestamp and ascending emitter id where the latter is tracked as ``creator`` of the version. The first version is selected to be the winner. Its vector timestamp is passed as argument to ``resolve`` which selects this version and discards all other versions. +Here, conflicting versions are sorted by descending wall clock timestamp and ascending emitter id where the latter is tracked as ``creator`` of the version. +The first version is selected to be the winner. +Its vector timestamp is passed as argument to ``resolve``, which selects this version and discards all other versions. + +More advanced conflict resolution could select a winner depending on the actual value of concurrent versions. +After selection, an application could even update the winner with the *merged* value of all conflicting versions\ [#]_. + +The Java code +(`japi.ResolveExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code the command line by typing: -More advanced conflict resolution could select a winner depending on the actual value of concurrent versions. After selection, an application could even update the winner with the *merged* value of all conflicting versions\ [#]_. +.. code-block:: none + + sbt "runMain sapi.ResolveExample" .. note:: - For replicas to converge, it is important that winner selection does not depend on the order of conflicting events. In our example, this is the case because wall clock timestamp and emitter id comparison is transitive. + For replicas to converge, it is important that winner selection does not depend on the order of conflicting events. + In our example, this is the case because wall clock timestamp and emitter id comparison is transitive. Interactive conflict resolution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Interactive conflict resolution does not resolve conflicts immediately but requests the user to inspect and resolve a conflict. +The following is a very simple example of interactive conflict resolution: a user selects a winner version if conflicting versions of application state exist. +First we need an additional ``Event`` definition: + +.. tabbed-code:: + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/Messages.scala + :snippet: conditional-requests + .. includecode:: ../../examples/user-guide/src/main/java/japi/Messages.java + :snippet: conditional-requests -Interactive conflict resolution does not resolve conflicts immediately but requests the user to inspect and resolve a conflict. The following is a very simple example of interactive conflict resolution: a user selects a winner version if conflicting versions of application state exist. +Now the code: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/InteractiveResolveExample.scala :snippet: interactive-conflict-resolution - .. includecode:: code/userguide/japi/ResolveExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ResolveExample.java :snippet: interactive-conflict-resolution -When a user tries to ``Append`` in presence of a conflict, the ``ExampleActor`` rejects the update and requests the user to select a winner version from a sequence of conflicting versions. The user then sends the update timestamp of the winner version as ``selectedTimestamp`` with a ``Resolve`` command from which a ``Resolved`` event is derived and persisted. Handling of ``Resolved`` at all replicas finally resolves the conflict. +When a user tries to ``Append`` in presence of a conflict, the ``ExampleActor`` rejects the update and requests the user +to select a winner version from a sequence of conflicting versions. +The user then sends the update timestamp of the winner version as ``selectedTimestamp`` with a ``Resolve`` command from +which a ``Resolved`` event is derived and persisted. Handling of ``Resolved`` at all replicas finally resolves the conflict. + +In addition to just selecting a winner, an application could also update the winner version in a second step, for example, +with a value derived from the merge result of conflicting versions. +Support for *atomic*, interactive conflict resolution with an application-defined merge function is planned for later Eventuate releases. -In addition to just selecting a winner, an application could also update the winner version in a second step, for example, with a value derived from the merge result of conflicting versions. Support for *atomic*, interactive conflict resolution with an application-defined merge function is planned for later Eventuate releases. +The Java code +(`japi.InteractiveResolveExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code the command line by typing: + +.. code-block:: none + + sbt "runMain sapi.InteractiveResolveExample" .. note:: - Interactive conflict resolution requires agreement among replicas that are affected by a given conflict: only one of them may emit the ``Resolved`` event. This does not necessarily mean distributed lock acquisition or leader (= resolver) election but can also rely on static rules such as *only the initial creator location of an aggregate is allowed to resolve the conflict*\ [#]_. This rule is implemented in the :ref:`example-application`. + Interactive conflict resolution requires agreement among replicas that are affected by a given conflict: only one of + them may emit the ``Resolved`` event. This does not necessarily mean distributed lock acquisition or leader (= resolver) + election but can also rely on static rules such as *only the initial creator location of an aggregate is allowed to + resolve the conflict*\ [#]_. This rule is implemented in the :ref:`example-application`. .. _commutative-replicated-data-types: -Operation-based CRDTs +Operation-Based CRDTs --------------------- +If state update operations commute there is no need to use Eventuate’s ``ConcurrentVersions`` utility. +To illustrate this, let's examine a simple example: a replicated counter, +which converges because its increment and decrement operations commute. -If state update operations commute, there’s no need to use Eventuate’s ``ConcurrentVersions`` utility. A simple example is a replicated counter, which converges because its increment and decrement operations commute. - -A formal to approach to commutative replicated data types (CmRDTs) or operation-based CRDTs is given in the paper `A comprehensive study of Convergent and Commutative Replicated Data Types`_ by Marc Shapiro et al. Eventuate is a good basis for implementing operation-based CRDTs: +A formal approach to commutative replicated data types +(also known as CmRDTs, or `operation-based CRDTs `_) +is given in the paper +`A comprehensive study of Convergent and Commutative Replicated Data Types`_ by Marc Shapiro et al. +Eventuate provides a strong foundation for implementing operation-based CRDTs because: - Update operations can be modeled as events and reliably broadcasted to all replicas by a :ref:`replicated-event-log`. -- The command and event handler of an event-sourced actor can be used to implement the two update phases mentioned in the paper: *atSource* and *downstream*, respectively. -- All *downstream* preconditions mentioned in the paper are satisfied in case of causal delivery of update operations which is guaranteed for actors consuming from a replicated event log. - -Eventuate currently implements 5 out of 12 operation-based CRDTs specified in the paper. These are *Counter*, *MV-Register*, *LWW-Register*, *OR-Set* and *OR-Cart* (a shopping cart CRDT). They can be instantiated and used via their corresponding *CRDT services*. CRDT operations are asynchronous methods on the service interfaces. CRDT services free applications from dealing with low-level details like event-sourced actors or command messages directly. The following is the definition of ORSetService_: +- The command and event handler of an event-sourced actor can be used to implement the two update phases mentioned in + the aforementioned paper: *atSource* (first mentioned on page 7) and *downstream* (first mentioned on page 5). +- All *downstream* preconditions mentioned in the paper are satisfied in case of causal delivery of update operations + which is guaranteed for actors consuming from a replicated event log. + +Eventuate currently implements 5 out of 12 operation-based CRDTs specified in the cited paper. +These are *Counter*, *MV-Register*, *LWW-Register*, *OR-Set* and *OR-Cart* (a shopping cart CRDT). +They can be instantiated and used via their corresponding *CRDT services*. +CRDT operations are asynchronous methods on the service interfaces. +CRDT services free applications from dealing with low-level details like event-sourced actors or command messages directly. +This is the definition of ORSetService_: .. tabbed-code:: .. includecode:: ../../eventuate-crdt/src/main/scala/com/rbmhtechnology/eventuate/crdt/ORSet.scala :snippet: or-set-service - .. includecode:: code/userguide/japi/CrdtExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/CrdtExample.java :snippet: or-set-service -The ORSetService_ is a CRDT service that manages ORSet_ instances. It implements the asynchronous ``add`` and ``remove`` methods and inherits the ``value(id: String): Future[Set[A]]`` method from ``CRDTService[ORSet[A], Set[A]]`` for reading the current value. Their ``id`` parameter identifies an ``ORSet`` instance. Instances are automatically created by the service on demand. A usage example is the ReplicatedOrSetSpec_ that is based on Akka’s `multi node testkit`_. +The ORSetService_ is a CRDT service that manages ORSet_ instances. +It implements the asynchronous ``add`` and ``remove`` methods and inherits the ``value(id: String): Future[Set[A]]`` +method from ``CRDTService[ORSet[A], Set[A]]`` for reading the current value. +Their ``id`` parameter identifies an ``ORSet`` instance. +Instances are automatically created by the service on demand. +A usage example is the ReplicatedOrSetSpec_ that is based on Akka’s `multi node testkit`_. -A CRDT service also implements a ``save(id: String): Future[SnapshotMetadata]`` method for saving CRDT snapshots. :ref:`snapshots` may reduce recovery times of CRDTs with a long update history but are not required for CRDT persistence. +A CRDT service also implements a ``save(id: String): Future[SnapshotMetadata]`` method for saving CRDT snapshots. +:ref:`snapshots` may reduce recovery times of CRDTs with a long update history but are not required for CRDT persistence. -New operation-based CRDTs and their corresponding services can be developed with the CRDT development framework, by defining an instance of the CRDTServiceOps_ type class and implementing the CRDTService_ trait. Take a look at the `CRDT sources`_ for examples. +New operation-based CRDTs and their corresponding services can be developed with the CRDT development framework, +by defining an instance of the CRDTServiceOps_ type class and implementing the CRDTService_ trait. +Take a look at the `CRDT sources`_ for examples. -.. hint:: - Eventuate’s CRDT approach is also described in `this article`_. +Eventuate’s CRDT approach is described in `A service framework for operation-based CRDTs`_, by Martin Krasser (2016). -.. _this article: http://krasserm.github.io/2016/10/19/operation-based-crdt-framework/ +.. _A service framework for operation-based CRDTs: http://krasserm.github.io/2016/10/19/operation-based-crdt-framework/ .. _guide-event-sourced-views: -Event-sourced views +Event-Sourced Views ------------------- - -Event-sourced views are a functional subset of event-sourced actors. They can only consume events from an event log but cannot produce new events. Concrete event-sourced views must implement the ``EventsourcedView`` trait. In the following example, the view counts all ``Appended`` and ``Resolved`` events emitted by all event-sourced actors to the same ``eventLog``: +Event-sourced views are a functional subset of event-sourced actors. +They can only consume events from an event log but cannot produce new events. +Concrete event-sourced views must implement the ``EventsourcedView`` trait. +In the following example, the view counts all ``Appended`` and ``Resolved`` events emitted by all event-sourced actors +to the same ``eventLog``: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/EventsourcedViews.scala :snippet: event-sourced-view - .. includecode:: code/userguide/japi/ViewExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ViewExample.java :snippet: event-sourced-view -Event-sourced views handle events in the same way as event-sourced actors by implementing an ``onEvent`` handler. The ``onCommand`` handler in the example processes the queries ``GetAppendCount`` and ``GetResolveCount``. +The Java code +(`japi.ViewExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code the command line by typing: + +.. code-block:: none + + sbt "runMain sapi.ViewExample" + +Event-sourced views handle events in the same way as event-sourced actors by implementing an ``onEvent`` handler. +The ``onCommand`` handler in the example processes the queries ``GetAppendCount`` and ``GetResolveCount``. -``ExampleView`` implements the mandatory global unique ``id`` but doesn’t define an ``aggregateId``. A view that doesn’t define an ``aggregateId`` can consume events from all event-sourced actors on the same event log. If it defines an ``aggregateId`` it can only consume events from event-sourced actors with the same ``aggregateId`` (assuming the default :ref:`event-routing` rules). +``ExampleView`` implements the mandatory global unique ``id`` but does not define an ``aggregateId``. +A view that does not define an ``aggregateId`` can consume events from all event-sourced actors on the same event log. +If instead it does define an ``aggregateId``, it can only consume events from event-sourced actors with the same ``aggregateId``, +providing that it follows the default :ref:`event-routing` rules. .. hint:: - While event-sourced views maintain view state in-memory, :ref:`ref-event-sourced-writers` can be used to persist view state to external databases. A specialization of event-sourced writers are :ref:`ref-event-sourced-processors` whose external database is an event log. + While event-sourced views maintain view state in-memory, :ref:`ref-event-sourced-writers` can be used to persist + view state to external databases. + A specialization of event-sourced writers are :ref:`ref-event-sourced-processors` whose external database is an event log. + TODO say why this is significant. .. _conditional-requests: -Conditional requests +Conditional Requests -------------------- +Causal read consistency is the default when reading state from a single event-sourced actor or view. +The event stream received by that actor is always causally ordered, hence, it will never see an *effect* before having seen its *cause*. -Causal read consistency is the default when reading state from a single event-sourced actor or view. The event stream received by that actor is always causally ordered, hence, it will never see an *effect* before having seen its *cause*. +The situation is different when a client reads from multiple actors. +Imagine two event-sourced actor replicas where a client updates one replica and observes the updated state with the reply. +A subsequent (TODO what?) from the other replica, made by the same client, may return the old state which violates causal consistency. -The situation is different when a client reads from multiple actors. Imagine two event-sourced actor replicas where a client updates one replica and observes the updated state with the reply. A subsequent from the other replica, made by the same client, may return the old state which violates causal consistency. +Similar considerations can be made for reading from an event-sourced view after having made an update to an event-sourced actor. +For example, an application that successfully appended an entry to ``ExampleActor`` may not immediately see that update in +the ``appendCount`` of ``ExampleView``. +To achieve causal read consistency, the view should delay command processing until the emitted event has been consumed by the view. +This can be achieved with a ``ConditionalRequest``. +First we'll define a new message: -Similar considerations can be made for reading from an event-sourced view after having made an update to an event-sourced actor. For example, an application that successfully appended an entry to ``ExampleActor`` may not immediately see that update in the ``appendCount`` of ``ExampleView``. To achieve causal read consistency, the view should delay command processing until the emitted event has been consumed by the view. This can be achieved with a ``ConditionalRequest``. +.. tabbed-code:: + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/Messages.scala + :snippet: conditional-requests + .. includecode:: ../../examples/user-guide/src/main/java/japi/Messages.java + :snippet: conditional-requests + +Now the code: .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/ConditionalExample.scala :snippet: conditional-requests - .. includecode:: code/userguide/japi/ConditionalExample.java + .. includecode:: ../../examples/user-guide/src/main/java/japi/ConditionalExample.java :snippet: conditional-requests -Here, the ``ExampleActor`` includes the event’s vector timestamp in its ``AppendSuccess`` reply. Together with the actual ``GetAppendCount`` command, the timestamp is included as condition in a ``ConditionalRequest`` and sent to the view. For ``ConditionalRequest`` processing, an event-sourced view must extend the ``ConditionalRequests`` trait. ``ConditionalRequests`` internally delays the command, if needed, and only dispatches ``GetAppendCount`` to the view’s ``onCommand`` handler if the condition timestamp is in the *causal past* of the view (which is earliest the case when the view consumed the update event). When running the example with an empty event log, it should print:: +.. raw:: html + +

+ Here, the ExampleActor includes the event’s vector timestamp in its AppendSuccess(entry, lastVectorTimestamp) reply. +

+ +Together with the actual ``GetAppendCount`` command, the timestamp is included as condition in a ``ConditionalRequest`` +and sent to the view. +For ``ConditionalRequest`` processing, an event-sourced view must extend the ``ConditionalRequests`` trait. +``ConditionalRequests`` internally delays the command, if needed, and only dispatches ``GetAppendCount`` to the +view’s ``onCommand`` handler if the condition timestamp is in the *causal past* of the view (which is earliest the case +when the view consumed the update event). + +The Java code +(`japi.ConditionalExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code the command line by typing: + +.. code-block:: none + + sbt "runMain sapi.ConditionalExample" + +When running the example with an empty event log, it should display: + +.. code-block:: none append count = 1 .. note:: - Not only event-sourced views but also event-sourced actors, stateful event-sourced writers and processors can extend ``ConditionalRequests``. Delaying conditional requests may re-order them relative to other conditional and non-conditional requests. + Event-sourced actors, stateful + `event-sourced writers `_ + and `processors `_ + can also extend ``ConditionalRequests``, just as event-sourced views can. + +.. note:: + Delaying conditional requests may cause them to be re-ordered relative to other conditional and non-conditional requests. + TODO Describe what this looks like .. _guide-event-driven-communication: -Event-driven communication +Event-Driven Communication -------------------------- +Earlier sections have already shown one form of event collaboration: *state replication*. +For that purpose, event-sourced actors of the same type exchange their events to re-construct actor state at different locations. + +In more general cases, event-sourced actors of different type exchange events to achieve a common goal. +They react on received events by updating internal state and producing new events. +This form of event collaboration is called *event-driven communication*. + +In the following example, two event-sourced actors collaborate in a ping-pong game. + +.. tabbed-code:: + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/CommunicationExample.scala + :snippet: event-driven-communication1 + .. includecode:: ../../examples/user-guide/src/main/java/japi/CommunicationExample.java + :snippet: event-driven-communication1 + + +A ``PingActor`` emits a ``PingEvent`` on receiving a ``PongEvent`` -Earlier sections have already shown one form of event collaboration: *state replication*. For that purpose, event-sourced actors of the same type exchange their events to re-construct actor state at different locations. +.. tabbed-code:: + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/CommunicationExample.scala + :snippet: ping-actor + .. includecode:: ../../examples/user-guide/src/main/java/japi/CommunicationExample.java + :snippet: ping-actor -In more general cases, event-sourced actors of different type exchange events to achieve a common goal. They react on received events by updating internal state and producing new events. This form of event collaboration is called *event-driven communication*. In the following example, two event-actors collaborate in a ping-pong game where -- a ``PingActor`` emits a ``Ping`` event on receiving a ``Pong`` event and -- a ``PongActor`` emits a ``Pong`` event on receiving a ``Ping`` event +A ``PongActor`` emits a ``PongEvent`` on receiving a ``PingEvent``. .. tabbed-code:: - .. includecode:: code/UserGuideDoc.scala - :snippet: event-driven-communication - .. includecode:: code/userguide/japi/CommunicationExample.java - :snippet: event-driven-communication + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/CommunicationExample.scala + :snippet: pong-actor + .. includecode:: ../../examples/user-guide/src/main/java/japi/CommunicationExample.java + :snippet: pong-actor + +Now we can run the Ping-Pong game: -The ping-pong game is started by sending the ``PingActor`` a ``”serve”`` command which ``persist``\ s the first ``Ping`` event. This event however is not consumed by the emitter but rather by the ``PongActor``. The ``PongActor`` reacts on the ``Ping`` event by emitting a ``Pong`` event. Other than in previous examples, the event is not emitted in the actor’s ``onCommand`` handler but in the ``onEvent`` handler. For that purpose, the actor has to mixin the ``PersistOnEvent`` trait and use the ``persistOnEvent`` method. The emitted ``Pong`` too isn’t consumed by its emitter but rather by the ``PingActor``, emitting another ``Ping``, and so on. The game ends when the ``PingActor`` received the 10th ``Pong``. +.. tabbed-code:: + .. includecode:: ../../examples/user-guide/src/main/scala/sapi/CommunicationExample.scala + :snippet: event-driven-communication2 + .. includecode:: ../../examples/user-guide/src/main/java/japi/CommunicationExample.java + :snippet: event-driven-communication2 + +The ping-pong game is started by sending the ``PingActor`` a ``”serve”`` command which ``persist``\ s the first ``PingEvent``. +This event however is not consumed by the emitter but rather by the ``PongActor``. +The ``PongActor`` reacts on the ``PingEvent`` by emitting a ``PongEvent``. Other than in previous examples, +the event is not emitted in the actor’s ``onCommand`` handler but in the ``onEvent`` handler. +For that purpose, the actor has to mixin the ``PersistOnEvent`` trait and use the ``persistOnEventMethod`` method. +The emitted ``PongEvent`` too isn’t consumed by its emitter but rather by the ``PingActor``, emitting another ``PingEvent``, and so on. +The game ends when the ``PingActor`` received the 10th ``PongEvent``. + +The Java code +(`japi.CommunicationExample `_) +is not executable at this time; hopefully we will address that soon. +You can run the Scala code the command line by typing: + +.. code-block:: none + + sbt "runMain sapi.CommunicationExample" .. note:: - The ping-pong game is **reliable**. When an actor crashes and is re-started, the game is reliably resumed from where it was interrupted. The ``persistOnEvent`` method is idempotent i.e. no duplicates are written under failure conditions and later event replay. When deployed at different location, the ping-pong actors are also **partition-tolerant**. When their game is interrupted by a network partition, it is automatically resumed when the partition heals. + The ping-pong game is **reliable**. + When an actor crashes and is restarted, the game is reliably resumed from where it was interrupted. + The ``persistOnEvent`` method is idempotent i.e. no duplicates are written under failure conditions and later event replay. + When deployed at different location, the ping-pong actors are also **partition-tolerant**. + When their game is interrupted by a network partition, it is automatically resumed when the partition heals. - Furthermore, the actors don’t need to care about idempotency in their business logic i.e. they can assume to receive a **de-duplicated** and **causally-ordered** event stream in their ``onEvent`` handler. This is a significant advantage over at-least-once delivery based communication with ConfirmedDelivery_, for example, which can lead to duplicates and message re-ordering. + Furthermore, the actors don’t need to care about idempotency in their business logic + i.e. they can assume to receive a **de-duplicated** and **causally-ordered** event stream in their ``onEvent`` handler. + This is a significant advantage over at-least-once delivery based communication with ConfirmedDelivery_, for example, + which can lead to duplicates and message re-ordering. -In a more real-world example, there would be several actors of different type collaborating to achieve a common goal, for example, in a distributed business process. These actors can be considered as event-driven and event-sourced *microservices*, collaborating on a causally ordered event stream in a reliable and partition-tolerant way. Furthermore, when partitioned, they remain available for local writes and automatically catch up with their collaborators when the partition heals. +In a more real-world example, there would be several actors of different type collaborating to achieve a common goal, +for example, in a distributed business process. +These actors can be considered as event-driven and event-sourced *microservices*, +collaborating on a causally ordered event stream in a reliable and partition-tolerant way. +Furthermore, when partitioned, they remain available for local writes and automatically catch up with their collaborators when the partition heals. -.. hint:: - Further ``persistOnEvent`` details are described in the PersistOnEvent_ API docs. +Further ``persistOnEvent`` details are described in the PersistOnEvent_ API docs. .. _ZooKeeper: http://zookeeper.apache.org/ .. _event sourcing: http://martinfowler.com/eaaDev/EventSourcing.html @@ -336,7 +749,7 @@ In a more real-world example, there would be several actors of different type co .. _Lamport timestamps: http://en.wikipedia.org/wiki/Lamport_timestamps .. _multi node testkit: http://doc.akka.io/docs/akka/2.4/dev/multi-node-testing.html .. _ReplicatedOrSetSpec: https://github.com/RBMHTechnology/eventuate/blob/master/src/multi-jvm/scala/com/rbmhtechnology/eventuate/crdt/ReplicatedORSetSpec.scala -.. _CRDT sources: https://github.com/RBMHTechnology/eventuate/tree/master/eventuate-crdt/src/main/scala/com/rbmhtechnology/eventuate/crdt +.. _CRDT sources: https://github.com/RBMHTechnology/eventuate/tree/master/eventuate-crdt/main/scala/com/rbmhtechnology/eventuate/crdt .. _A comprehensive study of Convergent and Commutative Replicated Data Types: http://hal.upmc.fr/file/index/docid/555588/filename/techreport.pdf .. _Versioned: latest/api/index.html#com.rbmhtechnology.eventuate.Versioned @@ -345,9 +758,9 @@ In a more real-world example, there would be several actors of different type co .. _CRDTService: latest/api/index.html#com.rbmhtechnology.eventuate.crdt.CRDTService .. _CRDTServiceOps: latest/api/index.html#com.rbmhtechnology.eventuate.crdt.CRDTServiceOps .. _ConfirmedDelivery: latest/api/index.html#com.rbmhtechnology.eventuate.ConfirmedDelivery -.. _PersistOnEvent: latest/api/index.html#com.rbmhtechnology.eventuate.PersistOnEvent .. [#] ``EventsourcedActor``\ s and ``EventsourcedView``\ s that have an undefined ``aggregateId`` can consume events from all other actors on the same event log. -.. [#] Attached update timestamps are not version vectors because Eventuate uses `vector clock update rules`_ instead of `version vector update rules`_. Consequently, update timestamp equivalence cannot be used as criterion for replica convergence. +.. [#] Attached update timestamps are not version vectors because Eventuate uses `vector clock update rules`_ instead of `version vector update rules`_. + Consequently, update timestamp equivalence cannot be used as criterion for replica convergence. .. [#] A formal approach to automatically *merge* concurrent versions of application state are convergent replicated data types (CvRDTs) or state-based CRDTs. .. [#] Distributed lock acquisition or leader election require an external coordination service like ZooKeeper_, for example, whereas static rules do not.