Start With the Caller
Before choosing channels or traits, write the experience we want:
let runner = JobRunner::builder()
.queue_capacity(100)
.concurrency_limit(3)
.start()?;
let handle = runner.submit(async move {
fetch_user(user_id).await
}).await?;
let user = handle.await?;
runner.shutdown().await;
The two awaits mean different things:
submit(...).awaitwaits for queue capacity. Success means the runner accepted ownership of the job.handle.awaitwaits for job completion and returns its typed value or business error.
queue_capacity is clearer than max_jobs: it counts waiting jobs, not running
jobs, lifetime submissions, or bytes. Arbitrary futures can own String,
Vec, and Arc graphs whose total heap use is not described by
size_of_val. Strict memory budgeting would require a different contract such
as caller-supplied weights.
Version one accepts futures rather than requiring a Job trait. In-memory
work needs no serialization or durable identity, and async move already
captures precisely the owned input needed to execute later. A named trait can
be layered on as an application convention.