Skip to content

Commit 6ccc3de

Browse files
authored
Merge branch 'master' into feature/340-resource-event-listener
2 parents 5ce14f2 + a6b5b5c commit 6ccc3de

22 files changed

Lines changed: 934 additions & 30 deletions

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.jenkins-ci.plugins</groupId>
77
<artifactId>plugin</artifactId>
8-
<version>5.2102.v5f5fe09fccf1</version>
8+
<version>6.2152.ve00a_731c3ce9</version>
99
<relativePath />
1010
</parent>
1111

@@ -71,7 +71,7 @@
7171
<dependency>
7272
<groupId>io.jenkins.tools.bom</groupId>
7373
<artifactId>bom-${jenkins.baseline}.x</artifactId>
74-
<version>6269.v7a_159d68a_366</version>
74+
<version>6329.v403d8c87a_5ce</version>
7575
<type>pom</type>
7676
<scope>import</scope>
7777
</dependency>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
## Lock with allocation timeout
2+
3+
By default, the `lock` step waits indefinitely until the requested resource becomes available.
4+
With `timeoutForAllocateResource` you can set a maximum wait time — if the resource is not
5+
acquired within that period, the build fails immediately instead of blocking the queue forever.
6+
7+
This is useful when:
8+
- You prefer a fast failure over an indefinitely blocked pipeline
9+
- You want to detect resource starvation early
10+
- Your CI/CD has SLAs that cap how long a job may wait
11+
12+
### Pipeline (scripted)
13+
14+
```groovy
15+
node {
16+
// Wait up to 5 minutes for the resource, then fail
17+
lock(resource: 'my-printer', timeoutForAllocateResource: 5, timeoutUnit: 'MINUTES') {
18+
echo "Printer locked, printing ..."
19+
}
20+
}
21+
```
22+
23+
### Pipeline (declarative)
24+
25+
```groovy
26+
pipeline {
27+
agent any
28+
stages {
29+
stage('Deploy') {
30+
options {
31+
lock(resource: 'staging-env', timeoutForAllocateResource: 10, timeoutUnit: 'MINUTES')
32+
}
33+
steps {
34+
echo "Deploying to staging ..."
35+
}
36+
}
37+
}
38+
}
39+
```
40+
41+
### Label-based locking with timeout
42+
43+
```groovy
44+
pipeline {
45+
agent any
46+
stages {
47+
stage('Test') {
48+
steps {
49+
lock(label: 'phone', quantity: 1, variable: 'PHONE',
50+
timeoutForAllocateResource: 2, timeoutUnit: 'MINUTES') {
51+
echo "Running tests on ${env.PHONE}"
52+
}
53+
}
54+
}
55+
}
56+
}
57+
```
58+
59+
### Freestyle jobs
60+
61+
In a freestyle job configuration, go to **This build requires lockable resources** and set:
62+
- **Lock wait timeout**: the maximum time to wait (e.g. `5`)
63+
- **Timeout unit**: `SECONDS`, `MINUTES`, or `HOURS`
64+
65+
If the resource is not available within the configured timeout, the build is automatically
66+
removed from the Jenkins queue.
67+
68+
### Timeout values
69+
70+
| `timeoutUnit` | Description |
71+
|---------------|-------------|
72+
| `SECONDS` | Timeout in seconds |
73+
| `MINUTES` | Timeout in minutes (default) |
74+
| `HOURS` | Timeout in hours |
75+
76+
Setting `timeoutForAllocateResource: 0` (the default) disables the timeout — the build
77+
waits indefinitely, which preserves the original behaviour.

