├── .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 | [![Gitter chat](https://badges.gitter.im/nokia/akka-typed-persistence.png)](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 | 135 | 136 | 151 | 152 | 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 | --------------------------------------------------------------------------------