Skip to content

Commit 48ab644

Browse files
authored
Merge pull request #104 from lerna-stack/improve-multi-jvm-test-stability
Improve test stability
2 parents c90fb00 + cdd2f41 commit 48ab644

16 files changed

+186
-110
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ on:
77
branches: [ master, feature/** ]
88

99
env:
10-
SBT_OPTS: -Dlerna.enable.discipline
10+
# classic timefactor doesn't affect classic default-timeout
11+
SBT_OPTS: >-
12+
-Dlerna.enable.discipline
13+
-Dakka.test.timefactor=3.0
14+
-Dakka.actor.testkit.typed.timefactor=3.0
15+
-Dakka.test.default-timeout=15s
16+
-Dakka.testconductor.barrier-timeout=90s
1117
1218
jobs:
1319
test:
@@ -51,7 +57,7 @@ jobs:
5157

5258
- name: Run integration tests
5359
continue-on-error: true # results are reported by action-junit-report
54-
run: sh ./scripts/run-multijvm-test.sh 6
60+
run: sh ./scripts/run-multijvm-test.sh 1
5561

5662
- name: Publish test report
5763
uses: mikepenz/action-junit-report@v2

build.sbt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ lazy val lerna = (project in file("."))
3232
name := "akka-entity-replication",
3333
fork in Test := true,
3434
parallelExecution in Test := false,
35+
javaOptions in Test ++= sbtJavaOptions,
36+
jvmOptions in MultiJvm ++= sbtJavaOptions,
3537
libraryDependencies ++= Seq(
3638
"com.typesafe.akka" %% "akka-cluster-typed" % akkaVersion,
3739
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
@@ -83,6 +85,20 @@ lazy val lerna = (project in file("."))
8385
mimaReportSignatureProblems := true, // check also generic parameters
8486
)
8587

88+
/**
89+
* This is used to pass specific system properties (mostly from CI environment variables)
90+
* to the forked process by sbt
91+
*/
92+
val sbtJavaOptions: Seq[String] = {
93+
// selects properties starting with the following
94+
val includes = Set(
95+
"akka.",
96+
)
97+
sys.props.collect {
98+
case (k, v) if includes.exists(k.startsWith) => s"-D$k=$v"
99+
}.toSeq
100+
}
101+
86102
addCommandAlias(
87103
"testCoverage",
88104
Seq(

src/multi-jvm/resources/multi-jvm-testing.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ akka.actor {
2828
}
2929
}
3030

31+
akka.cluster {
32+
// Not rquired for tesintg
33+
jmx.enabled = off
34+
}
35+
3136
akka.test {
3237
single-expect-default = 15s
3338
}

src/multi-jvm/scala/lerna/akka/entityreplication/ConsistencyTestNormal.scala

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.util.concurrent.atomic.AtomicInteger
44
import akka.actor.{ ActorRef, Props }
55
import akka.remote.testkit.MultiNodeSpec
66
import lerna.akka.entityreplication.ConsistencyTestBase.{ ConsistencyTestBaseConfig, ConsistencyTestReplicationActor }
7+
import lerna.akka.entityreplication.util.AtLeastOnceComplete
78
import org.scalatest.Inside
89

910
import scala.annotation.nowarn
@@ -45,16 +46,16 @@ class ConsistencyTestNormal extends MultiNodeSpec(ConsistencyTestBaseConfig) wit
4546
extractShardId = ConsistencyTestReplicationActor.extractShardId,
4647
)
4748

49+
enterBarrier("ClusterReplication started")
50+
4851
// check the ClusterReplication healthiness
4952
val requestId = generateUniqueId()
50-
awaitAssert {
51-
clusterReplication ! GetStatus(id = "check-healthiness", requestId)
52-
expectMsgType[Status](max = 1.seconds)
53-
}
54-
ignoreMsg {
55-
// ignore Status messages that were sent for checking healthy
56-
case Status(_, `requestId`) => true
57-
}
53+
AtLeastOnceComplete
54+
.askTo(
55+
clusterReplication,
56+
GetStatus(id = "check-healthiness", requestId),
57+
retryInterval = 1.seconds,
58+
).await
5859
}
5960

