Skip to content

[Feature] ShardCommand types (StartEntity, GetShardStats, GetClusterShardingStats) #151

Description

@pathosDev

Size / Priority

Rationale

Akka exposes a small set of ops-oriented commands on ShardRegion that let operators / dashboards inspect and influence shard state:

  • StartEntity(entityId) — explicitly activate an entity. Use case: pre-warm hot entities at startup, or recover an entity that was passivated but whose data needs to be live before the next request.
  • GetShardStats — ask one region for its per-shard stats (entity count per shard). Use case: per-node monitoring dashboard.
  • GetClusterShardingStats — ask the coordinator for cluster-wide per-shard stats (which shards live where, how many entities each holds). Use case: cluster-wide dashboard, capacity planning.

actor-ts has the internal state these would expose — ShardRegion knows its entities-per-shard, ShardCoordinator knows the cluster-wide allocation map. There's no public command surface to query them. Operators have to add custom message types per app + plumb them through.

Promoting these to first-class, documented public commands closes a small but real ops gap.

Reference: what Akka does

// StartEntity — explicitly activate
import akka.cluster.sharding.ShardRegion.StartEntity
val region: ActorRef = ClusterSharding(system).shardRegion("Counter")
region ! StartEntity("entity-42")

// GetShardStats — per-region
import akka.cluster.sharding.ShardRegion.GetShardRegionStats
val stats: Future[ShardRegionStats] = region ? GetShardRegionStats()
// ShardRegionStats(stats: Map[ShardId, Int])

// GetClusterShardingStats — cluster-wide
import akka.cluster.sharding.ClusterSharding.GetClusterShardingStats
val coordinator: ActorRef = ClusterSharding(system).clusterSharding
val clusterStats: Future[ClusterShardingStats] = coordinator ? GetClusterShardingStats(timeout)
// ClusterShardingStats(regions: Map[Address, ShardRegionStats])

Design sketch — actor-ts equivalents

// src/cluster/sharding/ShardCommands.ts (new file)

/** Explicitly activate an entity.  No reply.  Fire-and-forget. */
export class StartEntity {
  constructor(public readonly entityId: string) {}
}

/** Ask one ShardRegion for its per-shard entity counts. */
export class GetShardRegionStats {
  // marker — no fields
}

export class ShardRegionStats {
  constructor(
    /** Map<shardId, entity count in that shard on this region>. */
    public readonly shards: ReadonlyMap<number, number>,
    /** Failed shards (allocation pending or in-flight failure). */
    public readonly failedShards: ReadonlyMap<number, string>,
  ) {}
}

/** Ask the coordinator for cluster-wide per-region stats. */
export class GetClusterShardingStats {
  constructor(public readonly timeoutMs: number = 5_000) {}
}

export class ClusterShardingStats {
  constructor(
    /** Map<region-address-string, ShardRegionStats>. */
    public readonly regions: ReadonlyMap<string, ShardRegionStats>,
  ) {}
}

Usage:

import { StartEntity, GetShardRegionStats, GetClusterShardingStats } from 'actor-ts/cluster/sharding';

const region = sharding.shardRegion('Counter');

// Pre-warm
region.tell(new StartEntity('entity-42'));

// Per-region stats (via ask)
const stats = await ask(region, new GetShardRegionStats(), 1_000);
console.log('shard 7 has', stats.shards.get(7), 'entities');

// Cluster-wide stats (via ask, talks to coordinator)
const clusterStats = await ask(coordinator, new GetClusterShardingStats(), 5_000);
for (const [region, regionStats] of clusterStats.regions) {
  console.log(region, '→ shards:', [...regionStats.shards.entries()]);
}

Integration with existing actor-ts subsystems

  • ShardRegion: existing onReceive chain gets new arms for StartEntity, GetShardRegionStats. Already tracks entities-per-shard internally; new code wraps it up as ShardRegionStats.
  • ShardCoordinator: existing coordinator tracks the allocation map (region → shards). For GetClusterShardingStats, the coordinator broadcasts GetShardRegionStats to every region, collects responses with a timeout, returns aggregated ClusterShardingStats.
  • HTTP management endpoint (Cluster-management HTTP endpoints — extended (shards, leave-and-shutdown, metrics) #56): thin wrapper — GET /actor-ts/cluster/sharding/{typeName}/stats returns ClusterShardingStats as JSON.

Out of scope / non-goals

  • Entity-level introspection (GetEntityStats(entityId) returning mailbox-depth, message count): out of scope; that's a deeper observability item (track separately if demand).
  • Restart-all-entities command: out of scope; users can passivate-all then trigger natural re-activation.
  • Shard migration (MoveShard(shardId, targetRegion)): out of scope; that's the manual rebalance API in [Feature] External shard allocation + manual rebalance API #150.

Open design questions

  1. StartEntity reply: Akka returns no reply (fire-and-forget). Some apps want confirmation. Add optional replyTo: ActorRef<EntityStarted> field? Recommend: stay no-reply for parity; users that want confirmation can ask the entity itself afterward.
  2. ShardRegionStats.failedShards: include shards stuck in allocation-pending state? Useful for ops; adds a small extra map. Recommend: yes.
  3. GetClusterShardingStats timeout: what happens if some regions don't respond within timeoutMs? Return partial results with a missingRegions set, or fail entirely? Recommend partial + missing set.
  4. Persistence interaction with StartEntity: starting an entity loads its persistence state — same as any other message. Document this explicitly so users don't expect it to be free.

Test plan

  1. StartEntity activates — region with 0 entities; tell StartEntity('foo'); verify entity actor exists in region's child set.
  2. StartEntity on already-active is no-op — second tell doesn't double-activate; no error.
  3. GetShardRegionStats accurate — region with entities in shards 1, 2, 5; ask returns map {1: N1, 2: N2, 5: N5}.
  4. GetShardRegionStats empty region — region with no entities; ask returns empty map.
  5. GetClusterShardingStats aggregates — 3 regions each with entities; ask coordinator; returns merged stats with all 3 regions present.
  6. GetClusterShardingStats partial on timeout — 1 of 3 regions unresponsive; ask returns 2 regions + 1 missing.
  7. HTTP endpointGET /actor-ts/cluster/sharding/Counter/stats returns JSON with expected shape.
  8. Regression — existing sharding tests pass.

Acceptance criteria

  • StartEntity, GetShardRegionStats, ShardRegionStats, GetClusterShardingStats, ClusterShardingStats exported from src/cluster/sharding/.
  • ShardRegion.onReceive handles the new commands.
  • ShardCoordinator.onReceive handles GetClusterShardingStats with the fan-out + aggregate flow.
  • HTTP endpoint GET /actor-ts/cluster/sharding/{typeName}/stats.
  • Documentation: ops-oriented section "Inspecting sharding state".
  • Test suite covers all 8 cases.
  • CHANGELOG entry under "Sharding: public ShardCommand types".

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: lowNice-to-have / niche / demand-driven

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions