258 lines
9.8 KiB
Scala
258 lines
9.8 KiB
Scala
/*
|
|
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
|
*/
|
|
|
|
package akka.persistence
|
|
|
|
import akka.actor._
|
|
import akka.testkit._
|
|
|
|
object SnapshotSpec {
|
|
case object TakeSnapshot
|
|
|
|
class SaveSnapshotTestPersistentActor(name: String, probe: ActorRef) extends NamedPersistentActor(name) {
|
|
var state = List.empty[String]
|
|
|
|
override def receiveRecover: Receive = {
|
|
case payload: String ⇒ state = s"${payload}-${lastSequenceNr}" :: state
|
|
case SnapshotOffer(_, snapshot: List[_]) ⇒ state = snapshot.asInstanceOf[List[String]]
|
|
}
|
|
|
|
override def receiveCommand = {
|
|
case payload: String ⇒
|
|
persist(payload) { _ ⇒
|
|
state = s"${payload}-${lastSequenceNr}" :: state
|
|
}
|
|
case TakeSnapshot ⇒ saveSnapshot(state)
|
|
case SaveSnapshotSuccess(md) ⇒ probe ! md.sequenceNr
|
|
case GetState ⇒ probe ! state.reverse
|
|
}
|
|
}
|
|
|
|
class LoadSnapshotTestPersistentActor(name: String, _recovery: Recovery, probe: ActorRef) extends NamedPersistentActor(name) {
|
|
override def recovery: Recovery = _recovery
|
|
|
|
override def receiveRecover: Receive = {
|
|
case payload: String ⇒ probe ! s"${payload}-${lastSequenceNr}"
|
|
case offer @ SnapshotOffer(md, s) ⇒ probe ! offer
|
|
case other ⇒ probe ! other
|
|
}
|
|
|
|
override def receiveCommand = {
|
|
case "done" ⇒ probe ! "done"
|
|
case payload: String ⇒
|
|
persist(payload) { _ ⇒
|
|
probe ! s"${payload}-${lastSequenceNr}"
|
|
}
|
|
case offer @ SnapshotOffer(md, s) ⇒ probe ! offer
|
|
case other ⇒ probe ! other
|
|
}
|
|
}
|
|
|
|
class IgnoringSnapshotTestPersistentActor(name: String, _recovery: Recovery, probe: ActorRef) extends NamedPersistentActor(name) {
|
|
override def recovery: Recovery = _recovery
|
|
|
|
override def receiveRecover: Receive = {
|
|
case payload: String ⇒ probe ! s"${payload}-${lastSequenceNr}"
|
|
case other if !other.isInstanceOf[SnapshotOffer] ⇒ probe ! other
|
|
}
|
|
|
|
override def receiveCommand = {
|
|
case "done" ⇒ probe ! "done"
|
|
case payload: String ⇒
|
|
persist(payload) { _ ⇒
|
|
probe ! s"${payload}-${lastSequenceNr}"
|
|
}
|
|
case other ⇒ probe ! other
|
|
}
|
|
}
|
|
|
|
final case class Delete1(metadata: SnapshotMetadata)
|
|
final case class DeleteN(criteria: SnapshotSelectionCriteria)
|
|
|
|
class DeleteSnapshotTestPersistentActor(name: String, _recovery: Recovery, probe: ActorRef) extends LoadSnapshotTestPersistentActor(name, _recovery, probe) {
|
|
override def receiveCommand = receiveDelete orElse super.receiveCommand
|
|
def receiveDelete: Receive = {
|
|
case Delete1(metadata) ⇒ deleteSnapshot(metadata.sequenceNr)
|
|
case DeleteN(criteria) ⇒ deleteSnapshots(criteria)
|
|
}
|
|
}
|
|
}
|
|
|
|
class SnapshotSpec extends PersistenceSpec(PersistenceSpec.config("leveldb", "SnapshotSpec")) with ImplicitSender {
|
|
import SnapshotSpec._
|
|
import SnapshotProtocol._
|
|
|
|
override protected def beforeEach(): Unit = {
|
|
super.beforeEach()
|
|
|
|
val persistentActor = system.actorOf(Props(classOf[SaveSnapshotTestPersistentActor], name, testActor))
|
|
persistentActor ! "a"
|
|
persistentActor ! TakeSnapshot
|
|
persistentActor ! "b"
|
|
persistentActor ! TakeSnapshot
|
|
persistentActor ! "c"
|
|
persistentActor ! "d"
|
|
persistentActor ! TakeSnapshot
|
|
persistentActor ! "e"
|
|
persistentActor ! "f"
|
|
expectMsgAllOf(1L, 2L, 4L)
|
|
}
|
|
|
|
"A persistentActor" must {
|
|
"recover state starting from the most recent snapshot" in {
|
|
val persistentActor = system.actorOf(Props(classOf[LoadSnapshotTestPersistentActor], name, Recovery(), testActor))
|
|
val persistenceId = name
|
|
|
|
expectMsgPF() {
|
|
case SnapshotOffer(SnapshotMetadata(`persistenceId`, 4, timestamp), state) ⇒
|
|
state should ===(List("a-1", "b-2", "c-3", "d-4").reverse)
|
|
timestamp should be > (0L)
|
|
}
|
|
expectMsg("e-5")
|
|
expectMsg("f-6")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"recover completely if snapshot is not handled" in {
|
|
val persistentActor = system.actorOf(Props(classOf[IgnoringSnapshotTestPersistentActor], name, Recovery(), testActor))
|
|
val persistenceId = name
|
|
|
|
expectMsg("a-1")
|
|
expectMsg("b-2")
|
|
expectMsg("c-3")
|
|
expectMsg("d-4")
|
|
expectMsg("e-5")
|
|
expectMsg("f-6")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"recover state starting from the most recent snapshot matching an upper sequence number bound" in {
|
|
val persistentActor = system.actorOf(Props(classOf[LoadSnapshotTestPersistentActor], name, Recovery(toSequenceNr = 3), testActor))
|
|
val persistenceId = name
|
|
|
|
expectMsgPF() {
|
|
case SnapshotOffer(SnapshotMetadata(`persistenceId`, 2, timestamp), state) ⇒
|
|
state should ===(List("a-1", "b-2").reverse)
|
|
timestamp should be > (0L)
|
|
}
|
|
expectMsg("c-3")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"recover state starting from the most recent snapshot matching an upper sequence number bound (without further replay)" in {
|
|
val persistentActor = system.actorOf(Props(classOf[LoadSnapshotTestPersistentActor], name, Recovery(toSequenceNr = 4), testActor))
|
|
val persistenceId = name
|
|
|
|
persistentActor ! "done"
|
|
|
|
expectMsgPF() {
|
|
case SnapshotOffer(SnapshotMetadata(`persistenceId`, 4, timestamp), state) ⇒
|
|
state should ===(List("a-1", "b-2", "c-3", "d-4").reverse)
|
|
timestamp should be > (0L)
|
|
}
|
|
expectMsg(RecoveryCompleted)
|
|
expectMsg("done")
|
|
}
|
|
"recover state starting from the most recent snapshot matching criteria" in {
|
|
val recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria(maxSequenceNr = 2))
|
|
val persistentActor = system.actorOf(Props(classOf[LoadSnapshotTestPersistentActor], name, recovery, testActor))
|
|
val persistenceId = name
|
|
|
|
expectMsgPF() {
|
|
case SnapshotOffer(SnapshotMetadata(`persistenceId`, 2, timestamp), state) ⇒
|
|
state should ===(List("a-1", "b-2").reverse)
|
|
timestamp should be > (0L)
|
|
}
|
|
expectMsg("c-3")
|
|
expectMsg("d-4")
|
|
expectMsg("e-5")
|
|
expectMsg("f-6")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"recover state starting from the most recent snapshot matching criteria and an upper sequence number bound" in {
|
|
val recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria(maxSequenceNr = 2), toSequenceNr = 3)
|
|
val persistentActor = system.actorOf(Props(classOf[LoadSnapshotTestPersistentActor], name, recovery, testActor))
|
|
val persistenceId = name
|
|
|
|
expectMsgPF() {
|
|
case SnapshotOffer(SnapshotMetadata(`persistenceId`, 2, timestamp), state) ⇒
|
|
state should ===(List("a-1", "b-2").reverse)
|
|
timestamp should be > (0L)
|
|
}
|
|
expectMsg("c-3")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"recover state from scratch if snapshot based recovery is disabled" in {
|
|
val recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria.None, toSequenceNr = 3)
|
|
val persistentActor = system.actorOf(Props(classOf[LoadSnapshotTestPersistentActor], name, recovery, testActor))
|
|
|
|
expectMsg("a-1")
|
|
expectMsg("b-2")
|
|
expectMsg("c-3")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"support single snapshot deletions" in {
|
|
val deleteProbe = TestProbe()
|
|
|
|
// recover persistentActor from 3rd snapshot and then delete snapshot
|
|
val recovery = Recovery(toSequenceNr = 4)
|
|
val persistentActor1 = system.actorOf(Props(classOf[DeleteSnapshotTestPersistentActor], name, recovery, testActor))
|
|
val persistenceId = name
|
|
|
|
system.eventStream.subscribe(deleteProbe.ref, classOf[DeleteSnapshot])
|
|
|
|
persistentActor1 ! "done"
|
|
|
|
val metadata = expectMsgPF() {
|
|
case SnapshotOffer(md @ SnapshotMetadata(`persistenceId`, 4, _), state) ⇒
|
|
state should ===(List("a-1", "b-2", "c-3", "d-4").reverse)
|
|
md
|
|
}
|
|
expectMsg(RecoveryCompleted)
|
|
expectMsg("done")
|
|
|
|
persistentActor1 ! Delete1(metadata)
|
|
deleteProbe.expectMsgType[DeleteSnapshot]
|
|
expectMsgPF() { case m @ DeleteSnapshotSuccess(SnapshotMetadata(`persistenceId`, 4, _)) ⇒ }
|
|
|
|
// recover persistentActor from 2nd snapshot (3rd was deleted) plus replayed messages
|
|
val persistentActor2 = system.actorOf(Props(classOf[DeleteSnapshotTestPersistentActor], name, recovery, testActor))
|
|
|
|
expectMsgPF(hint = "" + SnapshotOffer(SnapshotMetadata(`persistenceId`, 2, 0), null)) {
|
|
case SnapshotOffer(md @ SnapshotMetadata(`persistenceId`, 2, _), state) ⇒
|
|
state should ===(List("a-1", "b-2").reverse)
|
|
md
|
|
}
|
|
expectMsg("c-3")
|
|
expectMsg("d-4")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
"support bulk snapshot deletions" in {
|
|
val deleteProbe = TestProbe()
|
|
|
|
val recovery = Recovery(toSequenceNr = 4)
|
|
val persistentActor1 = system.actorOf(Props(classOf[DeleteSnapshotTestPersistentActor], name, recovery, testActor))
|
|
val persistenceId = name
|
|
|
|
system.eventStream.subscribe(deleteProbe.ref, classOf[DeleteSnapshots])
|
|
|
|
// recover persistentActor and the delete first three (= all) snapshots
|
|
val criteria = SnapshotSelectionCriteria(maxSequenceNr = 4)
|
|
persistentActor1 ! DeleteN(criteria)
|
|
expectMsgPF() {
|
|
case SnapshotOffer(md @ SnapshotMetadata(`persistenceId`, 4, _), state) ⇒
|
|
state should ===(List("a-1", "b-2", "c-3", "d-4").reverse)
|
|
}
|
|
expectMsg(RecoveryCompleted)
|
|
deleteProbe.expectMsgType[DeleteSnapshots]
|
|
expectMsgPF() { case DeleteSnapshotsSuccess(`criteria`) ⇒ }
|
|
|
|
// recover persistentActor from replayed messages (all snapshots deleted)
|
|
val persistentActor2 = system.actorOf(Props(classOf[DeleteSnapshotTestPersistentActor], name, recovery, testActor))
|
|
|
|
expectMsg("a-1")
|
|
expectMsg("b-2")
|
|
expectMsg("c-3")
|
|
expectMsg("d-4")
|
|
expectMsg(RecoveryCompleted)
|
|
}
|
|
}
|
|
}
|