← Interactive Code Note

Essay

Behind the executable note runner

How the runner moves work through small containers, big containers, a waiting queue, and three timeout boundaries.

  • Systems
  • Go
  • Insights

The runner uses two prewarmed Docker pools. Small containers handle the quick path; code that exceeds that path’s deadline is offered to a larger pool. The original architecture can be reduced to one flow:

The original scheduling model. The current source differs in how it pulls and requeues work, noted below.

Small containers

The checked-in configuration creates four small containers. Each has a buffered task channel with room for 10 jobs. The dispatcher polls Redis every 50 ms, compares those channel lengths, and sends the next job to the least-loaded small container. A non-blocking send keeps the dispatcher from waiting on a full channel; a job that cannot be sent goes back to Redis.

Execution in a small container receives a 5-second context deadline. Finishing within it writes the result to Redis. Returning context.DeadlineExceeded invokes transferTaskToBigContainer, which starts the escalation path shown above.

Big containers

The larger pool contains two containers in the current configuration. They have higher CPU and memory limits, plus their own buffered channels of 10 jobs each. Escalation tries each big-container channel without blocking. If one accepts the job, that container runs it with a 10-second deadline. A second timeout is logged and the task is discarded rather than returned to the small pool.

The checked-in dispatcher does not enforce every branch in the diagram. A full small-container channel sends the job back to Redis instead of directly to the big pool, while the big-container dispatcher also removes fresh jobs from Redis every 100 ms. The source therefore does not strictly guarantee that every job visits a small container first. The Mermaid diagram represents the intended two-stage model; the current dispatcher is a looser version of it.

Waiting queue

“The queue” is really two layers. Redis is the global waiting list, implemented with RPUSH and LPOP. Each container also owns a bounded Go channel. When a small channel is full—or every big channel rejects a transfer—the job is pushed back into Redis for another dispatch attempt.

The per-container channels are bounded, but the Redis list has no capacity or per-job expiry in this repository. So the diagram’s “等待超时” branch is not implemented as a queue TTL.

Timeout flow

The source has three separate clocks:

  1. 5 seconds in a small container → try the big-container channels.
  2. 10 seconds in a big container → log the timeout and discard that execution.
  3. 20 seconds at the HTTP handler → return 408 Request Timeout to the browser.

The 20-second response timeout does not remove the queued job. A request can therefore stop waiting while its job remains in Redis or continues executing. That distinction—execution timeout versus waiting timeout—is the main detail hidden by the compact Mermaid flow.

Implementation references: container pools and execution deadlines, dispatch and escalation, Redis queue, and HTTP response timeout.