-
-
Notifications
You must be signed in to change notification settings - Fork 8.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add extensible background build discarders #4368
Conversation
Default implementation: Run per-job configured discarder periodically
strategy.apply(job); | ||
} catch (Exception ex) { | ||
listener.error("An exception occurred when executing " + displayName + ": " + ex.getMessage()); | ||
LOGGER.log(Level.WARNING, "An exception occurred when executing " + displayName, ex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯 for logging with listener and the Java logger 👍
try { | ||
apply(run); | ||
} catch (IOException|InterruptedException ex) { | ||
// TODO should these actually be caught, or just thrown up to stop applying? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, IOException
should be logged well, but chomped. One "bad" build in a project with 1000 builds shouldn't block attempts to clean up the other 999.
InterruptedException
is a little less-certain: Since this would be happening in the background, what can raise an InterruptedException
in a background task? If there's anything that thinks it will stop a BG task by interrupting it, we'd like to honor that semantic.
If it's more of a "well, threads get interrupted sometimes and that checked exception is part of the Java interface," then, chomp & log along with IOException
.
core/src/main/java/jenkins/model/GlobalBuildDiscarderStrategy.java
Outdated
Show resolved
Hide resolved
@@ -27,7 +27,7 @@ THE SOFTWARE. | |||
--> | |||
<?jelly escape-by-default='true'?> | |||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt"> | |||
<j:if test="${it.parent.buildDiscarder!=null and it.canToggleLogKeep()}"> | |||
<j:if test="${it.canToggleLogKeep()}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥇 for remembering to enable Keep Build Forever
button in the case where a explicit build discarder isn't set for the job
this.discarder = discarder; | ||
} | ||
|
||
public BuildDiscarder getDiscarder() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: @CheckForNull
to catch potential bugs
I've tested this with jcasc, it works, but it's not possible to remove build discarders :( i.e. this:
to
doesn't remove |
@timja Interesting. Did you try to change some of the values to see whether any kind of update works at all? Or what happens when you go from only |
Note to self: Come up with some symbols that don't result in
|
I tried changing values and that worked |
removing was ignored |
@timja Thanks for that. Weird. I basically use the same approach as |
No the same problem does not exist there, Configured with this:
And then I updated it to:
It removed |
Weird I just tested this PR again and JCasC wise everything is working, (except some symbols would be nice to shorten the names) |
Which symbols are missing/wrong? Based on #4368 (comment) it looks OK to me. The ambiguous double use of "global" I already know and plan to fix. |
original:
possibly something like this (if symbols don't clash):
Ideally it would be flattened to not need the but it works so 🤷♂ |
This update now applies the globally configured build discarders after a build finishes so they're no longer only running once an hour. Labels (and probably class names…) are not yet updated accordingly and will still need to be. Naming is hard 😦 |
@timja |
Hmm, been taking a look around, This is what the configuration now looks like: unclassified:
buildDiscarders:
configuredBuildDiscarders:
- "jobBuildDiscarder"
- globalBuildDiscarder:
discarder:
logRotator:
daysToKeepStr: "7"
numToKeepStr: "10" |
core/src/main/java/jenkins/model/GlobalBuildDiscarderConfiguration.java
Outdated
Show resolved
Hide resolved
core/src/main/resources/jenkins/model/JobBackgroundBuildDiscarderStrategy/config.properties
Outdated
Show resolved
Hide resolved
Requested review from everyone who reviewed the previous PR #4336 |
String displayName = strategy.getDescriptor().getDisplayName(); | ||
listener.getLogger().println("Offering " + job.getFullName() + " to " + displayName); | ||
if (strategy.isApplicable(job)) { | ||
listener.getLogger().println(job.getFullName() + " accepted by " + displayName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm slightly concerned that this might be excessive logging on very large instances
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, but it's all going to a rotated set of log files, so will never fill up a disk (except when it's tiny to begin with):
jenkins/core/src/main/java/hudson/model/AsyncPeriodicWork.java
Lines 134 to 156 in eedf2a4
if (f.isFile()) { | |
if ((lastRotateMillis + logRotateMillis < System.currentTimeMillis()) | |
|| (logRotateSize > 0 && f.length() > logRotateSize)) { | |
lastRotateMillis = System.currentTimeMillis(); | |
File prev = null; | |
for (int i = 5; i >= 0; i--) { | |
File curr = i == 0 ? f : new File(f.getParentFile(), f.getName() + "." + i); | |
if (curr.isFile()) { | |
if (prev != null && !prev.exists()) { | |
if (!curr.renameTo(prev)) { | |
logger.log(getErrorLoggingLevel(), "Could not rotate log files {0} to {1}", | |
new Object[]{curr, prev}); | |
} | |
} else { | |
if (!curr.delete()) { | |
logger.log(getErrorLoggingLevel(), "Could not delete log file {0} to enable rotation", | |
curr); | |
} | |
} | |
} | |
prev = curr; | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks fine to me
Added another Open Question to the PR comment for reviewers' consideration. |
try { | ||
apply(run); | ||
} catch (IOException|InterruptedException ex) { | ||
// TODO should these actually be caught, or just thrown up to stop applying? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever you catch an InterruptedException
, it's usually a good idea to re-set the interrupt flag again via Thread.currentThread().interrupt();
Otherwise, you can end up with hung threads if they continue running (which in this case, it very well could).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I would not actually want to catch the InterruptedException
here? The idea was that if there's something wrong with deleting one build's artifacts, e.g. it taking too long to delete a billion files, an initial abort would continue with the next build, rather than just stop this process entirely.
Obviously the downside of that is that it's fairly difficult to really interrupt this thread while it's iterating here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fairly difficult to really interrupt this thread while it's iterating here.
What processes will intentionally attempt such an interrupt? Is there any jenkins-initiated or user-initiated workflow that can target a specific AsyncPeriodicWork and try to stop it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting the interrupt flag doesn't re-throw the interrupted exception btw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to leave a comment about this and noticed that I already did.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@daniel-beck Are you planning on making this change? either not catching the InterruptedException or setting the interrupt flag?
@@ -0,0 +1,3 @@ | |||
blurb = Build discarders configured for a job are only run after a build finishes. \ | |||
This option runs jobs'' configured build discarders periodically, applying configuration changes even when no new builds are run. \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applying configuration changes even when no new builds are run.
This wording seems a bit confusing to me. From where will configuration changes that can be applied to the job come, if not from the job itself?
What kind of configuration change could exist but in an "unapplied" state, that this option would apply?
Should it read
applying build discarder rules even when no new builds are run
or is it really talking about job configuration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or is it really talking about job configuration?
Well, build discarder rules are a part of the job configuration. So in a sense, yes.
Currently, you'd need to run a new build to apply a changed build discarder configuration, since it's only consulted after a build finishes. So without new builds, you can change this option and it doesn't have any consequences. For an obsolete/archived job, it would never actually delete builds/artifacts.
Perhaps
…applying changes to a job's build discarder configuration even when…
?
|
||
@Override | ||
public void apply(Job<?, ?> job) throws IOException, InterruptedException { | ||
job.logRotate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Job
is a non-final, public, abstract class, and logRotate()
is a non-final, public method on Job
- and neither are annotated with @NoExternalUse
.
It is not guaranteed that job.logRotate()
will actually use the build discarder that was checked in isApplicable(...)
. This may cause the documentation around this step to fail to align with what actually happens.
Is there a reason to prefer job.logRotate()
over job.getBuildDiscarder().perform( job )
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I think the expectation is that #logRotate()
not be overridden, but it's not enforced, true.
I am unsure which is the more correct option.
#logRotate()
is called by Run#execute(RunExecution)
, so an argument can be made that this is the expected way to rotate logs, and if the job overrides it in a way that differs from what getBuildDiscarder().perform(…)
does, #logRotate()
should be take precedence.
OTOH, the way this feature is explained on the UI would indicate we want getBuildDiscarder().perform(…)
instead. Obviously, much of that is by necessity.
private static final Logger LOGGER = Logger.getLogger(BackgroundGlobalBuildDiscarder.class.getName()); | ||
|
||
public BackgroundGlobalBuildDiscarder() { | ||
super("Periodic background build discarder"); // TODO i18n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the TODO
required for merge?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. I'm not sure we're even localizing these.
Would be good to allow multiple so that I.e a regex based extension could have different rules for master branch and other branches / prs |
This is still possible, just requires more complex individual discarders (as they'd need to support N patterns/rules instead of 1). Forgot to mention a downside: Especially the default (unconfigurable) jobBuildDiscarder could be added multiple times, possibly leading to user confusion; it seems to me it also potentially be more difficult to write build discarders that have rules that depend on a larger context (e.g. builds across jobs). |
hmm I guess that would be fine |
Could you clarify which behavior is preferable in your opinion? |
More complex individual discarders is fine, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall. Would certainly like to see this feature in production where disk space errors like to hang out.
try { | ||
apply(run); | ||
} catch (IOException|InterruptedException ex) { | ||
// TODO should these actually be caught, or just thrown up to stop applying? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to leave a comment about this and noticed that I already did.
|
||
f = namespace(lib.FormTagLib) | ||
|
||
f.section(title: _("Global Build Discarders")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: does this view really need to be Groovy? Seems fairly trivial to convert to Jelly.
@daniel-beck are the questions in the PR description still open / needing feedback?
There's 4 approvals on this, just checking if everything is in order to move forward on it |
I have received no responses to the first question. I'm fairly OK with the way it works, just flagging it for attention if someone cares. It doesn't look like it so far. I have only received one response to the second question (from you), and it was approving of the current implementation after a short conversation. FWIW this can always be opened up further, but restricting would be difficult. There's one unaddressed feedback of the code catching an |
There appears to be no outstanding issues and 4 approvals, looks like it's time to get this in. After a 24 hour timeout for last minute feedback, this can be merged. |
(Description updated 2019-12-15)
This is the extensible implementation of #4336, allowing to pull out regex matching into a plugin.
A new extension point for configuring global build discarders is added to core. These are run:
It provides two simple implementations of a new extension point in core:
Additionally, the API for the new extension point
BackgroundBuildDiscarderStrategy
should be flexible enough to support regexes and other complex criteria as discussed in the original issue #4336.Screenshot:
Configuration UI:
Manual testing hint: Trigger periodic execution manually using
ExtensionList.lookupSingleton(BackgroundBuildDiscarder.class).doRun()
in script console.On classification: Major bug if the lack of periodic execution of the configured build rotator is considered a bug at all, as it could result in data loss with carelessly configured projects relying on the previous behavior.
TODO List:
Open Questions (feedback welcome):
Proposed changelog entries
BackgroundBuildDiscarderStrategy
to allow more flexible build discarding strategies for the global build discarder configuration.Submitter checklist
* Use the
Internal:
prefix if the change has no user-visible impact (API, test frameworks, etc.)Desired reviewers
@awittha (author of #4336)
@res0nance (reviewer of #4336)