Skip to content

Commit 7aa7627

Browse files
committed
Merge branch 'logs-JENKINS-38381' into ConsoleLogFilter-JENKINS-45693
2 parents af477ab + a63fe88 commit 7aa7627

File tree

7 files changed

+110
-14
lines changed

7 files changed

+110
-14
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<parent>
2929
<groupId>org.jenkins-ci.plugins</groupId>
3030
<artifactId>plugin</artifactId>
31-
<version>3.19</version>
31+
<version>3.21</version>
3232
<relativePath />
3333
</parent>
3434
<groupId>org.jenkins-ci.plugins.workflow</groupId>

src/main/java/org/jenkinsci/plugins/workflow/graph/FlowNode.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
5252
import org.jenkinsci.plugins.workflow.graphanalysis.FlowScanningUtils;
5353
import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate;
54+
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
5455
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
5556
import org.kohsuke.accmod.Restricted;
5657
import org.kohsuke.accmod.restrictions.DoNotUse;
@@ -279,7 +280,17 @@ public String getDisplayFunctionName() {
279280
*/
280281
@Exported
281282
public BallColor getIconColor() {
282-
BallColor c = getError()!=null ? BallColor.RED : BallColor.BLUE;
283+
ErrorAction error = getError();
284+
BallColor c = null;
285+
if(error != null) {
286+
if(error.getError() instanceof FlowInterruptedException) {
287+
c = BallColor.ABORTED;
288+
} else {
289+
c = BallColor.RED;
290+
}
291+
} else {
292+
c = BallColor.BLUE;
293+
}
283294
if (isActive()) {
284295
c = c.anime();
285296
}

src/main/java/org/jenkinsci/plugins/workflow/log/FileLogStorage.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,12 @@ private synchronized void open() throws IOException {
8686
os = new FileOutputStream(log, true);
8787
if (index.isFile()) {
8888
try (BufferedReader r = Files.newBufferedReader(index.toPath(), StandardCharsets.UTF_8)) {
89+
// TODO would be faster to scan the file backwards for the penultimate \n, then convert the byte sequence from there to EOF to UTF-8 and set lastId accordingly
8990
String lastLine = null;
9091
while (true) {
92+
// Note that BufferedReader tolerates final lines without a line separator, so if for some reason the last write has been truncated this result could be incorrect.
93+
// In practice this seems unlikely since we explicitly flush after the newline, so we should be sending a single small block to the filesystem to persist.
94+
// Anyway at worst the result would be a (perhaps temporarily) incorrect line → step mapping, which is tolerable for one step of one build, and barely affects the overall build log.
9195
String line = r.readLine();
9296
if (line == null) {
9397
break;
@@ -122,6 +126,9 @@ private void checkId(String id) throws IOException {
122126
} else {
123127
indexOs.write(pos + " " + id + "\n");
124128
}
129+
// Could call FileChannel.force(true) like hudson.util.FileChannelWriter does for AtomicFileWriter,
130+
// though making index-log writes slower is likely a poor tradeoff for slightly more reliable log display,
131+
// since logs are often never read and this is transient data rather than configuration or valuable state.
125132
indexOs.flush();
126133
lastId = id;
127134
}
@@ -180,13 +187,23 @@ private final class IndexOutputStream extends OutputStream {
180187
try (BufferedReader indexBR = index.isFile() ? Files.newBufferedReader(index.toPath(), StandardCharsets.UTF_8) : new BufferedReader(new NullReader(0))) {
181188
ConsoleAnnotationOutputStream<FlowExecutionOwner.Executable> caos = new ConsoleAnnotationOutputStream<>(w, ConsoleAnnotators.createAnnotator(build), build, StandardCharsets.UTF_8);
182189
long r = this.writeRawLogTo(start, new FilterOutputStream(caos) {
190+
// To insert startStep/endStep annotations into the overall log, we need to simultaneously read index-log.
191+
// We use the standard LargeText.FileSession to get the raw log text (we need not think about ConsoleNote here), having seeked to the start position.
192+
// Then we read index-log in order, looking for transitions from one step to the next (or to or from non-step overall output).
193+
// Whenever we are about to write a byte which is at a boundary, or if there is a boundary at EOF, the HTML annotations are injected into the output;
194+
// the read of index-log is advanced lazily (it is not necessary to have the whole mapping in memory).
183195
long lastTransition = -1;
196+
boolean eof; // NullReader is strict and throws IOException (not EOFException) if you read() again after having already gotten -1
184197
String lastId;
185198
long pos = start;
186199
boolean hadLastId;
187200
@Override public void write(int b) throws IOException {
188-
String line;
189-
while (lastTransition < pos && (line = indexBR.readLine()) != null) {
201+
while (lastTransition < pos && !eof) {
202+
String line = indexBR.readLine();
203+
if (line == null) {
204+
eof = true;
205+
break;
206+
}
190207
int space = line.indexOf(' ');
191208
try {
192209
lastTransition = Long.parseLong(space == -1 ? line : line.substring(0, space));
@@ -226,6 +243,12 @@ private final class IndexOutputStream extends OutputStream {
226243
try (ByteBuffer buf = new ByteBuffer();
227244
RandomAccessFile raf = new RandomAccessFile(log, "r");
228245
BufferedReader indexBR = index.isFile() ? Files.newBufferedReader(index.toPath(), StandardCharsets.UTF_8) : new BufferedReader(new NullReader(0))) {
246+
// Check this _before_ reading index-log to reduce the chance of a race condition resulting in recent content being associated with the wrong step:
247+
long end = raf.length();
248+
// To produce just the output for a single step (again we do not need to pay attention to ConsoleNote here since AnnotatedLargeText handles it),
249+
// index-log is read looking for transitions that pertain to this step: beginning or ending its content, including at EOF if applicable.
250+
// (Other transitions, such as to or from unrelated steps, are irrelevant).
251+
// Once a start and end position have been identified, that block is copied to a memory buffer.
229252
String line;
230253
long pos = -1; // -1 if not currently in this node, start position if we are
231254
while ((line = indexBR.readLine()) != null) {
@@ -234,27 +257,42 @@ private final class IndexOutputStream extends OutputStream {
234257
try {
235258
lastTransition = Long.parseLong(space == -1 ? line : line.substring(0, space));
236259
} catch (NumberFormatException x) {
260+
// If index-log is corrupt for whatever reason, we given up on this step in this build;
261+
// there is no way we would be able to produce accurate output anyway.
262+
// Note that NumberFormatException is already logged separately for the overall build log,
263+
// which is in that case nonfatal: the whole-build HTML output always includes exactly what is in the main log file,
264+
// at worst with some missing or inaccurate startStep/endStep annotations.
237265
break; // corrupt index file; forget it
238266
}
239267
if (pos == -1) {
240268
if (space != -1 && line.substring(space + 1).equals(id)) {
241269
pos = lastTransition;
242270
}
243-
} else {
271+
} else if (lastTransition > pos) {
244272
raf.seek(pos);
245273
if (lastTransition > pos + Integer.MAX_VALUE) {
246274
throw new IOException("Cannot read more than 2Gib at a time"); // ByteBuffer does not support it anyway
247275
}
248-
// TODO can probably be done a bit more efficiently with FileChannel methods
276+
// Could perhaps be done a bit more efficiently with FileChannel methods,
277+
// at least if org.kohsuke.stapler.framework.io.ByteBuffer were replaced by java.nio.[Heap]ByteBuffer.
278+
// The overall bottleneck here is however the need to use a memory buffer to begin with:
279+
// LargeText.Source/Session are not public so, pending improvements to Stapler,
280+
// we cannot lazily stream per-step content the way we do for the overall log.
281+
// (Except perhaps by extending ByteBuffer and then overriding every public method!)
282+
// LargeText also needs to be improved to support opaque (non-long) cursors
283+
// (and callers such as progressiveText.jelly and Blue Ocean updated accordingly),
284+
// which is a hard requirement for efficient rendering of cloud-backed logs,
285+
// though for this implementation we do not need it since we can work with byte offsets.
249286
byte[] data = new byte[(int) (lastTransition - pos)];
250287
raf.readFully(data);
251288
buf.write(data);
252289
pos = -1;
253-
}
290+
} // else some sort of mismatch
254291
}
255-
if (pos != -1) {
292+
if (pos != -1 && /* otherwise race condition? */ end > pos) {
293+
// In case the build is ongoing and we are still actively writing content for this step,
294+
// we will hit EOF before any other transition. Otherwise identical to normal case above.
256295
raf.seek(pos);
257-
long end = raf.length();
258296
if (end > pos + Integer.MAX_VALUE) {
259297
throw new IOException("Cannot read more than 2Gib at a time");
260298
}

src/main/java/org/jenkinsci/plugins/workflow/log/package-info.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@
2424

2525
/**
2626
* APIs supporting the production and retrieval of log messages associated with Pipeline builds ({@link FlowExecutionOwner}) and individual steps ({@link FlowNode}).
27+
* <p>Note that the term “step” is used loosely in documentation here to refer to a {@link FlowNode},
28+
* which is only precise in the case of {@link AtomNode}s.
29+
* Block-scoped {@link Step}s which use {@link BodyInvoker} can be producing output interleaved with their children,
30+
* something the {@link FlowNode#getId} should track.
2731
* @see <a href="https://github.com/jenkinsci/jep/blob/master/jep/210/README.adoc">JEP-210</a>
2832
*/
2933
package org.jenkinsci.plugins.workflow.log;
3034

3135
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
36+
import org.jenkinsci.plugins.workflow.graph.AtomNode;
3237
import org.jenkinsci.plugins.workflow.graph.FlowNode;
38+
import org.jenkinsci.plugins.workflow.steps.BodyInvoker;
39+
import org.jenkinsci.plugins.workflow.steps.Step;

src/test/java/org/jenkinsci/plugins/workflow/graph/FlowNodeTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
package org.jenkinsci.plugins.workflow.graph;
2626

27+
import hudson.model.BallColor;
28+
import hudson.model.Result;
29+
2730
import java.lang.reflect.Method;
2831
import java.text.MessageFormat;
2932
import java.util.ArrayList;
@@ -38,22 +41,26 @@
3841
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
3942
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
4043
import org.jenkinsci.plugins.workflow.graphanalysis.FlowScanningUtils;
44+
import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate;
4145
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
4246
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
4347
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
4448
import org.junit.Ignore;
4549
import org.junit.Test;
4650

51+
import static org.hamcrest.Matchers.hasSize;
4752
import static org.junit.Assert.*;
4853
import org.junit.Rule;
4954
import org.junit.runners.model.Statement;
5055
import org.jvnet.hudson.test.Issue;
5156
import org.jvnet.hudson.test.LoggerRule;
57+
import org.jvnet.hudson.test.JenkinsRule;
5258
import org.jvnet.hudson.test.RestartableJenkinsRule;
5359

5460
public class FlowNodeTest {
5561

5662
@Rule public RestartableJenkinsRule rr = new RestartableJenkinsRule();
63+
@Rule public JenkinsRule r = new JenkinsRule();
5764
@Rule public LoggerRule logging = new LoggerRule().record(FlowNode.class, Level.FINER);
5865

5966
@Issue("JENKINS-38223")
@@ -408,6 +415,27 @@ public void evaluate() throws Throwable {
408415
});
409416
}
410417

418+
@Test public void useAbortedStatusWhenFailFast() throws Exception {
419+
WorkflowJob job = r.jenkins.createProject(WorkflowJob.class, "p");
420+
job.setDefinition(new CpsFlowDefinition(
421+
"jobs = [failFast:true]\n" +
422+
"jobs['one'] = {\n" +
423+
" sleep 5\n" +
424+
"}\n" +
425+
"jobs['two'] = {\n" +
426+
" error 'failing'\n" +
427+
"}\n" +
428+
"parallel jobs", true));
429+
WorkflowRun b = r.assertBuildStatus(Result.FAILURE, job.scheduleBuild2(0).get());
430+
431+
List<FlowNode> coreStepNodes = new DepthFirstScanner().filteredNodes(b.getExecution(), new NodeStepTypePredicate("sleep"));
432+
assertThat(coreStepNodes, hasSize(1));
433+
assertEquals("sleep", coreStepNodes.get(0).getDisplayFunctionName());
434+
assertNotNull(coreStepNodes.get(0).getError());
435+
assertNotNull(coreStepNodes.get(0).getError().getError());
436+
assertEquals(BallColor.ABORTED, coreStepNodes.get(0).getIconColor());
437+
}
438+
411439
private void assertExpectedEnclosing(FlowExecution execution, String nodeId, String enclosingId) throws Exception {
412440
FlowNode node = execution.getNode(nodeId);
413441
assertNotNull(node);

src/test/java/org/jenkinsci/plugins/workflow/log/FileLogStorageTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424

2525
package org.jenkinsci.plugins.workflow.log;
2626

27+
import hudson.model.TaskListener;
2728
import java.io.File;
29+
import static org.junit.Assert.*;
2830
import org.junit.Before;
2931
import org.junit.Rule;
32+
import org.junit.Test;
3033
import org.junit.rules.TemporaryFolder;
3134

3235
public class FileLogStorageTest extends LogStorageTestBase {
@@ -42,4 +45,13 @@ public class FileLogStorageTest extends LogStorageTestBase {
4245
return FileLogStorage.forFile(log);
4346
}
4447

48+
@Test public void oldFormat() throws Exception {
49+
LogStorage ls = createStorage();
50+
TaskListener overall = ls.overallListener();
51+
overall.getLogger().println("stuff");
52+
close(overall);
53+
assertTrue(new File(log + "-index").delete());
54+
assertOverallLog(0, "stuff\n", true);
55+
}
56+
4557
}

src/test/java/org/jenkinsci/plugins/workflow/log/LogStorageTestBase.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public abstract class LogStorageTestBase {
134134
assertLength(overallHtmlPos);
135135
}
136136

137-
private static void close(TaskListener listener) throws Exception {
137+
protected static void close(TaskListener listener) throws Exception {
138138
if (listener instanceof AutoCloseable) {
139139
((AutoCloseable) listener).close();
140140
}
@@ -230,11 +230,11 @@ private static final class RemotePrint extends MasterToSlaveCallable<Void, Excep
230230

231231
// TODO test missing final newline
232232

233-
private long assertOverallLog(long start, String expected, boolean html) throws Exception {
233+
protected final long assertOverallLog(long start, String expected, boolean html) throws Exception {
234234
return assertLog(() -> text(), start, expected, html, html);
235235
}
236236

237-
private long assertStepLog(String id, long start, String expected, boolean html) throws Exception {
237+
protected final long assertStepLog(String id, long start, String expected, boolean html) throws Exception {
238238
return assertLog(() -> text(id), start, expected, html, false);
239239
}
240240

@@ -258,11 +258,11 @@ private long assertLog(Callable<AnnotatedLargeText<?>> text, long start, String
258258
return pos;
259259
}
260260

261-
private void assertLength(long length) throws Exception {
261+
protected final void assertLength(long length) throws Exception {
262262
assertLength(text(), length);
263263
}
264264

265-
private void assertLength(String id, long length) throws Exception {
265+
protected final void assertLength(String id, long length) throws Exception {
266266
assertLength(text(id), length);
267267
}
268268

0 commit comments

Comments
 (0)