├── .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 |
5 |
6 |
7 |
8 |
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 |
110 |
111 |
112 |
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 |
5 |
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 |
--------------------------------------------------------------------------------