6061
"正常系(直列に処理した場合)" should {

src/multi-jvm/scala/lerna/akka/entityreplication/RaftActorCompactionSpec.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class RaftActorCompactionSpec extends MultiNodeSpec(RaftActorCompactionSpecConfi
153153
runOn(node1, node2, node3) {
154154
awaitAssert {
155155
replicationActor(myself) ! DummyReplicationActor.GetState(entityId)
156-
expectMsg(DummyReplicationActor.State(1))
156+
expectMsg(max = 1.second, DummyReplicationActor.State(1))
157157
}
158158
}
159159
enterBarrier("all ReplicationActor applied an event")
@@ -178,14 +178,14 @@ class RaftActorCompactionSpec extends MultiNodeSpec(RaftActorCompactionSpecConfi
178178
// ReplicationActor which has not been isolated applied all events
179179
awaitAssert {
180180
replicationActor(myself) ! DummyReplicationActor.GetState(entityId)
181-
expectMsg(DummyReplicationActor.State(4))
181+
expectMsg(max = 1.second, DummyReplicationActor.State(4))
182182
}
183183
}
184184
runOn(node3) {
185185
// ReplicationActor which has been isolated did not apply any events
186186
awaitAssert {
187187
replicationActor(myself) ! DummyReplicationActor.GetState(entityId)
188-
expectMsg(DummyReplicationActor.State(1))
188+
expectMsg(max = 1.second, DummyReplicationActor.State(1))
189189
}
190190
}
191191
enterBarrier("ReplicationActor which has not been isolated applied all events")

src/multi-jvm/scala/lerna/akka/entityreplication/RaftEventSourcedSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,14 @@ class RaftEventSourcedSpec extends MultiNodeSpec(RaftEventSourcedSpecConfig) wit
199199
awaitAssert(
200200
{
201201
clusterReplication ! DummyReplicationActor.Increment(entityId, requestId1, amount = 3)
202-
expectMsgType[DummyReplicationActor.State].knownRequestId should contain(requestId1)
202+
expectMsgType[DummyReplicationActor.State](max = 1.second).knownRequestId should contain(requestId1)
203203
},
204204
initializationTimeout,
205205
)
206206

207207
awaitAssert {
208208
clusterReplication ! DummyReplicationActor.Increment(entityId, requestId2, amount = 10)
209-
expectMsgType[DummyReplicationActor.State].knownRequestId should contain(requestId2)
209+
expectMsgType[DummyReplicationActor.State](max = 1.second).knownRequestId should contain(requestId2)
210210
}
211211

212212
val readJournal =

src/multi-jvm/scala/lerna/akka/entityreplication/ReplicationRegionSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class ReplicationRegionSpec extends MultiNodeSpec(ReplicationRegionSpecConfig) w
285285
val replicationActorPath = s"/system/sharding/raft-shard-$typeName-$role/$clusterShard/$raftShardId/$entityId"
286286
awaitAssert {
287287
system.actorSelection(replicationActorPath) ! GetStatus(entityId)
288-
expectMsg(Status(3))
288+
expectMsg(max = 1.second, Status(3))
289289
}
290290
}
291291
}

src/multi-jvm/scala/lerna/akka/entityreplication/raft/RaftActorMultiNodeSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,6 @@ class RaftActorMultiNodeSpec extends MultiNodeSpec(RaftActorSpecConfig) with STM
654654

655655
protected def getState(raftActor: ActorRef): RaftState = {
656656
raftActor ! GetState
657-
expectMsgType[RaftState]
657+
expectMsgType[RaftState](max = 1.second)
658658
}
659659
}

src/multi-jvm/scala/lerna/akka/entityreplication/typed/ReplicatedEntityMultiNodeSpec.scala

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package lerna.akka.entityreplication.typed
22

33
import akka.actor.testkit.typed.scaladsl.ActorTestKit
4-
import akka.actor.typed.ActorRef
4+
import akka.actor.typed.{ ActorRef, ActorSystem }
55
import akka.actor.typed.receptionist.{ Receptionist, ServiceKey }
66
import akka.actor.typed.scaladsl.Behaviors
77
import akka.actor.typed.scaladsl.adapter._
88
import akka.remote.testconductor.RoleName
99
import akka.remote.testkit.{ MultiNodeConfig, MultiNodeSpec }
1010
import com.typesafe.config.{ ConfigFactory, ConfigValueFactory }
1111
import lerna.akka.entityreplication.raft.routing.MemberIndex
12+
import lerna.akka.entityreplication.util.AtLeastOnceComplete
1213
import lerna.akka.entityreplication.{ STMultiNodeSerializable, STMultiNodeSpec }
1314

