pjfanning opened a new issue, #3552:
URL: https://github.com/apache/pekko/issues/3552

   ### Summary
   
   On a single-node cluster with `remember-entities = on`, 
`CoordinatedShutdown` takes ~9–10 seconds in the 
`cluster-sharding-shutdown-region` phase when messages keep arriving for shards 
while the region is shutting down. All shards hand off within milliseconds; the 
rest of the time the `ShardRegion` sits with buffered messages, re-sending 
`GetShardHome` every `retry-interval` (2s) to a coordinator that never answers, 
until `GracefulShutdownTimeout` (phase timeout − 1s) fires and stops the region 
anyway.
   
   Nothing useful happens during that wait: with one member there is no other 
region the shards could move to, and the buffered messages are dropped when the 
timeout fires regardless.
   
   Practical impact: the phase lands just under the 10s default 
`cluster-sharding-shutdown-region` timeout, so test suites that start one 
`ActorSystem` per class and use `TestKit.shutdownActorSystem` see a 
multi-second stall per class, logged as a warning rather than failed.
   
   ### Reproduction
   
   Pekko `main` at de6afb0459, JDK 17.0.19, macOS. Single-node cluster, 
`PekkoSpec`, config:
   
   ```
   pekko.actor.provider = "cluster"
   pekko.remote.artery.canonical.port = 0
   pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
   pekko.cluster.sharding.remember-entities = on
   pekko.cluster.sharding.remember-entities-store = eventsourced
   pekko.cluster.sharding.state-store-mode = ddata
   ```
   
   1. `cluster.join(cluster.selfAddress)` and wait for `Up`.
   2. `ClusterSharding(system).start(...)` with an `EchoActor` entity, 
`extractShardId = msg.toString` so each entity is its own shard.
   3. Send messages `1 to 14` and wait for each echo, so 14 shards are hosted.
   4. Start a background thread that keeps sending `1 to 14` to the region 
every 5ms.
   5. `Await.result(CoordinatedShutdown(system).run(UnknownReason), 
30.seconds)` and time it.
   
   Observed: **10314ms**. Same run with no traffic during shutdown, or with 
`remember-entities = off`, or with a single shard: ~20ms.
   
   The shard count matters because the problem needs at least one shard to have 
been deallocated while others are still alive: with one shard the region stops 
on that shard's `Terminated` before anything can be buffered.
   
   ### Log timeline (Pekko, `verbose-debug-logging = on`)
   
   ```
   23.550  type1: Starting graceful shutdown of region and all its shards
   23.566  type1: Starting shutting down shards 
[12,8,4,13,5,10,14,2,7,3,11,9,6,1] ...
   23.683  type1: Shard [12] deallocation completed successfully.
   23.759  type1: Shard [2] shutting down
   ...
   23.774  type1: GetShardHome [12] request from [.../system/sharding/type1] 
stashed, because waiting for initial state or update of state.
           (133 stashed GetShardHome requests in total, from the region and 
from pekko://local/ignore)
   24.826  type1: Requesting shard home for [5] from coordinator ... [188] 
buffered messages.
   24.827  type1: Sending graceful shutdown to 
List(.../type1Coordinator/singleton/coordinator)
   26.845  type1: Requesting shard home for [5] from coordinator ... [520] 
buffered messages.
   28.864  type1: Requesting shard home for [5] from coordinator ... [852] 
buffered messages.
   30.886  type1: Requesting shard home for [5] from coordinator ... [1196] 
buffered messages.
   32.575  WARN type1: Graceful shutdown of shard region timed out, region will 
be stopped. Remaining shards [], remaining buffered messages [20725].
   32.576  type1: ShardRegion stopped
   ```
   
   ### Root cause
   
   Two pieces of Pekko code interact:
   
   **1. The region will not finish graceful shutdown while it holds buffered 
messages** — `ShardRegion.tryCompleteGracefulShutdownIfInProgress` 
(`cluster-sharding/src/main/scala/org/apache/pekko/cluster/sharding/ShardRegion.scala`):
   
   ```scala
   if (gracefulShutdownInProgress && shards.isEmpty && shardBuffers.isEmpty) {
     context.stop(self)
   }
   ```
   
   During graceful shutdown, once shard A has been handed off, a new message 
for A goes through `deliverMessage` → `regionByShard.get(shardId)` is `None` → 
the region sends `GetShardHome(A)` to the coordinator and buffers the message. 
The buffer can only drain when a `ShardHome(A, otherRegion)` arrives.
   
   **2. The coordinator silently ignores `GetShardHome` when there is no region 
it could allocate to** — `ShardCoordinator.receive`:
   
   ```scala
   case GetShardHome(shard) =>
     if (!handleGetShardHome(shard)) {
       val activeRegions = (state.regions -- gracefulShutdownInProgress) -- 
regionTerminationInProgress
       if (activeRegions.nonEmpty) {
         ... allocate ...
       }
       // else: no reply, no log
     }
   ```
   
   On a single node the only region is in `gracefulShutdownInProgress`, so 
`activeRegions` is empty and every request is dropped without a reply. The 
region retries every `retry-interval`, gets nothing, and only exits when 
`GracefulShutdownTimeout` stops it.
   
   The "stashed, because waiting for initial state or update of state" lines 
are a secondary effect: the ddata coordinator stashes `GetShardHome` while each 
`ShardHomeDeallocated` write is in flight, and later replays them into the same 
silent no-op. (Note also that the no-op branch never calls 
`unstashOneGetShardHomeRequest()`, so replay only continues after the next 
state update.)
   
   `remember-entities` matters because each shard stop goes through the 
remember-entities store, which widens the window between "shard A handed off" 
and "last shard handed off" in which messages for A get buffered.
   
   ### Possible directions
   
   - **Region side:** when graceful shutdown is in progress, all shards are 
gone, and no other cluster member could host this region's shards (same data 
centre, region role if configured), drop the buffered messages to dead letters 
and stop the region immediately. Those messages are dropped by 
`GracefulShutdownTimeout` today anyway; this only changes *when*. Multi-node 
behaviour would be untouched: if another candidate member exists the region 
keeps buffering so messages can be forwarded to the shard's new home. No 
protocol change.
   - **Coordinator side:** reply to `GetShardHome` when `activeRegions` is 
empty so the region can react. This needs a new coordinator→region message, 
hence a serializer change and rolling-upgrade considerations, so it is the 
heavier option.
   
   A regression test should assert the behaviour (buffered messages go to dead 
letters and the region stops) rather than just the timing.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to