src/doc/examples/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ If you have a question, please open a [GitHub issue](https://github.com/jenkinsc
1414
- [Timeout inside lock](timeout-inside-lock.md)
1515
- [Dynamic resource pool expansion](dynamic-resource-pool-expansion.md)
1616
- [Resource event notifications](resource-event-notifications.md)
17+
- [Lock with allocation timeout](lock-with-timeout.md)

src/main/java/org/jenkins/plugins/lockableresources/LockStep.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ public class LockStep extends Step implements Serializable {
7171
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
7272
public int priority = 0;
7373

74+
/**
75+
* Timeout in the specified {@link #timeoutUnit} for waiting to acquire the resource.
76+
* 0 means no timeout (wait indefinitely). When the timeout expires, the step fails
77+
* with an exception instead of waiting forever.
78+
*/
79+
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
80+
public long timeoutForAllocateResource = 0;
81+
82+
/**
83+
* Time unit for {@link #timeoutForAllocateResource}. Defaults to MINUTES.
84+
*/
85+
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
86+
public String timeoutUnit = "MINUTES";
87+
7488
// it should be LockStep() - without params. But keeping this for backward compatibility
7589
// so `lock('resource1')` still works and `lock(label: 'label1', quantity: 3)` works too (resource
7690
// is not required)
@@ -154,6 +168,24 @@ public void setExtra(@CheckForNull List<LockStepResource> extra) {
154168
this.extra = extra;
155169
}
156170

171+
@DataBoundSetter
172+
public void setTimeoutForAllocateResource(long timeoutForAllocateResource) {
173+
this.timeoutForAllocateResource = Math.max(0, timeoutForAllocateResource);
174+
}
175+
176+
@DataBoundSetter
177+
public void setTimeoutUnit(String timeoutUnit) {
178+
if (timeoutUnit != null && !timeoutUnit.trim().isEmpty()) {
179+
// Validate it is a valid TimeUnit name
180+
try {
181+
java.util.concurrent.TimeUnit.valueOf(timeoutUnit.toUpperCase(Locale.ENGLISH));
182+
} catch (IllegalArgumentException e) {
183+
throw new IllegalArgumentException("Invalid timeoutUnit: " + timeoutUnit);
184+
}
185+
this.timeoutUnit = timeoutUnit.toUpperCase(Locale.ENGLISH);
186+
}
187+
}
188+
157189
@Extension
158190
public static final class DescriptorImpl extends StepDescriptor {
159191

@@ -229,6 +261,20 @@ public static FormValidation doCheckResourceSelectStrategy(
229261
return FormValidation.ok();
230262
}
231263

264+
@RequirePOST
265+
public ListBoxModel doFillTimeoutUnitItems(@AncestorInPath Item item) {
266+
if (item != null) {
267+
item.checkPermission(Item.CONFIGURE);
268+
} else {
269+
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
270+
}
271+
ListBoxModel items = new ListBoxModel();
272+
items.add("Seconds", "SECONDS");
273+
items.add("Minutes", "MINUTES");
274+
items.add("Hours", "HOURS");
275+
return items;
276+
}
277+
232278
@Override
233279
public Set<Class<?>> getRequiredContext() {
234280
return Collections.singleton(TaskListener.class);

src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,12 @@ private void onLockFailed(PrintStream logger, List<LockableResourcesStruct> reso
147147
getContext().onSuccess(null);
148148
} else {
149149
this.printBlockCause(logger, resourceHolderList);
150-
LockableResourcesManager.printLogs(
151-
"[" + step + "] is not free, waiting for execution ...", Level.FINE, LOGGER, logger);
150+
String waitMsg = "[" + step + "] is not free, waiting for execution ...";
151+
if (step.timeoutForAllocateResource > 0) {
152+
waitMsg += " (timeout: " + step.timeoutForAllocateResource + " "
153+
+ step.timeoutUnit.toLowerCase(java.util.Locale.ENGLISH) + ")";
154+
}
155+
LockableResourcesManager.printLogs(waitMsg, Level.FINE, LOGGER, logger);
152156
LockableResourcesManager lrm = LockableResourcesManager.get();
153157
lrm.queueContext(
154158
getContext(),
@@ -157,7 +161,9 @@ private void onLockFailed(PrintStream logger, List<LockableResourcesStruct> reso
157161
step.variable,
158162
step.inversePrecedence,
159163
step.priority,
160-
step.reason);
164+
step.reason,
165+
step.timeoutForAllocateResource,
166+
step.timeoutUnit);
161167
}
162168
}
163169

@@ -205,8 +211,8 @@ public static void proceed(
205211
context.newBodyInvoker().withCallback(new Callback(resourceNames, resourceDescription));
206212
if (variable != null && !variable.isEmpty()) {
207213
// set the variable for the duration of the block
208-
bodyInvoker.withContext(
209-
EnvironmentExpander.merge(context.get(EnvironmentExpander.class), new EnvironmentExpander() {
214+
bodyInvoker.withContext(EnvironmentExpander.merge(
215+
context.get(EnvironmentExpander.class), new EnvironmentExpander() {
210216
private static final long serialVersionUID = -3431466225193397896L;
211217

212218
@Override

0 commit comments

Comments
 (0)