Class BlockedAttemptAudit
ChangeType.MARKER_REUSE_BLOCKED, ChangeType.REMOTE_RUN_BLOCKED,
ChangeType.TRIGGER_BLOCKED), with a bound on how often one repeated attempt may append
(S-21).
Why a bound is needed. Both callers run inside queue scheduling, and core takes the
global queue lock before it runs the Queue.QueueDecisionHandlers
(hudson/model/Queue#schedule2: lock.lock() precedes the handler loop). A
synchronous JSONL append therefore happens with the whole instance's scheduling blocked. An
unbounded append on that path is a self-inflicted denial of service as soon as anything retries
in a loop, and the ChangeType.REMOTE_RUN_BLOCKED caller makes that reachable over plain
HTTP: a build-token holder can POST /job/X/build?token=… as fast as the network allows,
and every one of those refusals wants a record.
Why coalescing rather than a cap. A global cap ("stop after N records") would silently
drop new and different attempts once a noisy one had filled the budget, which is the
opposite of what an audit trail is for. What repeats carries no new information, so this class
keeps every distinct attempt and merges repeats of one: the first attempt under a key always
writes, further attempts under the same key inside COOLDOWN do not. A merged attempt is
still logged, so nothing becomes invisible — it just stops appending to a file under a global
lock.
The tracking map is itself bounded (MAX_TRACKED_KEYS, least recently written key
evicted first), so an actor cycling job names cannot grow it without limit; evicting a key only
means the next attempt under it writes a record, which is the safe direction. The map is dropped
when the Jenkins instance changes, so a restart starts from a clean slate rather than
merging away the first post-restart attempt (the instance-scoped cache pattern
security.GrantService already uses).
This class does not decide whether an attempt is worth recording, only how often the same one may be written; the callers own that judgement.
-
Method Summary
Modifier and TypeMethodDescriptionstatic voidS-22-03: writes the closing count record of every open summary before Jenkins stops, so no summary is left promising a count that never follows.voidThe per-minute work (S-22-03, S-22-04): collects, under this object's lock, the closing count records of every summary whose window has ended and removes idle budgets, then appends the records outside the lock, so a queue thread refusing something meanwhile never waits behind these appends.static BlockedAttemptAuditget()booleanAppends one refused-attempt record unless the same attempt by the same account was already recorded insideCOOLDOWN.booleanrecord(ChangeType type, String attemptKey, String target, String user, String detail, String grantId) Asrecord(ChangeType, String, String, String, String), naming the grant involved (D-40: a refused CREATE of a name the grant's restriction does not allow).booleanrecordCoalesced(ChangeType type, String attemptKey, Duration cooldown, String target, String user, String detail) Appends one refused-attempt record unless one was already written under the sametypeandattemptKeyinsidecooldown, whoever made the attempt.booleanrecordPersonRefusal(ChangeType type, String attemptKey, String target, String user, String build, String detail) D-51, D-51a: appends the record of a re-run a person submitted and was refused.static StoreswapStoreForTesting(Store replacement) Test seam (S-24-04): replaces the store this class appends to and returns the previous one, so a test can make appends fail and restore the real store afterwards.
-
Method Details
-
swapStoreForTesting
@Restricted(org.kohsuke.accmod.restrictions.NoExternalUse.class) public static Store swapStoreForTesting(Store replacement) Test seam (S-24-04): replaces the store this class appends to and returns the previous one, so a test can make appends fail and restore the real store afterwards. Not for production use. -
get
-
record
public boolean record(ChangeType type, String attemptKey, String target, String user, String detail) Appends one refused-attempt record unless the same attempt by the same account was already recorded insideCOOLDOWN.- Parameters:
type- the refusal type; one of the*_BLOCKEDtypesattemptKey- what makes two attempts "the same" for merging purposes, chosen by the caller: everything that would make a second record say something new belongs in it (for a blocked marker re-use that is the request id as well as the job — two different approvals replayed on one job are two facts)target- the item the attempt was aimed at (the record'starget)user- the account that made the attemptdetail- the human-readable description stored on the record- Returns:
trueif a record was appended,falseif it was merged into an earlier one
-
record
public boolean record(ChangeType type, String attemptKey, String target, String user, String detail, String grantId) Asrecord(ChangeType, String, String, String, String), naming the grant involved (D-40: a refused CREATE of a name the grant's restriction does not allow). -
recordCoalesced
public boolean recordCoalesced(ChangeType type, String attemptKey, Duration cooldown, String target, String user, String detail) Appends one refused-attempt record unless one was already written under the sametypeandattemptKeyinsidecooldown, whoever made the attempt. Used for refusals where the account says nothing new: a blocked timer run is alwaysSYSTEM, and a blocked upstream run is whatever the upstream build ran as, so a per-user key would neither shorten the history nor add information.- Parameters:
cooldown- how long a written key merges further attempts- Returns:
trueif a record was appended,falseif it was merged
-
recordPersonRefusal
public boolean recordPersonRefusal(ChangeType type, String attemptKey, String target, String user, String build, String detail) D-51, D-51a: appends the record of a re-run a person submitted and was refused. A repeat of the same attempt (attemptKey) by the same user withinCOOLDOWNis merged. Per user at mostPERSON_BUDGETsuch records are written per rollingPERSON_WINDOW; the next refusal in that window writes one summary record, and the ones after it are counted. The store is append-only (JSONL), so the count is written as a closing record when the summary's window ends (flushPersonSummaries()on the per-minute work, the user's own next refusal, orflushAtShutdown()); until then each counted refusal is logged at INFO.Both maps are bounded at
MAX_PERSON_KEYS. Evicting an attempt key only lets that attempt write again, which still spends the user's budget. A user's budget is evicted only once it is idle (no write inside the window, no open summary); when the map is full of users still inside their window, a new user is counted under one shared overflow budget, so no budget is ever reopened by eviction.The bookkeeping is constant-time per call (S-22-04): the only other work is closing the caller's own expired summary, at most one more append. Other users' summaries are closed and idle budgets removed by
flushPersonSummaries()on the per-minute work, so the queue lock this runs under (see the class comment) is held no longer than the hand-off to the store (D-51a, S-21-05).- Parameters:
build- the re-run build's number as text (""when unknown), named in a summary- Returns:
trueif a record (per-attempt or summary) was appended
-
flushPersonSummaries
public void flushPersonSummaries()The per-minute work (S-22-03, S-22-04): collects, under this object's lock, the closing count records of every summary whose window has ended and removes idle budgets, then appends the records outside the lock, so a queue thread refusing something meanwhile never waits behind these appends. Never runs on the queue lock. -
flushAtShutdown
S-22-03: writes the closing count record of every open summary before Jenkins stops, so no summary is left promising a count that never follows. A crash still loses an open count (the records are append-only; documented).
-