Skip to content

Development Guide

Guide for building, testing, and running the Virtufin WorkManager locally.

Table of Contents


Prerequisites

Required Software

Software Version Purpose
.NET SDK 10.0+ Runtime and build
Dapr CLI 1.18+ Local development sidecar
Docker Latest Redis/Valkey for state + pub/sub
Python 3.9+ Python engine workers (subprocess)
buf 1.40+ Protobuf code generation (optional)

Install .NET SDK

# macOS
brew install dotnet

# Linux (Ubuntu)
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt update
sudo apt install dotnet-sdk-10.0

# Windows
winget install Microsoft.DotNet.SDK.10

Install Dapr CLI

# macOS
brew install dapr/tap/dapr

# Linux
curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | bash

# Initialize Dapr (downloads placement, redis-placement, and zipkin containers)
dapr init

Project Structure

src/
├── Virtufin.WorkManager/                  # Main application
│   ├── Configuration/                     # Options + PortConstants
│   ├── Engines/                           # IEngine + IAsyncDisposable
│   ├── HostedServices/                    # WorkManagerRecovery
│   ├── Services/                          # gRPC service + Dapr
│   ├── Runtime/                           # Process orchestration
│   └── Program.cs
├── Virtufin.WorkManager.Protos/           # Proto definitions
├── Virtufin.WorkManager.Client/           # .NET client NuGet library
├── Virtufin.WorkManager.Engine.Python/    # Python engine (subprocess)
├── Virtufin.WorkManager.Engine.CSharpSource/  # C# source engine (Roslyn)
├── Virtufin.WorkManager.Engine.DotNetDll/     # .NET DLL engine (hostfxr + ALC)

tests/
├── Virtufin.WorkManager.Tests/            # Service-level tests
├── Virtufin.WorkManager.Client.Tests/     # Client library tests
├── python/                                # Python client tests + DevKit tests
└── typescript/                            # TypeScript client tests

Building

# Build the entire solution
dotnet build Virtufin.WorkManager.slnx

# Build a specific project
dotnet build src/Virtufin.WorkManager/Virtufin.WorkManager.csproj

The build output is placed in src/*/bin/Debug/net10.0/.

Running Locally

1. Start Dapr and Redis

dapr init   # one-time
docker compose up -d redis

2. Run the WorkManager

cd src/Virtufin.WorkManager
DAPR_COMPONENTS_PATH=/path/to/deploy-local/components  # set to your path
dapr run --app-id workmanager \
  --app-protocol grpc \
  --app-port 5102 \
  --dapr-http-port 3500 \
  --resources-path "$DAPR_COMPONENTS_PATH" \
  -- dotnet run

Clone the upstream virtufin/deploy-local repo (or copy its components/ directory to any path) to obtain the Dapr component definitions (pubsub, statestore, config).

For convenience, if you cloned deploy-local as a sibling of virtufin-workmanager/, the path becomes ../deploy-local/components.

The service exposes:

  • gRPC on :5102 (with reflection enabled)
  • HTTP/REST + Swagger on :5001

3. Verify

# Health check
curl http://localhost:5001/health

# Swagger UI
open http://localhost:5001/swagger

# gRPC reflection service list
grpcurl -plaintext localhost:5102 list

Testing

Run All Tests

dotnet test Virtufin.WorkManager.slnx

Run Tests with Coverage

dotnet test Virtufin.WorkManager.slnx \
  --collect:"XPlat Code Coverage" \
  --results-directory ./TestResults

Run Specific Tests

# By class name
dotnet test --filter "FullyQualifiedName~PythonEngineTests"

# By trait/category
dotnet test --filter "Category=Integration"

Test Projects

Test Project Description
Virtufin.WorkManager.Tests Service-level tests (engines, runtime, recovery)
Virtufin.WorkManager.Client.Tests Client library tests (hand-written + generated)
tests/python/ Python client + DevKit tests
tests/typescript/ TypeScript client tests

Writing Tests

  • Place unit tests next to the class they test in *.Tests/
  • Engine tests use mock I/O — they do not spawn real Python subprocesses
  • Runtime tests use WebApplicationFactory<Program> for in-process integration testing

Debugging

Visual Studio Code

.vscode/launch.json includes a debug profile that runs the WorkManager under Dapr:

# In VS Code
code .
# Press F5 → "Launch WorkManager (Dapr)" profile

JetBrains Rider

Rider auto-detects the launchSettings profile. Right-click Virtufin.WorkManager → "Run/Debug" → choose the "Dapr" launch profile.

Common Debug Scenarios

  • Python worker not starting: set a breakpoint in PythonEngine.StartProcessAsync and verify the AcceptTcpClientAsync call completes
  • Worker recovery failing: check WorkManagerRecoveryHostedService logs and the state store contents (docker exec -it dapr_redis_1 redis-cli KEYS '*')
  • gRPC serialization errors: enable EnableDetailedErrors in Program.cs (gated on IsDevelopment())

Generating Protobuf Files

Proto files live in src/Virtufin.WorkManager.Protos/proto/. To regenerate stubs after editing a .proto file:

cd src/Virtufin.WorkManager.Protos/proto
buf generate

This regenerates:

  • C# stubssrc/Virtufin.WorkManager/Generated/
  • Python stubssrc/python/virtufin/workmanager/workmanager_pb2*.py
  • TypeScript stubssrc/typescript/src/generated/workmanager_pb.js

Generated files are excluded from git.

Regenerate Client Tests

After regenerating stubs, also regenerate the cross-language client tests:

# The repos are NOT a monorepo, so the script is fetched
# directly from the virtufin-common Gitea repo at runtime.
python3 <(curl -L https://git.haenerconsulting.com/virtufin/virtufin-common/raw/branch/master/scripts/generate-client-tests.py) \
  src/Virtufin.WorkManager.Protos/proto/workmanager.proto \
  --prefix M

Troubleshooting

"Python worker process exited with code N"

Check the Python engine logs for the StartProcessAsync call. Common causes:

  • Python 3 not on PATH — the engine calls EnsurePythonAvailableAsync with a 10s timeout; verify with python3 --version
  • Sandbox module blocked — the worker's import statement tries to load a blocked module (see PYTHON_BLOCKED_MODULES config)
  • Stdin closed — the worker process may have crashed; check stderr capture in the logs

"Dapr sidecar not responding"

Check that Dapr is initialized and the placement service is running:

dapr status
docker ps | grep dapr

If dapr_redis-placement_1 is not running, restart Dapr: dapr init --slim.

"Worker not found" after restart

The recovery hosted service re-creates workers from state on startup. If a worker is missing:

  1. Verify the state store contains the worker record: docker exec -it dapr_redis_1 redis-cli KEYS 'worker|*'
  2. Check WorkManagerRecoveryHostedService logs for LoadAsync errors
  3. Force a clean state: dapr stop --app-id workmanager && docker volume prune

Port conflicts

If 5001/5102 are in use, override via env vars:

HttpPort=5001 GrpcPort=5102 dapr run --app-port 5102 -- dotnet run

Or command-line switches:

dotnet run -- --http-port 6001 --grpc-port 6102

Memory leaks (long-running workers)

Each worker holds an engine instance with a Python subprocess. If the worker count grows without bound, check:

  • The reclaim hosted service is running and reclaiming dead workers
  • The state store TTL on worker records is set (recommended: 24h)
  • Your application code is calling DeleteWorker when workers are no longer needed