├── .gitignore ├── .idea ├── hydra.xml ├── misc.xml ├── modules.xml ├── modules │ ├── akka-persistence-build.iml │ └── akka-persistence.iml ├── sbt.xml ├── scala_compiler.xml └── vcs.xml ├── README.md ├── build.sbt ├── cqlsh.sh ├── docker-clean.sh ├── docker-compose.yml ├── project └── build.properties ├── psql.sh ├── sql └── create-database.sql └── src └── main ├── resources └── application.conf └── scala ├── part1_recap ├── AkkaRecap.scala └── ScalaRecap.scala ├── part2_event_sourcing ├── MultiplePersists.scala ├── PersistAsyncDemo.scala ├── PersistentActors.scala ├── PersistentActorsExercise.scala ├── RecoveryDemo.scala └── Snapshots.scala ├── part3_stores_serialization ├── Cassandra.scala ├── CustomSerialization.scala ├── LocalStores.scala ├── Postgres.scala └── SimplePersistentActor.scala ├── part4_practices ├── DetachingModels.scala ├── EventAdapters.scala └── PersistenceQueryDemo.scala └── playground └── Playground.scala /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/sbt,java,scala,intellij 3 | # Edit at https://www.gitignore.io/?templates=sbt,java,scala,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | ### Intellij Patch ### 74 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 75 | 76 | # *.iml 77 | # modules.xml 78 | # .idea/misc.xml 79 | # *.ipr 80 | 81 | # Sonarlint plugin 82 | .idea/sonarlint 83 | 84 | ### Java ### 85 | # Compiled class file 86 | *.class 87 | 88 | # Log file 89 | *.log 90 | 91 | # BlueJ files 92 | *.ctxt 93 | 94 | # Mobile Tools for Java (J2ME) 95 | .mtj.tmp/ 96 | 97 | # Package Files # 98 | *.jar 99 | *.war 100 | *.nar 101 | *.ear 102 | *.zip 103 | *.tar.gz 104 | *.rar 105 | 106 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 107 | hs_err_pid* 108 | 109 | ### SBT ### 110 | # Simple Build Tool 111 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 112 | 113 | dist/* 114 | target/ 115 | lib_managed/ 116 | src_managed/ 117 | project/boot/ 118 | project/plugins/project/ 119 | .history 120 | .cache 121 | .lib/ 122 | 123 | ### Scala ### 124 | 125 | # End of https://www.gitignore.io/api/sbt,java,scala,intellij 126 | -------------------------------------------------------------------------------- /.idea/hydra.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules/akka-persistence-build.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 113 | -------------------------------------------------------------------------------- /.idea/modules/akka-persistence.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.idea/sbt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/scala_compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The official repository for the Rock the JVM Akka Persistence with Scala course 2 | 3 | **(for the Udemy edition, click [here](https://github.com/rockthejvm/udemy-akka-persistence))** 4 | 5 | Powered by [Rock the JVM!](rockthejvm.com) 6 | 7 | This repository contains the code we wrote during [Rock the JVM's Akka Persistence with Scala](https://rockthejvm.com/course/akka-persistence) course. Unless explicitly mentioned, the code in this repository is exactly what was caught on camera. 8 | 9 | ### How to install 10 | - either clone the repo or download as zip 11 | - open with IntelliJ as an SBT project 12 | 13 | No need to do anything else, as the IDE will take care to download and apply the appropriate library dependencies. 14 | 15 | ### How to start 16 | 17 | Clone this repository and checkout the `start` tag: 18 | 19 | ``` 20 | git checkout start 21 | ``` 22 | 23 | ### How to run an intermediate state 24 | 25 | The repository was built while recording the lectures. Prior to each lecture, I tagged each commit so you can easily go back to an earlier state of the repo! 26 | 27 | The tags are as follows: 28 | 29 | * `start` 30 | * `1.1-scala-recap` 31 | * `1.2-akka-recap` 32 | * `2.2-persistent-actors` 33 | * `2.3-persisting-extras` 34 | * `2.4-persistent-actors-exercises` 35 | * `2.5-multiple-persists` 36 | * `2.6-snapshots` 37 | * `2.7-recovery` 38 | * `2.8-persist-async` 39 | * `3.1-local-stores` 40 | * `3.2-postgres` 41 | * `3.3-cassandra` 42 | * `3.4-custom-serialization` 43 | * `4.1-event-adapters` 44 | * `4.2-detaching-models` 45 | * `4.3-persistence-query` 46 | 47 | When you watch a lecture, you can `git checkout` the appropriate tag and the repo will go back to the exact code I had when I started the lecture. 48 | 49 | ### For questions or suggestions 50 | 51 | If you have changes to suggest to this repo, either 52 | - submit a GitHub issue 53 | - tell me in the course Q/A forum 54 | - submit a pull request! 55 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "akka-persistence" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.12.7" 6 | lazy val akkaVersion = "2.5.13" // must be 2.5.13 so that it's compatible with the stores plugins (JDBC and Cassandra) 7 | lazy val leveldbVersion = "0.7" 8 | lazy val leveldbjniVersion = "1.8" 9 | lazy val postgresVersion = "42.2.2" 10 | lazy val cassandraVersion = "0.91" 11 | lazy val json4sVersion = "3.2.11" 12 | lazy val protobufVersion = "3.6.1" 13 | 14 | // some libs are available in Bintray's JCenter 15 | resolvers += Resolver.jcenterRepo 16 | 17 | libraryDependencies ++= Seq( 18 | "com.typesafe.akka" %% "akka-persistence" % akkaVersion, 19 | 20 | // local levelDB stores 21 | "org.iq80.leveldb" % "leveldb" % leveldbVersion, 22 | "org.fusesource.leveldbjni" % "leveldbjni-all" % leveldbjniVersion, 23 | 24 | // JDBC with PostgreSQL 25 | "org.postgresql" % "postgresql" % postgresVersion, 26 | "com.github.dnvriend" %% "akka-persistence-jdbc" % "3.4.0", 27 | 28 | // Cassandra 29 | "com.typesafe.akka" %% "akka-persistence-cassandra" % cassandraVersion, 30 | "com.typesafe.akka" %% "akka-persistence-cassandra-launcher" % cassandraVersion % Test, 31 | 32 | // Google Protocol Buffers 33 | "com.google.protobuf" % "protobuf-java" % protobufVersion, 34 | ) -------------------------------------------------------------------------------- /cqlsh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "================== Help for cqlsh =========================" 3 | echo "DESCRIBE tables : Prints all tables in the current keyspace" 4 | echo "DESCRIBE keyspaces : Prints all keyspaces in the current cluster" 5 | echo "DESCRIBE : Prints table detail information" 6 | echo "help : for more cqlsh commands" 7 | echo "help [cqlsh command] : Gives information about cqlsh commands" 8 | echo "quit : quit" 9 | echo "==================================================================" 10 | docker exec -it cassandra cqlsh -------------------------------------------------------------------------------- /docker-clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker rm -f $(docker ps -aq) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | postgres: 5 | image: postgres:latest 6 | container_name: postgres 7 | environment: 8 | - "TZ=Europe/Amsterdam" 9 | - "POSTGRES_USER=docker" 10 | - "POSTGRES_PASSWORD=docker" 11 | ports: 12 | - "5432:5432" 13 | volumes: 14 | - "./sql:/docker-entrypoint-initdb.d" 15 | 16 | cassandra: 17 | image: cassandra:3 18 | container_name: cassandra 19 | ports: 20 | - "7000:7000" 21 | - "9042:9042" 22 | environment: 23 | - "CASSANDRA_CLUSTER_NAME=OUR_DOCKERIZED_CASSANDRA_SINGLE_NODE_CLUSTER" 24 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.2.7 -------------------------------------------------------------------------------- /psql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "================== Help for psql =========================" 3 | echo "\\dt : Describe the current database" 4 | echo "\\t [table] : Describe a table" 5 | echo "\\c : Connect to a database" 6 | echo "\\h : help with SQL commands" 7 | echo "\\? : help with psql commands" 8 | echo "\\q : quit" 9 | echo "==================================================================" 10 | docker exec -it postgres psql -U docker -d rtjvm 11 | -------------------------------------------------------------------------------- /sql/create-database.sql: -------------------------------------------------------------------------------- 1 | --Setup database 2 | DROP DATABASE IF EXISTS rtjvm; 3 | CREATE DATABASE rtjvm; 4 | \c rtjvm; 5 | 6 | 7 | -- pretty much standard config for postgres in the context of Akka Persistence 8 | -- there are almost no things you should change regardless of your message structure 9 | 10 | DROP TABLE IF EXISTS public.journal; 11 | 12 | CREATE TABLE IF NOT EXISTS public.journal ( 13 | ordering BIGSERIAL, 14 | persistence_id VARCHAR(255) NOT NULL, 15 | sequence_number BIGINT NOT NULL, 16 | deleted BOOLEAN DEFAULT FALSE, 17 | tags VARCHAR(255) DEFAULT NULL, 18 | message BYTEA NOT NULL, 19 | PRIMARY KEY(persistence_id, sequence_number) 20 | ); 21 | 22 | CREATE UNIQUE INDEX journal_ordering_idx ON public.journal(ordering); 23 | 24 | DROP TABLE IF EXISTS public.snapshot; 25 | 26 | CREATE TABLE IF NOT EXISTS public.snapshot ( 27 | persistence_id VARCHAR(255) NOT NULL, 28 | sequence_number BIGINT NOT NULL, 29 | created BIGINT NOT NULL, 30 | snapshot BYTEA NOT NULL, 31 | PRIMARY KEY(persistence_id, sequence_number) 32 | ); 33 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.persistence.journal.plugin = "akka.persistence.journal.leveldb" 2 | akka.persistence.journal.leveldb.dir = "target/rtjvm/journal" 3 | 4 | akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local" 5 | akka.persistence.snapshot-store.local.dir = "target/rtjvm/snapshots" 6 | 7 | localStores { 8 | akka.persistence.journal.plugin = "akka.persistence.journal.leveldb" 9 | akka.persistence.journal.leveldb.dir = "target/localStores/journal" 10 | 11 | akka.persistence.journal.leveldb.compaction-intervals { 12 | simple-persistent-actor = 1000 13 | "*" = 5000 14 | } 15 | 16 | akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local" 17 | akka.persistence.snapshot-store.local.dir = "target/localStores/snapshots" 18 | } 19 | 20 | postgresDemo { 21 | akka.persistence.journal.plugin = "jdbc-journal" 22 | akka.persistence.snapshot-store.plugin = "jdbc-snapshot-store" 23 | 24 | akka-persistence-jdbc { 25 | shared-databases { 26 | slick { 27 | profile = "slick.jdbc.PostgresProfile$" 28 | db { 29 | numThreads = 10 30 | driver = "org.postgresql.Driver" 31 | url = "jdbc:postgresql://localhost:5432/rtjvm" 32 | user = "docker" 33 | password = "docker" 34 | } 35 | } 36 | } 37 | } 38 | 39 | jdbc-journal { 40 | use-shared-db = "slick" 41 | } 42 | 43 | jdbc-snapshot-store { 44 | use-shared-db = "slick" 45 | } 46 | } 47 | 48 | cassandraDemo { 49 | akka.persistence.journal.plugin = "cassandra-journal" 50 | akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store" 51 | 52 | // default values 53 | } 54 | 55 | customSerializerDemo { 56 | akka.persistence.journal.plugin = "cassandra-journal" 57 | akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store" 58 | 59 | akka.actor { 60 | serializers { 61 | java = "akka.serialization.JavaSerializer" 62 | rtjvm = "part3_stores_serialization.UserRegistrationSerializer" 63 | } 64 | 65 | serialization-bindings { 66 | "part3_stores_serialization.UserRegistered" = rtjvm 67 | // java serializer is used by default 68 | } 69 | } 70 | } 71 | 72 | eventAdapters { 73 | akka.persistence.journal.plugin = "cassandra-journal" 74 | akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store" 75 | 76 | cassandra-journal { 77 | event-adapters { 78 | guitar-inventory-enhancer = "part4_practices.EventAdapters$GuitarReadEventAdapter" 79 | } 80 | 81 | event-adapter-bindings { 82 | "part4_practices.EventAdapters$GuitarAdded" = guitar-inventory-enhancer 83 | } 84 | } 85 | } 86 | 87 | detachingModels { 88 | akka.persistence.journal.plugin = "cassandra-journal" 89 | akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store" 90 | 91 | cassandra-journal { 92 | event-adapters { 93 | detach-adapter = "part4_practices.ModelAdapter" 94 | } 95 | 96 | event-adapter-bindings { 97 | "part4_practices.DomainModel$CouponApplied" = detach-adapter 98 | "part4_practices.DataModel$WrittenCouponApplied" = detach-adapter 99 | "part4_practices.DataModel$WrittenCouponAppliedV2" = detach-adapter 100 | } 101 | } 102 | } 103 | 104 | persistenceQuery { 105 | akka.persistence.journal.plugin = "cassandra-journal" 106 | akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store" 107 | 108 | cassandra-journal { 109 | event-adapters { 110 | tagging = "part4_practices.PersistenceQueryDemo$MusicStoreEventAdapter" 111 | } 112 | 113 | event-adapter-bindings { 114 | "part4_practices.PersistenceQueryDemo$PlaylistPurchased" = tagging 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/main/scala/part1_recap/AkkaRecap.scala: -------------------------------------------------------------------------------- 1 | package part1_recap 2 | 3 | import akka.actor.SupervisorStrategy.{Restart, Stop} 4 | import akka.actor.{Actor, ActorLogging, ActorSystem, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy} 5 | import akka.util.Timeout 6 | 7 | object AkkaRecap extends App { 8 | 9 | class SimpleActor extends Actor with ActorLogging with Stash { 10 | override def receive: Receive = { 11 | case "createChild" => 12 | val childActor = context.actorOf(Props[SimpleActor], "myChild") 13 | childActor ! "hello" 14 | case "stashThis" => 15 | stash() 16 | case "change handler NOW" => 17 | unstashAll() 18 | context.become(anotherHandler) 19 | 20 | case "change" => context.become(anotherHandler) 21 | case message => println(s"I received: $message") 22 | } 23 | 24 | def anotherHandler: Receive = { 25 | case message => println(s"In another receive handler: $message") 26 | } 27 | 28 | override def preStart(): Unit = { 29 | log.info("I'm starting") 30 | } 31 | 32 | override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { 33 | case _: RuntimeException => Restart 34 | case _ => Stop 35 | } 36 | } 37 | 38 | // actor encapsulation 39 | val system = ActorSystem("AkkaRecap") 40 | // #1: you can only instantiate an actor through the actor system 41 | val actor = system.actorOf(Props[SimpleActor], "simpleActor") 42 | // #2: sending messages 43 | actor ! "hello" 44 | /* 45 | - messages are sent asynchronously 46 | - many actors (in the millions) can share a few dozen threads 47 | - each message is processed/handled ATOMICALLY 48 | - no need for locks 49 | */ 50 | 51 | // changing actor behavior + stashing 52 | // actors can spawn other actors 53 | // guardians: /system, /user, / = root guardian 54 | 55 | // actors have a defined lifecycle: they can be started, stopped, suspended, resumed, restarted 56 | 57 | // stopping actors - context.stop 58 | actor ! PoisonPill 59 | 60 | // logging 61 | // supervision 62 | 63 | // configure Akka infrastructure: dispatchers, routers, mailboxes 64 | 65 | // schedulers 66 | import scala.concurrent.duration._ 67 | import system.dispatcher 68 | system.scheduler.scheduleOnce(2 seconds) { 69 | actor ! "delayed happy birthday!" 70 | } 71 | 72 | // Akka patterns including FSM + ask pattern 73 | import akka.pattern.ask 74 | implicit val timeout = Timeout(3 seconds) 75 | 76 | val future = actor ? "question" 77 | 78 | // the pipe pattern 79 | import akka.pattern.pipe 80 | val anotherActor = system.actorOf(Props[SimpleActor], "anotherSimpleActor") 81 | future.mapTo[String].pipeTo(anotherActor) 82 | } 83 | -------------------------------------------------------------------------------- /src/main/scala/part1_recap/ScalaRecap.scala: -------------------------------------------------------------------------------- 1 | package part1_recap 2 | 3 | import scala.concurrent.Future 4 | import scala.util.{Failure, Success} 5 | 6 | object ScalaRecap extends App { 7 | 8 | val aCondition: Boolean = false 9 | def myFunction(x: Int) = { 10 | // code 11 | if (x > 4) 42 else 65 12 | } 13 | // instructions vs expressions 14 | // types + type inference 15 | 16 | // OO features of Scala 17 | class Animal 18 | trait Carnivore { 19 | def eat(a: Animal): Unit 20 | } 21 | 22 | object Carnivore 23 | 24 | // generics 25 | abstract class MyList[+A] 26 | 27 | // method notations 28 | 1 + 2 // infix notation 29 | 1.+(2) 30 | 31 | // FP 32 | val anIncrementer: Int => Int = (x: Int) => x + 1 33 | anIncrementer(1) 34 | 35 | List(1,2,3).map(anIncrementer) 36 | // HOF: flatMap, filter 37 | // for-comprehensions 38 | 39 | // Monads: Option, Try 40 | 41 | // Pattern matching! 42 | val unknown: Any = 2 43 | val order = unknown match { 44 | case 1 => "first" 45 | case 2 => "second" 46 | case _ => "unknown" 47 | } 48 | 49 | try { 50 | // code that can throw an exception 51 | throw new RuntimeException 52 | } catch { 53 | case e: Exception => println("I caught one!") 54 | } 55 | 56 | /** 57 | * Scala advanced 58 | */ 59 | 60 | // multithreading 61 | 62 | import scala.concurrent.ExecutionContext.Implicits.global 63 | val future = Future { 64 | // long computation here 65 | // executed on SOME other thread 66 | 42 67 | } 68 | // map, flatMap, filter + other niceties e.g. recover/recoverWith 69 | 70 | future.onComplete { 71 | case Success(value) => println(s"I found the meaning of life: $value") 72 | case Failure(exception) => println(s"I found $exception while searching for the meaning of life!") 73 | } // on SOME thread 74 | 75 | val partialFunction: PartialFunction[Int, Int] = { 76 | case 1 => 42 77 | case 2 => 65 78 | case _ => 999 79 | } 80 | // based on pattern matching! 81 | 82 | // type aliases 83 | type AkkaReceive = PartialFunction[Any, Unit] 84 | def receive: AkkaReceive = { 85 | case 1 => println("hello!") 86 | case _ => println("confused...") 87 | } 88 | 89 | // Implicits! 90 | implicit val timeout = 3000 91 | def setTimeout(f: () => Unit)(implicit timeout: Int) = f() 92 | 93 | setTimeout(() => println("timeout"))// other arg list injected by the compiler 94 | 95 | // conversions 96 | // 1) implicit methods 97 | case class Person(name: String) { 98 | def greet: String = s"Hi, my name is $name" 99 | } 100 | 101 | implicit def fromStringToPerson(name: String) = Person(name) 102 | "Peter".greet 103 | // fromStringToPerson("Peter").greet 104 | 105 | // 2) implicit classes 106 | implicit class Dog(name: String) { 107 | def bark = println("Bark!") 108 | } 109 | "Lassie".bark 110 | // new Dog("Lassie").bark 111 | 112 | // implicit organizations 113 | // local scope 114 | implicit val numberOrdering: Ordering[Int] = Ordering.fromLessThan(_ > _) 115 | List(1,2,3).sorted //(numberOrdering) => List(3,2,1) 116 | 117 | // imported scope 118 | 119 | // companion objects of the types involved in the call 120 | object Person { 121 | implicit val personOrdering: Ordering[Person] = Ordering.fromLessThan((a, b) => a.name.compareTo(b.name) < 0) 122 | } 123 | 124 | List(Person("Bob"), Person("Alice")).sorted // (Person.personOrdering) 125 | // => List(Person("Alice"), Person("Bob")) 126 | 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/scala/part2_event_sourcing/MultiplePersists.scala: -------------------------------------------------------------------------------- 1 | package part2_event_sourcing 2 | 3 | import java.util.Date 4 | 5 | import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props} 6 | import akka.persistence.PersistentActor 7 | 8 | object MultiplePersists extends App { 9 | 10 | /* 11 | Diligent accountant: with every invoice, will persist TWO events 12 | - a tax record for the fiscal authority 13 | - an invoice record for personal logs or some auditing authority 14 | */ 15 | 16 | // COMMAND 17 | case class Invoice(recipient: String, date: Date, amount: Int) 18 | 19 | // EVENTS 20 | case class TaxRecord(taxId: String, recordId: Int, date: Date, totalAmount: Int) 21 | case class InvoiceRecord(invoiceRecordId: Int, recipient: String, date: Date, amount: Int) 22 | 23 | 24 | object DiligentAccountant { 25 | def props(taxId: String, taxAuthority: ActorRef) = Props(new DiligentAccountant(taxId, taxAuthority)) 26 | } 27 | 28 | class DiligentAccountant(taxId: String, taxAuthority: ActorRef) extends PersistentActor with ActorLogging { 29 | 30 | var latestTaxRecordId = 0 31 | var latestInvoiceRecordId = 0 32 | 33 | override def persistenceId: String = "diligent-accountant" 34 | 35 | override def receiveCommand: Receive = { 36 | case Invoice(recipient, date, amount) => 37 | // journal ! TaxRecord 38 | persist(TaxRecord(taxId, latestTaxRecordId, date, amount / 3)) { record => 39 | taxAuthority ! record 40 | latestTaxRecordId += 1 41 | 42 | persist("I hereby declare this tax record to be true and complete.") { declaration => 43 | taxAuthority ! declaration 44 | } 45 | } 46 | // journal ! InvoiceRecord 47 | persist(InvoiceRecord(latestInvoiceRecordId, recipient, date, amount)) { invoiceRecord => 48 | taxAuthority ! invoiceRecord 49 | latestInvoiceRecordId += 1 50 | 51 | persist("I hereby declare this invoice record to be true.") { declaration => 52 | taxAuthority ! declaration 53 | } 54 | } 55 | } 56 | 57 | override def receiveRecover: Receive = { 58 | case event => log.info(s"Recovered: $event") 59 | } 60 | } 61 | 62 | class TaxAuthority extends Actor with ActorLogging { 63 | override def receive: Receive = { 64 | case message => log.info(s"Received: $message") 65 | } 66 | } 67 | 68 | val system = ActorSystem("MulitplePersistsDemo") 69 | val taxAuthority = system.actorOf(Props[TaxAuthority], "HMRC") 70 | val accountant = system.actorOf(DiligentAccountant.props("UK52352_58325", taxAuthority)) 71 | 72 | accountant ! Invoice("The Sofa Company", new Date, 2000) 73 | 74 | /* 75 | The message ordering (TaxRecord -> InvoiceRecord) is GUARANTEED. 76 | */ 77 | 78 | /** 79 | * PERSISTENCE IS ALSO BASED ON MESSAGE PASSING. 80 | */ 81 | 82 | // nested persisting 83 | 84 | accountant ! Invoice("The Supercar Company", new Date, 20004302) 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/scala/part2_event_sourcing/PersistAsyncDemo.scala: -------------------------------------------------------------------------------- 1 | package part2_event_sourcing 2 | 3 | import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | 6 | object PersistAsyncDemo extends App { 7 | 8 | 9 | case class Command(contents: String) 10 | case class Event(contents: String) 11 | 12 | object CriticalStreamProcessor { 13 | def props(eventAggregator: ActorRef) = Props(new CriticalStreamProcessor(eventAggregator)) 14 | } 15 | 16 | class CriticalStreamProcessor(eventAggregator: ActorRef) extends PersistentActor with ActorLogging { 17 | 18 | override def persistenceId: String = "critical-stream-processor" 19 | 20 | override def receiveCommand: Receive = { 21 | case Command(contents) => 22 | eventAggregator ! s"Processing $contents" 23 | // mutate 24 | persistAsync(Event(contents)) /* TIME GAP */ { e => 25 | eventAggregator ! e 26 | // mutate 27 | } 28 | 29 | // some actual computation 30 | val processedContents = contents + "_processed" 31 | persistAsync(Event(processedContents)) /* TIME GAP */ { e => 32 | eventAggregator ! e 33 | } 34 | } 35 | 36 | override def receiveRecover: Receive = { 37 | case message => log.info(s"Recovered: $message") 38 | } 39 | } 40 | 41 | class EventAggregator extends Actor with ActorLogging { 42 | override def receive: Receive = { 43 | case message => log.info(s"$message") 44 | } 45 | } 46 | 47 | 48 | val system = ActorSystem("PersistAsyncDemo") 49 | val eventAggregator = system.actorOf(Props[EventAggregator], "eventAggregator") 50 | val streamProcessor = system.actorOf(CriticalStreamProcessor.props(eventAggregator), "streamProcessor") 51 | 52 | streamProcessor ! Command("command1") 53 | streamProcessor ! Command("command2") 54 | 55 | /* 56 | persistAsync vs persist 57 | - perf: high-throughput environments 58 | 59 | persist vs persisAsync 60 | - ordering guarantees 61 | */ 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/part2_event_sourcing/PersistentActors.scala: -------------------------------------------------------------------------------- 1 | package part2_event_sourcing 2 | 3 | import java.util.Date 4 | 5 | import akka.actor.{ActorLogging, ActorSystem, PoisonPill, Props} 6 | import akka.persistence.PersistentActor 7 | 8 | 9 | object PersistentActors extends App { 10 | 11 | /* 12 | Scenario: we have a business and an accountant which keeps track of our invoices. 13 | */ 14 | 15 | // COMMANDS 16 | case class Invoice(recipient: String, date: Date, amount: Int) 17 | case class InvoiceBulk(invoices: List[Invoice]) 18 | 19 | // Special messsages 20 | case object Shutdown 21 | 22 | // EVENTS 23 | case class InvoiceRecorded(id: Int, recipient: String, date: Date, amount: Int) 24 | 25 | class Accountant extends PersistentActor with ActorLogging { 26 | 27 | var latestInvoiceId = 0 28 | var totalAmount = 0 29 | 30 | override def persistenceId: String = "simple-accountant" // best practice: make it unique 31 | 32 | /** 33 | * The "normal" receive method 34 | */ 35 | override def receiveCommand: Receive = { 36 | case Invoice(recipient, date, amount) => 37 | /* 38 | When you receive a command 39 | 1) you create an EVENT to persist into the store 40 | 2) you persist the event, the pass in a callback that will get triggered once the event is written 41 | 3) we update the actor's state when the event has persisted 42 | */ 43 | log.info(s"Receive invoice for amount: $amount") 44 | persist(InvoiceRecorded(latestInvoiceId, recipient, date, amount)) 45 | /* time gap: all other messages sent to this actor are STASHED */ 46 | { e => 47 | // SAFE to access mutable state here 48 | 49 | // update state 50 | latestInvoiceId += 1 51 | totalAmount += amount 52 | 53 | // correctly identify the sender of the COMMAND 54 | sender() ! "PersistenceACK" 55 | log.info(s"Persisted $e as invoice #${e.id}, for total amount $totalAmount") 56 | } 57 | 58 | case InvoiceBulk(invoices) => 59 | /* 60 | 1) create events (plural) 61 | 2) persist all the events 62 | 3) update the actor state when each event is persisted 63 | */ 64 | val invoiceIds = latestInvoiceId to (latestInvoiceId + invoices.size) 65 | val events = invoices.zip(invoiceIds).map { pair => 66 | val id = pair._2 67 | val invoice = pair._1 68 | 69 | InvoiceRecorded(id, invoice.recipient, invoice.date, invoice.amount) 70 | } 71 | persistAll(events) { e => 72 | latestInvoiceId += 1 73 | totalAmount += e.amount 74 | log.info(s"Persisted SINGLE $e as invoice #${e.id}, for total amount $totalAmount") 75 | } 76 | 77 | case Shutdown => 78 | context.stop(self) 79 | 80 | // act like a normal actor 81 | case "print" => 82 | log.info(s"Latest invoice id: $latestInvoiceId, total amount: $totalAmount") 83 | } 84 | 85 | /** 86 | Handler that will be called on recovery 87 | */ 88 | override def receiveRecover: Receive = { 89 | /* 90 | best practice: follow the logic in the persist steps of receiveCommand 91 | */ 92 | case InvoiceRecorded(id, _, _, amount) => 93 | latestInvoiceId = id 94 | totalAmount += amount 95 | log.info(s"Recovered invoice #$id for amount $amount, total amount: $totalAmount") 96 | } 97 | 98 | /* 99 | This method is called if persisting failed. 100 | The actor will be STOPPED. 101 | 102 | Best practice: start the actor again after a while. 103 | (use Backoff supervisor) 104 | */ 105 | override def onPersistFailure(cause: Throwable, event: Any, seqNr: Long): Unit = { 106 | log.error(s"Fail to persist $event because of $cause") 107 | super.onPersistFailure(cause, event, seqNr) 108 | } 109 | 110 | /* 111 | Called if the JOURNAL fails to persist the event 112 | The actor is RESUMED. 113 | */ 114 | override def onPersistRejected(cause: Throwable, event: Any, seqNr: Long): Unit = { 115 | log.error(s"Persist rejected for $event because of $cause") 116 | super.onPersistRejected(cause, event, seqNr) 117 | } 118 | } 119 | 120 | val system = ActorSystem("PersistentActors") 121 | val accountant = system.actorOf(Props[Accountant], "simpleAccountant") 122 | 123 | for (i <- 1 to 10) { 124 | accountant ! Invoice("The Sofa Company", new Date, i * 1000) 125 | } 126 | 127 | /* 128 | Persistence failures 129 | */ 130 | 131 | /** 132 | * Persisting multiple events 133 | * 134 | * persistAll 135 | */ 136 | val newInvoices = for (i <- 1 to 5) yield Invoice("The awesome chairs", new Date, i * 2000) 137 | // accountant ! InvoiceBulk(newInvoices.toList) 138 | 139 | /* 140 | NEVER EVER CALL PERSIST OR PERSISTALL FROM FUTURES. 141 | */ 142 | 143 | /** 144 | * Shutdown of persistent actors 145 | * 146 | * Best practice: define your own "shutdown" messages 147 | */ 148 | // accountant ! PoisonPill 149 | accountant ! Shutdown 150 | } 151 | -------------------------------------------------------------------------------- /src/main/scala/part2_event_sourcing/PersistentActorsExercise.scala: -------------------------------------------------------------------------------- 1 | package part2_event_sourcing 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | 6 | import scala.collection.mutable 7 | 8 | object PersistentActorsExercise extends App { 9 | 10 | /* 11 | Persistent actor for a voting station 12 | Keep: 13 | - the citizens who voted 14 | - the poll: mapping between a candidate and the number of received votes so far 15 | 16 | The actor must be able to recover its state if it's shut down or restarted 17 | */ 18 | case class Vote(citizenPID: String, candidate: String) 19 | 20 | class VotingStation extends PersistentActor with ActorLogging { 21 | 22 | // ignore the mutable state for now 23 | val citizens: mutable.Set[String] = new mutable.HashSet[String]() 24 | val poll: mutable.Map[String, Int] = new mutable.HashMap[String, Int]() 25 | 26 | override def persistenceId: String = "simple-voting-station" 27 | 28 | override def receiveCommand: Receive = { 29 | case vote @ Vote(citizenPID, candidate) => 30 | if (!citizens.contains(vote.citizenPID)) { 31 | /* 32 | 1) create the event 33 | 2) persist the event 34 | 3) handle a state change after persisting is successful 35 | */ 36 | persist(vote) { _ => // COMMAND sourcing 37 | log.info(s"Persisted: $vote") 38 | handleInternalStateChange(citizenPID, candidate) 39 | } 40 | } else { 41 | log.warning(s"Citizen $citizenPID is trying to vote multiple times!") 42 | } 43 | case "print" => 44 | log.info(s"Current state: \nCitizens: $citizens\npolls: $poll") 45 | } 46 | 47 | def handleInternalStateChange(citizenPID: String, candidate: String): Unit = { 48 | citizens.add(citizenPID) 49 | val votes = poll.getOrElse(candidate, 0) 50 | poll.put(candidate, votes + 1) 51 | } 52 | 53 | override def receiveRecover: Receive = { 54 | case vote @ Vote(citizenPID, candidate) => 55 | log.info(s"Recovered: $vote") 56 | handleInternalStateChange(citizenPID, candidate) 57 | } 58 | } 59 | 60 | val system = ActorSystem("PersistentActorsExercise") 61 | val votingStation = system.actorOf(Props[VotingStation], "simpleVotingStation") 62 | 63 | val votesMap = Map[String, String]( 64 | "Alice" -> "Martin", 65 | "Bob" -> "Roland", 66 | "Charlie" -> "Martin", 67 | "David" -> "Jonas", 68 | "Daniel" -> "Martin" 69 | ) 70 | 71 | // votesMap.keys.foreach { citizen => 72 | // votingStation ! Vote(citizen, votesMap(citizen)) 73 | // } 74 | 75 | votingStation ! Vote("Daniel", "Daniel") 76 | votingStation ! "print" 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/part2_event_sourcing/RecoveryDemo.scala: -------------------------------------------------------------------------------- 1 | package part2_event_sourcing 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.{PersistentActor, Recovery, RecoveryCompleted, SnapshotSelectionCriteria} 5 | 6 | object RecoveryDemo extends App { 7 | 8 | case class Command(contents: String) 9 | case class Event(id: Int, contents: String) 10 | 11 | class RecoveryActor extends PersistentActor with ActorLogging { 12 | 13 | override def persistenceId: String = "recovery-actor" 14 | 15 | 16 | override def receiveCommand: Receive = online(0) 17 | 18 | def online(latestPersistedEventId: Int): Receive = { 19 | case Command(contents) => 20 | persist(Event(latestPersistedEventId, contents)) { event => 21 | log.info(s"Successfully persisted $event, recovery is ${if (this.recoveryFinished) "" else "NOT"} finished.") 22 | context.become(online(latestPersistedEventId + 1)) 23 | } 24 | } 25 | 26 | 27 | override def receiveRecover: Receive = { 28 | case RecoveryCompleted => 29 | // additional initialization 30 | log.info("I have finished recovering") 31 | 32 | case Event(id, contents) => 33 | // if (contents.contains("314")) 34 | // throw new RuntimeException("I can't take this anymore!") 35 | log.info(s"Recovered: $contents, recovery is ${if (this.recoveryFinished) "" else "NOT"} finished.") 36 | context.become(online(id + 1)) 37 | /* 38 | this will NOT change the event handler during recovery 39 | AFTER recovery the "normal" handler will be the result of ALL the stacking of context.becomes. 40 | */ 41 | } 42 | 43 | override def onRecoveryFailure(cause: Throwable, event: Option[Any]): Unit = { 44 | log.error("I failed at recovery") 45 | super.onRecoveryFailure(cause, event) 46 | } 47 | 48 | // override def recovery: Recovery = Recovery(toSequenceNr = 100) 49 | // override def recovery: Recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria.Latest) 50 | // override def recovery: Recovery = Recovery.none 51 | } 52 | 53 | 54 | val system = ActorSystem("RecoveryDemo") 55 | val recoveryActor = system.actorOf(Props[RecoveryActor], "recoveryActor") 56 | 57 | /* 58 | Stashing commands 59 | */ 60 | // for (i <- 1 to 1000) { 61 | // recoveryActor ! Command(s"command $i") 62 | // } 63 | // ALL COMMANDS SENT DURING RECOVERY ARE STASHED 64 | 65 | /* 66 | 2 - failure during recovery 67 | - onRecoveryFailure + the actor is STOPPED. 68 | */ 69 | 70 | /* 71 | 3 - customizing recovery 72 | - DO NOT persist more events after a customized _incomplete_ recovery 73 | */ 74 | 75 | /* 76 | 4 - recovery status or KNOWING when you're done recovering 77 | - getting a signal when you're done recovering 78 | */ 79 | 80 | /* 81 | 5 - stateless actors 82 | */ 83 | recoveryActor ! Command(s"special command 1") 84 | recoveryActor ! Command(s"special command 2") 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/scala/part2_event_sourcing/Snapshots.scala: -------------------------------------------------------------------------------- 1 | package part2_event_sourcing 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.{PersistentActor, SaveSnapshotFailure, SaveSnapshotSuccess, SnapshotOffer} 5 | 6 | import scala.collection.mutable 7 | 8 | object Snapshots extends App { 9 | 10 | // commands 11 | case class ReceivedMessage(contents: String) // message FROM your contact 12 | case class SentMessage(contents: String) // message TO your contact 13 | 14 | // events 15 | case class ReceivedMessageRecord(id: Int, contents: String) 16 | case class SentMessageRecord(id: Int, contents: String) 17 | 18 | object Chat { 19 | def props(owner: String, contact: String) = Props(new Chat(owner, contact)) 20 | } 21 | 22 | class Chat(owner: String, contact: String) extends PersistentActor with ActorLogging { 23 | val MAX_MESSAGES = 10 24 | 25 | var commandsWithoutCheckpoint = 0 26 | var currentMessageId = 0 27 | val lastMessages = new mutable.Queue[(String, String)]() 28 | 29 | override def persistenceId: String = s"$owner-$contact-chat" 30 | 31 | override def receiveCommand: Receive = { 32 | case ReceivedMessage(contents) => 33 | persist(ReceivedMessageRecord(currentMessageId, contents)) { e => 34 | log.info(s"Received message: $contents") 35 | maybeReplaceMessage(contact, contents) 36 | currentMessageId += 1 37 | maybeCheckpoint() 38 | } 39 | case SentMessage(contents) => 40 | persist(SentMessageRecord(currentMessageId, contents)) { e => 41 | log.info(s"Sent message: $contents") 42 | maybeReplaceMessage(owner, contents) 43 | currentMessageId += 1 44 | maybeCheckpoint() 45 | } 46 | case "print" => 47 | log.info(s"Most recent messages: $lastMessages") 48 | // snapshot-related messages 49 | case SaveSnapshotSuccess(metadata) => 50 | log.info(s"saving snapshot succeeded: $metadata") 51 | case SaveSnapshotFailure(metadata, reason) => 52 | log.warning(s"saving snapshot $metadata failed because of $reason") 53 | } 54 | 55 | override def receiveRecover: Receive = { 56 | case ReceivedMessageRecord(id, contents) => 57 | log.info(s"Recovered received message $id: $contents") 58 | maybeReplaceMessage(contact, contents) 59 | currentMessageId = id 60 | case SentMessageRecord(id, contents) => 61 | log.info(s"Recovered sent message $id: $contents") 62 | maybeReplaceMessage(owner, contents) 63 | currentMessageId = id 64 | case SnapshotOffer(metadata, contents) => 65 | log.info(s"Recovered snapshot: $metadata") 66 | contents.asInstanceOf[mutable.Queue[(String, String)]].foreach(lastMessages.enqueue(_)) 67 | } 68 | 69 | def maybeReplaceMessage(sender: String, contents: String): Unit = { 70 | if (lastMessages.size >= MAX_MESSAGES) { 71 | lastMessages.dequeue() 72 | } 73 | lastMessages.enqueue((sender, contents)) 74 | } 75 | 76 | def maybeCheckpoint(): Unit = { 77 | commandsWithoutCheckpoint += 1 78 | if (commandsWithoutCheckpoint >= MAX_MESSAGES) { 79 | log.info("Saving checkpoint...") 80 | saveSnapshot(lastMessages) // asynchronous operation 81 | commandsWithoutCheckpoint = 0 82 | } 83 | } 84 | } 85 | 86 | val system = ActorSystem("SnapshotsDemo") 87 | val chat = system.actorOf(Chat.props("daniel123", "martin345")) 88 | 89 | // for (i <- 1 to 100000) { 90 | // chat ! ReceivedMessage(s"Akka Rocks $i") 91 | // chat ! SentMessage(s"Akka Rules $i") 92 | // } 93 | 94 | // snapshots come in. 95 | chat ! "print" 96 | 97 | /* 98 | event 1 99 | event 2 100 | event 3 101 | snapshot 1 102 | event 4 103 | snapshot 2 104 | event 5 105 | event 6 106 | */ 107 | 108 | /* 109 | pattern: 110 | - after each persist, maybe save a snapshot (logic is up to you) 111 | - if you save a snapshot, handle the SnapshotOffer message in receiveRecover 112 | - (optional, but best practice) handle SaveSnapshotSuccess and SaveSnapshotFailure in receiveCommand 113 | - profit from the extra speed!!! 114 | */ 115 | } 116 | -------------------------------------------------------------------------------- /src/main/scala/part3_stores_serialization/Cassandra.scala: -------------------------------------------------------------------------------- 1 | package part3_stores_serialization 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import com.typesafe.config.ConfigFactory 5 | 6 | object Cassandra extends App { 7 | 8 | val cassandraActorSystem = ActorSystem("cassandraSystem", ConfigFactory.load().getConfig("cassandraDemo")) 9 | val persistentActor = cassandraActorSystem.actorOf(Props[SimplePersistentActor], "simplePersistentActor") 10 | 11 | for (i <- 1 to 10) { 12 | persistentActor ! s"I love Akka [$i]" 13 | } 14 | persistentActor ! "print" 15 | persistentActor ! "snap" 16 | 17 | for (i <- 11 to 20) { 18 | persistentActor ! s"I love Akka [$i]" 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/part3_stores_serialization/CustomSerialization.scala: -------------------------------------------------------------------------------- 1 | package part3_stores_serialization 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | import akka.serialization.Serializer 6 | import com.typesafe.config.ConfigFactory 7 | 8 | // command 9 | case class RegisterUser(email: String, name: String) 10 | // event 11 | case class UserRegistered(id: Int, email: String, name: String) 12 | 13 | // serializer 14 | class UserRegistrationSerializer extends Serializer { 15 | 16 | val SEPARATOR = "//" 17 | override def identifier: Int = 53278 18 | 19 | override def toBinary(o: AnyRef): Array[Byte] = o match { 20 | case event @ UserRegistered(id, email, name) => 21 | println(s"Serializing $event") 22 | s"[$id$SEPARATOR$email$SEPARATOR$name]".getBytes() 23 | case _ => 24 | throw new IllegalArgumentException("only user registration events supported in this serializer") 25 | } 26 | 27 | override def fromBinary(bytes: Array[Byte], manifest: Option[Class[_]]): AnyRef = { 28 | val string = new String(bytes) 29 | val values = string.substring(1, string.length() - 1).split(SEPARATOR) 30 | val id = values(0).toInt 31 | val email = values(1) 32 | val name = values(2) 33 | 34 | val result = UserRegistered(id, email, name) 35 | println(s"Deserialized $string to $result") 36 | 37 | result 38 | } 39 | 40 | override def includeManifest: Boolean = false 41 | } 42 | 43 | 44 | class UserRegistrationActor extends PersistentActor with ActorLogging { 45 | override def persistenceId: String = "user-registration" 46 | var currentId = 0 47 | 48 | override def receiveCommand: Receive = { 49 | case RegisterUser(email, name) => 50 | persist(UserRegistered(currentId, email, name)) { e => 51 | currentId += 1 52 | log.info(s"Persisted: $e") 53 | } 54 | } 55 | 56 | override def receiveRecover: Receive = { 57 | case event @ UserRegistered(id, _, _) => 58 | log.info(s"Recovered: $event") 59 | currentId = id 60 | } 61 | } 62 | 63 | object CustomSerialization extends App { 64 | 65 | /* 66 | send command to the actor 67 | actor calls persist 68 | serializer serializes the event into bytes 69 | the journal writes the bytes 70 | */ 71 | 72 | val system = ActorSystem("CustomSerialization", ConfigFactory.load().getConfig("customSerializerDemo")) 73 | val userRegistrationActor = system.actorOf(Props[UserRegistrationActor], "userRegistration") 74 | 75 | // for (i <- 1 to 10) { 76 | // userRegistrationActor ! RegisterUser(s"user_$i@rtjvm.com", s"User $i") 77 | // } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/part3_stores_serialization/LocalStores.scala: -------------------------------------------------------------------------------- 1 | package part3_stores_serialization 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import com.typesafe.config.ConfigFactory 5 | 6 | object LocalStores extends App { 7 | 8 | val localStoresActorSystem = ActorSystem("localStoresSystem", ConfigFactory.load().getConfig("localStores")) 9 | val persistentActor = localStoresActorSystem.actorOf(Props[SimplePersistentActor], "simplePersistentActor") 10 | 11 | for (i <- 1 to 10) { 12 | persistentActor ! s"I love Akka [$i]" 13 | } 14 | persistentActor ! "print" 15 | persistentActor ! "snap" 16 | 17 | for (i <- 11 to 20) { 18 | persistentActor ! s"I love Akka [$i]" 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/part3_stores_serialization/Postgres.scala: -------------------------------------------------------------------------------- 1 | package part3_stores_serialization 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import com.typesafe.config.ConfigFactory 5 | 6 | object Postgres extends App { 7 | 8 | val postgresActorSystem = ActorSystem("postgresSystem", ConfigFactory.load().getConfig("postgresDemo")) 9 | val persistentActor = postgresActorSystem.actorOf(Props[SimplePersistentActor], "simplePersistentActor") 10 | 11 | for (i <- 1 to 10) { 12 | persistentActor ! s"I love Akka [$i]" 13 | } 14 | persistentActor ! "print" 15 | persistentActor ! "snap" 16 | 17 | for (i <- 11 to 20) { 18 | persistentActor ! s"I love Akka [$i]" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/part3_stores_serialization/SimplePersistentActor.scala: -------------------------------------------------------------------------------- 1 | package part3_stores_serialization 2 | 3 | import akka.actor.ActorLogging 4 | import akka.persistence._ 5 | 6 | class SimplePersistentActor extends PersistentActor with ActorLogging { 7 | 8 | override def persistenceId: String = "simple-persistent-actor" 9 | 10 | // mutable state 11 | var nMessages = 0 12 | 13 | override def receiveCommand: Receive = { 14 | case "print" => 15 | log.info(s"I have persisted $nMessages so far") 16 | case "snap" => 17 | saveSnapshot(nMessages) 18 | case SaveSnapshotSuccess(metadata) => 19 | log.info(s"Save snapshot was successful: $metadata") 20 | case SaveSnapshotFailure(_, cause) => 21 | log.warning(s"Save snapshot failed: $cause") 22 | case message => persist(message) { _ => 23 | log.info(s"Persisting $message") 24 | nMessages += 1 25 | } 26 | } 27 | 28 | override def receiveRecover: Receive = { 29 | case RecoveryCompleted => 30 | log.info("Recovery done") 31 | case SnapshotOffer(_, payload: Int) => 32 | log.info(s"Recovered snapshot: $payload") 33 | nMessages = payload 34 | case message => 35 | log.info(s"Recovered: $message") 36 | nMessages += 1 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/scala/part4_practices/DetachingModels.scala: -------------------------------------------------------------------------------- 1 | package part4_practices 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | import akka.persistence.journal.{EventAdapter, EventSeq} 6 | import com.typesafe.config.ConfigFactory 7 | 8 | import scala.collection.mutable 9 | 10 | object DetachingModels extends App { 11 | 12 | class CouponManager extends PersistentActor with ActorLogging { 13 | import DomainModel._ 14 | 15 | val coupons: mutable.Map[String, User] = new mutable.HashMap[String, User]() 16 | 17 | override def persistenceId: String = "coupon-manager" 18 | 19 | override def receiveCommand: Receive = { 20 | case ApplyCoupon(coupon, user) => 21 | if (!coupons.contains(coupon.code)) { 22 | persist(CouponApplied(coupon.code, user)) { e => 23 | log.info(s"Persisted $e") 24 | coupons.put(coupon.code, user) 25 | } 26 | } 27 | } 28 | 29 | override def receiveRecover: Receive = { 30 | case event @ CouponApplied(code, user) => 31 | log.info(s"Recovered $event") 32 | coupons.put(code, user) 33 | } 34 | } 35 | 36 | import DomainModel._ 37 | val system = ActorSystem("DetachingModels", ConfigFactory.load().getConfig("detachingModels")) 38 | val couponManager = system.actorOf(Props[CouponManager], "couponManager") 39 | 40 | // for (i <- 10 to 15) { 41 | // val coupon = Coupon(s"MEGA_COUPON_$i", 100) 42 | // val user = User(s"$i", s"user_$i@rtjvm.com", s"John Doe $i") 43 | // 44 | // couponManager ! ApplyCoupon(coupon, user) 45 | // } 46 | 47 | 48 | } 49 | 50 | object DomainModel { 51 | case class User(id: String, email: String, name: String) 52 | case class Coupon(code: String, promotionAmount: Int) 53 | 54 | // command 55 | case class ApplyCoupon(coupon: Coupon, user: User) 56 | // event 57 | case class CouponApplied(code: String, user: User) 58 | } 59 | 60 | object DataModel { 61 | case class WrittenCouponApplied(code: String, userId: String, userEmail: String) 62 | case class WrittenCouponAppliedV2(code: String, userId: String, userEmail: String, username: String) 63 | } 64 | 65 | class ModelAdapter extends EventAdapter { 66 | import DomainModel._ 67 | import DataModel._ 68 | 69 | override def manifest(event: Any): String = "CMA" 70 | 71 | // journal -> serializer -> fromJournal -> to the actor 72 | override def fromJournal(event: Any, manifest: String): EventSeq = event match { 73 | case event @ WrittenCouponApplied(code, userId, userEmail) => 74 | println(s"Converting $event to DOMAIN model") 75 | EventSeq.single(CouponApplied(code, User(userId, userEmail, ""))) 76 | case event @ WrittenCouponAppliedV2(code, userId, userEmail, username) => 77 | println(s"Converting $event to DOMAIN model") 78 | EventSeq.single(CouponApplied(code, User(userId, userEmail, username))) 79 | 80 | case other => 81 | EventSeq.single(other) 82 | } 83 | 84 | // actor -> toJournal -> serializer -> journal 85 | override def toJournal(event: Any): Any = event match { 86 | case event @ CouponApplied(code, user) => 87 | println(s"Converting $event to DATA model") 88 | WrittenCouponAppliedV2(code, user.id, user.email, user.name) 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/scala/part4_practices/EventAdapters.scala: -------------------------------------------------------------------------------- 1 | package part4_practices 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | import akka.persistence.journal.{EventSeq, ReadEventAdapter} 6 | import com.typesafe.config.ConfigFactory 7 | 8 | import scala.collection.mutable 9 | 10 | object EventAdapters extends App { 11 | 12 | // store for acoustic guitars 13 | val ACOUSTIC = "acoustic" 14 | val ELECTRIC = "electric" 15 | 16 | // data structures 17 | case class Guitar(id: String, model: String, make: String, guitarType: String = ACOUSTIC) 18 | // command 19 | case class AddGuitar(guitar: Guitar, quantity: Int) 20 | // event 21 | case class GuitarAdded(guitarId: String, guitarModel: String, guitarMake: String, quantity: Int) 22 | case class GuitarAddedV2(guitarId: String, guitarModel: String, guitarMake: String, quantity: Int, guitarType: String) 23 | 24 | class InventoryManager extends PersistentActor with ActorLogging { 25 | override def persistenceId: String = "guitar-inventory-manager" 26 | 27 | val inventory: mutable.Map[Guitar, Int] = new mutable.HashMap[Guitar,Int]() 28 | 29 | override def receiveCommand: Receive = { 30 | case AddGuitar(guitar @ Guitar(id, model, make, guitarType), quantity) => 31 | persist(GuitarAddedV2(id, model, make, quantity, guitarType)) { _ => 32 | addGuitarInventory(guitar, quantity) 33 | log.info(s"Added $quantity x $guitar to inventory") 34 | } 35 | case "print" => 36 | log.info(s"Current inventory is: $inventory") 37 | } 38 | 39 | override def receiveRecover: Receive = { 40 | case event @ GuitarAddedV2(id, model, make, quantity, guitarType) => 41 | log.info(s"Recovered $event") 42 | val guitar = Guitar(id, model, make, guitarType) 43 | addGuitarInventory(guitar, quantity) 44 | } 45 | 46 | def addGuitarInventory(guitar: Guitar, quantity: Int) = { 47 | val existingQuantity = inventory.getOrElse(guitar, 0) 48 | inventory.put(guitar, existingQuantity + quantity) 49 | } 50 | } 51 | 52 | class GuitarReadEventAdapter extends ReadEventAdapter { 53 | /* 54 | journal -> serializer -> read event adapter -> actor 55 | (bytes) (GA) (GAV2) (receiveRecover) 56 | */ 57 | override def fromJournal(event: Any, manifest: String): EventSeq = event match { 58 | case GuitarAdded(guitarId, guitarModel, guitarMake, quantity) => 59 | EventSeq.single(GuitarAddedV2(guitarId, guitarModel, guitarMake, quantity, ACOUSTIC)) 60 | case other => EventSeq.single(other) 61 | } 62 | } 63 | // WriteEventAdapter - used for backwards compat 64 | // actor -> write event adapter -> serializer -> journal 65 | // EventAdapter 66 | 67 | val system = ActorSystem("eventAdapters", ConfigFactory.load().getConfig("eventAdapters")) 68 | val inventoryManager = system.actorOf(Props[InventoryManager], "inventoryManager") 69 | 70 | val guitars = for (i <- 1 to 10) yield Guitar(s"$i", s"Hakker $i", "RockTheJVM") 71 | // guitars.foreach { guitar => 72 | // inventoryManager ! AddGuitar(guitar, 5) 73 | // } 74 | inventoryManager ! "print" 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/part4_practices/PersistenceQueryDemo.scala: -------------------------------------------------------------------------------- 1 | package part4_practices 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | import akka.persistence.cassandra.query.scaladsl.CassandraReadJournal 6 | import akka.persistence.journal.{Tagged, WriteEventAdapter} 7 | import akka.persistence.query.{Offset, PersistenceQuery} 8 | import akka.stream.ActorMaterializer 9 | import com.typesafe.config.ConfigFactory 10 | 11 | import scala.concurrent.duration._ 12 | import scala.util.Random 13 | 14 | object PersistenceQueryDemo extends App { 15 | 16 | val system = ActorSystem("PersistenceQueryDemo", ConfigFactory.load().getConfig("persistenceQuery")) 17 | 18 | // read journal 19 | val readJournal = PersistenceQuery(system).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier) 20 | 21 | // give me all persistence IDs 22 | val persistenceIds = readJournal.currentPersistenceIds() 23 | 24 | // boilerplate so far 25 | implicit val materializer = ActorMaterializer()(system) 26 | // persistenceIds.runForeach { persistenceId => 27 | // println(s"Found persistence ID: $persistenceId") 28 | // } 29 | 30 | class SimplePersistentActor extends PersistentActor with ActorLogging { 31 | override def persistenceId: String = "persistence-query-id-1" 32 | 33 | override def receiveCommand: Receive = { 34 | case m => persist(m) { _ => 35 | log.info(s"Persisted: $m") 36 | } 37 | } 38 | 39 | override def receiveRecover: Receive = { 40 | case e => log.info(s"Recovered: $e") 41 | } 42 | } 43 | 44 | val simpleActor = system.actorOf(Props[SimplePersistentActor], "simplePersistentActor") 45 | 46 | import system.dispatcher 47 | // system.scheduler.scheduleOnce(5 seconds) { 48 | // val message = "hello a second time" 49 | // simpleActor ! message 50 | // } 51 | 52 | // events by persistence ID 53 | val events = readJournal.eventsByPersistenceId("persistence-query-id-1", 0, Long.MaxValue) 54 | events.runForeach { event => 55 | println(s"Read event: $event") 56 | } 57 | 58 | // events by tags 59 | val genres = Array("pop", "rock", "hip-hop", "jazz", "disco") 60 | case class Song(artist: String, title: String, genre: String) 61 | // command 62 | case class Playlist(songs: List[Song]) 63 | // event 64 | case class PlaylistPurchased(id: Int, songs: List[Song]) 65 | 66 | class MusicStoreCheckoutActor extends PersistentActor with ActorLogging { 67 | override def persistenceId: String = "music-store-checkout" 68 | 69 | var latestPlaylistId = 0 70 | 71 | override def receiveCommand: Receive = { 72 | case Playlist(songs) => 73 | persist(PlaylistPurchased(latestPlaylistId, songs)) { _ => 74 | log.info(s"User purchased: $songs") 75 | latestPlaylistId += 1 76 | } 77 | } 78 | 79 | override def receiveRecover: Receive = { 80 | case event @ PlaylistPurchased(id, _) => 81 | log.info(s"Recovered: $event") 82 | latestPlaylistId = id 83 | } 84 | } 85 | 86 | class MusicStoreEventAdapter extends WriteEventAdapter { 87 | override def manifest(event: Any): String = "musicStore" 88 | 89 | override def toJournal(event: Any): Any = event match { 90 | case event @ PlaylistPurchased(_, songs) => 91 | val genres = songs.map(_.genre).toSet 92 | Tagged(event, genres) 93 | case event => event 94 | } 95 | } 96 | 97 | val checkoutActor = system.actorOf(Props[MusicStoreCheckoutActor], "musicStoreActor") 98 | 99 | val r = new Random 100 | for (_ <- 1 to 10) { 101 | val maxSongs = r.nextInt(5) 102 | val songs = for (i <- 1 to maxSongs) yield { 103 | val randomGenre = genres(r.nextInt(5)) 104 | Song(s"Artist $i", s"My Love Song $i", randomGenre) 105 | } 106 | 107 | checkoutActor ! Playlist(songs.toList) 108 | } 109 | 110 | val rockPlaylists = readJournal.eventsByTag("rock", Offset.noOffset) 111 | rockPlaylists.runForeach { event => 112 | println(s"Found a playlist with a rock song: $event") 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/scala/playground/Playground.scala: -------------------------------------------------------------------------------- 1 | package playground 2 | 3 | import akka.actor.{ActorLogging, ActorSystem, Props} 4 | import akka.persistence.PersistentActor 5 | 6 | object Playground extends App { 7 | 8 | /** 9 | * A simple persistent actor that just logs all commands and events. 10 | */ 11 | class SimplePersistentActor extends PersistentActor with ActorLogging { 12 | override def persistenceId: String = "simple-persistence" 13 | 14 | override def receiveCommand: Receive = { 15 | case message => log.info(s"Received: $message") 16 | } 17 | 18 | override def receiveRecover: Receive = { 19 | case event => log.info(s"Recovered: $event") 20 | } 21 | } 22 | 23 | val system = ActorSystem("Playground") 24 | val simpleActor = system.actorOf(Props[SimplePersistentActor], "simplePersistentActor") 25 | simpleActor ! "I love Akka!" 26 | 27 | /* 28 | If you're first starting with the project, just COMPILE the code (no need to run) to see that the libraries have correctly installed. 29 | Only run it after you've made the necessary configurations in application.conf. 30 | 31 | If the code compiles, you're good to go. Feel free to delete this code and go wild with your experiments with Akka Persistence! 32 | */ 33 | } 34 | --------------------------------------------------------------------------------