15+
import java.util.UUID
1416
import java.util.concurrent.atomic.AtomicInteger
1517
import scala.jdk.CollectionConverters._
1618

@@ -53,7 +55,7 @@ class ReplicatedEntityMultiNodeSpec extends MultiNodeSpec(ReplicatedEntityMultiN
5355
import ReplicatedEntityMultiNodeSpec._
5456
import ReplicatedEntityMultiNodeSpecConfig._
5557

56-
private[this] val typedSystem = system.toTyped
58+
private implicit val typedSystem: ActorSystem[_] = system.toTyped
5759

5860
private[this] val clusterReplication = ClusterReplication(typedSystem)
5961

@@ -90,28 +92,30 @@ class ReplicatedEntityMultiNodeSpec extends MultiNodeSpec(ReplicatedEntityMultiN
9092
val probe = actorTestKit.createTestProbe[Receptionist.Listing]()
9193

9294
runOn(node1) {
93-
val entity = clusterReplication.entityRefFor(PingPongEntity.typeKey, createSeqEntityId())
94-
val replyTo = actorTestKit.createTestProbe[PingPongEntity.Pong]()
95+
val entity = clusterReplication.entityRefFor(PingPongEntity.typeKey, createSeqEntityId())
9596

96-
entity ! PingPongEntity.Ping(replyTo.ref)
97-
replyTo.receiveMessage().count should be(1)
98-
entity ! PingPongEntity.Ping(replyTo.ref)
99-
replyTo.receiveMessage().count should be(2)
97+
def ping(requestId: String): PingPongEntity.Pong = {
98+
AtLeastOnceComplete
99+
.askTo(entity, PingPongEntity.Ping(_, requestId), retryInterval = remainingOrDefault / 5).await
100+
}
101+
102+
ping(createUniqueRequestId()).count should be(1)
103+
ping(createUniqueRequestId()).count should be(2)
100104
}
101105
runOn(node1, node2, node3) {
102106
// find entity
103107
var actor: ActorRef[PingPongEntity.Command] = null
104108
awaitAssert {
105109
typedSystem.receptionist ! Receptionist.Subscribe(PingPongEntity.serviceKey, probe.ref)
106-
val listing = probe.receiveMessage()
110+
val listing = probe.receiveMessage(max = remainingOrDefault / 5)
107111
actor = listing.serviceInstances(PingPongEntity.serviceKey).head
108112
typedSystem.receptionist ! Receptionist.Deregister(PingPongEntity.serviceKey, actor)
109113
}
110114
// check state
111115
val stateReceiver = actorTestKit.createTestProbe[PingPongEntity.State]()
112116
awaitAssert {
113117
actor ! PingPongEntity.UnsafeGetState(stateReceiver.ref)
114-
stateReceiver.receiveMessage().count should be(2)
118+
stateReceiver.receiveMessage(max = remainingOrDefault / 5).count should be(2)
115119
}
116120
}
117121
}
@@ -120,18 +124,17 @@ class ReplicatedEntityMultiNodeSpec extends MultiNodeSpec(ReplicatedEntityMultiN
120124
clusterReplication.init(PingPongEntity())
121125

122126
runOn(node1) {
123-
val entity = clusterReplication.entityRefFor(PingPongEntity.typeKey, createSeqEntityId())
124-
val replyTo = actorTestKit.createTestProbe[PingPongEntity.Pong]()
127+
val entity = clusterReplication.entityRefFor(PingPongEntity.typeKey, createSeqEntityId())
125128

126-
entity ! PingPongEntity.Ping(replyTo.ref)
127-
replyTo.receiveMessage().count should be(1)
128-
entity ! PingPongEntity.Ping(replyTo.ref)
129-
replyTo.receiveMessage().count should be(2)
130-
entity ! PingPongEntity.Break()
131-
awaitAssert {
132-
entity ! PingPongEntity.Ping(replyTo.ref)
133-
replyTo.receiveMessage(max = remainingOrDefault / 5).count should be(3)
129+
def ping(requestId: String): PingPongEntity.Pong = {
130+
AtLeastOnceComplete
131+
.askTo(entity, PingPongEntity.Ping(_, requestId), retryInterval = remainingOrDefault / 5).await
134132
}
133+
134+
ping(createUniqueRequestId()).count should be(1)
135+
ping(createUniqueRequestId()).count should be(2)
136+
entity ! PingPongEntity.Break()
137+
ping(createUniqueRequestId()).count should be(3)
135138
}
136139
}
137140

@@ -147,7 +150,7 @@ class ReplicatedEntityMultiNodeSpec extends MultiNodeSpec(ReplicatedEntityMultiN
147150
runOn(node1, node2, node3) {
148151
awaitAssert {
149152
typedSystem.receptionist ! Receptionist.Subscribe(EphemeralEntity.serviceKey, probe.ref)
150-
val listing = probe.receiveMessage()
153+
val listing = probe.receiveMessage(max = remainingOrDefault / 5)
151154
actor = listing.serviceInstances(EphemeralEntity.serviceKey).head
152155
typedSystem.receptionist ! Receptionist.Deregister(EphemeralEntity.serviceKey, actor)
153156
}
@@ -182,7 +185,7 @@ class ReplicatedEntityMultiNodeSpec extends MultiNodeSpec(ReplicatedEntityMultiN
182185
val subscriber = actorTestKit.createTestProbe[Receptionist.Listing]()
183186
awaitAssert {
184187
typedSystem.receptionist ! Receptionist.Subscribe(EphemeralEntity.serviceKey, subscriber.ref)
185-
val listing = subscriber.receiveMessage()
188+
val listing = subscriber.receiveMessage(max = remainingOrDefault / 5)
186189
actor = listing.serviceInstances(EphemeralEntity.serviceKey).head
187190
typedSystem.receptionist ! Receptionist.Deregister(EphemeralEntity.serviceKey, actor)
188191
}
@@ -204,6 +207,8 @@ class ReplicatedEntityMultiNodeSpec extends MultiNodeSpec(ReplicatedEntityMultiN
204207

205208
private[this] val idGenerator = new AtomicInteger(0)
206209
private[this] def createSeqEntityId(): String = s"replication-${idGenerator.incrementAndGet()}"
210+
211+
private def createUniqueRequestId(): String = UUID.randomUUID().toString
207212
}
208213

209214
object ReplicatedEntityMultiNodeSpec {
@@ -212,23 +217,27 @@ object ReplicatedEntityMultiNodeSpec {
212217
val typeKey: ReplicatedEntityTypeKey[Command] = ReplicatedEntityTypeKey("PingPong")
213218
val serviceKey: ServiceKey[Command] = ServiceKey[Command]("PingPongService")
214219

215-
sealed trait Command extends STMultiNodeSerializable
216-
final case class Ping(replyTo: ActorRef[Pong]) extends Command
217-
final case class Pong(count: Int) extends STMultiNodeSerializable
218-
final case class Break() extends Command
219-
final case class UnsafeGetState(replyTo: ActorRef[State]) extends Command
220+
sealed trait Command extends STMultiNodeSerializable
221+
final case class Ping(replyTo: ActorRef[Pong], requestId: String) extends Command
222+
final case class Pong(count: Int) extends STMultiNodeSerializable
223+
final case class Break() extends Command
224+
final case class UnsafeGetState(replyTo: ActorRef[State]) extends Command
220225

221-
sealed trait Event extends STMultiNodeSerializable
222-
final case class CountUp() extends Event
226+
sealed trait Event extends STMultiNodeSerializable
227+
final case class CountUp(requestId: String) extends Event
223228

224-
final case class State(count: Int) extends STMultiNodeSerializable {
229+
final case class State(count: Int, processedRequests: Set[String]) extends STMultiNodeSerializable {
225230

226231
def onMessage(message: Command): Effect[Event, State] =
227232
message match {
228-
case Ping(replyTo) =>
229-
Effect
230-
.replicate(CountUp())
231-
.thenReply(replyTo)(s => Pong(s.count))
233+
case Ping(replyTo, requestId) =>
234+
if (processedRequests.contains(requestId)) {
235+
Effect.reply(replyTo)(Pong(count))
236+
} else {
237+
Effect
238+
.replicate(CountUp(requestId))
239+
.thenReply(replyTo)(s => Pong(s.count))
240+
}
232241
case Break() =>
233242
Effect
234243
.none[Event, State]
@@ -243,7 +252,7 @@ object ReplicatedEntityMultiNodeSpec {
243252

244253
def applyEvent(event: Event): State =
245254
event match {
246-
case _ => copy(count = count + 1)
255+
case CountUp(requestId) => copy(count = count + 1, processedRequests = processedRequests + requestId)
247256
}
248257
}
249258

@@ -253,7 +262,7 @@ object ReplicatedEntityMultiNodeSpec {
253262
context.system.receptionist ! Receptionist.Register(serviceKey, context.self)
254263
ReplicatedEntityBehavior[Command, Event, State](
255264
entityContext = entityContext,
256-
emptyState = State(count = 0),
265+
emptyState = State(count = 0, processedRequests = Set.empty),
257266
commandHandler = _ onMessage _,
258267
eventHandler = _ applyEvent _,
259268
)

src/multi-jvm/scala/lerna/akka/entityreplication/typed/ReplicatedEntitySnapshotMultiNodeSpec.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class ReplicatedEntitySnapshotMultiNodeSpec
9696
awaitAssert {
9797
// update state
9898
entityRef ! DummyEntity.Increment(transactionSeq1, amount = 1, replyTo.ref)
99-
replyTo.receiveMessage(1.seconds).count should be(1)
99+
replyTo.receiveMessage(500.millis).count should be(1)
100100
}
101101
}
102102
enterBarrier("sent a command")
@@ -106,7 +106,7 @@ class ReplicatedEntitySnapshotMultiNodeSpec
106106
awaitAssert {
107107
actorRef = findEntityActorRef()
108108
actorRef ! DummyEntity.GetState(replyTo.ref)
109-
replyTo.receiveMessage(1.seconds).count should be(1)
109+
replyTo.receiveMessage(500.millis).count should be(1)
110110
}
111111
}
112112
enterBarrier("all entities applied an event")
@@ -120,7 +120,7 @@ class ReplicatedEntitySnapshotMultiNodeSpec
120120
var latestCount = 1
121121
awaitAssert {
122122
entityRef ! DummyEntity.Increment(transactionSeq, amount = 1, replyTo.ref)
123-
val reply = replyTo.receiveMessage(1.seconds)
123+
val reply = replyTo.receiveMessage(500.millis)
124124
reply.count should be > latestCount
125125
latestCount = reply.count
126126
}
@@ -132,14 +132,14 @@ class ReplicatedEntitySnapshotMultiNodeSpec
132132
// entity which has not been isolated applied all events
133133
awaitAssert {
134134
actorRef ! DummyEntity.GetState(replyTo.ref)
135-
replyTo.receiveMessage(1.seconds).count should be(5)
135+
replyTo.receiveMessage(500.millis).count should be(5)
136136
}
137137
}
138138
runOn(node3) {
139139
// entity which has been isolated did not apply any events
140140
awaitAssert {
141141
actorRef ! DummyEntity.GetState(replyTo.ref)
142-
replyTo.receiveMessage(1.seconds).count should be(1)
142+
replyTo.receiveMessage(500.millis).count should be(1)
143143
}
144144
}
145145
enterBarrier("entity which has not been isolated applied all events")
@@ -151,11 +151,11 @@ class ReplicatedEntitySnapshotMultiNodeSpec
151151
awaitAssert {
152152
// attempt to create entities on all nodes
153153
entityRef ! DummyEntity.Increment(transactionSeq2, amount = 0, replyTo.ref)
154-
replyTo.receiveMessage(1.seconds)
154+
replyTo.receiveMessage(500.millis)
155155
// All ReplicationActor states will eventually be the same as any other after the isolation is resolved
156156
actorRef = findEntityActorRef()
157157
actorRef ! DummyEntity.GetState(replyTo.ref)
158-
replyTo.receiveMessage(1.seconds).count should be(5)
158+
replyTo.receiveMessage(500.millis).count should be(5)
159159
}
160160
}
161161
}

0 commit comments

Comments
 (0)