Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@
import java.io.File;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.jenkinsci.plugins.workflow.actions.LogAction;
Expand All @@ -68,22 +73,28 @@
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.jenkinsci.plugins.workflow.steps.StepExecutions;
import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.TestExtension;
import org.jvnet.hudson.test.recipes.WithTimeout;
import org.kohsuke.stapler.DataBoundConstructor;

@Issue("JENKINS-38381")
public class DefaultLogStorageTest {

private static final Logger LOGGER = Logger.getLogger(DefaultLogStorageTest.class.getName());

@Rule public JenkinsRule r = new JenkinsRule();
@Rule public LoggerRule logging = new LoggerRule();
@Rule public ErrorCollector errors = new ErrorCollector();

@Test public void consoleNotes() throws Exception {
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
Expand Down Expand Up @@ -143,7 +154,7 @@ static class Execution extends SynchronousStepExecution<Void> {
@Test public void performance() throws Exception {
assumeFalse(Functions.isWindows()); // needs newline fixes; not bothering for now
WorkflowJob p = r.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("@NonCPS def giant() {(0..999999).join('\\n')}; echo giant(); sleep 0", true));
p.setDefinition(new CpsFlowDefinition("giant(6); echo 'quick message at the end'", true));
long start = System.nanoTime();
WorkflowRun b = r.buildAndAssertSuccess(p);
System.out.printf("Took %dms to run the build%n", (System.nanoTime() - start) / 1000 / 1000);
Expand All @@ -167,7 +178,7 @@ static class Execution extends SynchronousStepExecution<Void> {
System.out.printf("Took %dms to write truncated HTML of whole build%n", (System.nanoTime() - start) / 1000 / 1000);
assertThat(sw.toString(), not(containsString("\n456789\n")));
assertThat(sw.toString(), containsString("\n999923\n"));
/* Whether or not this echo step is annotated in the truncated log is not really important:
/* Whether or not this step is annotated in the truncated log is not really important:
assertThat(sw.toString(), containsString("\n999999\n</span>"));
*/
// Plain text:
Expand All @@ -180,10 +191,9 @@ static class Execution extends SynchronousStepExecution<Void> {
String rawLog = FileUtils.readFileToString(new File(b.getRootDir(), "log"), StandardCharsets.UTF_8);
assertThat(rawLog, containsString("0\n"));
assertThat(rawLog, containsString("\n999999\n"));
assertThat(rawLog, containsString("sleep any longer"));
assertThat(rawLog, containsString("quick message at the end"));
// Per node:
FlowNode echo = new DepthFirstScanner().findFirstMatch(b.getExecution(), new NodeStepTypePredicate("echo"));
LogAction la = echo.getAction(LogAction.class);
LogAction la = new DepthFirstScanner().findFirstMatch(b.getExecution(), new NodeStepTypePredicate("giant")).getAction(LogAction.class);
assertNotNull(la);
baos = new ByteArrayOutputStream();
la.getLogText().writeRawLogTo(0, baos);
Expand Down Expand Up @@ -214,21 +224,90 @@ static class Execution extends SynchronousStepExecution<Void> {
System.out.printf("Took %dms to write plain text of one long node%n", (System.nanoTime() - start) / 1000 / 1000);
assertThat(baos.toString(), containsString("\n456789\n"));
// Node with litte text:
FlowNode sleep = new DepthFirstScanner().findFirstMatch(b.getExecution(), new NodeStepTypePredicate("sleep"));
la = sleep.getAction(LogAction.class);
la = new DepthFirstScanner().findFirstMatch(b.getExecution(), new NodeStepTypePredicate("echo")).getAction(LogAction.class);
assertNotNull(la);
sw = new StringWriter();
start = System.nanoTime();
la.getLogText().writeHtmlTo(0, sw);
System.out.printf("Took %dms to write HTML of one short node%n", (System.nanoTime() - start) / 1000 / 1000);
assertThat(sw.toString(), containsString("No need to sleep any longer"));
assertThat(sw.toString(), containsString("quick message at the end"));
// Length check
start = System.nanoTime();
length = la.getLogText().length();
System.out.printf("Took %dms to compute length of one short node%n", (System.nanoTime() - start) / 1000 / 1000);
assertThat(length, lessThan(50L));
}

public static final class GiantStep extends Step {
final int digits;
@DataBoundConstructor public GiantStep(int digits) {
this.digits = digits;
}
@Override public StepExecution start(StepContext context) throws Exception {
return StepExecutions.synchronousNonBlockingVoid(context, c -> {
var ps = c.get(TaskListener.class).getLogger();
for (int i = 0; i < Math.pow(10, digits); i++) {
ps.printf("%0" + digits + "d%n", i);
}
});
}
@TestExtension public static final class DescriptorImpl extends StepDescriptor {
@Override public String getFunctionName() {
return "giant";
}
@Override public Set<? extends Class<?>> getRequiredContext() {
return Set.of(TaskListener.class);
}
}
}

// Access large logs via HTTP:

@Ignore("for interactive use")
@WithTimeout(600)
@Issue("JENKINS-75081")
@Test public void giantLogRunning() throws Exception {
Copy link
Member Author

@jglick jglick Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before: passes in 132s.

After: passes in 135s.

Code path not changed here, either way uses an inefficient system but at least only in smallish chunks.

var p = r.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("for (int i = 0; i < 10; i++) {giant(7)}", true));
var b = p.scheduleBuild2(0).waitForStart();
LOGGER.info("Running");
var base = URI.create(p.getAbsoluteUrl() + "/1/");
var client = HttpClient.newHttpClient();
String start = "0";
while (true) {
LOGGER.info("progressiveHtml?start=" + start);
var headers = client.send(HttpRequest.newBuilder(base.resolve("logText/progressiveHtml?start=" + start)).build(), HttpResponse.BodyHandlers.discarding()).headers();
if (Boolean.parseBoolean(headers.firstValue("X-More-Data").orElse("false"))) {
start = headers.firstValue("X-Text-Size").get();
} else {
break;
}
}
LOGGER.info("Complete");
r.assertBuildStatusSuccess(r.waitForCompletion(b));
}

@Ignore("for interactive use")
@WithTimeout(600)
@Issue("JENKINS-75081")
@Test public void giantLogCompleted() throws Exception {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before: crashes with multiple OOMEs.

After: OOMEs in WebClient, so I guess I need to rewrite the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also after:

java.lang.OutOfMemoryError: Java heap space
	at org.kohsuke.stapler.framework.io.CharSpool.renew(CharSpool.java:63)
	at org.kohsuke.stapler.framework.io.CharSpool.write(CharSpool.java:50)
	at java.base/java.io.Writer.implWrite(Writer.java:334)
	at java.base/java.io.Writer.write(Writer.java:318)
	at java.base/java.io.Writer.write(Writer.java:278)
	at hudson.console.ConsoleAnnotationOutputStream.eol(ConsoleAnnotationOutputStream.java:148)
	at hudson.console.LineTransformationOutputStream.eol(LineTransformationOutputStream.java:69)
	at hudson.console.LineTransformationOutputStream.write(LineTransformationOutputStream.java:62)
	at java.base/java.io.FilterOutputStream.write(FilterOutputStream.java:89)
	at org.jenkinsci.plugins.workflow.log.FileLogStorage$1$1.write(FileLogStorage.java:252)
	at java.base/java.io.FilterOutputStream.write(FilterOutputStream.java:139)
	at org.apache.commons.io.output.ProxyOutputStream.write(ProxyOutputStream.java:213)
	at org.kohsuke.stapler.framework.io.LargeText.writeLogUncounted(LargeText.java:250)
	at org.kohsuke.stapler.framework.io.LargeText.writeLogTo(LargeText.java:235)
	at hudson.console.AnnotatedLargeText.writeRawLogTo(AnnotatedLargeText.java:234)
	at org.jenkinsci.plugins.workflow.log.FileLogStorage$1.writeHtmlTo(FileLogStorage.java:217)
	at hudson.console.AnnotatedLargeText.writeLogTo(AnnotatedLargeText.java:208)
	at org.kohsuke.stapler.framework.io.LargeText.doProgressTextImpl(LargeText.java:319)
	at org.kohsuke.stapler.framework.io.LargeText.doProgressText(LargeText.java:277)
	at hudson.console.AnnotatedLargeText.doProgressiveHtmlImpl(AnnotatedLargeText.java:119)
	at hudson.console.AnnotatedLargeText.doProgressiveHtml(AnnotatedLargeText.java:104)

which as noted cannot be solved (we cannot avoid buffering in HTML mode).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before, with new HTTP client, and after ceasing to even bother to test progressiveHtml on a completed build:

console fails

java.lang.OutOfMemoryError: Java heap space
	at java.base/java.util.Arrays.copyOf(Arrays.java:3541)
	at java.base/java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:100)
	at java.base/java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:132)
	at org.apache.commons.io.output.ProxyOutputStream.write(ProxyOutputStream.java:147)
	at org.kohsuke.stapler.framework.io.LargeText.writeLogTo(LargeText.java:244)
	at hudson.console.AnnotatedLargeText.writeRawLogTo(AnnotatedLargeText.java:226)
	at org.jenkinsci.plugins.workflow.job.WorkflowRun$$Lambda/0x000000010099e798.writeLogTo(Unknown Source)
	at org.jenkinsci.plugins.workflow.job.WorkflowRun.writeLogTo(WorkflowRun.java:1167)
	at org.jenkinsci.plugins.workflow.job.WorkflowRun.getLogInputStream(WorkflowRun.java:1135)
	at hudson.model.Run.writeLogTo(Run.java:1464)

consoleFull passes (oddly)

consoleText passes

progressiveText fails

java.lang.OutOfMemoryError: Java heap space
	at org.kohsuke.stapler.framework.io.CharSpool.renew(CharSpool.java:63)
	at org.kohsuke.stapler.framework.io.CharSpool.write(CharSpool.java:50)
	at org.kohsuke.stapler.framework.io.WriterOutputStream.flushOutput(WriterOutputStream.java:90)
	at org.kohsuke.stapler.framework.io.WriterOutputStream.decode(WriterOutputStream.java:125)
	at org.kohsuke.stapler.framework.io.WriterOutputStream.write(WriterOutputStream.java:73)
	at hudson.console.PlainTextConsoleOutputStream.eol(PlainTextConsoleOutputStream.java:86)
	at hudson.console.LineTransformationOutputStream.eol(LineTransformationOutputStream.java:69)
	at hudson.console.LineTransformationOutputStream.write(LineTransformationOutputStream.java:62)
	at hudson.console.LineTransformationOutputStream.write(LineTransformationOutputStream.java:84)
	at org.apache.commons.io.output.ProxyOutputStream.write(ProxyOutputStream.java:147)
	at org.kohsuke.stapler.framework.io.LargeText.writeLogTo(LargeText.java:244)
	at hudson.console.AnnotatedLargeText.writeLogTo(AnnotatedLargeText.java:217)
	at org.kohsuke.stapler.framework.io.LargeText.writeLogTo(LargeText.java:218)
	at hudson.console.AnnotatedLargeText.writeLogTo(AnnotatedLargeText.java:207)
	at org.kohsuke.stapler.framework.io.LargeText.doProgressTextImpl(LargeText.java:311)
	at org.kohsuke.stapler.framework.io.LargeText.doProgressText(LargeText.java:274)
	at hudson.console.AnnotatedLargeText.doProgressiveText(AnnotatedLargeText.java:128)

After:

all pass.

var p = r.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("giant(8)", true)); // 859Mb
LOGGER.info("running build");
var b = r.buildAndAssertSuccess(p);
LOGGER.info("completed");
var base = URI.create(p.getAbsoluteUrl() + "/1/");
var client = HttpClient.newHttpClient();
LOGGER.info("console");
errors.checkSucceeds(() -> client.send(HttpRequest.newBuilder(base.resolve("console")).build(), HttpResponse.BodyHandlers.discarding()));
LOGGER.info("consoleFull");
errors.checkSucceeds(() -> client.send(HttpRequest.newBuilder(base.resolve("consoleFull")).build(), HttpResponse.BodyHandlers.discarding()));
LOGGER.info("consoleText");
errors.checkSucceeds(() -> client.send(HttpRequest.newBuilder(base.resolve("consoleText")).build(), HttpResponse.BodyHandlers.discarding()));
LOGGER.info("progressiveText");
errors.checkSucceeds(() -> client.send(HttpRequest.newBuilder(base.resolve("logText/progressiveText")).build(), HttpResponse.BodyHandlers.discarding()));
}

@Ignore("Currently not asserting anything, just here for interactive evaluation.")
@Test public void parallelLogStreaming() throws Exception {
assumeFalse(Functions.isWindows());
Expand Down