How Jenkins Handles Concurrent Build Execution Using Executor and RunnerStack
Jenkins handles concurrent build execution by assigning each build to a dedicated Executor thread on a node, while RunnerStack maintains a per-thread mapping so the system can always identify which RunExecution is active on the current thread.
Jenkins, as implemented in the jenkinsci/jenkins repository, scales build throughput by parallelizing work across multiple executor threads rather than running jobs sequentially. Understanding how Jenkins concurrent build execution actually works requires examining the interaction between the Executor class that hosts build threads and the RunnerStack utility that tracks them.
Executor Lifecycle and Thread Assignment
When a node is created, the number of executors it can host is stored in Computer.numExecutors. For each configured slot, a hudson.model.Executor object is instantiated, but the underlying Java thread is lazy-started only when the queue assigns a WorkUnit to it via Executor.start(WorkUnit).
Inside Executor.run() and State Locking
Once started, the executor thread enters Executor.run(). According to the Jenkins source code in core/src/main/java/hudson/model/Executor.java, this method performs the following steps:
- Acquires the internal
ReadWriteLockreferred to aslockto protect mutable state such asexecutable,workUnit, andasynchronousExecution. - Asks the queue to
callWithLockso that the transition from idle to building is atomic. - Creates the
Executableby invokingcreateExecutable()and stores it. - Calls
queue.execute(executable, task), which finally delegates to the build’sRunExecution.run()method.
The executor therefore isolates each build in its own Java thread, while the Queue guarantees that two executors never pick the same WorkUnit. Concurrency is limited only by the number of executors configured on the node.
RunnerStack: Per-Thread RunExecution Bookkeeping
While a build is running, Jenkins must know which RunExecution is associated with the current thread—for example, when handling checkpoints, processing interruptions, or rendering the “who is building this?” UI. The hudson.model.RunnerStack class solves this by keeping a WeakHashMap<Executor, Stack<RunExecution>>.
Pushing the Current RunExecution on Build Start
When a build starts, Run.onStartBuilding() pushes the current RunExecution onto the stack:
// Run.java
protected void onStartBuilding() {
…
if (runner != null)
RunnerStack.INSTANCE.push(runner);
…
}
(source: Run.java
Popping the Stack on Build Completion
When the build ends, Run.onEndBuilding() pops the stack, finalizes checkpoint bookkeeping, and clears the runner reference:
protected void onEndBuilding() {
…
if (runner != null) {
runner.checkpoints.allDone(); // finish checkpoint bookkeeping
runner = null;
RunnerStack.INSTANCE.pop();
}
…
}
(source: Run.java
Querying the Current Build Context
RunnerStack.peek() is used throughout Jenkins core—such as in checkpoint handling inside Run.reportCheckpoint—to obtain the currently executing build for the calling thread:
// Run.java
Run<?, ?>.RunExecution exec = RunnerStack.INSTANCE.peek();
(source: Run.java
Concurrency Safety Mechanisms
All mutable fields in Executor are guarded by its internal ReadWriteLock. The queue interacts with the executor under Queue.callWithLock, ensuring that only one thread can modify an executor’s state at a time.
RunnerStack itself is a synchronized structure, and because the underlying map uses weak references, finished executors do not leak memory. These layers ensure that Jenkins concurrent build execution remains safe without requiring plugins to manage their own locking.
Querying and Controlling Executors in Practice
Query an Executor’s State via the Remote API
You can inspect whether an executor is idle or running a build using a Groovy script:
import jenkins.model.Jenkins
def node = Jenkins.instance.getComputer('my‑agent')
node.getExecutors().each { exec ->
println "Executor #${exec.getNumber()} on ${node.getName()}: " +
(exec.isIdle() ? 'idle' : "running ${exec.getCurrentExecutable().getFullDisplayName()}")
}
Uses: Executor.isIdle(), Executor.getCurrentExecutable() — see Executor.java.
Using RunnerStack Inside a Plugin to Find the Current Build
Plugins can discover the active build for the current thread without explicit reference passing:
import hudson.model.RunnerStack;
import hudson.model.Run;
Run<?, ?>.RunExecution exec = RunnerStack.INSTANCE.peek();
if (exec != null) {
Run<?,?> build = exec.getBuild();
// e.g. log the job name
listener.getLogger().println("Current build: " + build.getFullDisplayName());
}
Reference: RunnerStack.peek() implementation — see RunnerStack.java.
Manual Creation of a WorkUnit for Testing
For integration tests or advanced automation, you can manually create a WorkUnit and assign it to an idle executor:
import hudson.model.Executor;
import hudson.model.Queue;
import hudson.model.Queue.WorkUnit;
// Assume 'computer' is a Computer instance with at least one idle executor
Executor exec = computer.getExecutors().stream()
.filter(Executor::isIdle)
.findFirst()
.orElseThrow(() -> new IllegalStateException("No idle executor"));
WorkUnit wu = new WorkUnit(/* task */, /* actions */, /* actions */);
exec.start(wu); // executor thread will be started on demand
Key method: Executor.start(WorkUnit) — see Executor.java.
Summary
- Executor threads are created lazily via
Executor.start(WorkUnit)and run builds in isolated Java threads, with state protected by aReadWriteLock. - The Queue assigns
WorkUnits atomically usingQueue.callWithLock, ensuring no two executors claim the same work. - RunnerStack provides a synchronized, per-thread stack of
RunExecutioninstances, enabling components to query the current build viapeek(). - Builds push their
RunExecutiononto the stack inRun.onStartBuilding()and pop it inRun.onEndBuilding(), preventing stale context. - Concurrency is bounded only by
Computer.numExecutors, making scalability a matter of node configuration.
Frequently Asked Questions
What limits how many builds Jenkins can run concurrently on one node?
The hard limit is Computer.numExecutors, which defines how many hudson.model.Executor instances (and therefore Java threads) are created for that node. The Queue will not assign more WorkUnits than there are idle executors available.
How does Jenkins prevent race conditions when an executor picks up a new build?
Jenkins uses Queue.callWithLock to perform the idle-to-building transition atomically. Inside Executor.run(), the executor acquires its internal ReadWriteLock before mutating state, so the Queue and the executor thread never corrupt shared data.
Why does Jenkins use RunnerStack instead of passing RunExecution references directly?
RunnerStack provides a global, thread-local lookup via WeakHashMap<Executor, Stack<RunExecution>>. This design lets internal systems—such as checkpoint reporting and the “who is building this?” UI—discover the current execution context without threading explicit references through every API boundary.
Can a plugin safely call RunnerStack.peek() during a build?
Yes. As implemented in the Jenkins source code, RunnerStack is synchronized and intended for this exact purpose. If the calling thread is an executor running a build, peek() returns the active RunExecution; otherwise it returns null.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →