Table of Contents

Namespace Virtufin.WorkManager.Actors

Classes

ActorTypeNames
CreateRuntimeWorkerPayload

Exactly one of CodeBlob / CodeSource must be set.

CreateWorkerPayload
HistoryPayload
LoadCodeCredential

Flattened, plain-C# stand-in for Virtufin.WorkManager.Protos.SourceCredential -- never puts the protobuf message type itself on the actor wire (a generated gRPC message is not a shape System.Text.Json source generation is meant to serialize). Exactly the same trust boundary as the credential already crossed in the pre-actor design (a full peer-to-peer gRPC replay of the original request), just carried by an actor invocation instead.

LoadCodePayload
LoadRuntimeCodePayload

Exactly one of CodeBlob / CodeSource must be set.

RuntimeActorTypeNames

Client-side mirror of Virtufin.WorkManager.Runtime.Actors.IWorkerRuntimeActor (the Runtime service's own copy of this contract) -- this gateway project never hosts these actor types, only proxies them, so it keeps its own copy of the interface/payload shapes. Confirmed during Phase 0 that this is the normal Dapr actors pattern: client and host each keep their own copy of a cross-service contract, no shared assembly needed.

One actor type name per mime type this gateway routes to the Runtime service. CSharpSourceWorker shipped in Phase 1; DotnetDllWorker/NativeDllWorker in Phase 2; PythonWorker in Phase 3 -- all execution-affecting RPCs (Create/Start/Stop/Delete/LoadCode, the only ones that check ForMimeType(string)) for every real mime type now route here. text/x-python and application/x-python both map to PythonWorker: they're the same execution mechanism (a python3 subprocess), so PythonEngine itself detects which code format it was given rather than needing two actor types (a merge of what used to be separate Python/PythonWheel engines and actors). The old in-process IWorkerActor/Worker path still exists and is still reachable for every mime type via GetTag(s)/SetTag(s)/GetWorkerHistory, none of which check this routing at all -- its recovery/on-demand-activation side effects were gated off for Runtime-routed workers in a separate fix; the remaining Tags/History surface stays gateway-local by design (see RuntimeWorkerRecord's doc comment), not a future phase.

RuntimeCloudEventPayload
RuntimeCodeSource

A code location the Runtime service fetches itself (issue #55) -- mirror of the Runtime project's own copy of this record. The gateway only sends one of these for URL-based sources whose credential is named (or absent); sources with a caller-supplied inline Basic/Bearer credential are pre-fetched here and shipped as bytes, so caller secrets never cross the service boundary.

RuntimeLoadResult

Mirror of the Runtime's RuntimeLoadResult (wire-compatible).

SetTagPayload
TagsPayload
WorkerActor

One actor instance per worker, keyed by worker 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 worker reaches the one process that actually has it loaded (engine + topic subscription).

A thin router, not a state owner: every method delegates straight to the pre-existing WorkManager, which still does the real work (engine load, topic subscribe/unsubscribe, persisted-config read/write via IDaprConnectionRepository's workmanager equivalent) exactly as before. This mirrors the one part of two prior attempts (this repo's own 2026 actor attempt, and websocketmanager's) that was never the problem -- reusing existing business logic rather than duplicating it in the actor.

Unlike the 2026 attempt, every RPC is routed through the actor, including GetTagAsync(string, CancellationToken)/GetTagsAsync(CancellationToken)/ GetHistoryAsync(CancellationToken) (dropped last time because the actor's own state channel was never wired to WorkerContractsJsonContext's polymorphic CodeSource serialization -- fixed here by reusing that exact context for the actor's wire serializer too, see WorkerActorSerialization).

OnActivateAsync(CancellationToken) self-heals a worker reactivated on an instance that never loaded it -- a genuine failover/rebalance. Reuses the existing RecoverSingleWorkerOnDemandAsync, the same method the old reclaim path already had, just triggered by actor activation instead of a failed-forward fallback branch.

CreateAsync(CreateWorkerPayload, CancellationToken) is actor-routed too -- an earlier version of this migration left worker creation as a plain, non-actor call (like a WebSocket connection, reasoning went, a worker has no external resource that needs pinning at create time). That reasoning was wrong in practice: if Dapr's placement later activated the worker's actor on a different instance than the one that ran a non-actor create, the creating instance's own in-memory copy became a permanently-orphaned duplicate -- confirmed live to make ListWorkersAsync resurrect a genuinely deleted worker forever, whenever a later list call happened to land back on that orphaned instance (not just wasted memory, a real correctness bug). Routing CreateAsync through the actor closes this at the root, the same way ConnectAsync does for websocketmanager: whichever instance Dapr picks for a fresh id's first activation is where the worker actually gets materialized, by construction.

WorkerActorDaprSerializer
WorkerActorWireSerializer

AOT-safe serialization for WorkerActor's method call/response payloads, backed by the existing, already-proven Virtufin.WorkManager.Contracts.WorkerContractsJsonContext (the same one WorkManager's state-store persistence already uses) instead of the SDK's reflection-based default. See https://docs.dapr.io/developing-applications/sdks/dotnet/dotnet-actors-next/dotnet-actorsnext-serialization/

Interfaces

IWorkerActor
IWorkerRuntimeActor