fix(communication): generation-suffixed, sanitized ClusterClient actor names to prevent recreate name collision

This commit is contained in:
Joseph Doherty
2026-07-08 14:55:05 -04:00
parent 7231c2044b
commit 75cbac4478
2 changed files with 79 additions and 1 deletions
@@ -32,11 +32,35 @@ public interface ISiteClientFactory
/// </summary>
public class DefaultSiteClientFactory : ISiteClientFactory
{
/// <summary>
/// Per-incarnation generation counter. Context.Stop of the previous client is
/// asynchronous — its actor name stays reserved until termination completes —
/// so a same-named recreate in the same message handling throws
/// InvalidActorNameException. A generation suffix makes every incarnation's
/// name unique by construction (mirrors SiteStreamGrpcServer._actorCounter).
/// </summary>
private long _generation;
/// <inheritdoc />
public IActorRef Create(ActorSystem system, string siteId, ImmutableHashSet<ActorPath> contacts)
{
var settings = ClusterClientSettings.Create(system).WithInitialContacts(contacts);
return system.ActorOf(ClusterClient.Props(settings), $"site-client-{siteId}");
var name = $"site-client-{SanitizeForActorName(siteId)}-{System.Threading.Interlocked.Increment(ref _generation)}";
return system.ActorOf(ClusterClient.Props(settings), name);
}
/// <summary>
/// Maps an arbitrary SiteIdentifier onto a valid Akka actor-path element:
/// letters/digits/'-'/'_' pass through, everything else becomes '_'. Uniqueness
/// is NOT required here (the generation suffix guarantees it); only validity is.
/// </summary>
internal static string SanitizeForActorName(string siteId)
{
if (string.IsNullOrEmpty(siteId)) return "site";
var sanitized = new string(siteId
.Select(c => char.IsAsciiLetterOrDigit(c) || c is '-' or '_' ? c : '_')
.ToArray());
return ActorPath.IsValidPathElement(sanitized) ? sanitized : "site";
}
}