JUSTIN ZHANG - CARLETON UNIVERSITY, SWAN LAB
Three research directions I'm considering, then a high-level walkthrough of the mockup I've built from your spec.

The orchestrator scores tasks and hands work to workers as they poll.
Right now the orchestrator has “amnesia” - every time a worker polls, it computes the score from scratch and decides which task to hand over.
In the current score formulation, 0.1 and 0.9 are hardcoded weights:
score = 0.1 * (Norm. backlog) + 0.9 * (Norm. wait time)DES already logs every scheduling decision it makes, so we could train an ML model on those historical logs without ever touching production. (This depends on whether you're open to sharing this data - if not, I can synthesize it, but it won't be realistic.)
Can an RL agent look at these logs and find better weights than 0.1 and 0.9?
Currently “wait time” just means how long a task has been sitting in the queue - but if we could predict how long a task will actually take, we could schedule smarter (e.g. priority to short-but-waited jobs).
Note: a small model could add overhead since the score is recomputed on each worker poll - it needs to be really, really fast.A mockup of your DES Task Orchestrator, built end to end so I can study the memoryless fair-scheduling rule under controlled, reproducible conditions.
Go service: the scheduler (DES), SQLite queue, HTTP API, metrics, KEDA external-scaler gRPC. The research artifact.
Python poll loop + processors, zero runtime deps. Each pool runs as a KEDA ScaledJob.
k8s + KEDA ScaledJob manifest templates, one per task-type pool.
IaC for the 2-VM split - provision, build, and deploy both VMs.
Deployed on two fresh Ubuntu 22.04 VMs, provisioned by Ansible from my machine (the controller).
des-orchestrator service, persistent SQLite queue, KEDA external-scaler gRPC.A single SQLite file. The atomic new > in_progress flip is one UPDATE ... RETURNING - a statement-level transaction, so concurrent workers can't double-claim.
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
task_type TEXT NOT NULL,
document TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
created_at INTEGER NOT NULL,
assigned_at INTEGER,
completed_at INTEGER,
result TEXT NOT NULL DEFAULT '',
error TEXT NOT NULL DEFAULT ''
);
GET /health - liveness.GET /tasks/available?taskType=X - worker poll; returns a Task JSON, or 204 if none.POST /tasks/{id}/complete - body {"result": "..."}; 204 on success.POST /tasks/{id}/error - body {"error": "..."}; 204 on success.GET /metrics - plain-text fairness + latency snapshot.A bounded-lifetime poll loop (spec 2.4): each KEDA Job processes some work and exits on MAX_TASKS / MAX_RUNTIME / N consecutive empty polls; KEDA respawns Jobs as depth warrants.
urllib HTTP client; no third-party packages.Pure (document: str) -> str. Adding a pool = a module + a ScaledJob; the image doesn't change.
JUSTIN ZHANG - CARLETON UNIVERSITY, SWAN LAB