Skip to content

Add a ClusterListener spec against a single-node cluster #7321

Description

@aglinxinyuan

Task Summary

ClusterListener (34 lines) is the last file in the engine at 0% coverage, and it has no spec.

It is worth covering because the count it maintains is what the frontend's cluster badge renders: updateClusterStatus recomputes numWorkerNodesInCluster on every membership event and pushes a ClusterStatusUpdateEvent to every open session. A listener that stops subscribing, or stops fanning out, leaves every client showing a stale node count with nothing failing.

It is testable without a second JVM. AmberRuntime.pekkoConfig selects the cluster provider with artery bound on port 0, so joining the node to itself makes it the leader and produces genuine MemberUp events. That is not a convenience — Member is private[cluster] and cannot be synthesized, so a real join is the only way to reach the event path at all.

Three things to be careful about, each of which produced a failing draft before it was understood:

  1. The catch-all arm needs TestActorRef.receive, not !. With a normal send, an exception from receive is taken by the supervisor and the actor is restarted — and a restarted listener answers the next request exactly like one that never failed. A draft written with ! stayed green with the catch-all replaced by a throw.
  2. Listeners must be stopped at the end of each case. One left running stays subscribed and keeps iterating SessionState.getAllSessionStates on every membership event.
  3. A mock session must be removed inside the case that created it. ScalaMock scopes expectations per test while the SessionState registry is JVM-global, so a leftover session gets called by a later test's listener against an expired mock.

Points 2 and 3 both stem from a real production race worth fixing separately: SessionState's registry is a plain unsynchronized mutable.HashMap, and ClusterListener.updateClusterStatus iterates it from the cluster-event thread while websocket open/close mutate it from container threads. A node joining or leaving while a user opens a tab can throw ConcurrentModificationException inside the listener.

Out of scope: the MemberRemoved recovery arm, which walks every live execution and calls notifyNodeFailure or forcefullyStop, so it needs real Amber clients.

Task Type

  • Refactor / Cleanup
  • DevOps / Deployment / CI
  • Testing / QA
  • Documentation
  • Performance
  • Other

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions