@page "/hosts" @* Akka cluster topology: each member's NodeId (host:port), roles, leader status. v2 reshapes v1's "driver host" page — there are no per-driver host rows yet (driver-instance child actors land with F7). For now this is the cluster-membership view; expand to per-driver rows when DriverHostActor starts spawning DriverInstanceActor children. *@ @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Akka.Actor @using Akka.Cluster @inject ActorSystem ActorSystem @implements IDisposable

Cluster hosts

Auto-refresh every @RefreshIntervalSeconds s. Last updated: @(_lastRefreshUtc?.ToString("HH:mm:ss 'UTC'") ?? "—")
Each row is one Akka cluster member identified by host:port. Roles drive which actors run on which node — admin nodes host the control-plane singletons, driver nodes host the per-node runtime actors. The leader columns identify which member currently owns each role's singletons.
@if (_rows is null) {

Loading…

} else if (_rows.Count == 0) {
No cluster members visible. The local node may still be joining.
} else {
Members
@_rows.Count
Up
@_rows.Count(r => r.Status == "Up")
Joining/Leaving
@_rows.Count(r => r.Status is "Joining" or "Leaving" or "Exiting")
Unreachable
@_rows.Count(r => r.Unreachable)
Members
@foreach (var r in _rows) { }
Address Status Roles Leader for
@r.Address @if (r.IsSelf) { self } @(r.Unreachable ? $"{r.Status} (unreachable)" : r.Status) @foreach (var role in r.Roles) { @role } @if (r.LeaderRoles.Count == 0) { } else { @foreach (var role in r.LeaderRoles) { @role } }
} @code { private const int RefreshIntervalSeconds = 5; private List? _rows; private bool _refreshing; private DateTime? _lastRefreshUtc; private Timer? _timer; protected override void OnInitialized() { Refresh(); _timer = new Timer(_ => InvokeAsync(() => { Refresh(); StateHasChanged(); }), null, TimeSpan.FromSeconds(RefreshIntervalSeconds), TimeSpan.FromSeconds(RefreshIntervalSeconds)); } private async Task RefreshAsync() { _refreshing = true; StateHasChanged(); try { await Task.Yield(); Refresh(); } finally { _refreshing = false; StateHasChanged(); } } private void Refresh() { var cluster = Akka.Cluster.Cluster.Get(ActorSystem); var state = cluster.State; var unreachable = state.Unreachable .Select(m => m.Address.ToString()).ToHashSet(); var selfAddress = cluster.SelfAddress.ToString(); _rows = state.Members.Select(m => { var address = m.Address.ToString(); var hostPort = $"{m.Address.Host ?? "?"}:{m.Address.Port ?? 0}"; var leaderRoles = m.Roles .Where(role => cluster.State.RoleLeader(role)?.ToString() == address) .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) .ToList(); return new MemberRow( Address: hostPort, Status: m.Status.ToString(), Roles: m.Roles.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(), LeaderRoles: leaderRoles, Unreachable: unreachable.Contains(address), IsSelf: address == selfAddress); }) .OrderBy(r => r.Address, StringComparer.OrdinalIgnoreCase) .ToList(); _lastRefreshUtc = DateTime.UtcNow; } private static string StatusChipClass(string status, bool unreachable) => (status, unreachable) switch { (_, true) => "chip-alert", ("Up", _) => "chip-ok", ("Joining", _) or ("Leaving", _) or ("Exiting", _) or ("WeaklyUp", _) => "chip-caution", ("Down", _) or ("Removed", _) => "chip-alert", _ => "chip-idle", }; public void Dispose() => _timer?.Dispose(); private sealed record MemberRow( string Address, string Status, IReadOnlyCollection Roles, IReadOnlyCollection LeaderRoles, bool Unreachable, bool IsSelf); }