-
Notifications
You must be signed in to change notification settings - Fork 80
Ensure log streams are flushed before garbage collection when using GCFlushedOutputStream by switching to Cleaner
#388
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
base: master
Are you sure you want to change the base?
Changes from all commits
de1c186
cf0a79f
2037ed8
346ad72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,9 @@ | |
|
|
||
| import static org.hamcrest.MatcherAssert.assertThat; | ||
| import static org.hamcrest.Matchers.empty; | ||
| import static org.hamcrest.Matchers.instanceOf; | ||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assume.assumeThat; | ||
|
|
||
| import edu.umd.cs.findbugs.annotations.NonNull; | ||
| import hudson.console.AnnotatedLargeText; | ||
|
|
@@ -47,6 +49,7 @@ | |
| import java.util.Map; | ||
| import java.util.Random; | ||
| import java.util.concurrent.Callable; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.function.BiFunction; | ||
| import java.util.logging.Level; | ||
| import jenkins.model.CauseOfInterruption; | ||
|
|
@@ -64,7 +67,9 @@ | |
| import org.jenkinsci.plugins.workflow.job.WorkflowRun; | ||
| import org.junit.Before; | ||
| import org.junit.ClassRule; | ||
| import org.junit.Rule; | ||
| import org.junit.Test; | ||
| import org.jvnet.hudson.test.FlagRule; | ||
| import org.jvnet.hudson.test.JenkinsRule; | ||
| import org.jvnet.hudson.test.LoggerRule; | ||
| import org.springframework.security.core.Authentication; | ||
|
|
@@ -78,6 +83,16 @@ | |
|
|
||
| @ClassRule public static LoggerRule logging = new LoggerRule(); | ||
|
|
||
| @Rule public FlagRule<Long> resetTuningMinRecurrence = new FlagRule<>( | ||
| () -> DelayBufferedOutputStream.Tuning.DEFAULT.minRecurrencePeriod, | ||
| x -> DelayBufferedOutputStream.Tuning.DEFAULT.minRecurrencePeriod = x); | ||
| @Rule public FlagRule<Long> resetTuningMaxRecurrence = new FlagRule<>( | ||
| () -> DelayBufferedOutputStream.Tuning.DEFAULT.maxRecurrencePeriod, | ||
| x -> DelayBufferedOutputStream.Tuning.DEFAULT.maxRecurrencePeriod = x); | ||
| @Rule public FlagRule<Boolean> resetGcFlushedOutputStream = new FlagRule<>( | ||
| () -> GCFlushedOutputStream.DISABLED, | ||
| x -> GCFlushedOutputStream.DISABLED = x); | ||
|
|
||
| /** Create a new storage implementation, but potentially reusing any data initialized in the last {@link Before} setup. */ | ||
| protected abstract LogStorage createStorage(); | ||
|
|
||
|
|
@@ -171,9 +186,66 @@ | |
| } | ||
|
|
||
| @Test public void remoting() throws Exception { | ||
| remotingParameterized(RemotingTestVariant.Regular); | ||
| } | ||
|
|
||
| /** | ||
| * Verifies that flushing explicitly makes {@link BufferedBuildListener} and {@link GCFlushedOutputStream} unnecessary. | ||
| * Only relevant for {@link LogStorage} implementations that use {@link BufferedBuildListener}. | ||
| */ | ||
| @Test public void remotingNormalFlushOnly() throws Exception { | ||
| remotingParameterized(RemotingTestVariant.ManualFlushOnly); | ||
| } | ||
|
|
||
| /** | ||
| * Verifies the behavior of {@link BufferedBuildListener}. | ||
| * Only relevant for {@link LogStorage} implementations that use {@link BufferedBuildListener}. | ||
| */ | ||
| @Test public void remotingDelayedAutoFlushOnly() throws Exception { | ||
| // To make this test fail, set `DelayBufferedOutputStream.Tuning.DEFAULT.*RecurrencePeriod` to very large values. | ||
| remotingParameterized(RemotingTestVariant.DelayedAutoFlushOnly); | ||
| } | ||
|
|
||
| /** | ||
| * Verifies the behavior of {@link GCFlushedOutputStream}. | ||
| * Only relevant for {@link LogStorage} implementations that use {@link BufferedBuildListener}. | ||
| */ | ||
| @Test public void remotingGcFlushOnly() throws Exception { | ||
| // To make this test fail, set `GCFlushedOutputStream.DISABLED` to true. | ||
| remotingParameterized(RemotingTestVariant.GcFlushOnly); | ||
| } | ||
|
|
||
| private enum RemotingTestVariant { | ||
| Regular(false, false, false), | ||
| ManualFlushOnly(true, true, false), | ||
| DelayedAutoFlushOnly(true, false, true), | ||
| GcFlushOnly(false, true, true); | ||
|
|
||
| boolean disableGcFlush; | ||
| boolean disableDelayedAutoFlush; | ||
| boolean disableManualFlush; | ||
|
|
||
| RemotingTestVariant(boolean disableGcFlush, boolean disableDelayedAutoFlush, boolean disableManualFlush) { | ||
| this.disableGcFlush = disableGcFlush; | ||
| this.disableDelayedAutoFlush = disableDelayedAutoFlush; | ||
| this.disableManualFlush = disableManualFlush; | ||
| } | ||
| } | ||
| private void remotingParameterized(RemotingTestVariant variant) throws Exception { | ||
| logging.capture(100).record(Channel.class, Level.WARNING); | ||
| if (variant.disableDelayedAutoFlush) { | ||
| DelayBufferedOutputStream.Tuning.DEFAULT.minRecurrencePeriod = TimeUnit.HOURS.toMillis(1); | ||
| DelayBufferedOutputStream.Tuning.DEFAULT.maxRecurrencePeriod = TimeUnit.HOURS.toMillis(1); | ||
| } | ||
| if (variant.disableGcFlush) { | ||
| GCFlushedOutputStream.DISABLED = true; | ||
| } | ||
| LogStorage ls = createStorage(); | ||
| TaskListener overall = ls.overallListener(); | ||
| if (variant != RemotingTestVariant.Regular) { | ||
| assumeThat("Skipping BufferedBuildListener-specific tests because listener is " + overall, | ||
| overall, instanceOf(BufferedBuildListener.class)); | ||
|
Comment on lines
+246
to
+247
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unnecessary, i.e. the new tests pass against If desired, we could instead move the new tests down into
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fine, I guess, but no one should rely on that; remote code printing to a |
||
| } | ||
| overall.getLogger().println("overall from controller"); | ||
| TaskListener step = ls.nodeListener(new MockNode("1")); | ||
| step.getLogger().println("step from controller"); | ||
|
|
@@ -185,9 +257,8 @@ | |
| DumbSlave s = r.createOnlineSlave(); | ||
| r.showAgentLogs(s, agentLoggers()); | ||
| VirtualChannel channel = s.getChannel(); | ||
| channel.call(new RemotePrint("overall from agent", overall)); | ||
| channel.call(new RemotePrint("step from agent", step)); | ||
| channel.call(new GC()); | ||
| channel.call(new RemotePrint("overall from agent", overall, variant)); | ||
| channel.call(new RemotePrint("step from agent", step, variant)); | ||
| overallPos = assertOverallLog(overallPos, lines( | ||
| "overall from agent", | ||
| "<span class=\"pipeline-node-1\">step from agent", | ||
|
|
@@ -202,22 +273,32 @@ | |
| } | ||
| private static final class RemotePrint extends MasterToSlaveCallable<Void, Exception> { | ||
| private final String message; | ||
| private final TaskListener listener; | ||
| RemotePrint(String message, TaskListener listener) { | ||
| private TaskListener listener; | ||
| private final RemotingTestVariant variant; | ||
| RemotePrint(String message, TaskListener listener, RemotingTestVariant variant) { | ||
| this.message = message; | ||
| this.listener = listener; | ||
| this.variant = variant; | ||
| } | ||
| @Override public Void call() { | ||
| @Override public Void call() throws Exception { | ||
| listener.getLogger().println(message); | ||
| listener.getLogger().flush(); | ||
| return null; | ||
| } | ||
| } | ||
| /** Checking behavior of {@link DelayBufferedOutputStream} garbage collection. */ | ||
| private static final class GC extends MasterToSlaveCallable<Void, Exception> { | ||
| @Override public Void call() { | ||
| System.gc(); | ||
| System.runFinalization(); | ||
| if (!variant.disableManualFlush) { | ||
| listener.getLogger().flush(); | ||
| } | ||
| switch (variant) { | ||
| case DelayedAutoFlushOnly -> { | ||
| // TODO: It would be better to wait for `DelayBufferedOutputStream.flushBuffer` to run exactly once. | ||
| Thread.sleep(DelayBufferedOutputStream.Tuning.DEFAULT.minRecurrencePeriod * 3); | ||
| } | ||
| case GcFlushOnly -> { | ||
| listener = null; | ||
| // Sleeping and calling `System.gc` more than once seem to be necessary for Cleaner to run reliably. | ||
| for (int i = 0; i < 3; i++) { | ||
| System.gc(); | ||
| Thread.sleep(1000); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+288
to
+301
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that in real-world scenarios, where we do not nicely wait after writing each individual line, there is no guarantee of ordering with |
||
| return null; | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
Use
SystemPropertiesgenerally. Also prefer positive to negative sense, e.g.