@@ -95,6 +95,12 @@ public class LockableResourcesManager extends GlobalConfiguration {
9595 private transient volatile AtomicBoolean savePending ;
9696 private transient volatile ScheduledExecutorService saveExecutor ;
9797
98+ /** Single scheduled timeout task. Guarded by {@link #syncResources}. */
99+ private transient java .util .concurrent .ScheduledFuture <?> nextTimeoutTask ;
100+
101+ /** Deadline (epoch ms) the current {@link #nextTimeoutTask} targets. 0 = none. */
102+ private transient long nextTimeoutDeadline ;
103+
98104 @ DataBoundSetter
99105 public void setAllowEmptyOrNullValues (boolean allowEmptyOrNullValues ) {
100106 this .allowEmptyOrNullValues = allowEmptyOrNullValues ;
@@ -870,8 +876,9 @@ private QueuedContextStruct getNextQueuedContext() {
870876
871877 LOGGER .fine ("current queue size: " + this .queuedContexts .size ());
872878 LOGGER .finest ("current queue: " + this .queuedContexts );
873- List <QueuedContextStruct > orphan = new ArrayList <>();
879+ List <QueuedContextStruct > toRemove = new ArrayList <>();
874880 QueuedContextStruct nextEntry = null ;
881+ long earliestDeadline = Long .MAX_VALUE ;
875882
876883 // the first one added lock is the oldest one, and this wins
877884
@@ -880,18 +887,43 @@ private QueuedContextStruct getNextQueuedContext() {
880887 // check queue list first
881888 if (!entry .isValid ()) {
882889 LOGGER .fine ("well be removed: " + idx + " " + entry );
883- orphan .add (entry );
890+ toRemove .add (entry );
891+ continue ;
892+ }
893+
894+ // check if the entry has timed out waiting for resources
895+ if (entry .isTimedOut ()) {
896+ LOGGER .info ("Queue entry timed out waiting for resources: " + entry );
897+ toRemove .add (entry );
898+ PrintStream logger = entry .getLogger ();
899+ String msg = "[" + entry .getResourceDescription ()
900+ + "] timed out waiting for resource allocation after "
901+ + entry .getTimeoutForAllocateResource () + " "
902+ + entry .getTimeoutUnit ().toLowerCase (java .util .Locale .ENGLISH );
903+ printLogs (msg , logger , Level .WARNING );
904+ entry .getContext ()
905+ .onFailure (new org .jenkins .plugins .lockableresources .queue .LockWaitTimeoutException (msg ));
884906 continue ;
885907 }
908+
909+ // track the earliest deadline among remaining entries for rescheduling
910+ long deadline = entry .getTimeoutDeadlineMillis ();
911+ if (deadline > 0 && deadline < earliestDeadline ) {
912+ earliestDeadline = deadline ;
913+ }
914+
886915 LOGGER .finest ("oldest win - index: " + idx + " " + entry );
887916
888917 nextEntry = getNextQueuedContextEntry (entry );
889918 }
890919
891- if (!orphan .isEmpty ()) {
892- this .queuedContexts .removeAll (orphan );
920+ if (!toRemove .isEmpty ()) {
921+ this .queuedContexts .removeAll (toRemove );
893922 }
894923
924+ // reschedule for the next earliest deadline
925+ scheduleTimeoutAt (earliestDeadline );
926+
895927 return nextEntry ;
896928 }
897929
@@ -1458,12 +1490,22 @@ public void queueContext(
14581490 String variableName ,
14591491 boolean inversePrecedence ,
14601492 int priority ) {
1461- queueContext (context , requiredResources , resourceDescription , variableName , inversePrecedence , priority , null );
1493+ queueContext (
1494+ context ,
1495+ requiredResources ,
1496+ resourceDescription ,
1497+ variableName ,
1498+ inversePrecedence ,
1499+ priority ,
1500+ null ,
1501+ 0 ,
1502+ "MINUTES" );
14621503 }
14631504
1505+ // ---------------------------------------------------------------------------
14641506 /*
14651507 * Adds the given context and the required resources to the queue if
1466- * this context is not yet queued.
1508+ * this context is not yet queued, with reason and timeout for resource allocation .
14671509 */
14681510 @ Restricted (NoExternalUse .class )
14691511 public void queueContext (
@@ -1473,7 +1515,9 @@ public void queueContext(
14731515 String variableName ,
14741516 boolean inversePrecedence ,
14751517 int priority ,
1476- String reason ) {
1518+ String reason ,
1519+ long timeoutForAllocateResource ,
1520+ String timeoutUnit ) {
14771521 synchronized (syncResources ) {
14781522 for (QueuedContextStruct entry : this .queuedContexts ) {
14791523 if (entry .getContext () == context ) {
@@ -1484,7 +1528,14 @@ public void queueContext(
14841528
14851529 int queueIndex = 0 ;
14861530 QueuedContextStruct newQueueItem = new QueuedContextStruct (
1487- context , requiredResources , resourceDescription , variableName , priority , reason );
1531+ context ,
1532+ requiredResources ,
1533+ resourceDescription ,
1534+ variableName ,
1535+ priority ,
1536+ reason ,
1537+ timeoutForAllocateResource ,
1538+ timeoutUnit );
14881539
14891540 if (!inversePrecedence || priority != 0 ) {
14901541 queueIndex = this .queuedContexts .size () - 1 ;
@@ -1506,6 +1557,13 @@ public void queueContext(
15061557 Level .FINE );
15071558
15081559 save ();
1560+
1561+ // If this entry has a timeout and its deadline is earlier than the
1562+ // currently scheduled one, (re)schedule so it fires on time.
1563+ long deadline = newQueueItem .getTimeoutDeadlineMillis ();
1564+ if (deadline > 0 && (nextTimeoutDeadline == 0 || deadline < nextTimeoutDeadline )) {
1565+ scheduleTimeoutAt (deadline );
1566+ }
15091567 }
15101568 }
15111569
@@ -1562,7 +1620,7 @@ public void refreshQueue() {
15621620 // Invalidate cached candidates so waiting jobs re-evaluate with current labels
15631621 cachedCandidates .invalidateAll ();
15641622
1565- // Process waiting pipeline jobs
1623+ // Process waiting pipeline jobs (also handles timeouts)
15661624 synchronized (syncResources ) {
15671625 while (proceedNextContext ()) {
15681626 // process as many contexts as possible
@@ -1573,6 +1631,61 @@ public void refreshQueue() {
15731631 scheduleQueueMaintenance ();
15741632 }
15751633
1634+ // ---------------------------------------------------------------------------
1635+ /**
1636+ * Checks for timed-out entries in the pipeline lock queue and fails them.
1637+ * Called by {@link org.jenkins.plugins.lockableresources.queue.LockWaitTimeoutPeriodicWork}
1638+ * as a safety net.
1639+ */
1640+ @ Restricted (NoExternalUse .class )
1641+ public void checkTimeouts () {
1642+ synchronized (syncResources ) {
1643+ // proceedNextContext → getNextQueuedContext handles timeouts + rescheduling
1644+ while (proceedNextContext ()) {
1645+ // process as many contexts as possible
1646+ }
1647+ }
1648+ }
1649+
1650+ // ---------------------------------------------------------------------------
1651+ /**
1652+ * Schedules (or reschedules) the single timeout task to fire at the given
1653+ * deadline. If {@code deadline} is {@link Long#MAX_VALUE} the current task
1654+ * is cancelled and nothing new is scheduled.
1655+ * Must be called while holding {@link #syncResources}.
1656+ */
1657+ private void scheduleTimeoutAt (long deadline ) {
1658+ // Cancel the current task — we will either replace it or clear it
1659+ if (nextTimeoutTask != null ) {
1660+ nextTimeoutTask .cancel (false );
1661+ nextTimeoutTask = null ;
1662+ nextTimeoutDeadline = 0 ;
1663+ }
1664+
1665+ if (deadline == Long .MAX_VALUE || deadline <= 0 ) {
1666+ return ;
1667+ }
1668+
1669+ nextTimeoutDeadline = deadline ;
1670+ // Small buffer so the deadline has definitely passed when we check
1671+ long delayMs = Math .max (0 , deadline - System .currentTimeMillis ()) + 500L ;
1672+ LOGGER .fine ("Scheduling timeout check in " + delayMs + "ms" );
1673+ nextTimeoutTask = jenkins .util .Timer .get ()
1674+ .schedule (
1675+ () -> {
1676+ LOGGER .fine ("Scheduled timeout check fired" );
1677+ synchronized (syncResources ) {
1678+ nextTimeoutDeadline = 0 ;
1679+ nextTimeoutTask = null ;
1680+ while (proceedNextContext ()) {
1681+ // process as many contexts as possible
1682+ }
1683+ }
1684+ },
1685+ delayMs ,
1686+ java .util .concurrent .TimeUnit .MILLISECONDS );
1687+ }
1688+
15761689 // ---------------------------------------------------------------------------
15771690 private AtomicBoolean getSavePending () {
15781691 AtomicBoolean sp = savePending ;
0 commit comments