Namespace Virtufin.WorkManager.Actors
Classes
- CreateRuntimeWorkerPayload
Exactly one of CodeBlob / CodeSource must be set.
- 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.
- 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-pythonandapplication/x-pythonboth map to PythonWorker: they're the same execution mechanism (a python3 subprocess), soPythonEngineitself 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 (seeRuntimeWorkerRecord's doc comment), not a future phase.
- 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).
- 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
InstanceIddirectory + heartbeat + forward-or-reclaim pattern (Virtufin.Service.MultiInstance, removed by this migration -- see itsVirtufin.Service0.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
CreateAsyncthrough the actor closes this at the root, the same wayConnectAsyncdoes for websocketmanager: whichever instance Dapr picks for a fresh id's first activation is where the worker actually gets materialized, by construction.
- 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/