├── .gitignore
├── LICENSE.txt
├── README.md
├── build.sbt
├── examples
└── src
│ ├── main
│ ├── resources
│ │ └── application.conf
│ └── scala
│ │ └── com
│ │ └── example
│ │ └── PingActor.scala
│ └── test
│ └── scala
│ └── com
│ └── example
│ └── PingActorSpec.scala
├── project
├── Utils.scala
├── build.properties
├── plugins.sbt
└── project
│ └── plugins.sbt
├── scalastyle-config.xml
└── src
├── main
├── java
│ └── com
│ │ └── nokia
│ │ └── ntp
│ │ └── ct
│ │ └── persistence
│ │ └── PublicActorAdapter.java
├── resources
│ └── reference.conf
└── scala
│ └── com
│ └── nokia
│ └── ntp
│ └── ct
│ └── persistence
│ ├── ActorDeployer.scala
│ ├── PersistenceSettings.scala
│ ├── PersistentActor.scala
│ ├── Recovery.scala
│ ├── SelfDeployingBehavior.scala
│ ├── Update.scala
│ ├── package.scala
│ ├── persistenceApi.scala
│ ├── testkit
│ └── TestInterpreter.scala
│ └── typedPersistentActor.scala
└── test
├── resources
└── application.conf
└── scala
└── com
└── nokia
└── ntp
└── ct
└── persistence
├── AbstractPersistenceSpec.scala
├── DeploymentSpec.scala
├── MockSnapshotStore.scala
├── PersistenceSpec.scala
├── SnapshotProbe.scala
└── testkit
└── TestExample.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Nokia Solutions and Networks Oy
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Eclipse:
16 | .cache-main
17 | .cache-tests
18 | .classpath
19 | .project
20 | .settings/
21 | bin/
22 |
23 | # sbt:
24 | target/
25 |
26 | # Idea:
27 | .idea
28 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
18 |
19 | # Akka Typed Persistence
20 |
21 | [](https://gitter.im/nokia/akka-typed-persistence "Gitter chat")
22 |
23 | **Event sourcing for Akka Typed**
24 |
25 | This library implements actor persistence for
26 | [Akka Typed](http://doc.akka.io/docs/akka/2.5.4/scala/typed.html)
27 | with event sourcing. It provides:
28 |
29 | * an actor definition API, which
30 | * is integrated with Akka Typed,
31 | * statically _type safe_,
32 | * and _supports actor persistence_ (with event sourcing and snapshots);
33 | * and an _implementation_ of this API
34 | * based on `akka-typed` and `akka-persistence`.
35 |
36 | A conference talk introducing the library was presented at
37 | [Scala By the Bay](http://sched.co/7iUT) ([slides](https://t.co/IsENEuxShc)).
38 | For the full code of the example in the talk, see
39 | [this file](examples/src/main/scala/com/example/PingActor.scala).
40 |
41 |
42 | ## Motivation
43 |
44 | [Akka Typed](http://doc.akka.io/docs/akka/2.5.4/scala/typed.html)
45 | provides a type safe API for defining Akka actors. However, originally
46 | it had no solution for actor persistence. The goal of this library was
47 | exactly that: integrating Akka Typed and Akka Persistence. (Since then,
48 | Akka Typed have been extended to include a
49 | [persistence API](https://akka.io/blog/2017/10/13/typed-persistence).)
50 |
51 |
52 | ## Getting started
53 |
54 | This library is currently not published, but you can use it by
55 | depending on this git repository in sbt:
56 |
57 | ```scala
58 | dependsOn(ProjectRef(uri("https://github.com/nokia/akka-typed-persistence.git#master"), "persistence"))
59 | ```
60 |
61 | For how to use the library, see
62 | [this example](examples/src/main/scala/com/example/PingActor.scala).
63 |
64 | **Dependencies:**
65 |
66 | * Scala 2.11 or 2.12
67 | * Akka 2.5.4
68 | * Cats 1.0.0-MF, cats-effect 0.4 and shapeless 2.3.2
69 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import com.typesafe.sbt.SbtScalariform.ScalariformKeys
18 | import scalariform.formatter.preferences._
19 |
20 | lazy val persistence = (project in file(".")).
21 | settings(name := "akka-typed-persistence").
22 | settings(commonSettings: _*).
23 | settings(publishSettings: _*)
24 |
25 | lazy val examples = (project in file("examples")).
26 | settings(name := "akka-typed-persistence-examples").
27 | dependsOn(persistence).
28 | settings(commonSettings: _*).
29 | settings(publishArtifact := false)
30 |
31 | lazy val commonSettings = Seq(
32 |
33 | // Scala:
34 | scalaVersion := "2.12.3",
35 | crossScalaVersions := Seq(scalaVersion.value, "2.11.11"),
36 | scalacOptions ++= Seq(
37 | "-feature",
38 | "-deprecation",
39 | "-unchecked",
40 | "-encoding", "UTF-8",
41 | "-target:jvm-1.8",
42 | "-Xlint:_",
43 | "-Xfuture",
44 | "-Ypartial-unification",
45 | "-Yno-adapted-args",
46 | "-Ywarn-numeric-widen",
47 | "-Ywarn-dead-code",
48 | "-Ywarn-unused-import"
49 | ),
50 |
51 | // first compile Java sources with regular
52 | // javac, then Scala souces with scalac
53 | // (we need this due to a hack in PublicActorAdapter.java)
54 | compileOrder := CompileOrder.JavaThenScala,
55 |
56 | // TODO: due to the same hack, scaladoc
57 | // doesn't work, so we disable it for now
58 | sources in (Compile, doc) := List(),
59 |
60 | // Static analysis:
61 | scalastyleFailOnError := true,
62 | wartremoverErrors ++= Seq(
63 | Wart.EitherProjectionPartial,
64 | Wart.OptionPartial,
65 | Wart.TraversableOps,
66 | Wart.JavaConversions,
67 | Wart.Return,
68 | Wart.Enumeration
69 | ),
70 | wartremoverWarnings ++= Seq(
71 | Wart.Product,
72 | Wart.Serializable,
73 | Wart.Option2Iterable,
74 | Wart.AsInstanceOf,
75 | Wart.IsInstanceOf,
76 | Wart.FinalCaseClass,
77 | Wart.TryPartial,
78 | Wart.Equals,
79 | Wart.Var,
80 | Wart.Null,
81 | Wart.MutableDataStructures,
82 | Wart.ExplicitImplicitTypes,
83 | Wart.ImplicitConversion,
84 | Wart.StringPlusAny
85 | ),
86 |
87 | // Code formatter:
88 | ScalariformKeys.preferences := ScalariformKeys.preferences.value
89 | .setPreference(PreserveSpaceBeforeArguments, true),
90 |
91 | // Dependencies:
92 | libraryDependencies ++= Seq(
93 | dependencies.akka.all,
94 | dependencies.cats,
95 | Seq(dependencies.shapeless),
96 | Seq(dependencies.scalatest, dependencies.scalacheck)
97 | ).flatten
98 | )
99 |
100 | lazy val publishSettings = Seq(
101 | organization := "com.nokia",
102 | version := "0.1.0-SNAPSHOT",
103 | publishMavenStyle := true,
104 | pomIncludeRepository := { repo => false }
105 | )
106 |
107 | lazy val dependencies = new {
108 |
109 | val cats = List(
110 | "org.typelevel" %% "cats-core" % "1.0.0-MF",
111 | "org.typelevel" %% "cats-free" % "1.0.0-MF",
112 | "org.typelevel" %% "cats-effect" % "0.4"
113 | )
114 |
115 | val shapeless = "com.chuusai" %% "shapeless" % "2.3.2"
116 |
117 | val scalatest = "org.scalatest" %% "scalatest" % "3.0.2" % "test-internal"
118 | val scalacheck = "org.scalacheck" %% "scalacheck" % "1.13.5" % "test-internal"
119 |
120 | val akka = new {
121 |
122 | val version = "2.5.4"
123 | val group = "com.typesafe.akka"
124 |
125 | val actor = group %% "akka-actor" % version
126 | val typed = group %% "akka-typed" % version
127 | val typedTestkit = group %% "akka-typed-testkit" % version
128 | val persistence = group %% "akka-persistence" % version
129 |
130 | val all = Seq(
131 | actor,
132 | typed,
133 | typedTestkit, // TODO: create separate -testkit module
134 | persistence
135 | )
136 | }
137 | }
138 |
139 | // For CI et al.:
140 | addCommandAlias("staticAnalysis", ";test:compile;scalastyle;examples/test:compile;examples/scalastyle") // additional tools can be added here
141 | addCommandAlias("testAll", ";test;examples/test")
142 | addCommandAlias("validate", ";staticAnalysis;testAll")
143 | addCommandAlias("measureCoverage", ";clean;coverage;test;coverageReport;coverageOff")
144 |
--------------------------------------------------------------------------------
/examples/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Nokia Solutions and Networks Oy
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | akka {
16 | persistence {
17 | journal.plugin = "akka.persistence.journal.inmem"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/src/main/scala/com/example/PingActor.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example
18 |
19 | import akka.typed.ActorRef
20 |
21 | import com.nokia.ntp.ct.persistence.{ PersistentActor, Update }
22 |
23 | object PingActor {
24 |
25 | final case class MyMsg(s: String, replyTo: ActorRef[String])
26 |
27 | final case class Increment(amount: Int)
28 |
29 | object Increment {
30 | implicit val updater: Update[Int, Increment] =
31 | Update.instance(_ + _.amount)
32 | }
33 |
34 | val myBehavior = PersistentActor.immutable[MyMsg, Increment, Int](
35 | 0,
36 | _.self.path.name
37 | ) { state => ctx => msg =>
38 | msg match {
39 | case MyMsg("ping", r) =>
40 | for {
41 | state <- ctx.apply(Increment(1))
42 | } yield {
43 | r ! s"${state} pings so far"
44 | state
45 | }
46 | case MyMsg("stop", r) =>
47 | r ! "OK"
48 | ctx.stop
49 | case MyMsg(_, _) =>
50 | ctx.same
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/examples/src/test/scala/com/example/PingActorSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example
18 |
19 | import scala.concurrent.duration._
20 |
21 | import org.scalatest.{ FlatSpecLike, Matchers }
22 | import org.scalatest.concurrent.ScalaFutures
23 |
24 | import akka.actor.ActorSystem
25 | import akka.actor.Scheduler
26 | import akka.testkit.TestKit
27 | import akka.typed.ActorRef
28 | import akka.typed.scaladsl.AskPattern._
29 | import akka.typed.scaladsl.adapter.actorRefAdapter
30 | import akka.util.Timeout
31 |
32 | class PingActorSpec extends TestKit(ActorSystem("pingSystem")) with FlatSpecLike with Matchers with ScalaFutures {
33 |
34 | import PingActor.MyMsg
35 |
36 | implicit val timeout: Timeout =
37 | Timeout(1.second)
38 |
39 | implicit val scheduler: Scheduler =
40 | this.system.scheduler
41 |
42 | implicit override def patienceConfig: PatienceConfig =
43 | PatienceConfig(timeout = super.patienceConfig.timeout * 5)
44 |
45 | val dummyRef: ActorRef[Any] =
46 | actorRefAdapter(this.testActor)
47 |
48 | "PingActor" should "reply with the number of pings it received so far" in {
49 | val ref: ActorRef[MyMsg] = PingActor.myBehavior.deployInto(this.system, "pingActor")
50 | ref.?[String](MyMsg("ping", _)).futureValue should be ("1 pings so far")
51 | ref ! MyMsg("foo", dummyRef)
52 | this.expectNoMsg()
53 | ref ! MyMsg("bar", dummyRef)
54 | this.expectNoMsg()
55 | ref.?[String](MyMsg("ping", _)).futureValue should be ("2 pings so far")
56 | ref.?[String](MyMsg("stop", _)).futureValue should be ("OK")
57 | val ref2: ActorRef[MyMsg] = PingActor.myBehavior.deployInto(this.system, "pingActor")
58 | ref2.?[String](MyMsg("ping", _)).futureValue should be ("3 pings so far")
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/project/Utils.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import sbt._
18 | import sbt.Keys._
19 | import org.scalastyle.sbt.ScalastylePlugin.scalastyle
20 |
21 | object Utils {
22 |
23 | def compileWithScalastyle: Def.Setting[_] =
24 | compileWithScalastyle(Compile)
25 |
26 | def compileWithScalastyle(cfg: Configuration): Def.Setting[_] = {
27 | (compile in cfg) := seq(
28 | (compile in cfg),
29 | (scalastyle in cfg).toTask("")
30 | ).value
31 | }
32 |
33 | private def seq[A, B](first: Def.Initialize[Task[A]], second: Def.Initialize[Task[B]]): Def.Initialize[Task[A]] = {
34 | Def.taskDyn {
35 | val r = first.value
36 | sec[A, B](r, second)
37 | }
38 | }
39 |
40 | private def sec[A, B](res: A, second: Def.Initialize[Task[B]]): Def.Initialize[Task[A]] = {
41 | Def.task {
42 | val _ = second.value
43 | res
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.16
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "0.8.0")
18 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0")
19 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.1.1")
20 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0")
21 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC12")
22 |
--------------------------------------------------------------------------------
/project/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC12")
18 |
--------------------------------------------------------------------------------
/scalastyle-config.xml:
--------------------------------------------------------------------------------
1 |
18 |
19 | Scalastyle standard configuration
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | [A-Za-z]+
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | -1,0,1,2,3
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 | true
93 | true
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | ^([a-z][A-Za-z0-9]*)|([\+\-\*\/\!])$
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
153 |
154 |
--------------------------------------------------------------------------------
/src/main/java/com/nokia/ntp/ct/persistence/PublicActorAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct.persistence;
18 |
19 | /**
20 | * The whole reason for the existence of this Java
21 | * class is that `akka.typed.adapter.ActorAdapter`
22 | * is package private, but we must extend it.
23 | */
24 | public abstract class PublicActorAdapter extends akka.typed.internal.adapter.ActorAdapter {
25 |
26 | public PublicActorAdapter(akka.typed.Behavior initialBehavior) {
27 | super(initialBehavior);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Nokia Solutions and Networks Oy
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | com.nokia.ntp.ct.persistence {
16 |
17 | # Whether to automatically delete
18 | # old events and snapshots after
19 | # taking a snapshot have been
20 | # successfully completed:
21 | auto-delete = on
22 |
23 | # Fine-grained debug logging:
24 | debug = off
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/ActorDeployer.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import akka.{ actor => au, typed => at }
21 | import akka.typed.scaladsl.ActorContext
22 | import akka.typed.scaladsl.adapter._
23 |
24 | import shapeless.tag.@@
25 |
26 | /**
27 | * Type class for abstracting over
28 | * things which can start actors
29 | * (e.g., ActorSystems or ActorContexts).
30 | */
31 | trait ActorDeployer[D] {
32 |
33 | def deploy[A](d: D)(props: au.Props @@ A, name: String): at.ActorRef[A] =
34 | actorRef[A](deployUntyped(d)(props, name))
35 |
36 | def deployAnonymous[A](d: D)(props: au.Props @@ A): at.ActorRef[A] =
37 | actorRef[A](deployUntypedAnonymous(d)(props))
38 |
39 | protected def deployUntyped(d: D)(props: au.Props, name: String): au.ActorRef
40 |
41 | protected def deployUntypedAnonymous(d: D)(props: au.Props): au.ActorRef
42 | }
43 |
44 | object ActorDeployer {
45 |
46 | def apply[A](implicit ev: ActorDeployer[A]): ActorDeployer[A] = ev
47 |
48 | implicit val untypedActorSystemDeployerInstance: ActorDeployer[au.ActorSystem] = new ActorDeployer[au.ActorSystem] {
49 | override def deployUntyped(d: au.ActorSystem)(props: au.Props, name: String): au.ActorRef =
50 | d.actorOf(props, name)
51 | override def deployUntypedAnonymous(d: au.ActorSystem)(props: au.Props): au.ActorRef =
52 | d.actorOf(props)
53 | }
54 |
55 | implicit val untypedActorContextDeployer: ActorDeployer[au.ActorContext] = new ActorDeployer[au.ActorContext] {
56 | override def deployUntyped(d: au.ActorContext)(props: au.Props, name: String): au.ActorRef =
57 | d.actorOf(props, name)
58 | override def deployUntypedAnonymous(d: au.ActorContext)(props: au.Props): au.ActorRef =
59 | d.actorOf(props)
60 | }
61 |
62 | implicit def typedActorContextDeployerInstance[A]: ActorDeployer[ActorContext[A]] = new ActorDeployer[ActorContext[A]] {
63 |
64 | override def deployUntyped(d: ActorContext[A])(props: au.Props, name: String): au.ActorRef =
65 | d.actorOf(props, name)
66 |
67 | override def deployUntypedAnonymous(d: ActorContext[A])(props: au.Props): au.ActorRef =
68 | d.actorOf(props)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/PersistenceSettings.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import com.typesafe.config.Config
21 |
22 | import akka.{ actor => au }
23 |
24 | final class PersistenceSettings private (private[this] val system: au.ActorSystem) extends au.Extension {
25 |
26 | val config: Config = system.settings.config
27 |
28 | val debug: Boolean = config.getBoolean("com.nokia.ntp.ct.persistence.debug")
29 |
30 | val autoDelete: Boolean = config.getBoolean("com.nokia.ntp.ct.persistence.auto-delete")
31 | }
32 |
33 | object PersistenceSettings extends au.ExtensionId[PersistenceSettings] with au.ExtensionIdProvider {
34 |
35 | override def createExtension(system: au.ExtendedActorSystem): PersistenceSettings =
36 | new PersistenceSettings(system)
37 |
38 | override def lookup(): akka.actor.ExtensionId[_ <: au.Extension] =
39 | PersistenceSettings
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/PersistentActor.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import akka.{ actor => au, typed => at }
21 |
22 | /**
23 | * A `Behavior`, which must be deployed as a `TypedPersistentActor`.
24 | * Use the provided `props` or `deployInto` methods for this. Instances of
25 | * this trait can be created with the `Persistent` factory method.
26 | *
27 | * @tparam A the message type
28 | * @tparam D the event type
29 | * @tparam S the state (and snapshot) type
30 | */
31 | sealed trait PersistentActor[A, D, S] extends SelfDeployingBehavior[A] {
32 |
33 | def onSignal(s: (S, PersistenceApi[A, D, S]) => PartialFunction[at.Signal, Proc[S]]): PersistentActor[A, D, S]
34 |
35 | private[persistence] def undefer(ctx: at.scaladsl.ActorContext[A]): PersistentActor.PersistentActorImpl[A, D, S]
36 |
37 | // it's safe, because we're contravariant
38 | @SuppressWarnings(Array(unsafeCast))
39 | def narrowTo[B <: A]: PersistentActor[B, D, S] =
40 | this.asInstanceOf[PersistentActor[B, D, S]]
41 | }
42 |
43 | object PersistentActor {
44 |
45 | def immutable[A, D, S](
46 | initialState: S,
47 | pid: at.scaladsl.ActorContext[A] => PersistenceId
48 | )(f: S => PersistenceApi[A, D, S] => A => Proc[S])(
49 | implicit
50 | m: Update[S, D]
51 | ): PersistentActor[A, D, S] = {
52 | withRecovery[A, D, S](initialState, pid, Recovery.fromUpdate(m))(f)
53 | }
54 |
55 | def withRecovery[A, D, S](
56 | initialState: S,
57 | pid: at.scaladsl.ActorContext[A] => PersistenceId,
58 | recovery: Recovery[A, D, S]
59 | )(f: S => PersistenceApi[A, D, S] => A => Proc[S]): PersistentActor[A, D, S] = {
60 | new PersistentActorImpl[A, D, S](
61 | _ => initialState,
62 | pid,
63 | recovery,
64 | (s, api, msg) => f(s)(api)(msg),
65 | (s, api) => PartialFunction.empty
66 | )
67 | }
68 |
69 | def deferred[A, D, S](
70 | factory: at.scaladsl.ActorContext[A] => PersistentActor[A, D, S]
71 | ): PersistentActor[A, D, S] = {
72 | new DeferredPersistentActorImpl[A, D, S](factory, None)
73 | }
74 |
75 | private class DeferredPersistentActorImpl[A, D, S](
76 | factory: at.scaladsl.ActorContext[A] => PersistentActor[A, D, S],
77 | signalHandler: Option[(S, PersistenceApi[A, D, S]) => PartialFunction[at.Signal, Proc[S]]]
78 | ) extends PersistentActor[A, D, S] {
79 |
80 | override def onSignal(s: (S, PersistenceApi[A, D, S]) => PartialFunction[at.Signal, Proc[S]]): PersistentActor[A, D, S] =
81 | new DeferredPersistentActorImpl(factory, Some(s))
82 |
83 | private[ct] override def undefer(ctx: at.scaladsl.ActorContext[A]): PersistentActorImpl[A, D, S] = {
84 | val b = factory(ctx).undefer(ctx)
85 | signalHandler match {
86 | case Some(sh) => b.onSignal(sh)
87 | case None => b
88 | }
89 | }
90 |
91 | protected[ct] def untypedProps: akka.actor.Props =
92 | au.Props(new TypedPersistentActor(this))
93 | }
94 |
95 | private[persistence] class PersistentActorImpl[A, D, S](
96 | initialState: at.scaladsl.ActorContext[A] => S,
97 | pid: at.scaladsl.ActorContext[A] => PersistenceId,
98 | rec: Recovery[A, D, S],
99 | message: (S, PersistenceApi[A, D, S], A) => Proc[S],
100 | signal: (S, PersistenceApi[A, D, S]) => PartialFunction[at.Signal, Proc[S]]
101 | ) extends at.ExtensibleBehavior[A] with PersistentActor[A, D, S] {
102 |
103 | private[persistence] override def undefer(ctx: at.scaladsl.ActorContext[A]): PersistentActorImpl[A, D, S] =
104 | this
105 |
106 | private[this] def api(ctx: at.scaladsl.ActorContext[A]) =
107 | new PersistenceApiImpl[A, D, S](ctx)
108 |
109 | def state(ctx: at.scaladsl.ActorContext[A]): S =
110 | initialState(ctx)
111 |
112 | val recovery: Recovery[A, D, S] =
113 | rec
114 |
115 | def persistenceId(ctx: at.scaladsl.ActorContext[A]): PersistenceId =
116 | pid(ctx)
117 |
118 | def withState(ctx: at.scaladsl.ActorContext[A], newState: S): PersistentActorImpl[A, D, S] =
119 | new PersistentActorImpl(_ => newState, pid, rec, message, signal)
120 |
121 | override def onSignal(s: (S, PersistenceApi[A, D, S]) => PartialFunction[at.Signal, Proc[S]]): PersistentActorImpl[A, D, S] =
122 | new PersistentActorImpl[A, D, S](initialState, pid, rec, message, s)
123 |
124 | def managementProc(ctx: at.scaladsl.ActorContext[A], sig: at.Signal): Proc[S] =
125 | signal(state(ctx), api(ctx)).applyOrElse[at.Signal, Proc[S]](sig, _ => ProcA.same[S])
126 |
127 | def messageProc(ctx: at.scaladsl.ActorContext[A], msg: A): Proc[S] =
128 | message(state(ctx), api(ctx), msg)
129 |
130 | override def receiveSignal(ctx: at.ActorContext[A], sig: at.Signal): at.Behavior[A] =
131 | PersistentActor.Wrap[A, D, S](managementProc(ctx.asScala, sig), this)
132 |
133 | override def receiveMessage(ctx: at.ActorContext[A], msg: A): at.Behavior[A] =
134 | PersistentActor.Wrap[A, D, S](messageProc(ctx.asScala, msg), this)
135 |
136 | protected[ct] override val untypedProps = {
137 | // TODO: bounded mailbox (?)
138 | au.Props(new TypedPersistentActor(this))
139 | }
140 | }
141 |
142 | private[this] final val warning = "maybe the persistent typed behavior was deployed as a regular actor?"
143 |
144 | private[persistence] final case class Wrap[A, D, S](proc: Proc[S], current: PersistentActorImpl[A, D, S])
145 | extends at.ExtensibleBehavior[A] {
146 |
147 | override def receiveSignal(ctx: at.ActorContext[A], msg: at.Signal): at.Behavior[A] =
148 | impossible(s"${this.getClass.getSimpleName}.receiveSignal called with $msg; $warning")
149 |
150 | override def receiveMessage(ctx: at.ActorContext[A], msg: A): at.Behavior[A] =
151 | impossible(s"${this.getClass.getSimpleName}.receiveMessage called with $msg; $warning")
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/Recovery.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import akka.typed.scaladsl.ActorContext
21 |
22 | /**
23 | * Recovery behavior definition of a persistent actor
24 | *
25 | * @param folder Computes the next state from the
26 | * current state and a persisted event.
27 | * @param failure Handler which will be invoked
28 | * in case of recovery failure (the actor
29 | * will be stopped after calling the callback).
30 | * @param completed Handler which will be invoked
31 | * at the end of recovery (can change the state
32 | * one last time before starting regular operation).
33 | */
34 | final case class Recovery[A, D, S](
35 | recovery: (S, D, ActorContext[A]) => S,
36 | failure: (S, Throwable, ActorContext[A]) => Unit = { (s: S, ex: Throwable, ctx: ActorContext[A]) => () },
37 | completed: (S, ActorContext[A]) => S = { (s: S, ctx: ActorContext[A]) => s }
38 | )
39 |
40 | object Recovery {
41 | def fromUpdate[A, D, S](implicit m: Update[S, D]): Recovery[A, D, S] =
42 | Recovery((s, e, _) => m.update(s, e))
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/SelfDeployingBehavior.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import shapeless.tag.@@
21 |
22 | import akka.{ actor => au, typed => at }
23 |
24 | /**
25 | * Behavior, which must be started in a specific way.
26 | * To make this easier, it provides a pre-configured
27 | * `Props` instance, as well as a `deployInto` method.
28 | */
29 | trait SelfDeployingBehavior[A] {
30 |
31 | private[this] val tagger = shapeless.tag[A]
32 |
33 | private[this] lazy val cachedProps =
34 | tagger(untypedProps)
35 |
36 | def props: au.Props @@ A =
37 | cachedProps
38 |
39 | def deployInto[C: ActorDeployer](ctx: C): at.ActorRef[A] =
40 | ActorDeployer[C].deployAnonymous(ctx)(props)
41 |
42 | def deployInto[C: ActorDeployer](ctx: C, name: String): at.ActorRef[A] =
43 | ActorDeployer[C].deploy(ctx)(props, name)
44 |
45 | protected[ct] def untypedProps: au.Props
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/Update.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | /**
21 | * Type class for abstracting over the
22 | * operation of updating the managed state
23 | * of a persistent actor with an event.
24 | *
25 | * And instance of this type class is
26 | * required to use PersistenceApi#apply.
27 | * For a few common types, instances are
28 | * provided in the `Update` companion object;
29 | * for all other types, the user can define
30 | * their own (e.g., in the companion object
31 | * of their `S` state type).
32 | */
33 | trait Update[S, E] {
34 |
35 | /**
36 | * Computes the next state
37 | * from the current state and
38 | * an event.
39 | */
40 | def update(state: S, event: E): S
41 | }
42 |
43 | object Update {
44 |
45 | def apply[S, E](implicit inst: Update[S, E]): Update[S, E] =
46 | inst
47 |
48 | def instance[S, E](f: (S, E) => S): Update[S, E] = new Update[S, E] {
49 | override def update(state: S, event: E): S =
50 | f(state, event)
51 | }
52 |
53 | implicit def managedUpdaterForMap[K, V]: Update[Map[K, V], (K, V)] =
54 | instance { (s, e) => s.updated(e._1, e._2) }
55 |
56 | implicit def managedUpdaterForList[A]: Update[List[A], A] =
57 | instance { (s, e) => e :: s }
58 |
59 | implicit def managedUpdaterForVector[A]: Update[Vector[A], A] =
60 | instance { (s, e) => s :+ e }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/package.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 |
19 | import akka.{ typed => at }
20 |
21 | import cats.free.Free
22 |
23 | /**
24 | * Persistence API for akka-typed actors
25 | */
26 | package object persistence {
27 |
28 | /** An actor ID, which should be stable between different incarnations */
29 | type PersistenceId = String
30 |
31 | /**
32 | * The description of a "persistence process".
33 | * For the possible commands, see `ProcOps`. Note, that
34 | * this is a monad, so commands can be chained with flatMap.
35 | */
36 | type Proc[A] = Free[ProcA, A]
37 |
38 | // Internal utilities:
39 |
40 | private[persistence] final val unsafeCast =
41 | "org.wartremover.warts.AsInstanceOf"
42 |
43 | private[persistence] final val unsafeVar =
44 | "org.wartremover.warts.Var"
45 |
46 | private[persistence] def impossible(msg: => String): Nothing =
47 | throw new IllegalStateException(msg)
48 |
49 | /** ActorRefs have a correct equals */
50 | private[persistence] implicit def actorRefEq[A]: cats.Eq[akka.typed.ActorRef[A]] =
51 | cats.Eq.fromUniversalEquals
52 |
53 | /** SnapshotMetadata has a correct equals */
54 | private[persistence] implicit val snapshotMetadataEq: cats.Eq[akka.persistence.SnapshotMetadata] =
55 | cats.Eq.fromUniversalEquals
56 |
57 | /** For creating typed ActorRef adapters */
58 | private[persistence] def actorRef[A](untyped: akka.actor.ActorRef): at.ActorRef[A] =
59 | at.scaladsl.adapter.actorRefAdapter(untyped)
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/persistenceApi.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import akka.{ persistence => ap, typed => at }
21 |
22 | import cats.free.Free
23 |
24 | /**
25 | * `Proc` primitives and combinators.
26 | * (See also `ProcOps`.)
27 | */
28 | sealed trait PersistenceApi[A, D, S] {
29 |
30 | /**
31 | * Persists the event `e`, and updates the managed
32 | * state after the persistence succeeds. Returns
33 | * the updated state.
34 | *
35 | * @param e The event to persist and use to change the state.
36 | * @param sync If true, it is guaranteed
37 | * that no other messages are handled
38 | * before the event is persisted.
39 | * @param m The type class instance to update the state with.
40 | */
41 | def apply(e: D, sync: Boolean = false)(implicit m: Update[S, D]): Proc[S] =
42 | persist(e, sync = sync).flatMap(s => change(m.update(s, e)))
43 |
44 | /**
45 | * A `Proc` which persists the specified event,
46 | * and returns the managed state AFTER the
47 | * persistence successfully completed.
48 | *
49 | * @param e The event to persist
50 | * @param sync If true, it is guaranteed
51 | * that no other messages are handled
52 | * before the event is persisted.
53 | *
54 | * @note This is rarely needed, see `apply`
55 | * for the common case.
56 | */
57 | def persist(e: D, sync: Boolean = false): Proc[S] =
58 | Free.liftF[ProcA, S](ProcA.Persist[D, S](e, async = !sync))
59 |
60 | /**
61 | * A `Proc` which takes a snapshot,
62 | * and returns the managed state AFTER the
63 | * snapshot successfully completed.
64 | */
65 | def snapshot: Proc[S] =
66 | Free.liftF[ProcA, S](ProcA.Snapshot[S]())
67 |
68 | /**
69 | * A `Proc` which changes the managed state to
70 | * the specified state immediately.
71 | *
72 | * @note This is rarely needed, see `apply`
73 | * for the common case.
74 | */
75 | def change(state: S): Proc[S] =
76 | Free.liftF[ProcA, S](ProcA.Change[S](state))
77 |
78 | /**
79 | * Returns the sequence number
80 | * of the last written (or replayed)
81 | * event.
82 | */
83 | def lastSequenceNr: Proc[Long] =
84 | Free.liftF[ProcA, Long](ProcA.SeqNr)
85 |
86 | /**
87 | * A `Proc` which changes the managed state to
88 | * the stopped state.
89 | */
90 | def stop: Proc[S] =
91 | Free.liftF[ProcA, S](ProcA.Stop[S]())
92 |
93 | /**
94 | * A `Proc` which returns the current
95 | * managed state.
96 | */
97 | def same: Proc[S] =
98 | Free.liftF[ProcA, S](ProcA.Same[S]())
99 |
100 | /**
101 | * A `Proc` which fails with the specified exception.
102 | */
103 | def fail[X](ex: ProcException): Proc[X] =
104 | ProcA.fail(ex)
105 |
106 | /**
107 | * A `Proc` which returns the specified value.
108 | */
109 | def pure[X](x: X): Proc[X] =
110 | ProcA.pure(x)
111 |
112 | /**
113 | * The context of the persistent actor.
114 | */
115 | def ctx: at.scaladsl.ActorContext[A]
116 | }
117 |
118 | private final class PersistenceApiImpl[A, D, S](override val ctx: at.scaladsl.ActorContext[A])
119 | extends PersistenceApi[A, D, S]
120 |
121 | /** Possible exceptions during a persistence process */
122 | sealed abstract class ProcException(cause: Throwable)
123 | extends RuntimeException(cause) {
124 | def cause: Throwable
125 | }
126 |
127 | /** Possible exceptions during persisting an event/snapshot */
128 | sealed abstract class PersistenceException(cause: Throwable, seqNr: Long)
129 | extends ProcException(cause) {
130 | final def sequenceNr: Long = seqNr
131 | }
132 |
133 | /** Failed to persist an event; the actor will be automatically stopped. */
134 | final case class PersistFailure(cause: Throwable, seqNr: Long)
135 | extends PersistenceException(cause, seqNr)
136 |
137 | /** The persistence plugin rejected to persist an event. */
138 | final case class PersistRejected(cause: Throwable, seqNr: Long)
139 | extends PersistenceException(cause, seqNr)
140 |
141 | /** Failed to save a snapshot. */
142 | final case class SnapshotFailure(cause: Throwable, metadata: ap.SnapshotMetadata)
143 | extends PersistenceException(cause, metadata.sequenceNr)
144 |
145 | /** Other wrapped exception */
146 | final case class UnexpectedException(cause: Throwable)
147 | extends ProcException(cause)
148 |
149 | // TODO: add a user definable ProcException subclass
150 |
151 | /** INTERNAL API: don't use directly, see `Proc` instead */
152 | sealed trait ProcA[A]
153 |
154 | /** INTERNAL API: don't use directly, see `Proc` instead */
155 | object ProcA {
156 |
157 | /**
158 | * Extension methods on `Proc`.
159 | */
160 | implicit class ProcOps[A](p: Proc[A]) {
161 |
162 | /** Error handling combinator (catch and reify) */
163 | def attempt: Proc[Either[ProcException, A]] =
164 | Free.liftF[ProcA, Either[ProcException, A]](ProcA.Attempt[A](p))
165 |
166 | /** Error handling combinator (catch) */
167 | def recover(f: PartialFunction[ProcException, A]): Proc[A] =
168 | recoverWith(f andThen ProcA.pure)
169 |
170 | /** Error handling combinator (catch with possible rethrow) */
171 | def recoverWith(f: PartialFunction[ProcException, Proc[A]]): Proc[A] = {
172 | attempt.flatMap {
173 | case Left(ex) => f.lift(ex) getOrElse ProcA.fail(ex)
174 | case Right(res) => ProcA.pure(res)
175 | }
176 | }
177 | }
178 |
179 | private[persistence] final case class Persist[D, S](data: D, async: Boolean) extends ProcA[S]
180 | private[persistence] final case class Snapshot[S]() extends ProcA[S]
181 | private[persistence] final case class Change[S](state: S) extends ProcA[S]
182 | private[persistence] final case object SeqNr extends ProcA[Long]
183 | private[persistence] final case class Same[S]() extends ProcA[S]
184 | private[persistence] final case class Stop[S]() extends ProcA[S]
185 | private[persistence] final case class Attempt[A](proc: Proc[A]) extends ProcA[Either[ProcException, A]]
186 | private[persistence] final case class Fail[A](ex: ProcException) extends ProcA[A]
187 |
188 | private[persistence] def same[X]: Proc[X] =
189 | Free.liftF[ProcA, X](Same[X]())
190 |
191 | private[persistence] def pure[X](x: X): Proc[X] =
192 | Free.pure(x)
193 |
194 | private[persistence] def fail[X](ex: ProcException): Proc[X] =
195 | Free.liftF[ProcA, X](ProcA.Fail[X](ex))
196 | }
197 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/testkit/TestInterpreter.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 | package testkit
20 |
21 | import scala.util.{ Failure, Success, Try }
22 | import scala.util.Random
23 |
24 | import akka.{ typed => at }
25 | import akka.typed.scaladsl.Actor
26 |
27 | import cats.{ ~>, Eq }
28 | import cats.data.StateT
29 | import cats.implicits._
30 |
31 | import PersistentActor.PersistentActorImpl
32 |
33 | /**
34 | * WIP testing framework for our persistence API.
35 | * The end goal is to be able to test persistent
36 | * actors without asynchrony or mocking persistence
37 | * plugins, ...
38 | *
39 | * @note The current implementation is incomplete.
40 | */
41 | @SuppressWarnings(Array(unsafeCast)) // NB: a ton of false positives
42 | abstract class TestInterpreter[M, D, S](
43 | name: String,
44 | initialBehavior: PersistentActor[M, D, S],
45 | sys: at.ActorSystem[Nothing]
46 | ) {
47 |
48 | sealed case class InterpState(
49 | name: String,
50 | store: Store,
51 | behavior: PersistentActorImpl[M, D, S],
52 | seqNr: Long,
53 | ctx: at.testkit.EffectfulActorContext[M]
54 | ) {
55 | def actorState: S = behavior.state(ctx)
56 | def changeState(newState: S): InterpState =
57 | this.copy(behavior = this.behavior.withState(ctx, newState))
58 | def update(event: D): InterpState =
59 | this.copy(store = this.store.update(event), seqNr = this.seqNr + 1L)
60 | def snap(snapshot: S): InterpState =
61 | this.copy(store = this.store.snap(snapshot))
62 | }
63 |
64 | sealed trait SpecState
65 | case object Stopped extends SpecState
66 | case class Error(ex: Throwable, st: InterpState) extends SpecState
67 |
68 | sealed trait Store {
69 | def update(event: D): Store
70 | def snap(snapshot: S): Store
71 | }
72 |
73 | object Store {
74 | val empty: Store = Journal(Nil)
75 | }
76 |
77 | case class Snapshot(snapshot: S) extends Store {
78 | def update(event: D): Store = JournalAndSnapshot(event :: Nil, snapshot)
79 | def snap(snapshot: S): Store = Snapshot(snapshot)
80 | }
81 |
82 | case class Journal(reversedEvents: List[D]) extends Store {
83 | def update(event: D): Store = Journal(event :: reversedEvents)
84 | def snap(snapshot: S): Store = Snapshot(snapshot)
85 | }
86 |
87 | case class JournalAndSnapshot(reversedEvents: List[D], snapshot: S) extends Store {
88 | def update(event: D): Store = JournalAndSnapshot(event :: reversedEvents, snapshot)
89 | def snap(snapshot: S): Store = Snapshot(snapshot)
90 | }
91 |
92 | type Xss[X] = Either[SpecState, X]
93 | type TestProc[A] = StateT[Xss, InterpState, A]
94 |
95 | val getInterpSt: TestProc[InterpState] =
96 | StateT.inspect[Xss, InterpState, InterpState](identity)
97 |
98 | val getActorSt: TestProc[S] =
99 | getInterpSt.map(_.actorState)
100 |
101 | val interpreter: ProcA ~> TestProc = new (ProcA ~> TestProc) {
102 | override def apply[X](proc: ProcA[X]): TestProc[X] = proc match {
103 | case p: ProcA.Persist[D, S] =>
104 | eventHook(p.data) match {
105 | case Success(ev) =>
106 | for {
107 | _ <- StateT.modify[Xss, InterpState](_.update(p.data))
108 | st <- getActorSt
109 | } yield st
110 | case Failure(ex) =>
111 | for {
112 | st <- getInterpSt
113 | ex <- StateT.lift[Xss, InterpState, S](Left(Error(ex, st)))
114 | } yield ex
115 | }
116 | case _: ProcA.Snapshot[S] =>
117 | for {
118 | st <- getActorSt
119 | _ <- StateT.modify[Xss, InterpState](_.snap(st))
120 | } yield st
121 | case p: ProcA.Change[S] =>
122 | for {
123 | _ <- StateT.modify[Xss, InterpState](_.changeState(p.state))
124 | } yield p.state
125 | case ProcA.SeqNr =>
126 | getInterpSt.map(_.seqNr)
127 | case _: ProcA.Same[S] =>
128 | getActorSt
129 | case _: ProcA.Stop[S] =>
130 | StateT.lift[Xss, InterpState, S](Left(Stopped))
131 | case att: ProcA.Attempt[a] =>
132 | att.proc.foldMap(interpreter).transformF[Xss, Either[ProcException, a]] {
133 | case x @ Left(Error(ex, st)) => ex match {
134 | case ex: TypedPersistentActor.ActorStop =>
135 | x.copy()
136 | case ex: ProcException =>
137 | Right((st, Left(ex)))
138 | case ex: Any =>
139 | Right((st, Left(UnexpectedException(ex))))
140 | }
141 | case x @ Left(Stopped) =>
142 | x.copy()
143 | case Right((st, x)) =>
144 | Right((st, Right(x)))
145 | }
146 | case f: ProcA.Fail[X] =>
147 | getInterpSt.flatMap { st =>
148 | StateT.lift[Xss, InterpState, X](Left(Error(f.ex, st)))
149 | }
150 | }
151 | }
152 |
153 | protected[this] def eventHook(ev: D): Try[D] =
154 | Success(ev)
155 |
156 | protected[this] def snapshotHook(s: S): Try[S] =
157 | Success(s)
158 |
159 | protected[this] def assert(b: Boolean, msg: String = ""): Try[Unit]
160 |
161 | protected[this] def fail(msg: String): Nothing
162 |
163 | val initialState = {
164 | val ctx = new at.testkit.EffectfulActorContext(name, Actor.deferred[M] { ctx => initialBehavior.undefer(ctx) }, _mailboxCapacity = 1000, _system = sys)
165 | ctx.currentBehavior match {
166 | case pbi: PersistentActorImpl[M, D, S] =>
167 | InterpState(name, Store.empty, pbi, 0L, ctx)
168 | case x: Any =>
169 | impossible(s"invalid undeferred persistent behavior: ${x}")
170 | }
171 | }
172 |
173 | def message(msg: M): TestProc[S] = for {
174 | st <- getInterpSt
175 | r <- Try(st.behavior.messageProc(st.ctx, msg)) match {
176 | case Success(p) => p.foldMap(interpreter)
177 | case Failure(ex) => StateT.lift[Xss, InterpState, S](Left(Error(ex, st)))
178 | }
179 | } yield r
180 |
181 | def signal(sig: at.Signal): TestProc[S] = for {
182 | st <- getInterpSt
183 | r <- Try(st.behavior.managementProc(st.ctx, sig)) match {
184 | case Success(p) => p.foldMap(interpreter)
185 | case Failure(ex) => StateT.lift[Xss, InterpState, S](Left(Error(ex, st)))
186 | }
187 | } yield r
188 |
189 | def ask[A](msg: at.ActorRef[A] => M): TestProc[A] = {
190 | val tmpName = Random.alphanumeric.take(10).mkString("")
191 | val inb = at.testkit.Inbox[A](tmpName)
192 | val mesg = msg(inb.ref)
193 | for {
194 | st <- message(mesg)
195 | } yield inb.receiveMsg()
196 | }
197 |
198 | def expect[A](msg: at.ActorRef[A] => M, expectedAnswer: A)(implicit A: Eq[A]): TestProc[Unit] = for {
199 | a <- ask(msg)
200 | _ <- assertEq(a, expectedAnswer)
201 | } yield ()
202 |
203 | def assertEq[A](x: A, y: A)(implicit A: Eq[A]): TestProc[Unit] = for {
204 | st <- getInterpSt
205 | _ <- assert(x === y, s"${x} was not equal to ${y}") match {
206 | case Success(()) => StateT.pure[Xss, InterpState, Unit](())
207 | case Failure(ex) => StateT.lift[Xss, InterpState, S](Left(Error(ex, st)))
208 | }
209 | } yield ()
210 |
211 | private def assertFlag(b: Boolean, msg: String): TestProc[Unit] = for {
212 | st <- getInterpSt
213 | _ <- if (b) {
214 | StateT.pure[Xss, InterpState, Unit](())
215 | } else {
216 | StateT.lift[Xss, InterpState, S](Left(Error(new AssertionError, st)))
217 | }
218 | } yield ()
219 |
220 | def expectSt[A](extract: S => A, expected: A)(implicit A: Eq[A]): TestProc[Unit] = for {
221 | st <- getActorSt
222 | _ <- assertEq(extract(st), expected)
223 | } yield ()
224 |
225 | def expectStore(p: PartialFunction[Store, Unit]): TestProc[Unit] = for {
226 | st <- getInterpSt
227 | _ <- assertFlag(p.isDefinedAt(st.store), "no match")
228 | } yield ()
229 |
230 | def expectStop: TestProc[Unit] = {
231 | getInterpSt.transformF[Xss, Unit] { x: Xss[(InterpState, InterpState)] =>
232 | x match {
233 | case Left(Stopped) =>
234 | Left[SpecState, (InterpState, Unit)](Stopped)
235 | case Left(Error(ex, st)) =>
236 | Left[SpecState, (InterpState, Unit)](Error(new AssertionError(s"expected stop, got exception: ${ex}"), st))
237 | case Right((st, _)) =>
238 | Left[SpecState, (InterpState, Unit)](Error(new AssertionError("expected stop"), st))
239 | }
240 | }
241 | }
242 |
243 | def run[A](p: TestProc[A]): Xss[A] =
244 | p.runA(initialState)
245 |
246 | def check[A](p: TestProc[A]): Unit = {
247 | run(p) match {
248 | case Left(Stopped) =>
249 | case Left(Error(ex, _)) => fail(ex.getMessage)
250 | case Right(st) =>
251 | }
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/main/scala/com/nokia/ntp/ct/persistence/typedPersistentActor.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import scala.util.control.ControlThrowable
21 |
22 | import akka.{ actor => au, persistence => ap, typed => at }
23 | import akka.typed.scaladsl.Actor
24 |
25 | import cats.~>
26 | import cats.effect.IO
27 |
28 | /**
29 | * Combined `PersistentActor` and `ActorAdapter`.
30 | */
31 | private abstract class PersistentActorAdapter[A](initialBehavior: at.Behavior[A])
32 | extends PublicActorAdapter[A](initialBehavior)
33 | with akka.persistence.PersistentActor
34 | with au.ActorLogging {
35 |
36 | protected[this] val (debug, autoDelete) = {
37 | val settings = PersistenceSettings(this.context.system)
38 | (settings.debug, settings.autoDelete)
39 | }
40 |
41 | override def receive: PartialFunction[Any, Unit] =
42 | super[PersistentActor].receive
43 |
44 | override def receiveCommand: PartialFunction[Any, Unit] =
45 | super[PublicActorAdapter].receive
46 |
47 | override def receiveRecover: PartialFunction[Any, Unit]
48 |
49 | protected[this] def debug(msg: => String): Unit = {
50 | if (this.debug) {
51 | log.debug(msg)
52 | }
53 | }
54 | }
55 |
56 | /** Special actor adapter able to run a `PersistentBehavior`. */
57 | private final class TypedPersistentActor[A, D, S](
58 | b: PersistentActor[A, D, S]
59 | ) extends PersistentActorAdapter[A](Actor.deferred { ctx => b.undefer(ctx) }) { actor =>
60 |
61 | import TypedPersistentActor._
62 |
63 | private[this] lazy val cachedPid =
64 | this.currentBehavior.persistenceId(this.ctx)
65 |
66 | private[this] lazy val recoveryHandler: Recovery[A, D, S] =
67 | this.currentBehavior.recovery
68 |
69 | override def persistenceId: String =
70 | cachedPid
71 |
72 | override def receiveCommand: PartialFunction[Any, Unit] = {
73 | case cmd: Any if this.handleCommand.isDefinedAt(cmd) =>
74 | this.handleCommand(cmd)
75 | fixupBehavior()
76 | case sm @ (au.Terminated(_) | au.ReceiveTimeout) =>
77 | super.receiveCommand(sm)
78 | fixupBehavior()
79 | case x: Any =>
80 | super.receiveCommand(x)
81 | fixupBehavior()
82 | }
83 |
84 | override def receiveRecover: PartialFunction[Any, Unit] = {
85 | case ap.RecoveryCompleted =>
86 | log.debug("Recovery completed")
87 | handleRecovery(RecoveryCompleted)
88 | case ap.SnapshotOffer(m, s) =>
89 | debug(s"Snapshot offer: $m")
90 | handleRecovery(SnapshotOffer(s.asInstanceOf[S], m))
91 | case d: Any =>
92 | debug(s"Recovery offer: $d")
93 | handleRecovery(RecoveryOffer(d.asInstanceOf[D]))
94 | }
95 |
96 | private[this] def handleCommand: PartialFunction[Any, Unit] = {
97 | case ap.SaveSnapshotSuccess(m) =>
98 | handleSnapshot(m, err = None)
99 | case ap.SaveSnapshotFailure(m, ex) =>
100 | handleSnapshot(m, err = Some(ex))
101 | case ap.DeleteSnapshotsSuccess(crit) =>
102 | debug(s"Successfully deleted snapshots corresponding to ${crit}")
103 | case ap.DeleteSnapshotsFailure(crit, ex) =>
104 | log.warning(s"Failed to delete snapshots (${crit}, due to ${ex.getClass}: ${ex.getMessage}), ignoring")
105 | case ap.DeleteMessagesSuccess(seqNr) =>
106 | debug(s"Successfully deleted events up to (and including) seqNr ${seqNr}")
107 | case ap.DeleteMessagesFailure(ex, seqNr) =>
108 | log.warning(s"Failed to delete events up to (and including) seqNr ${seqNr} (due to ${ex.getClass}: ${ex.getMessage}), ignoring")
109 | }
110 |
111 | private[this] def handleRecovery(rev: RecoveryEvent[D, S]): Unit = rev match {
112 | case RecoveryOffer(d) =>
113 | this.changeState(recoveryHandler.recovery(currentState, d, this.ctx))
114 | case SnapshotOffer(s, _) =>
115 | this.changeState(s)
116 | case RecoveryCompleted =>
117 | this.changeState(recoveryHandler.completed(currentState, this.ctx))
118 | case RecoveryFailure(ex) =>
119 | recoveryHandler.failure(currentState, ex, this.ctx)
120 | }
121 |
122 | private[this] def handleSnapshot(m: ap.SnapshotMetadata, err: Option[Throwable]): Unit = {
123 | debug("Taking snapshot ended, retrieving callback ...")
124 | val callback = this.snapshotTaskCallbacks.removeFirst()
125 | debug(s"Retrieved snapshot callback: ${callback}")
126 | err.fold {
127 | log.debug(s"Snapshot success (${m})")
128 | if (autoDelete) {
129 | // We have a correct snapshot at `m.sequenceNr`,
130 | // so we can delete every older snapshot, and every
131 | // event which is not newer than that (we wouldn't
132 | // encounter these during recovery anyway, since
133 | // replaying starts from the newest snapshot).
134 | debug(
135 | s"Successful snapshot at sequenceNr ${m.sequenceNr}, " +
136 | "deleting older snapshots and not newer events (auto-delete is true)"
137 | )
138 | deleteSnapshots(ap.SnapshotSelectionCriteria(maxSequenceNr = m.sequenceNr - 1))
139 | deleteMessages(m.sequenceNr)
140 | }
141 |
142 | debug("Calling callback after successful snapshot")
143 | callback(Right(()))
144 | } { ex =>
145 | log.warning(s"Snapshot failure of ${m} with ${ex.getClass}: ${ex.getMessage}")
146 | callback(Left(SnapshotFailure(ex, m)))
147 | }
148 | }
149 |
150 | private[this] def changeBehavior(next: at.Behavior[A]): Unit = {
151 | this.behavior = next
152 | this.fixupBehavior()
153 | }
154 |
155 | // TODO: clean this up (we're mutating left and right)
156 | private[this] def fixupBehavior(): Unit = {
157 | if (at.Behavior.isAlive(this.behavior)) {
158 | this.behavior = this.canonicalize(this.behavior)
159 | }
160 | if (!at.Behavior.isAlive(this.behavior)) {
161 | this.context.stop(this.self)
162 | }
163 | }
164 |
165 | override def onRecoveryFailure(cause: Throwable, event: Option[Any]): Unit = {
166 | super.onRecoveryFailure(cause, event)
167 | handleRecovery(RecoveryFailure(cause))
168 | }
169 |
170 | // FIXME: supervisorStrategy???
171 |
172 | override def preStart(): Unit = {
173 | super.preStart()
174 | fixupBehavior()
175 |
176 | // Force initializing recovery; make sure behavior is undeferred correctly:
177 | val _ = this.currentBehavior
178 | }
179 |
180 | override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
181 | fixupBehavior()
182 | super.preRestart(reason, message)
183 | fixupBehavior()
184 | }
185 |
186 | override def postRestart(reason: Throwable): Unit = {
187 | fixupBehavior()
188 | super.postRestart(reason)
189 | fixupBehavior()
190 | }
191 |
192 | override def postStop(): Unit = {
193 | fixupBehavior()
194 | super.postStop()
195 | fixupBehavior()
196 | }
197 |
198 | private[this] def canonicalize(next: at.Behavior[A]): at.Behavior[A] = {
199 | at.Behavior.canonicalize(next, this.behavior, this.ctx) match {
200 | case w: PersistentActor.Wrap[A, D, S] =>
201 | // It's a wrapped Proc, we'll have to execute it.
202 | // We'll handle both the intermediate (if any),
203 | // and the final result with this:
204 | def handleResult(r: Either[Throwable, S], previous: at.Behavior[A]): Unit = r match {
205 | case Left(_: ActorStop) =>
206 | log.debug(s"Stop requested, stopping the actor")
207 | this.behavior = Actor.stopped(previous)
208 | // if called asynchronously, the above
209 | // is not enough, we must call stop:
210 | this.context.stop(this.self)
211 | case Left(_: PersistFailure) =>
212 | // ignore, the actor will stop anyway
213 | log.error(s"Persist failure, the actor will be stopped")
214 | case Left(ex) =>
215 | log.error(ex, "Interpretation completed with an error")
216 | throw ex
217 | case Right(nextState) =>
218 | debug(s"Changing state to $nextState")
219 | this.changeState(nextState)
220 | }
221 |
222 | debug(s"Starting to interpret ${w.proc}")
223 | val task = interpret(w.proc)
224 | task.unsafeRunAsync { r =>
225 | handleResult(r, w.current)
226 | }
227 |
228 | // If it's not really async, handleResult
229 | // was already called, and already set the
230 | // correct behavior. Otherwise, we don't yet
231 | // have the next behavior, so we remain at
232 | // the current one.
233 | if (at.Behavior.isAlive(this.behavior)) {
234 | this.currentBehavior
235 | } else {
236 | // we already stopped
237 | this.behavior
238 | }
239 | case x: Any =>
240 | x
241 | }
242 | }
243 |
244 | private[this] def changeState(newState: S): Unit = {
245 | val next = this.currentBehavior.withState(this.ctx, newState)
246 | this.changeBehavior(next)
247 | }
248 |
249 | private[this] def currentState: S =
250 | this.currentBehavior.state(this.ctx)
251 |
252 | private[this] def currentBehavior: PersistentActor.PersistentActorImpl[A, D, S] = {
253 | this.behavior = at.Behavior.undefer(this.behavior, this.ctx)
254 | this.behavior match {
255 | case w: PersistentActor.Wrap[A, D, S] =>
256 | w.current
257 | case pb: PersistentActor[A, D, S] =>
258 | pb match {
259 | case pbi: PersistentActor.PersistentActorImpl[A, D, S] =>
260 | pbi
261 | // NB: it cannot be a DeferredPersistentBehavior, since it's an at.Behavior
262 | }
263 | case b: Any if !at.Behavior.isAlive(b) =>
264 | impossible("the actor already stopped")
265 | case x: Any =>
266 | impossible(s"PersistentBehavior transitioned into ${x.getClass.getName}")
267 | }
268 | }
269 |
270 | private[this] def interpret[X](proc: Proc[X]): IO[X] =
271 | proc.foldMap(interpreter)
272 |
273 | private[this] val persistTaskCallbacks: java.util.Deque[Either[ProcException, D] => Unit] =
274 | new java.util.LinkedList
275 |
276 | private[this] val snapshotTaskCallbacks: java.util.Deque[Either[SnapshotFailure, Unit] => Unit] =
277 | new java.util.LinkedList
278 |
279 | private[this] val interpreter: (ProcA ~> IO) = new (ProcA ~> IO) {
280 | override def apply[X](proc: ProcA[X]): IO[X] = proc match {
281 | case p: ProcA.Persist[D, S] =>
282 | val persist = IO.async[D] { cb =>
283 | if (actor.recoveryFinished) {
284 | actor.persistTaskCallbacks.addLast(cb)
285 | debug(s"Calling .persist${if (!p.async) "Sync" else ""} of data: ${p.data.getClass.getName}")
286 | val callback: (D => Unit) = { d =>
287 | debug(s"Persisting data: ${d.getClass.getName} completed")
288 | val popped = actor.persistTaskCallbacks.removeFirst()
289 | assert(popped eq cb)
290 | cb(Right(d))
291 | }
292 | if (p.async) {
293 | actor.persistAsync(p.data)(callback)
294 | } else {
295 | actor.persist(p.data)(callback)
296 | }
297 | } else {
298 | cb(Left(new IllegalStateException("Impossible: persist attempt during recovery")))
299 | }
300 | }
301 | persist.map { _ => actor.currentState }
302 | case s: ProcA.Snapshot[S] =>
303 | val snap = IO.async[Unit] { (cb: Either[Throwable, Unit] => Unit) =>
304 | debug(s"Taking snapshot ...")
305 | actor.saveSnapshot(actor.currentState)
306 | actor.snapshotTaskCallbacks.addLast(cb)
307 | }
308 | snap.map { _ => actor.currentState }
309 | case ch: ProcA.Change[S] =>
310 | IO {
311 | debug(s"Executing requested state change to ${ch.state}")
312 | actor.changeState(ch.state)
313 | ch.state
314 | }
315 | case ProcA.SeqNr =>
316 | IO(actor.lastSequenceNr)
317 | case _: ProcA.Same[S] =>
318 | IO(actor.currentBehavior.state(actor.ctx))
319 | case ProcA.Stop() =>
320 | IO.raiseError(new ActorStop)
321 | case ProcA.Attempt(p) =>
322 | val inner = interpret(p)
323 | inner.attempt.flatMap {
324 | case Left(ex: ActorStop) => IO.raiseError(ex)
325 | case Left(ex: ProcException) => IO.pure(Left(ex))
326 | case Left(ex) => IO.pure(Left(UnexpectedException(ex)))
327 | case Right(res) => IO.pure(Right(res))
328 | }
329 | case ProcA.Fail(ex) =>
330 | IO.raiseError(ex)
331 | }
332 | }
333 |
334 | override def onPersistFailure(cause: Throwable, event: Any, seqNr: Long): Unit = {
335 | super.onPersistFailure(cause, event, seqNr)
336 | persistError(PersistFailure(cause, seqNr))
337 | }
338 |
339 | override def onPersistRejected(cause: Throwable, event: Any, seqNr: Long): Unit = {
340 | super.onPersistRejected(cause, event, seqNr)
341 | persistError(PersistRejected(cause, seqNr))
342 | // FIXME: what should happen, if a PersistRejectedException
343 | // is NOT handled? Stop the actor? Ignore? Log a warning?
344 | }
345 |
346 | private[this] def persistError(ex: ProcException): Unit = {
347 | val cb = this.persistTaskCallbacks.removeFirst()
348 | debug(s"PersistError, calling callback")
349 | cb(Left(ex))
350 | }
351 | }
352 |
353 | private object TypedPersistentActor {
354 |
355 | /** Internal exception for signaling a requested stop */
356 | final class ActorStop extends ControlThrowable
357 |
358 | /** Internal data type for representing events related to recovery */
359 | private sealed trait RecoveryEvent[+D, +S]
360 |
361 | /** A previously persisted event is offered for recovery */
362 | private final case class RecoveryOffer[D](data: D) extends RecoveryEvent[D, Nothing]
363 |
364 | /** A previously persisted snapshot is offered for restoration */
365 | private final case class SnapshotOffer[S](snap: S, meta: ap.SnapshotMetadata) extends RecoveryEvent[Nothing, S]
366 |
367 | /** Recovery is done, no more `RecoveryEvent`s will happen */
368 | private final case object RecoveryCompleted extends RecoveryEvent[Nothing, Nothing]
369 |
370 | /**
371 | * An error was encountered while trying to recover the persisted
372 | * state of the actor. The actor will be automatically stopped after this.
373 | */
374 | private final case class RecoveryFailure(cause: Throwable) extends RecoveryEvent[Nothing, Nothing]
375 | }
376 |
--------------------------------------------------------------------------------
/src/test/resources/application.conf:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Nokia Solutions and Networks Oy
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | akka {
16 |
17 | loglevel = DEBUG
18 |
19 | actor {
20 | guardian-supervisor-strategy = "akka.actor.StoppingSupervisorStrategy"
21 | debug.lifecycle = on
22 | }
23 |
24 | persistence {
25 | journal.plugin = "akka.persistence.journal.inmem"
26 | snapshot-store.plugin = "com.nokia.ntp.ct.persistence.test.in-memory-snapshot-store"
27 | }
28 | }
29 |
30 | com.nokia.ntp.ct.persistence.test {
31 |
32 | dummy-journal {
33 | class = "com.nokia.ntp.ct.persistence.AbstractPersistenceSpec$FailingJournalPlugin"
34 | plugin-dispatcher = "akka.actor.default-dispatcher"
35 | persist-action = "ok"
36 | recovery-action = "ok"
37 | }
38 |
39 | dummy-snapshot {
40 | class = "com.nokia.ntp.ct.persistence.AbstractPersistenceSpec$FailingSnapshotPlugin"
41 | plugin-dispatcher = "akka.actor.default-dispatcher"
42 | snapshot-action = "ok"
43 | }
44 |
45 | in-memory-snapshot-store {
46 | class = "com.nokia.ntp.ct.persistence.MockSnapshotStore"
47 | plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
48 | stream-dispatcher = "akka.persistence.dispatchers.default-stream-dispatcher"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/test/scala/com/nokia/ntp/ct/persistence/AbstractPersistenceSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import java.util.UUID
21 |
22 | import scala.collection.{ immutable, mutable }
23 | import scala.concurrent.Future
24 | import scala.concurrent.Promise
25 | import scala.concurrent.duration._
26 | import scala.util.{ Failure, Success, Try }
27 | import scala.util.DynamicVariable
28 |
29 | import org.scalatest.BeforeAndAfterAll
30 | import org.scalatest.FlatSpecLike
31 | import org.scalatest.Matchers
32 | import org.scalatest.NonImplicitAssertions
33 | import org.scalatest.concurrent.ScalaFutures
34 |
35 | import com.typesafe.config.Config
36 | import com.typesafe.config.ConfigValueFactory
37 |
38 | import akka.{ actor => au, persistence => ap, typed => at }
39 | import akka.typed.scaladsl.Actor
40 | import akka.typed.scaladsl.AskPattern._
41 | import akka.typed.scaladsl.adapter._
42 | import akka.Done
43 | import akka.event.LoggingAdapter
44 | import akka.testkit.TestKit
45 | import akka.util.Timeout
46 |
47 | import cats.implicits._
48 |
49 | abstract class AbstractPersistenceSpec
50 | extends TestKit(au.ActorSystem(AbstractPersistenceSpec.randomSysName()))
51 | with FlatSpecLike
52 | with BeforeAndAfterAll
53 | with ScalaFutures
54 | with Matchers
55 | with NonImplicitAssertions {
56 |
57 | import AbstractPersistenceSpec._
58 |
59 | protected val dilationFactor: DynamicVariable[Double] = new DynamicVariable(5.0)
60 |
61 | def withDilatedTimeout[A](factor: Double)(block: => A): A =
62 | dilationFactor.withValue(factor)(block)
63 |
64 | implicit def timeout: Timeout =
65 | Timeout((5 * dilationFactor.value).seconds)
66 |
67 | implicit override def patienceConfig: PatienceConfig =
68 | PatienceConfig(timeout = super.patienceConfig.timeout.scaledBy(dilationFactor.value))
69 |
70 | implicit def defaultScheduler(implicit sys: akka.actor.ActorSystem): akka.actor.Scheduler =
71 | sys.scheduler
72 |
73 | private[this] val systemsToStop: mutable.ListBuffer[au.ActorSystem] =
74 | new mutable.ListBuffer
75 |
76 | protected def config: Config =
77 | this.system.settings.config
78 |
79 | def startWithDummyJournal(
80 | persistAction: PersistAction,
81 | recoverAction: RecoverAction,
82 | snapshotAction: SnapshotAction = No
83 | ): au.ActorSystem = {
84 | val cfg = config
85 | .withValue("akka.persistence.journal.plugin", ConfigValueFactory.fromAnyRef("com.nokia.ntp.ct.persistence.test.dummy-journal"))
86 | .withValue("com.nokia.ntp.ct.persistence.test.dummy-journal.persist-action", ConfigValueFactory.fromAnyRef(persistAction.toString))
87 | .withValue("com.nokia.ntp.ct.persistence.test.dummy-journal.recovery-action", ConfigValueFactory.fromAnyRef(recoverAction.toString))
88 | val finalConfig = if (snapshotAction eq No) {
89 | cfg
90 | } else {
91 | cfg
92 | .withValue("akka.persistence.snapshot-store.plugin", ConfigValueFactory.fromAnyRef("com.nokia.ntp.ct.persistence.test.dummy-snapshot"))
93 | .withValue("com.nokia.ntp.ct.persistence.test.dummy-snapshot.snapshot-action", ConfigValueFactory.fromAnyRef(snapshotAction.toString))
94 | }
95 | this.startAdditionalSystem(Some(finalConfig), true, wait = 100.millis)
96 | }
97 |
98 | def startAdditionalSystem(config: Option[Config] = None, newName: Boolean = false, wait: FiniteDuration = 1.second): au.ActorSystem = {
99 | val cfg = config.getOrElse(this.config)
100 | val sys = au.ActorSystem(
101 | if (newName) AbstractPersistenceSpec.randomSysName() else this.system.name,
102 | config = Some(cfg.withValue("akka.remote.netty.tcp.port", ConfigValueFactory.fromAnyRef(0)))
103 | )
104 | stopAfterAll(sys)
105 | Thread.sleep(wait.toMillis)
106 | sys
107 | }
108 |
109 | def stopAfterAll(sys: au.ActorSystem): Unit = {
110 | this.systemsToStop += sys
111 | }
112 |
113 | override def afterAll(): Unit = {
114 | super.afterAll()
115 | for (sys <- this.systemsToStop) {
116 | this.shutdown(sys, verifySystemShutdown = true)
117 | }
118 | }
119 |
120 | def ask(ref: at.ActorRef[Mes]): Int =
121 | askAsync(ref).futureValue
122 |
123 | def askAsync(ref: at.ActorRef[Mes]): Future[Int] =
124 | ref.?[Int](GetInt(_))
125 |
126 | def awaitTermination(ref: at.ActorRef[_])(implicit system: au.ActorSystem): Future[Unit] = {
127 | val p = Promise[Unit]()
128 | val wd = system.spawnAnonymous[Nothing](watchDog(ref, p))
129 | p.future
130 | }
131 |
132 | private def watchDog(ref: at.ActorRef[Nothing], p: Promise[Unit]): at.Behavior[Nothing] = {
133 | Actor.deferred[Done.type] { ctx =>
134 | ctx.watch[Nothing](ref)
135 | Actor.immutable[Done.type] { (ctx, msg) =>
136 | p.success(())
137 | Actor.stopped
138 | }.onSignal {
139 | case (ctx, at.Terminated(r)) =>
140 | if (ref === r) {
141 | // OK, it died, but we add some more
142 | // delay, because if the caller wants
143 | // to immediately recreate an actor
144 | // with the same name, that could fail.
145 | ctx.schedule(500.millis, ctx.self, Done)
146 | Actor.same
147 | } else {
148 | p.failure(new Exception(s"Unexpected termination message from ${r}"))
149 | Actor.stopped
150 | }
151 | }
152 | }.narrow
153 | }
154 |
155 | /**
156 | * Tries to get the second youngest snapshot of a persistent actor.
157 | *
158 | * @param persistenceId The persistence ID for which to get the snapshot.
159 | */
160 | def getPreviousSnapshot[A](persistenceId: String)(implicit system: au.ActorSystem): Future[A] = {
161 | import system.dispatcher
162 | for {
163 | snr <- getLastSnapshotSeqNr(persistenceId)
164 | ss <- getOldSnapshot[A](persistenceId, lessThanSeqNr = snr)
165 | } yield ss.a
166 | }
167 |
168 | private def getOldSnapshot[A](
169 | persistenceId: String,
170 | lessThanSeqNr: Long
171 | )(implicit system: au.ActorSystem): Future[SnapshotProbe.SnapshotInfo[A]] = {
172 | import SnapshotProbe._
173 | import system.dispatcher
174 | val p = Promise[SnapshotInfo[A]]()
175 | val ref = actorRef[Unit](system.actorOf(au.Props(new SnapshotProbe[A](
176 | persistenceId,
177 | lessThanSeqNr,
178 | p
179 | ))))
180 | ref ! (()) // stops the actor
181 |
182 | for {
183 | _ <- awaitTermination(ref)
184 | si <- p.future
185 | } yield si
186 | }
187 |
188 | private def getLastSnapshotSeqNr(persistenceId: String)(implicit system: au.ActorSystem): Future[Long] = {
189 | import system.dispatcher
190 | getOldSnapshot[Any](persistenceId, lessThanSeqNr = Long.MaxValue).map(_.m.sequenceNr)
191 | }
192 | }
193 |
194 | object AbstractPersistenceSpec {
195 |
196 | private[persistence] implicit class ContextOps[A](ctx: at.scaladsl.ActorContext[A]) {
197 | def log: LoggingAdapter =
198 | ctx.system.log
199 | }
200 |
201 | sealed trait Mes
202 | final case class GetInt(replyTo: at.ActorRef[Int]) extends Mes
203 | final case class SetInt(i: Int, ackTo: at.ActorRef[Int]) extends Mes
204 | final case object Stop extends Mes
205 |
206 | class MyException(msg: String) extends Exception(msg)
207 |
208 | sealed trait PP
209 | final case class N(i: Int) extends PP
210 | final case class AreYouAlive(replyTo: at.ActorRef[Boolean]) extends PP
211 |
212 | sealed trait PersistAction
213 | sealed trait RecoverAction
214 | sealed trait SnapshotAction
215 | case object Ok extends RecoverAction with PersistAction with SnapshotAction
216 | case object Fail extends RecoverAction with PersistAction with SnapshotAction
217 | case object No extends RecoverAction with SnapshotAction
218 | case object Reject extends PersistAction
219 |
220 | class FailingJournalPlugin(config: Config) extends akka.persistence.journal.AsyncWriteJournal {
221 |
222 | val persistAct: PersistAction = config.getString("persist-action").toLowerCase match {
223 | case "ok" => Ok
224 | case "fail" => Fail
225 | case "reject" => Reject
226 | case x: Any => throw new IllegalArgumentException(x)
227 | }
228 |
229 | val recoverAct: RecoverAction = config.getString("recovery-action").toLowerCase match {
230 | case "ok" => Ok
231 | case "fail" => Fail
232 | case "no" => No
233 | case x: Any => throw new IllegalArgumentException(x)
234 | }
235 |
236 | def asyncDeleteMessagesTo(persistenceId: String, toSequenceNr: Long): Future[Unit] =
237 | Future.successful(())
238 |
239 | def asyncReadHighestSequenceNr(persistenceId: String, fromSequenceNr: Long): Future[Long] = {
240 | Future.successful(if (recoverAct eq No) 0L else 1L)
241 | }
242 |
243 | def asyncReplayMessages(
244 | persistenceId: String,
245 | fromSequenceNr: Long,
246 | toSequenceNr: Long,
247 | max: Long
248 | )(recoveryCallback: akka.persistence.PersistentRepr => Unit): Future[Unit] = {
249 | recoverAct match {
250 | case No =>
251 | throw new IllegalStateException
252 | case Ok =>
253 | recoveryCallback(akka.persistence.PersistentRepr((), 1))
254 | Future.successful(())
255 | case Fail =>
256 | Future.failed(new MyException("recovery"))
257 | }
258 | }
259 |
260 | def asyncWriteMessages(messages: immutable.Seq[akka.persistence.AtomicWrite]): Future[immutable.Seq[Try[Unit]]] = {
261 | persistAct match {
262 | case Ok =>
263 | Future.successful(messages.map { _ => Success(()) })
264 | case Reject =>
265 | Future.successful(messages.map { _ => Failure(new MyException("persist")) })
266 | case Fail =>
267 | Future.failed(new MyException("persist"))
268 | }
269 | }
270 | }
271 |
272 | class FailingSnapshotPlugin(config: Config) extends akka.persistence.snapshot.SnapshotStore {
273 |
274 | val snapshotAct: SnapshotAction = config.getString("snapshot-action").toLowerCase match {
275 | case "ok" => Ok
276 | case "fail" => Fail
277 | case x: Any => throw new IllegalArgumentException(x)
278 | }
279 |
280 | override def loadAsync(persistenceId: String, criteria: ap.SnapshotSelectionCriteria): Future[Option[ap.SelectedSnapshot]] = {
281 | Future.successful(None)
282 | }
283 |
284 | override def saveAsync(metadata: ap.SnapshotMetadata, snapshot: Any): Future[Unit] = {
285 | snapshotAct match {
286 | case Ok | No => Future.successful(())
287 | case Fail => Future.failed(new MyException("snap"))
288 | }
289 | }
290 |
291 | override def deleteAsync(metadata: ap.SnapshotMetadata): Future[Unit] =
292 | Future.successful(())
293 |
294 | override def deleteAsync(persistenceId: String, criteria: ap.SnapshotSelectionCriteria): Future[Unit] =
295 | Future.successful(())
296 | }
297 |
298 | def randomSysName(): String =
299 | s"testSystem-${UUID.randomUUID()}"
300 | }
301 |
--------------------------------------------------------------------------------
/src/test/scala/com/nokia/ntp/ct/persistence/DeploymentSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import akka.{ typed => at }
21 | import at.scaladsl.AskPattern._
22 |
23 | class DeploymentSpec extends AbstractPersistenceSpec {
24 |
25 | import DeploymentSpec._
26 |
27 | "SelfDeployingBehavior" should "be able to start in an (untyped) ActorSystem" in {
28 | val name = "test1"
29 | val ref = child(name).deployInto(this.system, name)
30 | ref.?[Int](Num(42, _)).futureValue should === (43)
31 | ref.?[Int](Num(42, _)).futureValue should === (44)
32 | ref ! Stop
33 | awaitTermination(ref).futureValue
34 |
35 | val ref2 = child(name).deployInto(this.system, name)
36 | ref2.?[Int](Num(5, _)).futureValue should === (8)
37 | ref2.?[Int](Num(5, _)).futureValue should === (9)
38 | ref2 ! Stop
39 | awaitTermination(ref2).futureValue
40 | }
41 |
42 | it should "be able to start in an ActorContext (i.e., as a child actor)" in {
43 | val name = "test2"
44 | val ref = parent(name).deployInto(this.system, name)
45 | ref.?[Int](Num(42, _)).futureValue should === (43)
46 | ref.?[Int](Num(42, _)).futureValue should === (44)
47 | ref ! Stop
48 | awaitTermination(ref).futureValue
49 |
50 | val ref2 = parent(name).deployInto(this.system, name)
51 | ref2.?[Int](Num(9, _)).futureValue should === (12)
52 | ref2.?[Int](Num(9, _)).futureValue should === (13)
53 | ref2 ! Stop
54 | awaitTermination(ref2).futureValue
55 | }
56 | }
57 |
58 | object DeploymentSpec {
59 |
60 | sealed trait Mesg
61 | final case class Num(i: Int, replyTo: at.ActorRef[Int]) extends Mesg
62 | final case object Stop extends Mesg
63 |
64 | sealed trait ParentState
65 | final case object Uninitialized extends ParentState
66 | final case class Initialized(child: at.ActorRef[Mesg]) extends ParentState
67 |
68 | def parent(pid: PersistenceId) = PersistentActor.withRecovery[Mesg, at.ActorRef[Mesg], ParentState](
69 | initialState = Uninitialized,
70 | pid = _ => s"${pid}-parent",
71 | recovery = Recovery(
72 | (_, msg, ctx) => {
73 | val childRef = child(pid).deployInto(ctx)
74 | Initialized(childRef)
75 | }
76 | )
77 | ) { state => ctx => msg =>
78 | msg match {
79 | case Stop =>
80 | ctx.stop
81 | case n @ Num(_, _) =>
82 | state match {
83 | case Uninitialized =>
84 | val childRef = child(pid).deployInto(ctx.ctx)
85 | childRef ! n
86 | ctx.apply(childRef, sync = true)
87 | case Initialized(child) =>
88 | child ! n
89 | ctx.same
90 | }
91 | }
92 | }
93 |
94 | def child(pid: PersistenceId) = PersistentActor.immutable[Mesg, Unit, Int](
95 | initialState = 0,
96 | pid = _ => s"${pid}-child"
97 | ) { state => ctx =>
98 | {
99 | case Stop =>
100 | ctx.stop
101 | case Num(i, r) =>
102 | ctx.apply(()).map { state =>
103 | r ! i + state
104 | state
105 | }
106 | }
107 | }
108 |
109 | implicit val childUpdater: Update[Int, Unit] =
110 | Update.instance((s, _) => s + 1)
111 |
112 | implicit val parentUpdater: Update[ParentState, at.ActorRef[Mesg]] =
113 | Update.instance((_, e) => Initialized(e))
114 | }
115 |
--------------------------------------------------------------------------------
/src/test/scala/com/nokia/ntp/ct/persistence/MockSnapshotStore.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import scala.collection.mutable
21 | import scala.concurrent.Future
22 |
23 | import akka.persistence.SelectedSnapshot
24 | import akka.persistence.SnapshotMetadata
25 | import akka.persistence.SnapshotSelectionCriteria
26 | import akka.persistence.snapshot.SnapshotStore
27 |
28 | import cats.implicits._
29 |
30 | /** In-memory snapshot store, only for testing. */
31 | final class MockSnapshotStore extends SnapshotStore {
32 |
33 | // we have to store the snapshots somewhere ...
34 | @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures"))
35 | private[this] val state: mutable.Map[String, List[(SnapshotMetadata, Any)]] =
36 | mutable.Map()
37 |
38 | override def deleteAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Unit] = {
39 | val lst = state.getOrElse(persistenceId, Nil)
40 | state.put(persistenceId, lst.filterNot { case (md, _) => matches(md, criteria) })
41 | Future.successful(())
42 | }
43 |
44 | override def deleteAsync(metadata: SnapshotMetadata): Future[Unit] = {
45 | val lst = state.getOrElse(metadata.persistenceId, Nil)
46 | state.put(metadata.persistenceId, lst.filterNot { case (md, _) => md === metadata })
47 | Future.successful(())
48 | }
49 |
50 | override def loadAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Option[SelectedSnapshot]] = {
51 | val lst = state.getOrElse(persistenceId, Nil)
52 | val hit = lst.find {
53 | case (metadata, _) =>
54 | matches(metadata, criteria)
55 | }
56 |
57 | Future.successful(hit.map { case (md, ss) => SelectedSnapshot(md, ss) })
58 | }
59 |
60 | override def saveAsync(metadata: SnapshotMetadata, snapshot: Any): Future[Unit] = {
61 | val lst = state.getOrElse(metadata.persistenceId, Nil)
62 | state.put(metadata.persistenceId, (metadata, snapshot) :: lst)
63 | Future.successful(())
64 | }
65 |
66 | private[this] def matches(metadata: SnapshotMetadata, criteria: SnapshotSelectionCriteria): Boolean = {
67 | (metadata.sequenceNr <= criteria.maxSequenceNr) && (metadata.timestamp <= criteria.maxTimestamp) &&
68 | (metadata.sequenceNr >= criteria.minSequenceNr) && (metadata.timestamp >= criteria.minTimestamp)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/test/scala/com/nokia/ntp/ct/persistence/PersistenceSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import scala.collection.immutable
21 | import scala.concurrent.Promise
22 | import scala.util.{ Failure, Success, Try }
23 |
24 | import cats.implicits._
25 |
26 | import akka.{ typed => at }
27 | import akka.typed.scaladsl.Actor
28 | import akka.typed.scaladsl.AskPattern._
29 | import akka.typed.scaladsl.adapter._
30 |
31 | import shapeless._
32 | import shapeless.test.illTyped
33 |
34 | class PersistenceSpec extends AbstractPersistenceSpec {
35 |
36 | import AbstractPersistenceSpec._
37 | import PersistenceSpec._
38 |
39 | "Persistent dictionary with ack/nack" should "work as expected" in {
40 | val name = "hl-dictNoSnapshotWithAck-ok"
41 | val ref: at.ActorRef[DictMsg] = dictWithAck(DState(), name).deployInto(system, name)
42 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
43 | ref.?[DictAck](Set("foo", 9, _)).futureValue should be (Ack("foo", 9))
44 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (Some(9))
45 | ref ! StopDict
46 | awaitTermination(ref).futureValue
47 |
48 | val ref2: at.ActorRef[DictMsg] = dictWithAck(DState(), name).deployInto(system, name)
49 | ref2.?[Option[Int]](Get("foo", _)).futureValue should be (Some(9))
50 | ref2.?[DictAck](Set("bar", 3, _)).futureValue should be (Ack("bar", 3))
51 | ref2.?[Option[Int]](Get("bar", _)).futureValue should be (Some(3))
52 | ref2 ! StopDict
53 | awaitTermination(ref2).futureValue
54 | }
55 |
56 | it should "send a NAck if the persist is rejected" in {
57 | val name = "hl-dictNoSnapshotWithAck-reject"
58 | val sys = startWithDummyJournal(persistAction = Reject, recoverAction = No)
59 | val ref: at.ActorRef[DictMsg] = dictWithAck(DState(), name).deployInto(sys, name)
60 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
61 | ref.?[DictAck](Set("foo", 9, _)).futureValue should be (NAck("foo", 9))
62 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
63 | ref ! StopDict
64 | awaitTermination(ref).futureValue
65 | shutdown(sys, verifySystemShutdown = true)
66 | }
67 |
68 | "Managed persistent state" should "work without snapshots (dictionary)" in {
69 | val name = "hl-dictNoSnapshot"
70 | val ref: at.ActorRef[DictMsg] = dictionary2(DState(), name).deployInto(system, name)
71 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
72 | ref.?[DictAck](Set("foo", 9, _)).futureValue should be (Ack("foo", 9))
73 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (Some(9))
74 | ref ! StopDict
75 | awaitTermination(ref).futureValue
76 |
77 | val ref2: at.ActorRef[DictMsg] = dictionary2(DState(), name).deployInto(system, name)
78 | ref2.?[Option[Int]](Get("foo", _)).futureValue should be (Some(9))
79 | ref2.?[DictAck](Set("bar", 3, _)).futureValue should be (Ack("bar", 3))
80 | ref2.?[Option[Int]](Get("bar", _)).futureValue should be (Some(3))
81 | ref2 ! StopDict
82 | awaitTermination(ref2).futureValue
83 | }
84 |
85 | it should "work without snapshots (simple register)" in {
86 | val name = "hl-regNoSnapshot"
87 | val ref: at.ActorRef[Mes] = register(9, name).deployInto(system, name)
88 | ask(ref) should be (9)
89 | ref.?[Int](SetInt(10, _)).futureValue should be (10)
90 | ask(ref) should be (10)
91 | ref ! Stop
92 | awaitTermination(ref).futureValue
93 |
94 | val ref2: at.ActorRef[Mes] = register(999, name).deployInto(system, name)
95 | ask(ref2) should be (10)
96 | ref2.?[Int](SetInt(11, _)).futureValue should be (11)
97 | ask(ref2) should be (11)
98 | ref2 ! Stop
99 | awaitTermination(ref2).futureValue
100 | }
101 |
102 | it should "not work in a regular ActorAdapter" in {
103 | illTyped("""system.spawn(register(10, "foo"), "foo")""", ".*type mismatch.*")
104 | }
105 |
106 | "Error handling" should "be possible" in {
107 | val name = "hl-error-reject-ok"
108 | val sys = startWithDummyJournal(persistAction = Reject, recoverAction = No)
109 | val err = Promise[ProcException]()
110 | val ref: at.ActorRef[DictMsg] = dictionaryError(DState(), name, err).deployInto(sys, name)
111 | ref.?[DictAck](Set("foo", 9, _)) // we won't get a reply
112 | err.future.futureValue match {
113 | case PersistRejected(_, 1) =>
114 | case x: Any => fail(s"unexpected value: $x")
115 | }
116 | shutdown(sys, verifySystemShutdown = true)
117 | }
118 |
119 | "Persistence failure" should "stop the actor, even if handled" in {
120 | val name = "hl-error-fail_handled-ok"
121 | val sys = startWithDummyJournal(persistAction = Fail, recoverAction = Ok)
122 | val p = Promise[ProcException]()
123 | val ref: at.ActorRef[PP] = promiser(p, name).deployInto(sys, name)
124 | ref ! N(5)
125 | p.future.futureValue match {
126 | case PersistFailure(_: MyException, _) =>
127 | case x: Any => fail(s"unexpected: $x")
128 | }
129 | awaitTermination(ref).futureValue
130 | shutdown(sys, verifySystemShutdown = true)
131 | }
132 |
133 | it should "stop the actor (unhandled)" in {
134 | val name = "hl-error-fail_unhandled-ok"
135 | val sys = startWithDummyJournal(persistAction = Fail, recoverAction = Ok)
136 | val ref: at.ActorRef[PP] = unhandled(name).deployInto(sys, name)
137 | ref ! N(5)
138 | awaitTermination(ref).futureValue
139 | shutdown(sys, verifySystemShutdown = true)
140 | }
141 |
142 | "Persistence rejection" should "not stop the actor if handled" in {
143 | val name = "hl-error-reject_handled-ok"
144 | val sys = startWithDummyJournal(persistAction = Reject, recoverAction = Ok)
145 | val p = Promise[ProcException]()
146 | val ref: at.ActorRef[PP] = promiser(p, name).deployInto(sys, name)
147 | ref ! N(5)
148 | p.future.futureValue match {
149 | case PersistRejected(_: MyException, _) =>
150 | case x: Any => fail(s"unexpected: $x")
151 | }
152 | ref.?[Boolean](AreYouAlive(_)).futureValue should be (true)
153 | shutdown(sys, verifySystemShutdown = true)
154 | }
155 |
156 | it should "stop the actor if unhandled" in {
157 | val name = "hl-error-reject_unhandled-ok"
158 | val sys = startWithDummyJournal(persistAction = Reject, recoverAction = Ok)
159 | val ref: at.ActorRef[PP] = unhandled(name).deployInto(sys, name)
160 | ref ! N(5)
161 | awaitTermination(ref).futureValue
162 | shutdown(sys, verifySystemShutdown = true)
163 | }
164 |
165 | it should "stop, if the actor chooses to stop" in {
166 | val name = "hl-regStopOnReject"
167 | val sys = startWithDummyJournal(persistAction = Reject, recoverAction = No)
168 | val ref: at.ActorRef[Mes] = register(9, name).deployInto(sys, name)
169 | ask(ref) should be (9)
170 | ref.?[Int](SetInt(10, _)).futureValue should be (-2) // the actor calls p.stop()
171 | awaitTermination(ref).futureValue
172 | shutdown(sys, verifySystemShutdown = true)
173 | }
174 |
175 | "Recovery failure" should "be sent as a message, then the actor stopped" in {
176 | val name = "hl-error-ok-fail"
177 | val sys = startWithDummyJournal(persistAction = Ok, recoverAction = Fail)
178 | val p = Promise[RecoveryEvent]()
179 | val ref: at.ActorRef[PP] = recoveryPromiser(p, name).deployInto(sys, name)
180 | p.future.futureValue.select[Throwable] match {
181 | case Some(_: MyException) =>
182 | case x: Any => fail(s"unexpected: $x")
183 | }
184 | awaitTermination(ref).futureValue
185 | shutdown(sys, verifySystemShutdown = true)
186 | }
187 |
188 | "Exception in the callback after persist" should "stop the actor" in {
189 | val name = "hl-exception-in-callback"
190 | val ref: at.ActorRef[PP] = exception(name).deployInto(system, name)
191 | ref ! N(5)
192 | awaitTermination(ref).futureValue
193 | }
194 |
195 | "Exception in the callback after pure" should "stop the actor" in {
196 | val name = "hl-exception-in-callback-pure"
197 | val ref: at.ActorRef[PP] = exception(name).deployInto(system, name)
198 | ref ! N(0)
199 | awaitTermination(ref).futureValue
200 | }
201 |
202 | "The user" should "not be able to catch ActorStop" in {
203 | val name = "try-catch-actor-stop"
204 | val ref: at.ActorRef[PP] = tryCatchStop(name).deployInto(system, name)
205 | ref ! N(0)
206 | awaitTermination(ref).futureValue
207 | }
208 |
209 | "Requested state change" should "take effect immediately" in {
210 | val act = PersistentActor.withRecovery[Set, Int, Long](
211 | 0L,
212 | _ => "chchch",
213 | Recovery { (s, _, _) => s }
214 | ) { state => p =>
215 | {
216 | case Set("", v, replyTo) =>
217 | replyTo ! Ack("", v)
218 | p.stop
219 | case Set(k, _, replyTo) =>
220 | for {
221 | state <- p.persist(1)
222 | _ <- p.change(99L)
223 | state <- p.persist(2)
224 | } yield {
225 | replyTo ! Ack(k, state.toInt)
226 | state
227 | }
228 | }
229 | }
230 |
231 | val ref = act.deployInto(system, "chchch")
232 | ref.?[DictAck](Set("foo", 9, _)).futureValue should be (Ack("foo", 99))
233 |
234 | // this will stop the actor:
235 | ref.?[DictAck](Set("", 0, _)).futureValue should be (Ack("", 0))
236 | awaitTermination(ref).futureValue
237 | }
238 |
239 | "Persistent dictionary with ack/nack and persistSync" should "always reflect the last Set in its reply" in {
240 | val name = "hl-dictNoSnapshotWithAckSync-ok"
241 | val ref: at.ActorRef[DictMsg] = dictWithAck(DState(), name, async = false).deployInto(system, name)
242 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
243 |
244 | // starting Set:
245 | val futAckSet = ref.?[DictAck](Set("foo", 9, _))
246 |
247 | // sending a Get, which should be answered only
248 | // after the Set is persisted and takes effect:
249 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (Some(9))
250 |
251 | // and meanwhile the Set should be acked too:
252 | futAckSet.futureValue should be (Ack("foo", 9))
253 |
254 | ref ! StopDict
255 | awaitTermination(ref).futureValue
256 |
257 | val ref2: at.ActorRef[DictMsg] = dictWithAck(DState(), name, async = false).deployInto(system, name)
258 | ref2.?[Option[Int]](Get("foo", _)).futureValue should be (Some(9))
259 |
260 | // the same again:
261 | val futAckSet2 = ref2.?[DictAck](Set("bar", 3, _))
262 | ref2.?[Option[Int]](Get("bar", _)).futureValue should be (Some(3))
263 | futAckSet2.futureValue should be (Ack("bar", 3))
264 |
265 | ref2 ! StopDict
266 | awaitTermination(ref2).futureValue
267 | }
268 |
269 | "Snapshots" should "be used when recovering" in {
270 | import system.dispatcher
271 |
272 | val name = "hl-dict-withSnapshot-withAck-ok"
273 | val testRef = actorRef[Either[Throwable, DSS]](testActor)
274 | val testRefRec = actorRef[Set](testActor)
275 |
276 | def start() = {
277 | dictWithAckSnap(DState(), name, testRef, testRefRec, handleSnapError = false).deployInto(system, name)
278 | }
279 |
280 | val ref: at.ActorRef[DictMsg] = start()
281 |
282 | // sanity check:
283 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
284 |
285 | // send a few Sets to force snapshot:
286 | for (i <- 0 to snapThreshold) {
287 | ref.?[DictAck](Set("foo", i, _)).futureValue should be (Ack("foo", i))
288 | }
289 |
290 | // expect a snapshot:
291 | this.expectMsgPF(remainingOrDefault, "first snapshot") {
292 | case Right(DSS(0, map)) =>
293 | map should be (Map("foo" -> snapThreshold))
294 | }
295 |
296 | // state should be correct:
297 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (Some(snapThreshold))
298 |
299 | // force 2 more snapshots:
300 | for (i <- 0 to snapThreshold) {
301 | ref.?[DictAck](Set("bar", i, _)).futureValue should be (Ack("bar", i))
302 | ref.?[DictAck](Set("baz", i, _)).futureValue should be (Ack("baz", i))
303 | }
304 |
305 | // expect them:
306 | this.expectMsgPF(remainingOrDefault, "2nd snapshot") {
307 | case Right(DSS(0, map)) =>
308 | map should contain key ("foo")
309 | map should contain key ("bar")
310 | map should contain key ("baz")
311 | }
312 | this.expectMsgPF(remainingOrDefault, "3rd snapshot") {
313 | case Right(DSS(0, map)) =>
314 | map should be (Map("foo" -> snapThreshold, "bar" -> snapThreshold, "baz" -> snapThreshold))
315 | }
316 |
317 | // now the first 2 snapshots should have been deleted:
318 | val f = getPreviousSnapshot[DSS](name).map[Try[DSS]](Success(_)).recover { case ex: Any => Failure(ex) }
319 | withDilatedTimeout(10.0) {
320 | f.futureValue match {
321 | case Failure(_) => // OK, we didn't find an earlier snapshot
322 | case Success(x) => fail(s"Unexpected snapshot: ${x}")
323 | }
324 | }
325 |
326 | // stop:
327 | ref ! StopDict
328 | awaitTermination(ref).futureValue
329 |
330 | // recover from snapshots:
331 | val ref2: at.ActorRef[DictMsg] = start()
332 | this.expectNoMsg() // no recovery from events
333 |
334 | ref2.?[Option[Int]](Get("foo", _)).futureValue should be (Some(snapThreshold))
335 | ref2.?[Option[Int]](Get("bar", _)).futureValue should be (Some(snapThreshold))
336 | ref2.?[Option[Int]](Get("baz", _)).futureValue should be (Some(snapThreshold))
337 | this.expectNoMsg() // no recovery from events
338 |
339 | // let's take another snapshot:
340 | for (i <- 0 to snapThreshold) {
341 | ref2.?[DictAck](Set("xxx", i, _)).futureValue should be (Ack("xxx", i))
342 | }
343 |
344 | // expect it:
345 | this.expectMsgPF(remainingOrDefault, "4th snapshot") {
346 | case Right(DSS(0, map)) =>
347 | map should contain key ("xxx")
348 | }
349 |
350 | ref2.?[Option[Int]](Get("xxx", _)).futureValue should be (Some(snapThreshold))
351 | ref2.?[DictAck](Set("xyz", 5, _)).futureValue should be (Ack("xyz", 5))
352 |
353 | ref2 ! StopDict
354 | awaitTermination(ref2).futureValue
355 |
356 | // restart, now it should recover from the last snapshot and 1 event:
357 | val ref3: at.ActorRef[DictMsg] = start()
358 | this.expectMsgPF(remainingOrDefault, "recovery event") {
359 | case Set("xyz", 5, _) =>
360 | }
361 | this.expectNoMsg() // no more events
362 |
363 | ref3.?[Option[Int]](Get("xyz", _)).futureValue should be (Some(5))
364 |
365 | ref3 ! StopDict
366 | awaitTermination(ref3).futureValue
367 | }
368 |
369 | "SnapshotFailure" should "stop the actor if not handled" in {
370 | val name = "hl-dict-withSnapshot-withAck-snapshotFailure-notHandled"
371 | val sys = startWithDummyJournal(persistAction = Ok, recoverAction = No, snapshotAction = Fail)
372 | val testRef = actorRef[Either[Throwable, DSS]](testActor)
373 | val testRefRec = actorRef[Set](testActor)
374 |
375 | def start() = {
376 | dictWithAckSnap(DState(), name, testRef, testRefRec, handleSnapError = false).deployInto(sys, name)
377 | }
378 |
379 | val ref: at.ActorRef[DictMsg] = start()
380 |
381 | // sanity check:
382 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
383 |
384 | // send a few Sets to force snapshot:
385 | for (i <- 0 to snapThreshold) {
386 | ref.?[DictAck](Set("foo", i, _)).futureValue should be (Ack("foo", i))
387 | }
388 |
389 | // expect a snapshot failure:
390 | this.expectMsgPF(remainingOrDefault, "snapshot failure") {
391 | case Left(SnapshotFailure(_, _)) =>
392 | }
393 |
394 | // the actor should be stopped:
395 | awaitTermination(ref).futureValue should be (())
396 | }
397 |
398 | it should "be able to be handled with 'recoverWith'" in {
399 | val name = "hl-dict-withSnapshot-withAck-snapshotFailure-handled"
400 | val sys = startWithDummyJournal(persistAction = Ok, recoverAction = No, snapshotAction = Fail)
401 | val testRef = actorRef[Either[Throwable, DSS]](testActor)
402 | val testRefRec = actorRef[Set](testActor)
403 |
404 | def start() = {
405 | dictWithAckSnap(DState(), name, testRef, testRefRec, handleSnapError = true).deployInto(sys, name)
406 | }
407 |
408 | val ref: at.ActorRef[DictMsg] = start()
409 |
410 | // sanity check:
411 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (None)
412 |
413 | // send a few Sets to force snapshot:
414 | for (i <- 0 to snapThreshold) {
415 | ref.?[DictAck](Set("foo", i, _)).futureValue should be (Ack("foo", i))
416 | }
417 |
418 | // expect a snapshot failure:
419 | this.expectMsgPF(remainingOrDefault, "snapshot failure") {
420 | case Left(SnapshotFailure(_, _)) =>
421 | }
422 |
423 | // the actor should NOT be stopped:
424 | ref.?[Option[Int]](Get("foo", _)).futureValue should be (Some(snapThreshold))
425 | }
426 |
427 | "Last sequence number" should "be accessible in the actor" in {
428 | val name = "seqNr-normal"
429 | val ref = getSeqNr(name).deployInto(system, name)
430 | (ref ? GetSeqNr).futureValue should be (0L)
431 | (ref ? GetSeqNr).futureValue should be (0L)
432 | (ref ? Persist).futureValue
433 | (ref ? GetSeqNr).futureValue should be (1L)
434 | (ref ? GetSeqNr).futureValue should be (1L)
435 | (ref ? Persist).futureValue
436 | (ref ? Persist).futureValue
437 | (ref ? GetSeqNr).futureValue should be (3L)
438 | ref ! End
439 | awaitTermination(ref).futureValue
440 |
441 | val ref2 = getSeqNr(name).deployInto(system, name)
442 | (ref2 ? GetSeqNr).futureValue should be (3L)
443 | (ref2 ? Persist).futureValue
444 | (ref2 ? Persist).futureValue
445 | (ref2 ? GetSeqNr).futureValue should be (5L)
446 | ref2 ! End
447 | awaitTermination(ref2).futureValue
448 | }
449 |
450 | "Signal handling" should "be possible (not deferred)" in {
451 | val ps = Promise[Unit]()
452 | val b = PersistentActor.immutable[Boolean, Int, List[Int]](
453 | initialState = Nil,
454 | pid = _ => "signalHandlingNotDeferred"
455 | ) { state => p =>
456 | {
457 | case false =>
458 | p.same
459 | case true =>
460 | p.stop
461 | }
462 | }.onSignal { (state, p) =>
463 | {
464 | case at.PostStop =>
465 | ps.success(())
466 | p.same
467 | }
468 | }
469 |
470 | val ref = b.deployInto(system, "signalHandlingNotDeferred")
471 | ref ! true
472 | ps.future.futureValue should be (())
473 | awaitTermination(ref).futureValue
474 | }
475 |
476 | it should "be possible (deferred)" in {
477 | val ps = Promise[Unit]()
478 | val tm = Promise[Unit]()
479 | val b = PersistentActor.deferred[Boolean, Int, List[Int]] { ctx =>
480 |
481 | val child = ctx.spawn(Actor.immutable[Int] { (ctx, msg) => Actor.stopped }, "myChild")
482 | ctx.watch(child)
483 |
484 | PersistentActor.immutable[Boolean, Int, List[Int]](
485 | initialState = Nil,
486 | pid = _ => "signalHandlingDeferred"
487 | ) { state => p =>
488 | {
489 | case false =>
490 | child ! 42
491 | p.same
492 | case true =>
493 | p.stop
494 | }
495 | }.onSignal { (state, p) =>
496 | {
497 | case at.Terminated(ch) if ch === child =>
498 | tm.success(())
499 | p.same
500 | case at.PostStop =>
501 | ps.success(())
502 | p.same
503 | }
504 | }
505 | }
506 |
507 | val ref = b.deployInto(system, "signalHandlingDeferred")
508 | ref ! false
509 | tm.future.futureValue should be (())
510 | ref ! true
511 | ps.future.futureValue should be (())
512 | awaitTermination(ref).futureValue
513 | }
514 | }
515 |
516 | object PersistenceSpec {
517 |
518 | import AbstractPersistenceSpec._
519 |
520 | sealed trait DictMsg
521 | final case class Get(key: String, replyTo: at.ActorRef[Option[Int]]) extends DictMsg
522 |
523 | final case class Set(key: String, value: Int, ackTo: at.ActorRef[DictAck]) extends DictMsg
524 |
525 | object Set {
526 | implicit val forMap: Update[DState, Set] =
527 | Update.instance { (s, e) => s.updated(e.key, e.value) }
528 | }
529 |
530 | case object StopDict extends DictMsg
531 |
532 | sealed trait DictAck
533 | final case class Ack(key: String, value: Int) extends DictAck
534 | final case class NAck(key: String, value: Int) extends DictAck
535 |
536 | type DState = immutable.Map[String, Int]
537 | val DState = immutable.Map
538 |
539 | private final val snapThreshold = 10
540 |
541 | final case class DSS(ctr: Long, map: immutable.Map[String, Int]) {
542 | def update(ev: Set): DSS = {
543 | if (ctr >= snapThreshold) {
544 | copy(ctr = 0, map = map.updated(ev.key, ev.value))
545 | } else {
546 | copy(ctr = ctr + 1, map = map.updated(ev.key, ev.value))
547 | }
548 | }
549 | }
550 |
551 | object DSS {
552 | implicit val updater: Update[DSS, Set] = Update.instance(_ update _)
553 | }
554 |
555 | def dictWithAck(initialState: DState, pid: PersistenceId, async: Boolean = true) = PersistentActor.immutable[DictMsg, Set, DState](
556 | initialState,
557 | _ => pid
558 | ) { state => p =>
559 | {
560 | case StopDict =>
561 | p.stop
562 | case Get(k, r) =>
563 | r ! state.get(k)
564 | p.same
565 | case ev @ Set(k, v, ackTo) =>
566 | (for {
567 | st <- p.apply(ev, sync = !async)
568 | _ = ackTo ! Ack(k, v)
569 | } yield st).recoverWith {
570 | case PersistRejected(_, _) =>
571 | ackTo ! NAck(k, v)
572 | p.same
573 | case ex: Any =>
574 | ackTo ! NAck(k, v)
575 | p.fail(ex)
576 | }
577 | }
578 | }
579 |
580 | def dictWithAckSnap(
581 | initialMap: DState,
582 | pid: PersistenceId,
583 | snapshots: at.ActorRef[Either[Throwable, DSS]],
584 | recovery: at.ActorRef[Set],
585 | handleSnapError: Boolean
586 | ) = PersistentActor.withRecovery[DictMsg, Set, DSS](
587 | DSS(0, initialMap),
588 | _ => pid,
589 | Recovery(
590 | recovery = { (s, ev, ctx) =>
591 | ctx.log.debug(s"Recovery: ${ev}")
592 | recovery ! ev
593 | s.copy(map = s.map.updated(ev.key, ev.value))
594 | },
595 | completed = (s, _) => s.copy(ctr = 0)
596 | )
597 | ) { state => p =>
598 | {
599 | case StopDict =>
600 | p.stop
601 | case Get(k, r) =>
602 | r ! state.map.get(k)
603 | p.same
604 | case ev @ Set(k, v, ackTo) =>
605 | (for {
606 | st <- p.apply(ev)
607 | st <- {
608 | ackTo ! Ack(k, v)
609 | st match {
610 | case DSS(0, _) => p.change(st).flatMap { _ =>
611 | p.ctx.log.debug(s"SNAP: ${st} ...")
612 | p.snapshot.map { asst =>
613 | p.ctx.log.debug(s"SNAP ${st} DONE")
614 | snapshots ! Right(st)
615 | asst
616 | }.recoverWith {
617 | case ex: Throwable =>
618 | p.ctx.log.debug(s"SNAP ${st} FAILED")
619 | snapshots ! Left(ex)
620 | if (handleSnapError) p.same else p.fail(ex)
621 | }
622 | }
623 | case DSS(_, _) => p.pure(st)
624 | }
625 | }
626 | } yield st).recoverWith {
627 | case PersistRejected(_, _) =>
628 | ackTo ! NAck(k, v)
629 | p.same
630 | case ex: Any =>
631 | ackTo ! NAck(k, v)
632 | p.fail(ex)
633 | }
634 | }
635 | }
636 |
637 | def dictionary2(state: DState, pid: PersistenceId) = PersistentActor.immutable[DictMsg, Set, DState](
638 | state,
639 | _ => pid
640 | ) { state => p =>
641 | {
642 | case StopDict =>
643 | p.stop
644 | case Get(k, r) =>
645 | r ! state.get(k)
646 | p.same
647 | case ev @ Set(_, _, ackTo) =>
648 | for {
649 | st <- p.apply(ev)
650 | } yield {
651 | ackTo ! Ack(ev.key, ev.value)
652 | st
653 | }
654 | }
655 | }
656 |
657 | def dictionaryError(state: DState, pid: PersistenceId, error: Promise[ProcException]) = PersistentActor.immutable[DictMsg, Set, DState](
658 | state,
659 | _ => pid
660 | ) { state => p =>
661 | {
662 | case StopDict =>
663 | p.stop
664 | case Get(k, r) =>
665 | r ! state.get(k)
666 | p.same
667 | case ev @ Set(_, _, _) =>
668 | val proc = p.apply(ev)
669 | proc.recoverWith {
670 | case e @ PersistRejected(ex, _) =>
671 | error.success(e)
672 | p.same
673 | }
674 | }
675 | }
676 |
677 | /** Simple persistent `Int` register, doesn't use snapshots */
678 | def register(startAt: Int, pid: PersistenceId) = PersistentActor.withRecovery[Mes, Int, Int](
679 | startAt,
680 | _ => pid,
681 | Recovery { (_, ev, _) => ev }
682 | ) { state => p => {
683 | case SetInt(n, ackTo) =>
684 | p.ctx.log.info(s"Starting to persist $n")
685 | val proc = for {
686 | _ <- p.persist(n)
687 | _ = p.ctx.log.info(s"Persisted $n, changing state")
688 | _ = ackTo ! n
689 | } yield n
690 | proc.recoverWith {
691 | case ex: PersistenceException =>
692 | p.ctx.log.error(s"Persistence failed: ${ex.cause.getMessage}")
693 | ackTo ! -2
694 | p.stop
695 | }
696 | case GetInt(ref) =>
697 | p.ctx.log.info(s"Returning $state")
698 | ref ! state
699 | p.same
700 | case Stop =>
701 | p.stop
702 | }
703 | }
704 |
705 | sealed trait SeqNrOp
706 | final case class GetSeqNr(replyTo: at.ActorRef[Long]) extends SeqNrOp
707 | final case class Persist(ackTo: at.ActorRef[Unit]) extends SeqNrOp
708 | final case object End extends SeqNrOp
709 |
710 | implicit val updateUnit: Update[Unit, Unit] =
711 | Update.instance { (s, e) => s }
712 |
713 | def getSeqNr(pid: PersistenceId) = PersistentActor.withRecovery[SeqNrOp, Unit, Unit](
714 | (),
715 | _ => pid,
716 | Recovery { (_, _, _) => () }
717 | ) { state => p =>
718 | {
719 | case GetSeqNr(ref) =>
720 | for {
721 | snr <- p.lastSequenceNr
722 | _ = ref ! snr
723 | state <- p.same
724 | } yield state
725 | case Persist(ref) =>
726 | for {
727 | state <- p.apply(())
728 | _ = ref ! (())
729 | } yield state
730 | case End =>
731 | p.stop
732 | }
733 | }
734 |
735 | private def promiser(promise: Promise[ProcException], pid: PersistenceId) = PersistentActor.withRecovery[PP, Unit, Int](
736 | 0,
737 | _ => pid,
738 | Recovery { (s, e, _) => s }
739 | ) { state => p =>
740 | {
741 | case N(n) =>
742 | p.persist(()).recover {
743 | case ex: ProcException =>
744 | promise.success(ex)
745 | 0
746 | }
747 | case AreYouAlive(r) =>
748 | r ! true
749 | p.same
750 | }
751 | }
752 |
753 | private def unhandled(pid: PersistenceId) = PersistentActor.withRecovery[PP, Unit, Int](
754 | 0,
755 | _ => pid,
756 | Recovery { (s, _, _) => s }
757 | ) { state => p =>
758 | {
759 | case N(n) =>
760 | p.persist(())
761 | case AreYouAlive(r) =>
762 | r ! true
763 | p.same
764 | }
765 | }
766 |
767 | type RecoveryEvent = Int :+: Throwable :+: Unit :+: CNil
768 |
769 | private def recoveryPromiser(promise: Promise[RecoveryEvent], pid: PersistenceId) = PersistentActor.withRecovery[PP, Int, Int](
770 | 0,
771 | _ => pid,
772 | Recovery(
773 | (s, e, _) => {
774 | promise.success(Coproduct[RecoveryEvent](e))
775 | s
776 | },
777 | failure = (_, ex, ctx) => promise.success(Coproduct[RecoveryEvent](ex)),
778 | completed = (s, _) => {
779 | promise.success(Coproduct[RecoveryEvent](()))
780 | s
781 | }
782 | )
783 | ) { state => p =>
784 | {
785 | case x =>
786 | p.fail(UnexpectedException(new Exception(s"unexpected msg: $x")))
787 | }
788 | }
789 |
790 | private def exception(pid: PersistenceId) = PersistentActor.withRecovery[PP, Int, Int](
791 | 0,
792 | _ => pid,
793 | Recovery { (s, _, _) => s }
794 | ) { state => p =>
795 | {
796 | case N(0) =>
797 | for {
798 | _ <- p.pure(99)
799 | } yield thrw(new MyException("test exception (pure)"))
800 | case N(n) =>
801 | for {
802 | _ <- p.persist(n)
803 | } yield thrw(new MyException("test exception (persist)"))
804 | case _ =>
805 | p.same
806 | }
807 | }
808 |
809 | private def thrw(ex: Throwable): Nothing =
810 | throw ex
811 |
812 | private def tryCatchStop(pid: PersistenceId) = PersistentActor.withRecovery[PP, Int, Int](
813 | 0,
814 | _ => pid,
815 | Recovery { (s, _, _) => s }
816 | ) { state => p =>
817 | {
818 | case _ =>
819 | p.stop.recover { // this shouldn't work
820 | case _ => 99
821 | }
822 | }
823 | }
824 | }
825 |
--------------------------------------------------------------------------------
/src/test/scala/com/nokia/ntp/ct/persistence/SnapshotProbe.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 |
20 | import scala.concurrent.Promise
21 |
22 | import akka.{ persistence => ap }
23 |
24 | private final class SnapshotProbe[A](
25 | override val persistenceId: String,
26 | lessThanSeqNr: Long,
27 | promise: Promise[SnapshotProbe.SnapshotInfo[A]]
28 | ) extends ap.PersistentActor {
29 |
30 | import SnapshotProbe._
31 |
32 | private[this] val fail = new RuntimeException("no such snapshot")
33 |
34 | override def recovery = {
35 | super.recovery.copy(fromSnapshot = ap.SnapshotSelectionCriteria(
36 | maxSequenceNr = lessThanSeqNr - 1
37 | ))
38 | }
39 |
40 | def receiveCommand: PartialFunction[Any, Unit] = {
41 | case msg: Any =>
42 | promise.tryFailure(fail)
43 | this.context.stop(self)
44 | }
45 |
46 | def receiveRecover: PartialFunction[Any, Unit] = {
47 | case ap.RecoveryCompleted =>
48 | promise.tryFailure(fail)
49 | case ap.SnapshotOffer(m, s) =>
50 | promise.success(SnapshotInfo[A](m, s.asInstanceOf[A]))
51 | case rec: Any =>
52 | // recovery offer, ignore
53 | }
54 | }
55 |
56 | private object SnapshotProbe {
57 | final case class SnapshotInfo[A](m: ap.SnapshotMetadata, a: A)
58 | }
59 |
--------------------------------------------------------------------------------
/src/test/scala/com/nokia/ntp/ct/persistence/testkit/TestExample.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-2017 Nokia Solutions and Networks Oy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.nokia.ntp.ct
18 | package persistence
19 | package testkit
20 |
21 | import scala.util.Try
22 |
23 | import org.scalatest.FlatSpecLike
24 |
25 | import akka.testkit.TestKit
26 | import akka.typed._
27 |
28 | import cats.implicits._
29 |
30 | class TestExample extends TestKit(akka.actor.ActorSystem()) with FlatSpecLike { spec =>
31 |
32 | sealed trait MyMsg
33 | case class Add(n: Int, replyTo: ActorRef[Long]) extends MyMsg
34 | case object Snap extends MyMsg
35 | case object Stop extends MyMsg
36 | case class ReadSeqNr(replyTo: ActorRef[Long]) extends MyMsg
37 |
38 | sealed trait MyEv
39 | case class Incr(amount: Int) extends MyEv
40 |
41 | sealed case class MyState(ctr: Long) {
42 | def update(ev: MyEv): MyState = ev match {
43 | case Incr(n) => this.copy(ctr = ctr + n)
44 | }
45 | }
46 |
47 | object MyState {
48 | implicit val mngd: Update[MyState, MyEv] =
49 | Update.instance(_ update _)
50 | }
51 |
52 | val name = "TestExample"
53 |
54 | val b = PersistentActor.immutable[MyMsg, MyEv, MyState](
55 | MyState(ctr = 0),
56 | _ => name
57 | ) { state => p => {
58 | case Add(n, r) =>
59 | for {
60 | st <- p.apply(Incr(n))
61 | } yield {
62 | r ! st.ctr
63 | st
64 | }
65 | case Snap =>
66 | p.snapshot
67 | case Stop =>
68 | p.stop
69 | case ReadSeqNr(r) =>
70 | for {
71 | seqNr <- p.lastSequenceNr
72 | _ = r ! seqNr
73 | } yield state
74 | }
75 | }
76 |
77 | val ti = new TestInterpreter(name, b, ActorSystem.wrap(this.system)) {
78 | override def assert(b: Boolean, msg: String = ""): Try[Unit] =
79 | Try(spec.assert(b, msg))
80 | override def fail(msg: String): Nothing =
81 | spec.fail(msg)
82 | }
83 |
84 | "It" should "work" in {
85 | ti.check(for {
86 | _ <- ti.expect[Long](ReadSeqNr, 0L)
87 | _ <- ti.expect[Long](Add(3, _), 3L)
88 | _ <- ti.expect[Long](ReadSeqNr, 1L)
89 | _ <- ti.expect[Long](Add(2, _), 5L)
90 | _ <- ti.expect[Long](ReadSeqNr, 2L)
91 | _ <- ti.expectSt(_.ctr, 5L)
92 | _ <- ti.message(Stop)
93 | _ <- ti.expectStop
94 | } yield ())
95 | }
96 | }
97 |
--------------------------------------------------------------------------------