@@ -557,6 +557,155 @@ void lockInverseOrder2(JenkinsRule j) throws Exception {
557557 j .assertBuildStatusSuccess (j .waitForCompletion (b6 ));
558558 }
559559
560+ /**
561+ * Verify that inversePrecedence=true grants the lock to the newest build
562+ * when locking by <b>label</b> (not named resource).
563+ *
564+ * <pre>
565+ * start time | build | label | inversePrecedence
566+ * -----------|-------|--------|-------------------
567+ * 00:01 | b1 | label1 | true (acquires)
568+ * 00:02 | b2 | label1 | true (waits)
569+ * 00:03 | b3 | label1 | true (waits)
570+ *
571+ * expected lock order: b1 -> b3 -> b2
572+ * </pre>
573+ */
574+ @ Test
575+ @ Issue ({"JENKINS-40787" , "GITHUB-861" })
576+ void lockInverseOrderWithLabel (JenkinsRule j ) throws Exception {
577+ LockableResourcesManager .get ().createResourceWithLabel ("resource1" , "label1" );
578+ WorkflowJob p = j .jenkins .createProject (WorkflowJob .class , "p" );
579+ p .setDefinition (new CpsFlowDefinition ("""
580+ lock(label: 'label1', inversePrecedence: true) {
581+ semaphore 'wait-inside'
582+ }
583+ echo 'Finish'""" , true ));
584+
585+ WorkflowRun b1 = p .scheduleBuild2 (0 ).waitForStart ();
586+ SemaphoreStep .waitForStart ("wait-inside/1" , b1 );
587+
588+ WorkflowRun b2 = p .scheduleBuild2 (0 ).waitForStart ();
589+ j .waitForMessage ("[Label: label1] is locked by build " + b1 .getFullDisplayName (), b2 );
590+ isPaused (b2 , 1 , 1 );
591+
592+ WorkflowRun b3 = p .scheduleBuild2 (0 ).waitForStart ();
593+ j .waitForMessage ("[Label: label1] is locked by build " + b1 .getFullDisplayName (), b3 );
594+ isPaused (b3 , 1 , 1 );
595+
596+ // Release b1 — b3 (newest) must acquire before b2
597+ SemaphoreStep .success ("wait-inside/1" , null );
598+ j .waitForMessage ("Lock released on resource" , b1 );
599+ j .assertBuildStatusSuccess (j .waitForCompletion (b1 ));
600+
601+ SemaphoreStep .waitForStart ("wait-inside/2" , b3 );
602+ j .waitForMessage ("Trying to acquire lock on [Label: label1]" , b3 );
603+
604+ SemaphoreStep .success ("wait-inside/2" , null );
605+ j .waitForMessage ("Lock released on resource" , b3 );
606+ j .assertBuildStatusSuccess (j .waitForCompletion (b3 ));
607+
608+ SemaphoreStep .waitForStart ("wait-inside/3" , b2 );
609+ j .waitForMessage ("Trying to acquire lock on [Label: label1]" , b2 );
610+
611+ SemaphoreStep .success ("wait-inside/3" , null );
612+ j .assertBuildStatusSuccess (j .waitForCompletion (b2 ));
613+ }
614+
615+ /**
616+ * Verify that each waiting job's own {@code inversePrecedence} flag controls
617+ * queue ordering, not the releasing job's flag. Uses <b>separate</b> pipeline
618+ * jobs to match the original report.
619+ *
620+ * <pre>
621+ * start time | job | resource | inversePrecedence
622+ * -----------|------|-----------|-------------------
623+ * 00:01 | pA#1 | resource1 | true (acquires)
624+ * 00:02 | pB#1 | resource1 | false (waits — FIFO)
625+ * 00:03 | pA#2 | resource1 | true (waits — inversePrecedence, front)
626+ * 00:04 | pB#2 | resource1 | false (waits — FIFO, behind pB#1)
627+ *
628+ * expected lock order: pA#1 -> pA#2 -> pB#1 -> pB#2
629+ * </pre>
630+ */
631+ @ Test
632+ @ Issue ({"JENKINS-41070" , "GITHUB-864" })
633+ void lockInverseOrderMixedDifferentJobs (JenkinsRule j ) throws Exception {
634+ LockableResourcesManager .get ().createResourceWithLabel ("resource1" , "label1" );
635+
636+ // Job A — inversePrecedence = true
637+ WorkflowJob pA = j .jenkins .createProject (WorkflowJob .class , "pA" );
638+ pA .setDefinition (new CpsFlowDefinition ("""
639+ lock(resource: 'resource1', inversePrecedence: true) {
640+ echo 'locked-pA'
641+ semaphore 'wait-inside'
642+ }
643+ echo 'Finish'""" , true ));
644+
645+ // Job B — inversePrecedence = false
646+ WorkflowJob pB = j .jenkins .createProject (WorkflowJob .class , "pB" );
647+ pB .setDefinition (new CpsFlowDefinition ("""
648+ lock(resource: 'resource1', inversePrecedence: false) {
649+ echo 'locked-pB'
650+ semaphore 'wait-inside'
651+ }
652+ echo 'Finish'""" , true ));
653+
654+ // pA#1 acquires the lock
655+ WorkflowRun a1 = pA .scheduleBuild2 (0 ).waitForStart ();
656+ SemaphoreStep .waitForStart ("wait-inside/1" , a1 );
657+ j .assertLogContains ("locked-pA" , a1 );
658+
659+ // pB#1 waits (inversePrecedence=false → back of queue)
660+ WorkflowRun b1 = pB .scheduleBuild2 (0 ).waitForStart ();
661+ j .waitForMessage ("[resource1] is locked by build " + a1 .getFullDisplayName (), b1 );
662+
663+ // pA#2 waits (inversePrecedence=true → front of queue)
664+ WorkflowRun a2 = pA .scheduleBuild2 (0 ).waitForStart ();
665+ j .waitForMessage ("[resource1] is locked by build " + a1 .getFullDisplayName (), a2 );
666+
667+ // pB#2 waits (inversePrecedence=false → back of queue, behind pB#1)
668+ WorkflowRun b2 = pB .scheduleBuild2 (0 ).waitForStart ();
669+ j .waitForMessage ("[resource1] is locked by build " + a1 .getFullDisplayName (), b2 );
670+
671+ // Verify only a1 has the lock so far
672+ j .assertLogNotContains ("locked-pA" , a2 );
673+ j .assertLogNotContains ("locked-pB" , b1 );
674+ j .assertLogNotContains ("locked-pB" , b2 );
675+
676+ // Release pA#1 — pA#2 (inversePrecedence=true) must acquire next
677+ SemaphoreStep .success ("wait-inside/1" , null );
678+ j .waitForMessage ("Lock released on resource" , a1 );
679+
680+ SemaphoreStep .waitForStart ("wait-inside/2" , a2 );
681+ j .assertLogContains ("locked-pA" , a2 );
682+ j .assertLogNotContains ("locked-pB" , b1 );
683+ j .assertLogNotContains ("locked-pB" , b2 );
684+
685+ // Release pA#2 — pB#1 (FIFO among false) must acquire next
686+ SemaphoreStep .success ("wait-inside/2" , null );
687+ j .waitForMessage ("Lock released on resource" , a2 );
688+
689+ SemaphoreStep .waitForStart ("wait-inside/3" , b1 );
690+ j .assertLogContains ("locked-pB" , b1 );
691+ j .assertLogNotContains ("locked-pB" , b2 );
692+
693+ // Release pB#1 — pB#2 gets the lock last
694+ SemaphoreStep .success ("wait-inside/3" , null );
695+ j .waitForMessage ("Lock released on resource" , b1 );
696+
697+ SemaphoreStep .waitForStart ("wait-inside/4" , b2 );
698+ j .assertLogContains ("locked-pB" , b2 );
699+
700+ // Release pB#2 and verify all succeed
701+ SemaphoreStep .success ("wait-inside/4" , null );
702+
703+ j .assertBuildStatusSuccess (j .waitForCompletion (a1 ));
704+ j .assertBuildStatusSuccess (j .waitForCompletion (a2 ));
705+ j .assertBuildStatusSuccess (j .waitForCompletion (b1 ));
706+ j .assertBuildStatusSuccess (j .waitForCompletion (b2 ));
707+ }
708+
560709 /**
561710 * start time | job | resource | priority
562711 * ------ |--- |--- |---
0 commit comments