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:
graph TD
A[接收代码] --> B{分配给小容器}
B --> |成功| C[在小容器中执行]
C --> |完成| D[返回结果]
C --> |阻塞或超时| E{转移到大容器}
B --> |所有小容器忙| E
E --> |大容器可用| F[在大容器中执行]
E --> |大容器忙| G[加入等待队列]
G --> |大容器空闲| F
F --> |完成| D
F --> |超时| H[丢弃任务]
G --> |等待超时| HSmall 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:
- 5 seconds in a small container → try the big-container channels.
- 10 seconds in a big container → log the timeout and discard that execution.
- 20 seconds at the HTTP handler → return
408 Request Timeoutto 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.