Skip to content

Commit da0458a

Browse files
authored
Merge pull request #181 from lerna-stack/add-unit-tests-of-snapshot-store
Add unit tests of SnapshotStore
2 parents a074852 + c971d00 commit da0458a

File tree

5 files changed

+216
-46
lines changed

5 files changed

+216
-46
lines changed

src/main/scala/lerna/akka/entityreplication/raft/snapshot/SnapshotStore.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ private[entityreplication] object SnapshotStore {
1717
selfMemberIndex: MemberIndex,
1818
): Props =
1919
Props(new SnapshotStore(typeName, entityId, settings, selfMemberIndex))
20+
21+
/** Returns a persistence ID of SnapshotStore */
22+
def persistenceId(typeName: TypeName, entityId: NormalizedEntityId, selfMemberIndex: MemberIndex): String =
23+
ActorIds.persistenceId("SnapshotStore", typeName.underlying, entityId.underlying, selfMemberIndex.role)
24+
2025
}
2126

2227
private[entityreplication] class SnapshotStore(
@@ -29,7 +34,7 @@ private[entityreplication] class SnapshotStore(
2934
import SnapshotProtocol._
3035

3136
override def persistenceId: String =
32-
ActorIds.persistenceId("SnapshotStore", typeName.underlying, entityId.underlying, selfMemberIndex.role)
37+
SnapshotStore.persistenceId(typeName, entityId, selfMemberIndex)
3338

3439
override def journalPluginId: String = settings.journalPluginId
3540

Lines changed: 113 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,43 @@
11
package lerna.akka.entityreplication.raft.snapshot
22

3+
import akka.actor.testkit.typed.scaladsl.LoggingTestKit
4+
import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps
5+
36
import java.util.concurrent.atomic.AtomicInteger
47
import akka.actor.{ ActorRef, ActorSystem }
58
import akka.persistence.testkit.scaladsl.SnapshotTestKit
6-
import akka.persistence.testkit.{ PersistenceTestKitPlugin, PersistenceTestKitSnapshotPlugin }
9+
import akka.persistence.testkit.{
10+
ProcessingResult,
11+
ProcessingSuccess,
12+
SnapshotOperation,
13+
SnapshotStorage,
14+
WriteSnapshot,
15+
}
716
import akka.testkit.TestKit
8-
import com.typesafe.config.{ Config, ConfigFactory }
917
import lerna.akka.entityreplication.model.{ NormalizedEntityId, TypeName }
1018
import lerna.akka.entityreplication.raft.model.LogEntryIndex
1119
import lerna.akka.entityreplication.raft.routing.MemberIndex
12-
import lerna.akka.entityreplication.raft.snapshot.ShardSnapshotStoreFailureSpec._
1320
import lerna.akka.entityreplication.raft.snapshot.SnapshotProtocol._
1421
import lerna.akka.entityreplication.raft.{ ActorSpec, RaftSettings }
1522
import lerna.akka.entityreplication.testkit.KryoSerializable
1623

24+
import scala.concurrent.Promise
25+
import scala.util.Using
26+
1727
object ShardSnapshotStoreFailureSpec {
1828
final case object DummyState extends KryoSerializable
19-
20-
def configWithPersistenceTestKits: Config = {
21-
PersistenceTestKitPlugin.config
22-
.withFallback(PersistenceTestKitSnapshotPlugin.config)
23-
.withFallback(raftPersistenceConfigWithPersistenceTestKits)
24-
.withFallback(ConfigFactory.load())
25-
}
26-
27-
private val raftPersistenceConfigWithPersistenceTestKits: Config = ConfigFactory.parseString(
28-
s"""
29-
|lerna.akka.entityreplication.raft.persistence {
30-
| journal.plugin = ${PersistenceTestKitPlugin.PluginId}
31-
| snapshot-store.plugin = ${PersistenceTestKitSnapshotPlugin.PluginId}
32-
| # Might be possible to use PersistenceTestKitReadJournal
33-
| // query.plugin = ""
34-
|}
35-
|""".stripMargin,
36-
)
37-
3829
}
3930

4031
class ShardSnapshotStoreFailureSpec
4132
extends TestKit(
42-
ActorSystem("ShardSnapshotStoreFailureSpec", ShardSnapshotStoreFailureSpec.configWithPersistenceTestKits),
33+
ActorSystem("ShardSnapshotStoreFailureSpec", ShardSnapshotStoreSpecBase.configWithPersistenceTestKits),
4334
)
4435
with ActorSpec {
36+
import ShardSnapshotStoreFailureSpec._
4537

4638
private val snapshotTestKit = SnapshotTestKit(system)
39+
private val typeName = TypeName.from("test")
40+
private val memberIndex = MemberIndex("test-role")
4741

4842
override def beforeEach(): Unit = {
4943
super.beforeEach()
@@ -55,9 +49,9 @@ class ShardSnapshotStoreFailureSpec
5549
planAutoKill {
5650
childActorOf(
5751
ShardSnapshotStore.props(
58-
TypeName.from("test"),
52+
typeName,
5953
RaftSettings(system.settings.config),
60-
MemberIndex("test-role"),
54+
memberIndex,
6155
),
6256
)
6357
}
@@ -70,10 +64,11 @@ class ShardSnapshotStoreFailureSpec
7064
"ShardSnapshotStore(読み込みの異常)" should {
7165

7266
"FetchSnapshot に失敗した場合は応答無し(クライアント側でタイムアウトの実装が必要)" in {
73-
val entityId = generateUniqueEntityId()
74-
val shardSnapshotStore = createShardSnapshotStore()
67+
val entityId = generateUniqueEntityId()
68+
val shardSnapshotStore = createShardSnapshotStore()
69+
val snapshotStorePersistenceId = SnapshotStore.persistenceId(typeName, entityId, memberIndex)
7570

76-
snapshotTestKit.failNextRead()
71+
snapshotTestKit.failNextRead(snapshotStorePersistenceId)
7772
shardSnapshotStore ! FetchSnapshot(entityId, replyTo = testActor)
7873
expectNoMessage()
7974
}
@@ -82,16 +77,100 @@ class ShardSnapshotStoreFailureSpec
8277
"ShardSnapshotStore(書き込みの異常)" should {
8378

8479
"SaveSnapshot に失敗した場合は SaveSnapshotFailure が返信される" in {
80+
val entityId = generateUniqueEntityId()
81+
val shardSnapshotStore = createShardSnapshotStore()
82+
val snapshotStorePersistenceId = SnapshotStore.persistenceId(typeName, entityId, memberIndex)
83+
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
84+
val dummyEntityState = EntityState(DummyState)
85+
val snapshot = EntitySnapshot(metadata, dummyEntityState)
86+
87+
snapshotTestKit.failNextPersisted(snapshotStorePersistenceId)
88+
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
89+
expectMsg(SaveSnapshotFailure(metadata))
90+
}
91+
}
92+
93+
"ShardSnapshotStore (with time-consuming writes)" should {
94+
95+
// Emulates a time-consuming write
96+
class TimeConsumingWriteSnapshotPolicy extends SnapshotStorage.SnapshotPolicies.PolicyType with AutoCloseable {
97+
val processingResultPromise = Promise[ProcessingResult]()
98+
override def tryProcess(persistenceId: String, processingUnit: SnapshotOperation): ProcessingResult = {
99+
processingUnit match {
100+
case _: WriteSnapshot => processingResultPromise.future.await
101+
case _ => ProcessingSuccess
102+
}
103+
}
104+
override def close(): Unit = {
105+
processingResultPromise.trySuccess(ProcessingSuccess)
106+
}
107+
}
108+
109+
"reply with `SnapshotNotFound` to `FetchSnapshot` if it has no EntitySnapshot and is saving an EntitySnapshot" ignore {
110+
// TODO Change SnapshotStore.savingSnapshot such that this test passes.
85111
val entityId = generateUniqueEntityId()
86112
val shardSnapshotStore = createShardSnapshotStore()
87-
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
88-
val dummyEntityState = EntityState(DummyState)
89-
val snapshot = EntitySnapshot(metadata, dummyEntityState)
113+
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex(1))
114+
val snapshot = EntitySnapshot(metadata, EntityState(DummyState))
115+
116+
Using(new TimeConsumingWriteSnapshotPolicy()) { timeConsumingWriteSnapshotPolicy =>
117+
// Prepare: SnapshotStore is saving the snapshot
118+
snapshotTestKit.withPolicy(timeConsumingWriteSnapshotPolicy)
119+
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
120+
121+
// Test:
122+
shardSnapshotStore ! FetchSnapshot(entityId, replyTo = testActor)
123+
expectMsg(SnapshotNotFound)
124+
}
125+
}
90126

91-
snapshotTestKit.failNextPersisted()
92-
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
93-
expectMsg(SaveSnapshotFailure(metadata))
127+
"reply with `SnapshotFound` to `FetchSnapshot` if it has an EntitySnapshot and is saving a new EntitySnapshot" in {
128+
val entityId = generateUniqueEntityId()
129+
val shardSnapshotStore = createShardSnapshotStore()
130+
131+
val firstSnapshotMetadata = EntitySnapshotMetadata(entityId, LogEntryIndex(1))
132+
val firstSnapshot =
133+
EntitySnapshot(firstSnapshotMetadata, EntityState(DummyState))
134+
shardSnapshotStore ! SaveSnapshot(firstSnapshot, replyTo = testActor)
135+
expectMsg(SaveSnapshotSuccess(firstSnapshotMetadata))
136+
137+
Using(new TimeConsumingWriteSnapshotPolicy()) { timeConsumingWriteSnapshotPolicy =>
138+
// Prepare: SnapshotStore is saving the second snapshot
139+
snapshotTestKit.withPolicy(timeConsumingWriteSnapshotPolicy)
140+
val secondSnapshot =
141+
EntitySnapshot(EntitySnapshotMetadata(entityId, LogEntryIndex(5)), EntityState(DummyState))
142+
shardSnapshotStore ! SaveSnapshot(secondSnapshot, replyTo = testActor)
143+
144+
// Test:
145+
shardSnapshotStore ! FetchSnapshot(entityId, replyTo = testActor)
146+
expectMsg(SnapshotFound(firstSnapshot))
147+
}
94148
}
149+
150+
"reply with nothing to `SaveSnapshot` and log a warning if it is saving an EntitySnapshot" in {
151+
implicit val typedSystem: akka.actor.typed.ActorSystem[Nothing] = system.toTyped
152+
153+
val entityId = generateUniqueEntityId()
154+
val shardSnapshotStore = createShardSnapshotStore()
155+
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex(1))
156+
val snapshot = EntitySnapshot(metadata, EntityState(DummyState))
157+
158+
Using(new TimeConsumingWriteSnapshotPolicy()) { timeConsumingWriteSnapshotPolicy =>
159+
// Prepare: SnapshotStore is saving the snapshot
160+
snapshotTestKit.withPolicy(timeConsumingWriteSnapshotPolicy)
161+
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
162+
163+
// Test:
164+
LoggingTestKit
165+
.warn(
166+
s"Saving snapshot for an entity ($entityId) currently. Consider to increase log-size-threshold or log-size-check-interval.",
167+
).expect {
168+
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
169+
}
170+
expectNoMessage()
171+
}
172+
}
173+
95174
}
96175

97176
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package lerna.akka.entityreplication.raft.snapshot
2+
3+
import akka.persistence.testkit.{ PersistenceTestKitPlugin, PersistenceTestKitSnapshotPlugin }
4+
import com.typesafe.config.{ Config, ConfigFactory }
5+
6+
object ShardSnapshotStoreSpecBase {
7+
8+
def configWithPersistenceTestKits: Config = {
9+
PersistenceTestKitPlugin.config
10+
.withFallback(PersistenceTestKitSnapshotPlugin.config)
11+
.withFallback(raftPersistenceConfigWithPersistenceTestKits)
12+
.withFallback(ConfigFactory.load())
13+
}
14+
15+
private val raftPersistenceConfigWithPersistenceTestKits: Config = ConfigFactory.parseString(
16+
s"""
17+
|lerna.akka.entityreplication.raft.persistence {
18+
| journal.plugin = ${PersistenceTestKitPlugin.PluginId}
19+
| snapshot-store.plugin = ${PersistenceTestKitSnapshotPlugin.PluginId}
20+
| # Might be possible to use PersistenceTestKitReadJournal
21+
| // query.plugin = ""
22+
|}
23+
|""".stripMargin,
24+
)
25+
26+
}

src/test/scala/lerna/akka/entityreplication/raft/snapshot/ShardSnapshotStoreSuccessSpec.scala

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package lerna.akka.entityreplication.raft.snapshot
22

33
import java.util.concurrent.atomic.AtomicInteger
44
import akka.actor.{ ActorRef, ActorSystem, PoisonPill }
5+
import akka.persistence.testkit.scaladsl.SnapshotTestKit
56
import akka.testkit.TestKit
67
import com.typesafe.config.{ Config, ConfigFactory }
78
import lerna.akka.entityreplication.model.{ NormalizedEntityId, TypeName }
@@ -14,31 +15,66 @@ object ShardSnapshotStoreSuccessSpec {
1415
final case object DummyState extends KryoSerializable
1516
}
1617

17-
class ShardSnapshotStoreSuccessSpec extends TestKit(ActorSystem()) with ActorSpec {
18+
class ShardSnapshotStoreSuccessSpec
19+
extends TestKit(
20+
ActorSystem("ShardSnapshotStoreSuccessSpec", ShardSnapshotStoreSpecBase.configWithPersistenceTestKits),
21+
)
22+
with ActorSpec {
1823
import ShardSnapshotStoreSuccessSpec._
1924
import lerna.akka.entityreplication.raft.snapshot.SnapshotProtocol._
2025

26+
private val snapshotTestKit = SnapshotTestKit(system)
27+
private val typeName = TypeName.from("test")
28+
private val memberIndex = MemberIndex("test-role")
2129
private[this] val dummyEntityState = EntityState(DummyState)
2230

31+
override def beforeEach(): Unit = {
32+
super.beforeEach()
33+
snapshotTestKit.clearAll()
34+
snapshotTestKit.resetPolicy()
35+
}
36+
2337
"ShardSnapshotStore(正常系)" should {
2438

2539
"SaveSnapshot に成功した場合は SaveSnapshotSuccess が返信される" in {
26-
val entityId = generateUniqueEntityId()
27-
val shardSnapshotStore = createShardSnapshotStore()
28-
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
29-
val snapshot = EntitySnapshot(metadata, dummyEntityState)
40+
val entityId = generateUniqueEntityId()
41+
val shardSnapshotStore = createShardSnapshotStore()
42+
val snapshotStorePersistenceId = SnapshotStore.persistenceId(typeName, entityId, memberIndex)
43+
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
44+
val snapshot = EntitySnapshot(metadata, dummyEntityState)
45+
46+
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
47+
snapshotTestKit.expectNextPersisted(snapshotStorePersistenceId, snapshot)
48+
expectMsg(SaveSnapshotSuccess(metadata))
49+
}
50+
51+
"persist nothing and reply with SaveSnapshotSuccess to SaveSnapshot if it has the same EntitySnapshot" in {
52+
val entityId = generateUniqueEntityId()
53+
val shardSnapshotStore = createShardSnapshotStore()
54+
val snapshotStorePersistenceId = SnapshotStore.persistenceId(typeName, entityId, memberIndex)
55+
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
56+
val snapshot = EntitySnapshot(metadata, dummyEntityState)
57+
58+
// Prepare:
59+
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
60+
snapshotTestKit.expectNextPersisted(snapshotStorePersistenceId, snapshot)
61+
expectMsg(SaveSnapshotSuccess(metadata))
3062

63+
// Test:
3164
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
65+
snapshotTestKit.expectNothingPersisted(snapshotStorePersistenceId)
3266
expectMsg(SaveSnapshotSuccess(metadata))
3367
}
3468

3569
"FetchSnapshot に成功した場合は一度停止しても SnapshotFound でスナップショットが返信される" in {
36-
val entityId = generateUniqueEntityId()
37-
val shardSnapshotStore = createShardSnapshotStore()
38-
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
39-
val snapshot = EntitySnapshot(metadata, dummyEntityState)
70+
val entityId = generateUniqueEntityId()
71+
val shardSnapshotStore = createShardSnapshotStore()
72+
val snapshotStorePersistenceId = SnapshotStore.persistenceId(typeName, entityId, memberIndex)
73+
val metadata = EntitySnapshotMetadata(entityId, LogEntryIndex.initial())
74+
val snapshot = EntitySnapshot(metadata, dummyEntityState)
4075

4176
shardSnapshotStore ! SaveSnapshot(snapshot, replyTo = testActor)
77+
snapshotTestKit.expectNextPersisted(snapshotStorePersistenceId, snapshot)
4278
expectMsg(SaveSnapshotSuccess(metadata))
4379

4480
// terminate SnapshotStore
@@ -84,9 +120,9 @@ class ShardSnapshotStoreSuccessSpec extends TestKit(ActorSystem()) with ActorSpe
84120
planAutoKill {
85121
childActorOf(
86122
ShardSnapshotStore.props(
87-
TypeName.from("test"),
123+
typeName,
88124
RaftSettings(additionalConfig.withFallback(system.settings.config)),
89-
MemberIndex("test-role"),
125+
memberIndex,
90126
),
91127
)
92128
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package lerna.akka.entityreplication.raft.snapshot
2+
3+
import akka.actor.ActorSystem
4+
import akka.testkit.TestKit
5+
import lerna.akka.entityreplication.model.{ NormalizedEntityId, TypeName }
6+
import lerna.akka.entityreplication.raft.ActorSpec
7+
import lerna.akka.entityreplication.raft.routing.MemberIndex
8+
9+
final class SnapshotStoreSpec extends TestKit(ActorSystem("SnapshotStoreSpec")) with ActorSpec {
10+
11+
"SnapshotStore.persistenceId" should {
12+
13+
"return a persistence ID for the given type name, entity ID, and member index" in {
14+
val persistenceId = SnapshotStore.persistenceId(
15+
TypeName.from("test-type-name"),
16+
NormalizedEntityId.from("test-entity-id"),
17+
MemberIndex("test-member-index"),
18+
)
19+
assert(persistenceId === "SnapshotStore:test-type-name:test-entity-id:test-member-index")
20+
}
21+
22+
}
23+
24+
}

0 commit comments

Comments
 (0)