Class WebSocketConnectionActor
- Namespace
- Virtufin.WebSocketManager.Actors
- Assembly
- Virtufin.WebSocketManager.dll
One actor instance per WebSocket connection, keyed by connection id.
Dapr's placement service guarantees at most one live activation of a
given ActorId cluster-wide -- that guarantee, not a
hand-rolled InstanceId directory + heartbeat + forward-or-reclaim
pattern (Virtufin.Service.MultiInstance, removed by this
migration -- see its Virtufin.Service 0.1.0 CHANGELOG entry), is
what now ensures every RPC for a connection reaches the
one process that actually holds its live ClientWebSocket.
A thin router, not a state owner: every method delegates straight to the
pre-existing IWebSocketService, which still does the real
work (open/close the socket, persist metadata via
IDaprConnectionRepository) exactly as before. This mirrors
the one part of the original (classic Dapr.Actors) 2026 attempt
that was never the problem -- reusing existing business logic rather
than duplicating it in the actor. What that attempt got wrong, and this
one fixes: ConnectAsync(ConnectPayload, CancellationToken) is called immediately after the
connection id is minted (see WebSocketManagerGrpcService.Connect),
so the very first activation -- wherever Dapr's placement puts it -- is
also where the real socket gets opened. Nothing here ever assumes "the
socket is already open somewhere else."
[DaprActor("WebSocketConnection")]
public sealed class WebSocketConnectionActor : Actor, IWebSocketConnectionActor, IActor
- Inheritance
-
WebSocketConnectionActor
- Implements
- Inherited Members
Constructors
WebSocketConnectionActor(ActorActivationContext, IWebSocketService)
One actor instance per WebSocket connection, keyed by connection id.
Dapr's placement service guarantees at most one live activation of a
given ActorId cluster-wide -- that guarantee, not a
hand-rolled InstanceId directory + heartbeat + forward-or-reclaim
pattern (Virtufin.Service.MultiInstance, removed by this
migration -- see its Virtufin.Service 0.1.0 CHANGELOG entry), is
what now ensures every RPC for a connection reaches the
one process that actually holds its live ClientWebSocket.
A thin router, not a state owner: every method delegates straight to the
pre-existing IWebSocketService, which still does the real
work (open/close the socket, persist metadata via
IDaprConnectionRepository) exactly as before. This mirrors
the one part of the original (classic Dapr.Actors) 2026 attempt
that was never the problem -- reusing existing business logic rather
than duplicating it in the actor. What that attempt got wrong, and this
one fixes: ConnectAsync(ConnectPayload, CancellationToken) is called immediately after the
connection id is minted (see WebSocketManagerGrpcService.Connect),
so the very first activation -- wherever Dapr's placement puts it -- is
also where the real socket gets opened. Nothing here ever assumes "the
socket is already open somewhere else."
public WebSocketConnectionActor(ActorActivationContext context, IWebSocketService webSocketService)
Parameters
contextActorActivationContextwebSocketServiceIWebSocketService
Properties
Id
Gets the current actor id.
protected override ActorId Id { get; }
Property Value
State
Gets the actor state accessor for the current activation.
protected override IActorStateAccessor State { get; }
Property Value
Methods
ConnectAsync(ConnectPayload, CancellationToken)
public Task<WebSocketConnectionSnapshot> ConnectAsync(ConnectPayload body, CancellationToken cancellationToken = default)
Parameters
bodyConnectPayloadcancellationTokenCancellationToken
Returns
DisconnectAsync(CancellationToken)
public Task DisconnectAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationToken
Returns
GetTagAsync(string, CancellationToken)
public Task<string?> GetTagAsync(string key, CancellationToken cancellationToken = default)
Parameters
keystringcancellationTokenCancellationToken
Returns
GetTagsAsync(CancellationToken)
public Task<TagsPayload> GetTagsAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationToken
Returns
OnActivateAsync(CancellationToken)
Runs before any method call reaches this activation. A fresh ConnectAsync(ConnectPayload, CancellationToken) registers the connection locally itself, so this is a no-op for that (normal) case; it only does real work when Dapr has activated this connection's actor on an instance that never opened its socket -- a genuine failover/rebalance of an already-connected id -- closing the gap the 2026 classic-Dapr.Actors attempt left open (see the class doc comment above).
protected override ValueTask OnActivateAsync(CancellationToken cancellationToken)
Parameters
cancellationTokenCancellationToken
Returns
SendAsync(SendPayload, CancellationToken)
public Task<byte[]> SendAsync(SendPayload body, CancellationToken cancellationToken = default)
Parameters
bodySendPayloadcancellationTokenCancellationToken
Returns
SendRawAsync(SendRawPayload, CancellationToken)
public Task SendRawAsync(SendRawPayload body, CancellationToken cancellationToken = default)
Parameters
bodySendRawPayloadcancellationTokenCancellationToken
Returns
SetTagAsync(SetTagPayload, CancellationToken)
public Task SetTagAsync(SetTagPayload body, CancellationToken cancellationToken = default)
Parameters
bodySetTagPayloadcancellationTokenCancellationToken
Returns
SetTagsAsync(Dictionary<string, string>, CancellationToken)
public Task SetTagsAsync(Dictionary<string, string> tags, CancellationToken cancellationToken = default)
Parameters
tagsDictionary<string, string>cancellationTokenCancellationToken
Returns
StartPublishAsync(string, CancellationToken)
public Task StartPublishAsync(string topic, CancellationToken cancellationToken = default)
Parameters
topicstringcancellationTokenCancellationToken
Returns
StopPublishAsync(CancellationToken)
public Task StopPublishAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationToken
Returns
TouchAsync(CancellationToken)
No-op invocation whose only purpose is resetting Dapr's per-actor
idle timer. A connection that only ever receives (the common case:
subscribe once, then relay inbound messages to pub/sub) never calls
any other actor method after ConnectAsync(ConnectPayload, CancellationToken) -- without
this, ConnectionHeartbeatService's periodic call here
is what keeps a long-lived, quiet connection's actor from being
idle-deactivated (and, on a later invocation, reactivated on a
different node with no real socket) while the real socket is still
open. See the app-wide ActorIdleTimeout in Program.cs for the
backstop this is meant to make practically unreachable, not relied on.
public Task TouchAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationToken