Immutable
release. Only release title and notes can be modified.
✨ Features
- Topic subscription introspection with
TopicStats(#1238).ActorSystem.TopicStats(ctx, topic, timeout) (*TopicStats, error)returns a read-only, point-in-time snapshot of a topic's subscription state without ever exposing subscriber identities, so presence-style features ("how many clients follow this feed", "does any node have subscribers for this topic") no longer require a shadow registry alongside the built-in pub/sub.TopicStatsreportsLocalSubscriberCount(subscribers registered on this node's topic actor) andTopicInstanceCount(topic-actor instances, i.e. nodes, cluster-wide with at least one subscriber for the topic).- In cluster mode the local topic actor answers with its own count and fans a lightweight query out to every peer's topic actor; peers answer with their local view only and never fan the query out any further, which avoids query storms. Outside cluster mode
TopicInstanceCountis simply 0 or 1 depending on whether this node has local subscribers. - The count reflects only live subscribers, applying the same liveness filter the publish path uses, so it matches what a publish would actually deliver to.
TopicStatsprefers an explicit failure over an inconsistent count: if any peer cannot be reached withintimeout, the whole call returns an error rather than a silently undercounted result.- It requires the topic actor to be running (
WithPubSub()or clustering enabled), otherwise it returns the newErrTopicActorNotStarted. Publish, subscribe, and delivery semantics are unchanged.
- Cluster-wide single fire for cron schedules (#1236). In cluster mode,
ScheduleWithCronnow delivers exactly once per trigger tick across the cluster.- Every node keeps running its own cron trigger locally, so ticks stay in sync with no extra network round trips on the hot path.
- Immediately before delivery each node races to claim an exclusive, TTL-bounded slot for that specific tick in the cluster store, the same atomic put-if-absent primitive that guarantees single grain activation. Only the claim winner delivers; every other node silently skips that tick.
- There is no fixed leader to fail over: if the winning node leaves the cluster, arbitration for the next tick proceeds among the remaining nodes.
- The cron expression is evaluated in UTC in cluster mode so every node computes the same tick instants regardless of its local timezone (outside cluster mode it stays local-timezone, as before).
- The claim TTL scales with the cron period, clamped between one minute and 24 hours, and a node that reaches a tick more than the TTL late skips it rather than re-delivering an already-delivered tick.
Schedule(interval) andScheduleOnceremain node-local, in cluster mode or not: their fire times are anchored to each node's own registration clock, so there is no shared tick to arbitrate. Outside cluster mode nothing changes.
- Scheduler introspection with
ListSchedules(#1235).ActorSystem.ListSchedules() []ScheduleInforeturns a read-only snapshot of every schedule currently known to the scheduler, exposed alongside the existing reference-basedCancelSchedule/PauseSchedule/ResumeSchedulemanagement APIs.- Each
ScheduleInforeports the scheduleReference(user-supplied viaWithReferenceor auto-generated) and the target actorPath. - It has no effect on the schedules themselves, and a schedule stops appearing once it has been canceled or, for one-shot schedules created via
ScheduleOnce, once it has fired and been delivered.
- Each
- React to cluster leadership changes with the
LeaderChangedevent (#1233). Each node publishes aLeaderChangedevent on its eventstream, alongsideNodeJoined/NodeLeft, when the coordinator moves to a different node, so code can react to failovers without pollingIsLeader.- The event carries the new coordinator's peers address (
Address()) and transition time (Timestamp()). - The cluster engine emits it from the authoritative membership flag once membership settles after a topology change, at most once per transition; the initial coordinator is seeded silently, not announced.
- It is best-effort and eventually consistent (may be missed under heavy churn), so query
IsLeader(ctx)orLeader(ctx)for authoritative state. Nothing is emitted outside cluster mode.
- The event carries the new coordinator's peers address (
- Factory-free grain activation with
GrainOf(#1231). The new generic package-level functionactor.GrainOf[*MyGrain](ctx, system, name, opts...)retrieves or activates a grain without a factory.- The grain kind is derived from the type parameter, auto-registered in the kind registry, and the grain is constructed as a zero value only when local activation is actually needed.
- Initialization belongs in
OnActivate, with external resources supplied throughWithGrainDependencies, which is the same construction contract the cluster already applies when recreating or relocating grains. - Identity resolution no longer executes a factory or allocates a grain instance on lookups and remote activations.
- The testkit gains matching
testkit.GrainOf[T]andtestkit.NodeGrainOf[T]helpers. GrainOfrejects non pointer-to-struct type parameters with the newErrInvalidGrainKindand detects same-named kind collisions across packages with the newErrGrainKindConflictinstead of silently instantiating the wrong type.
⚠️ Behavior changes
ScheduleWithCronin cluster mode requires an explicit reference. The reference is the shared identity every node claims against, so an auto-generated per-node reference would let every node deliver independently. Calls withoutWithReferenceare rejected with the newErrScheduleReferenceRequired.- Cron schedules registered on every node with the same reference now deliver once per tick cluster-wide instead of once per node. To keep per-node delivery in cluster mode, register each node's schedule under a distinct reference.
ScheduleWithCronevaluates the cron expression in UTC when the actor system is in cluster mode (local timezone is unchanged outside cluster mode). Express clustered cron schedules in UTC; for example"0 0 9 * * *"fires at 09:00 UTC.
🗑️ Deprecations
- The factory-based grain APIs are deprecated but remain functional:
ActorSystem.GrainIdentity,GrainContext.GrainIdentity, theGrainFactorytype, and the testkitTestKit.GrainIdentity/TestNode.GrainIdentitywrappers. Migrate toGrainOf; factories that capture dependencies in their closure only ever took effect on the local node, since remote recreation always built the grain as a zero value.
🔧 Fixes
WithGrainDisableRelocationnow reaches the cluster (#1231). The option was stored in the grain configuration but never propagated: the wire record always carriedDisableRelocation=false, so the relocator relocated grains that had explicitly opted out. The flag now flows end to end through the claim record, the remote activation request (newDisableRelocationfield onremote.GrainRequest), and grain recreation, so peer-activated and recreated grains keep the opt-out.- Remote grain activation no longer discards the caller's configuration. The remote activation request hardcoded a 1s activation timeout with 5 retries and dropped dependencies and mailbox capacity. It now carries the configured
WithGrainInitTimeout,WithGrainInitMaxRetries,WithGrainMailboxCapacity, andWithGrainDependenciesvalues to the activating node.
🔀 Pull Requests
- #1232 feat(grain): simplify grain creation/activation by @Tochemey
- #1242 feat: cluster-wide single-fire option for scheduled messages (#1236) by @StringKe
- #1241 feat: scheduler introspection API to list scheduled messages (#1235) by @StringKe
- #1239 feat: expose cluster leader status and leadership change notifications (#1233) by @StringKe
- #1246 feat: ✨ add topic actor stats by @Tochemey
Full Changelog: v4.2.13...v4.3.0