Class RunRequestService

java.lang.Object
io.jenkins.plugins.batchcontrol.policy.RunRequestService

@Restricted(org.kohsuke.accmod.restrictions.NoExternalUse.class) public final class RunRequestService extends Object
The single entry point for every RunRequest state transition (SPEC items 3, 5, 7; D-20/D-21/D-22/D-23). No other class may change a request's status.

Concurrency (D-20): every load→validate→transition→persist sequence runs under one ReentrantLock, which makes each transition an effective compare-and-set: concurrent approve/approve or approve/cancel calls serialize, the loser sees the already-changed status and is refused. The lock is never held across Queue operations (queue submission and queue inspection happen outside it) to avoid lock-order inversion with the queue gate, which calls back into consumeMarker(java.lang.String, java.lang.String) while Jenkins holds the queue lock.

Failure families: IllegalArgumentException for input validation, IllegalStateException for wrong-state transitions, AccessDeniedException (403 on the web layer) for authorization refusals.

  • Field Details

    • REQUESTER_LACKS_BUILD_NOTICE

      public static final String REQUESTER_LACKS_BUILD_NOTICE
      D-38a: the sentence the request detail page and the approver notification show when the requester does not hold Item/Build on the request's job.
      See Also:
  • Method Details

    • get

      public static RunRequestService get()
    • load

      public RunRequest load(String id)
      Loads a request by id, or null. Its typed values stay in their own file (D-74); a screen, listing or listener never needs them.
    • list

      public List<RunRequest> list()
      All stored requests, in creation order.
    • countPendingFor

      public PendingCount countPendingFor(org.springframework.security.core.Authentication auth)
      D-61: the PENDING run requests that concern auth, read from the open-request index only (no history scan): those awaiting their decision as a designated approver holding Jenkins-level BatchControl/Approve, else their own. Every counted request is visible to auth under P-09. The single source for the tab badge and the section.
    • hasOwnRequests

      public boolean hasOwnRequests(org.springframework.security.core.Authentication auth)
      D-38b: whether auth filed at least one run request, in any status. The requester always sees their own requests (P-09), so this tells the web layer that a user without Jenkins-level BatchControl/Request still has something to see in the run requests section. Answered from the store's in-memory index; no item is visited.
    • canCancel

      public boolean canCancel(RunRequest request)
      D-38b: whether the current user may cancel request: its requester holding BatchControl/Request on the job, or a Manage holder. Permission only; the PENDING status is checked by cancel(String).
    • canChangeApprovers

      public boolean canChangeApprovers(RunRequest request)
      D-38b: whether the current user may change the approvers of request: its requester holding BatchControl/Request on the job. Permission only.
    • create

      public RunRequest create(Job<?,?> job, List<ParameterValue> values, String reason, String approver)
      Single-approver form of create(Job, List, String, List, String), without an incident.
    • canRequest

      public boolean canRequest(Job<?,?> job)
      Whether the current user could submit a run request for job (e2e-03 DEF-12, SPEC section 6 usability): BatchControl/Request and Item/Read on the job (D-38a), the permission checks of create(hudson.model.Job<?, ?>, java.util.List<hudson.model.ParameterValue>, java.lang.String, java.lang.String). Item/Build is not required (D-38a). Screens use it to show the Request Run entry and form only to such a user; create(hudson.model.Job<?, ?>, java.util.List<hudson.model.ParameterValue>, java.lang.String, java.lang.String) still checks for real.
    • requesterLacksBuild

      public boolean requesterLacksBuild(RunRequest request)
      D-38a (SPEC item 6): whether the requester of request (not the current user) lacks Item/Build on the request's job, so an approval also authorises a run the requester could not start. true also when that cannot be confirmed because the requester's account no longer resolves; false when the requester holds it, or when the job does not exist or is not visible to the current user (who then cannot see the request's job either). Read only. Evaluated as the current user, with no switch to ACL.SYSTEM2: the job is looked up with the caller's own Read, and the requester's permission is asked of the job's ACL with the requester's impersonated authentication, which needs no elevation.
    • requiresApprovalToRun

      public static boolean requiresApprovalToRun(Job<?,?> job)
      Whether a person's Build Now, Rebuild, Retry or Replay of job is refused by the queue gate and needs an approved run request instead (SPEC item 6): run control is on and the job requires approval. Screens use it to replace or hide those entries (e2e-03 DEF-25).
    • create

      public RunRequest create(Job<?,?> job, Map<String,String> parameters, String reason, String approver)
      Creates a PENDING run request from string values, designating a single approver: each value becomes a typed value through the job's parameter definition (SimpleParameterDefinition.createValue(String)), or a StringParameterValue where the job defines no such simple parameter, and the request is created as by create(Job, List, String, List, String). A value the definition refuses (a choice outside its choices) is an IllegalArgumentException. The requester is the current authentication.
    • create

      public RunRequest create(Job<?,?> job, List<ParameterValue> values, String reason, List<String> approvers, String incidentId)
      Creates a PENDING run request holding the submitted typed values (D-72), linked to an incident when incidentId is given (SPEC item 11: the run listener links a successful rerun back to the incident). The requester is the current authentication; every designated approver must pass the SPEC item 3 checks (D-37); validation failures throw IllegalArgumentException. The masked display map is derived from the values here, once. The approved build is scheduled with the values unchanged.

      D-72b (1)(2), security-35 S-35-01/03: before anything is stored, each parameter name may occur once, every value has a name, every stored text (a secret's plaintext included) and every display text obeys the 10,000-character limit, and names, texts and the reason hold only characters XML 1.0 can store (XmlChars); otherwise IllegalArgumentException with a message naming the parameter (or the reason), so the form can show it next to the field.

      SPEC item 11, D-72a: a linked request names an existing incident of job (else IllegalArgumentException, nothing stored) and is listed among the incident's rerun requests once stored, whoever created it: the incident rerun, or the Request Run form after IncidentService.linkableIncident(java.lang.String, hudson.model.Job<?, ?>) validated the reference it carried. A successful run of the request then records resolvedByRunId on the incident.

      The request takes over the temporary files of the values' file parameters: when the request is refused (permission, reason, size, approvers) they are disposed of before the exception propagates (ParameterFiles).

      D-82: right after the permission checks, a request for a job's sub-item (a matrix configuration, a Maven module) is refused with IllegalArgumentException naming the parent job to request instead; its files are disposed of and nothing is stored.

      D-74 (2): right after the permission checks, a request whose values would keep more than the body cap (RequestBodyLimit.keptSize(java.util.List<? extends hudson.model.ParameterValue>, java.util.Map<java.lang.String, java.lang.Long>)) is refused with RequestTooLargeException (an IllegalArgumentException), its files disposed of and nothing stored. This holds for every overload and for an incident rerun, which all end here; a caller without uploaded parts passes none.

    • create

      public RunRequest create(Job<?,?> job, List<ParameterValue> values, Map<String,Long> uploaded, String reason, List<String> approvers, String incidentId)
      As create(Job, List, String, List, String) for a web submission: uploaded maps a parameter name to the size of the uploaded parts its value was created from (RequestBodyLimit.uploadedSize(org.kohsuke.stapler.StaplerRequest2, java.util.Collection<java.lang.String>)), which the kept-size check counts for a value whose own size cannot be read (D-74 (2)).
    • approve

      public RunRequest approve(String id, String comment)
      Approves a PENDING request and submits the build. The caller must be the designated approver holding the Approve permission (or the admin self-approval path). The status is committed to APPROVED before submission and is never rolled back by a submission failure (quiet-down tolerance); startup recovery and expiry handle stragglers.

      D-72b (1): the stored typed values are read and checked (valuesProblem(io.jenkins.plugins.batchcontrol.model.RunRequest, java.util.List<hudson.model.ParameterValue>)) before the APPROVED commit; a request whose values do not match what the approver saw, or whose values file is missing although it has parameters, is refused with IllegalStateException and stays PENDING (it can still be rejected), and nothing is scheduled.

    • reject

      public RunRequest reject(String id, String comment)
      Rejects a PENDING request; the comment is mandatory (SPEC 5).
    • cancel

      public RunRequest cancel(String id)
      Cancels a PENDING request; requester or a Manage holder only (SPEC 7).
    • changeApprover

      public RunRequest changeApprover(String id, String newApprover)
      Single-approver form of changeApprovers(String, List).
    • changeApprovers

      public RunRequest changeApprovers(String id, List<String> newApprovers)
      Replaces the designated approver set of a PENDING request; requester only (SPEC item 3, D-26, D-37). Every new member must pass the designation checks; the change is recorded as (previous set, new set, changed by, time).
    • consumeMarker

      public boolean consumeMarker(String requestId, String jobFullName)
      Atomically claims the single consumption ticket of an approved request (D-23), including the D-20 check-at-submit expiry re-check. Called by the queue gate while Jenkins holds the queue lock; must therefore never be called while holding this service's lock on another thread path that also takes the queue lock.

      A refusal that is a genuine re-use of a marker the plugin once minted also lands in the audit history as ChangeRecord(MARKER_REUSE_BLOCKED) (D-30), so the attempt is visible on the history screen and in changes.csv and not only in the log. Two refusals qualify: the ticket is already spent (re-queue, rebuild, replay of the executed build), and the marker is presented on a job it was not issued for. The three remaining refusals are not re-use of a granted authorization and stay log-only: an unknown request id (nothing was ever approved under it), a request that was never approved or is no longer approved, and an approval that timed out before submission (D-20) — that one is the request's own EXPIRED transition, not an actor's attempt.

      Returns:
      true if the submission is authorized (ticket claimed just now)
    • markExecuted

      public void markExecuted(String requestId, String runId)
      Marks an APPROVED request as EXECUTED once its build has started (SPEC section 4). D-72b (5): the typed values file is deleted now; the build has its own copy and owns the files, and the masked display map stays as the record.

      The build has started whatever happens here, so a request file that cannot be written does not undo the transition: it stays in effect in memory (unsavedExecutions) and is written by the periodic work as soon as the store accepts it (retryUnsavedExecutions()). Never throws for that (the caller is a run listener on the build's own thread).

    • retryUnsavedExecutions

      public void retryUnsavedExecutions()
      Writes the EXECUTED states that could not be written when their build started (markExecuted(java.lang.String, java.lang.String)), with the deletion of the typed values file that goes with them. One that still fails stays for the next attempt; one whose request has no file any more, or whose file is no longer an APPROVED request waiting for its run, is dropped. Called by the periodic work; never throws.
    • expireOverdue

      public void expireOverdue(Set<String> queuedRequestIds, Instant queueSnapshotAt)
      Expires overdue requests: PENDING past pendingTimeoutHours and APPROVED, never-executed requests past approvedRunTimeoutMinutes whose submission is not sitting in the queue right now.

      Boundary-window hardening (spec-review-S2 MINOR 1): the queue snapshot is taken outside this service's lock (lock-order discipline with the queue gate), so a marker consumed between the snapshot and the per-request lock acquisition is missing from the snapshot even though the request WAS submitted in time. The consumption ticket (queuedAt), re-read here under the lock, closes that window: a ticket claimed at or after the snapshot instant proves the snapshot is stale for this request, so it is skipped this cycle (the next cycle sees it in the queue, executed, or genuinely gone).

      Parameters:
      queuedRequestIds - ids of requests that currently have a queue item carrying their approval marker (collected by the caller outside this service's lock)
      queueSnapshotAt - the instant just before the caller collected the snapshot
    • notifyExpiring

      public void notifyExpiring()
      D-36: sends NotificationEvent.EXPIRING once for every PENDING request whose pending timeout falls within notifyBeforeExpiryMinutes from now. The "notified" flag is persisted before dispatch, so a restart never resends. Expiry itself stays the clock comparison of expireOverdue(java.util.Set<java.lang.String>, java.time.Instant). Each request is handled on its own: one that cannot be read or written is logged and tried again on the next run, and never keeps the others from their notice.
    • invalidateForJob

      public List<String> invalidateForJob(String oldFullName, String reason)
      Invalidates every PENDING/APPROVED request that targets the given (old) job full name after a rename or move (D-21).

      Fail safe: an open request whose file cannot be read now (which job it names is unknown), or whose end cannot be written, is not ended here; the invalidation is remembered (MissedInvalidations), so the request cannot be approved, and it is ended INVALIDATED once it can be read and written, if it names this job (applyMissedInvalidations()). One such request never keeps the others from being invalidated.

      Returns:
      the ids of the requests that were invalidated
    • invalidateForDeletedItem

      public List<String> invalidateForDeletedItem(String fullName, boolean withDescendants)
      #38: an item was deleted. Every PENDING/APPROVED request on it ends INVALIDATED, and, with withDescendants (a deleted folder or any other item group), so does every such request on an item below it, so that no request outlives its job and a job later created under the same name never runs a request that was about the deleted one. The same rule as for a rename or move (D-21) and as for the activation requests of a deleted job (SPEC 6a), with the same fail-safe handling of a request whose file cannot be read or written now (invalidateForJob(java.lang.String, java.lang.String)): it cannot be approved, and it ends INVALIDATED as soon as it can be read and written.
      Returns:
      the ids of the requests that were invalidated
    • hasMissedInvalidation

      public boolean hasMissedInvalidation(String id)
      Whether request id missed a D-21 invalidation because its file could not be read or written (invalidateForJob(java.lang.String, java.lang.String)); its queued run, if any, is cancelled like that of an invalidated request.
    • hasMissedInvalidations

      public boolean hasMissedInvalidations()
      Whether any request missed a D-21 invalidation (hasMissedInvalidation(java.lang.String)).
    • applyMissedInvalidations

      public List<String> applyMissedInvalidations()
      Ends, as INVALIDATED, every request that missed a D-21 invalidation (invalidateForJob(java.lang.String, java.lang.String)) and can now be read and written, when the invalidation applies to the job it names; forgets the ones it does not apply to or that are no longer open. Run by the periodic work every minute; a request that still cannot be read stays for the next run.
      Returns:
      the ids of the requests ended now, whose queued runs the caller cancels
    • recoverApprovedRequests

      public void recoverApprovedRequests()
      Re-submits APPROVED, never-executed requests exactly once after a restart (SPEC item 4). Idempotency (T-RT-17): a request whose marker is already sitting in the restored queue, or whose build already exists, is skipped; otherwise its consumption ticket is re-issued and the request is submitted again. D-72b (7), security-35 S-35-07: a request whose queue item was cancelled (RunRequest.getQueueCancelledAt()) is never submitted again; the approved-run timeout ends it and its files are disposed of then. The approved-run timeout is judged from the recovery moment (SPEC item 7 exception), implemented by moving the request's expiry base forward.

      Ordering with the core queue restore: Queue.init (which restores queue.xml) runs in the same JOB_CONFIG_ADAPTED initializer band as StartupRecovery, in undefined order, and Queue.load() clears the live queue before reading. The whole recovery therefore runs under the queue lock: if queue.xml is still present the persisted queue is loaded here first (core's later load() re-reads the state re-saved below), so the recovery dedup always sees the restored queue items.

    • queuedMarkerRequestIds

      public static Set<String> queuedMarkerRequestIds(Jenkins jenkins)
      Request ids whose approval marker is currently sitting on a queue item, whoever asks.

      ACL.SYSTEM2 switch, for reading the queue only (R2-04): Queue.getItems() lists only the items the current user can read, and this answer decides whether an ending request's parameter files are still the queue's (a queued item disposes of its own files when it is cancelled) or are disposed of now; reading the queue as, for example, a user who just renamed a job they can no longer read would dispose of the files of a run that is still queued. No permission is exercised for the caller: the result is a set of request ids that is never shown to anyone, and every caller has completed its own permission checks before it ends a request.

    • recordQueueCancelled

      public void recordQueueCancelled(String requestId, String jobFullName)
      D-72b (7), security-35 S-35-07: the queue item of the approved run of requestId for job jobFullName was cancelled, so that run will not happen. The cancellation is recorded on the APPROVED request: startup recovery never submits it again (its ticket stays claimed, so the queue gate refuses any other submission of it too), and the approved-run timeout ends it. The cancelled item's own parameter types have already deleted the temporary files it carried (core's and the file-parameters plugin's cancelled-item listeners, see ParameterFiles); the typed values file, which nothing can use any more, is deleted now (D-72b (5)). Called by the queue listener, as SYSTEM, while Jenkins holds the queue lock (lock order as for consumeMarker(java.lang.String, java.lang.String)). Never throws.

      T-GAP-384: when the request file cannot be written, the mark stays in effect in memory (unsavedQueueCancels) and is written by the periodic work as soon as the store accepts it (retryUnsavedQueueCancels()), so a restart after that does not submit the run again either (LIMITATIONS 32).

    • retryUnsavedQueueCancels

      public void retryUnsavedQueueCancels()
      T-GAP-384, D-72b (7): writes the queue cancellations that could not be written when they happened (recordQueueCancelled(java.lang.String, java.lang.String)), with the deletion of the typed values file that goes with them. One that still fails stays for the next attempt; one whose request has no file any more, or whose file already carries a cancellation, is dropped. Called by the periodic work; never throws.