This is an automated email from the ASF dual-hosted git repository.
He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new a5cb8e9653 test: cover that harmless quarantine does not trigger
DownSelfQuarantinedByRemote (#3550)
a5cb8e9653 is described below
commit a5cb8e96538663637fdaa6d85e5116d5f5d7371d
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Oct 9 08:39:18 2026 +0100
test: cover that harmless quarantine does not trigger
DownSelfQuarantinedByRemote (#3550)
Motivation:
Port of akka/akka-core#32117 (backport of akka/akka#32050, issue
akka/akka#31095). That fix stopped `InboundQuarantineCheck` replying with a
`Quarantined` control message for later inbound messages from a harmlessly
quarantined (gracefully shut down) node, which made the remote node down itself
via `DownSelfQuarantinedByRemote` - notably during a network partition.
Pekko already stops propagating harmless quarantines by default: #1555
added `pekko.remote.artery.propagate-harmless-quarantine-events = off`, and
`QuarantinedTimestamp.harmless` / `newQuarantined(harmless)` already exist, so
no MiMa filters are needed. What was missing was the multi-jvm coverage of the
end-to-end cluster scenario.
Modification:
- `DowningWhenOtherHasQuarantinedThisActorSystemSpec`: ported the Akka
rewrite. Quarantines `second` directly via
`RARP(system).provider.quarantine(...)` instead of relying on split brain
resolver timing, asserts `second` receives `ThisActorSystemQuarantinedEvent`
and terminates within 5 s (shorter than `stable-after`, so it is not normal
downing), adds a `fourth` node and a new case "not be triggered by another node
shutting down during network partition" - the actual #31095 scenario [...]
- `Association` / `InboundQuarantineCheck`: log at info when a
`Quarantined` control message is sent, and mention "Harmless quarantine." in
the harmless quarantine log message, matching Akka. Log-only.
- `TestContext`: pass `harmless = false` explicitly, as in Akka.
Result:
Multi-jvm coverage of the `DownSelfQuarantinedByRemote` scenarios,
including harmless quarantine during a network partition. No behavioural change
beyond log messages.
Tests:
- `sbt "project cluster" "MultiJvm/testOnly
org.apache.pekko.cluster.DowningWhenOtherHasQuarantinedThisActorSystem"` - 4
nodes, 4 tests each, all passed
- `sbt "remote/testOnly
org.apache.pekko.remote.artery.HarmlessQuarantineSpec
org.apache.pekko.remote.artery.OutboundIdleShutdownSpec
org.apache.pekko.remote.artery.RemoteMessageSerializationSpec"` - 14 passed
- native scalafmt run on the changed files
- MiMa not run: no signature changes
References:
Refs #1555 - port of akka/akka-core#32117 (the `InboundQuarantineCheck` fix
was already present). Akka source is Apache-2.0 licensed as of 2023-09-20.
---
...henOtherHasQuarantinedThisActorSystemSpec.scala | 116 ++++++++++++++-------
.../apache/pekko/remote/artery/Association.scala | 5 +-
.../remote/artery/InboundQuarantineCheck.scala | 4 +-
.../apache/pekko/remote/artery/TestContext.scala | 2 +-
4 files changed, 85 insertions(+), 42 deletions(-)
diff --git
a/cluster/src/multi-jvm/scala/org/apache/pekko/cluster/DowningWhenOtherHasQuarantinedThisActorSystemSpec.scala
b/cluster/src/multi-jvm/scala/org/apache/pekko/cluster/DowningWhenOtherHasQuarantinedThisActorSystemSpec.scala
index 0e882f2bd9..22eb1ac0f1 100644
---
a/cluster/src/multi-jvm/scala/org/apache/pekko/cluster/DowningWhenOtherHasQuarantinedThisActorSystemSpec.scala
+++
b/cluster/src/multi-jvm/scala/org/apache/pekko/cluster/DowningWhenOtherHasQuarantinedThisActorSystemSpec.scala
@@ -16,14 +16,18 @@ package org.apache.pekko.cluster
import scala.concurrent.duration._
import org.apache.pekko
+import pekko.actor.ActorIdentity
import pekko.actor.ActorRef
import pekko.actor.Identify
import pekko.actor.RootActorPath
+import pekko.remote.RARP
import pekko.remote.artery.ArterySettings
import pekko.remote.artery.ThisActorSystemQuarantinedEvent
import pekko.remote.testkit.MultiNodeConfig
import pekko.remote.transport.ThrottlerTransportAdapter
+import pekko.testkit.EventFilter
import pekko.testkit.LongRunningTest
+import pekko.testkit.TestEvent.Mute
import com.typesafe.config.ConfigFactory
@@ -31,6 +35,7 @@ object DowningWhenOtherHasQuarantinedThisActorSystemSpec
extends MultiNodeConfig
val first = role("first")
val second = role("second")
val third = role("third")
+ val fourth = role("fourth")
commonConfig(
debugConfig(on = false)
@@ -39,15 +44,9 @@ object DowningWhenOtherHasQuarantinedThisActorSystemSpec
extends MultiNodeConfig
ConfigFactory.parseString("""
pekko.remote.artery.enabled = on
pekko.cluster.downing-provider-class =
"org.apache.pekko.cluster.sbr.SplitBrainResolverProvider"
- # speed up decision
- pekko.cluster.split-brain-resolver.stable-after = 5s
+ pekko.cluster.split-brain-resolver.stable-after = 10s
""")))
- // exaggerate the timing issue by ,making the second node decide slower
- // this is to more consistently repeat the scenario where the other side
completes downing
- // while the isolated part still has not made a decision and then see
quarantined connections from the other nodes
-
nodeConfig(second)(ConfigFactory.parseString("pekko.cluster.split-brain-resolver.stable-after
= 15s"))
-
testTransport(on = true)
}
@@ -57,11 +56,16 @@ class
DowningWhenOtherHasQuarantinedThisActorSystemMultiJvmNode2
extends DowningWhenOtherHasQuarantinedThisActorSystemSpec
class DowningWhenOtherHasQuarantinedThisActorSystemMultiJvmNode3
extends DowningWhenOtherHasQuarantinedThisActorSystemSpec
+class DowningWhenOtherHasQuarantinedThisActorSystemMultiJvmNode4
+ extends DowningWhenOtherHasQuarantinedThisActorSystemSpec
abstract class DowningWhenOtherHasQuarantinedThisActorSystemSpec
extends
MultiNodeClusterSpec(DowningWhenOtherHasQuarantinedThisActorSystemSpec) {
import DowningWhenOtherHasQuarantinedThisActorSystemSpec._
+ muteDeadLetters(classOf[ActorIdentity])()
+ system.eventStream.publish(Mute(EventFilter.info(pattern = ".*Ignoring
received gossip from unknown.*")))
+
"Cluster node downed by other" must {
if
(!ArterySettings(system.settings.config.getConfig("pekko.remote.artery")).Enabled)
{
@@ -71,43 +75,23 @@ abstract class
DowningWhenOtherHasQuarantinedThisActorSystemSpec
}
"join cluster" taggedAs LongRunningTest in {
- awaitClusterUp(first, second, third)
+ awaitClusterUp(first, second, third, fourth)
enterBarrier("after-1")
}
- "down itself" taggedAs LongRunningTest in {
- runOn(first) {
- testConductor.blackhole(first, second,
ThrottlerTransportAdapter.Direction.Both).await
- testConductor.blackhole(third, second,
ThrottlerTransportAdapter.Direction.Both).await
- }
- enterBarrier("blackhole")
-
- within(15.seconds) {
- runOn(first) {
- awaitAssert {
- cluster.state.unreachable.map(_.address) should
===(Set(address(second)))
- }
- awaitAssert {
- // second downed and removed
- cluster.state.members.map(_.address) should
===(Set(address(first), address(third)))
- }
- }
- runOn(second) {
- awaitAssert {
- cluster.state.unreachable.map(_.address) should
===(Set(address(first), address(third)))
- }
- }
+ "down itself with DownSelfQuarantinedByRemote when other has quarantined"
taggedAs LongRunningTest in {
+ runOn(first, second) {
+ system.eventStream.subscribe(testActor,
classOf[ThisActorSystemQuarantinedEvent])
}
- enterBarrier("down-second")
-
runOn(first) {
- testConductor.passThrough(first, second,
ThrottlerTransportAdapter.Direction.Both).await
- testConductor.passThrough(third, second,
ThrottlerTransportAdapter.Direction.Both).await
+ val secondUniqueAddress = cluster.state.members.find(_.address ==
address(second)).get.uniqueAddress
+ RARP(system).provider
+ .quarantine(secondUniqueAddress.address,
Some(secondUniqueAddress.longUid), "Quarantine from test")
}
- enterBarrier("pass-through")
+ enterBarrier("quarantined")
runOn(second) {
- within(10.seconds) {
+ within(5.seconds) { // this is shorter than
split-brain-resolver.stable-after, so it's not normal downing
awaitAssert {
// try to ping first (Cluster Heartbeat messages will not trigger
the Quarantine message)
system.actorSelection(RootActorPath(first) /
"user").tell(Identify(None), ActorRef.noSender)
@@ -115,6 +99,21 @@ abstract class
DowningWhenOtherHasQuarantinedThisActorSystemSpec
cluster.isTerminated should ===(true)
}
}
+ expectMsgType[ThisActorSystemQuarantinedEvent]
+ }
+ enterBarrier("second-shutdown")
+
+ runOn(first) {
+ expectNoMessage(1.second) // no ThisActorSystemQuarantinedEvent
+ }
+ enterBarrier("wait")
+
+ runOn(first) {
+ val sel = system.actorSelection(RootActorPath(second) / "user")
+ (1 to 15).foreach { _ =>
+ sel.tell(Identify(None), ActorRef.noSender) // try to ping second
+ expectNoMessage(200.millis) // no ThisActorSystemQuarantinedEvent
+ }
}
enterBarrier("after-2")
@@ -130,15 +129,56 @@ abstract class
DowningWhenOtherHasQuarantinedThisActorSystemSpec
cluster.shutdown()
}
+ runOn(first) {
+ expectNoMessage(1.second) // no ThisActorSystemQuarantinedEvent
+ }
+ enterBarrier("wait")
+
runOn(first) {
val sel = system.actorSelection(RootActorPath(third) / "user")
- (1 to 25).foreach { _ =>
+ (1 to 15).foreach { _ =>
sel.tell(Identify(None), ActorRef.noSender) // try to ping third
expectNoMessage(200.millis) // no ThisActorSystemQuarantinedEvent
}
}
- enterBarrier("after-2")
+ enterBarrier("after-3")
+ }
+
+ "not be triggered by another node shutting down during network partition"
taggedAs LongRunningTest in {
+ runOn(first) {
+ system.eventStream.subscribe(testActor,
classOf[ThisActorSystemQuarantinedEvent])
+ }
+ enterBarrier("subscribing")
+
+ runOn(first) {
+ testConductor.blackhole(first, fourth,
ThrottlerTransportAdapter.Direction.Both).await
+ }
+ enterBarrier("blackhole")
+
+ runOn(third) {
+ cluster.shutdown()
+ }
+
+ runOn(first) {
+ expectNoMessage(2.second) // no ThisActorSystemQuarantinedEvent
+ testConductor.passThrough(first, fourth,
ThrottlerTransportAdapter.Direction.Both).await
+ }
+
+ runOn(first) {
+ expectNoMessage(1.second) // no ThisActorSystemQuarantinedEvent
+ }
+ enterBarrier("wait")
+
+ runOn(first) {
+ val sel = system.actorSelection(RootActorPath(fourth) / "user")
+ (1 to 15).foreach { _ =>
+ sel.tell(Identify(None), ActorRef.noSender) // try to ping fourth
+ expectNoMessage(200.millis) // no ThisActorSystemQuarantinedEvent
+ }
+ }
+
+ enterBarrier("after-4")
}
}
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
b/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
index f0fc935036..19f6e3a025 100644
--- a/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
+++ b/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
@@ -560,8 +560,8 @@ private[remote] class Association(
// quarantine state change was performed
if (harmless) {
log.info(
- "Association to [{}] having UID [{}] has been stopped. All
" +
- "messages to this UID will be delivered to dead letters.
Reason: {}",
+ "Association to [{}] having UID [{}] has been stopped.
Harmless quarantine. " +
+ "All messages to this UID will be delivered to dead
letters. Reason: {}",
remoteAddress,
u,
reason)
@@ -585,6 +585,7 @@ private[remote] class Association(
send(ClearSystemMessageDelivery(current.incarnation),
OptionVal.None, OptionVal.None)
if (!harmless) {
// try to tell the other system that we have quarantined it
+ log.info("Sending Quarantined to [{}]", peer)
sendControl(Quarantined(localAddress, peer))
}
setupStopQuarantinedTimer()
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/artery/InboundQuarantineCheck.scala
b/remote/src/main/scala/org/apache/pekko/remote/artery/InboundQuarantineCheck.scala
index c6d2094149..b6e9002889 100644
---
a/remote/src/main/scala/org/apache/pekko/remote/artery/InboundQuarantineCheck.scala
+++
b/remote/src/main/scala/org/apache/pekko/remote/artery/InboundQuarantineCheck.scala
@@ -61,10 +61,12 @@ private[remote] class
InboundQuarantineCheck(inboundContext: InboundContext)
association.remoteAddress,
env.originUid)
// avoid starting outbound stream for heartbeats
- if (!env.message.isInstanceOf[Quarantined] &&
!isHeartbeat(env.message))
+ if (!env.message.isInstanceOf[Quarantined] &&
!isHeartbeat(env.message)) {
+ log.info("Sending Quarantined to [{}]",
association.remoteAddress)
inboundContext.sendControl(
association.remoteAddress,
Quarantined(inboundContext.localAddress,
UniqueAddress(association.remoteAddress, env.originUid)))
+ }
}
pull(in)
} else
diff --git
a/remote/src/test/scala/org/apache/pekko/remote/artery/TestContext.scala
b/remote/src/test/scala/org/apache/pekko/remote/artery/TestContext.scala
index 0212214d83..e44960b55a 100644
--- a/remote/src/test/scala/org/apache/pekko/remote/artery/TestContext.scala
+++ b/remote/src/test/scala/org/apache/pekko/remote/artery/TestContext.scala
@@ -103,7 +103,7 @@ private[remote] class TestOutboundContext(
}
override def quarantine(reason: String): Unit = synchronized {
- _associationState = _associationState.newQuarantined()
+ _associationState = _associationState.newQuarantined(harmless = false)
}
override def isOrdinaryMessageStreamActive(): Boolean = true
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]