Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -48,6 +48,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.input.NullReader;
import org.apache.commons.io.output.CountingOutputStream;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.kohsuke.accmod.Restricted;
Expand All @@ -58,6 +59,9 @@
* Simple implementation of log storage in a single file that maintains a side file with an index indicating where node transitions occur.
* Each line in the index file is a byte offset, optionally followed by a space and then a node ID.
*/
/* Note: Avoid FileChannel methods in this class, as they close the channel and its parent stream if the thread is
interrupted, which is problematic given that we do not control the threads which write to the log file.
*/
@Restricted(Beta.class)
public final class FileLogStorage implements LogStorage {

Expand All @@ -73,6 +77,8 @@ public static synchronized LogStorage forFile(File log) {
private final File index;
@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC", justification = "actually it is always accessed within the monitor")
private FileOutputStream os;
private long osStartPosition;
private CountingOutputStream cos;
@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC", justification = "we only care about synchronizing writes")
private OutputStream bos;
private Writer indexOs;
Expand All @@ -86,7 +92,9 @@ private FileLogStorage(File log) {
private synchronized void open() throws IOException {
if (os == null) {
os = new FileOutputStream(log, true);
bos = new GCFlushedOutputStream(new DelayBufferedOutputStream(os));
osStartPosition = log.length();
cos = new CountingOutputStream(os);
bos = new GCFlushedOutputStream(new DelayBufferedOutputStream(cos));
if (index.isFile()) {
try (BufferedReader r = Files.newBufferedReader(index.toPath(), StandardCharsets.UTF_8)) {
// 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
Expand Down Expand Up @@ -126,7 +134,7 @@ private void checkId(String id) throws IOException {
assert Thread.holdsLock(this);
if (!Objects.equals(id, lastId)) {
bos.flush();
long pos = os.getChannel().position();
long pos = osStartPosition + cos.getByteCount();
if (id == null) {
indexOs.write(pos + "\n");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,18 @@ public class FileLogStorageTest extends LogStorageTestBase {
assertOverallLog(0, lines("stuff"), true);
}

@Test public void interruptionDoesNotCloseStream() throws Exception {
LogStorage ls = createStorage();
TaskListener overall = ls.overallListener();
overall.getLogger().println("overall 1");
Thread.currentThread().interrupt();
TaskListener stepLog = ls.nodeListener(new MockNode("1"));
stepLog.getLogger().println("step 1");
assertTrue(Thread.interrupted());
close(stepLog);
overall.getLogger().println("overall 2");
close(overall);
assertOverallLog(0, lines("overall 1", "<span class=\"pipeline-node-1\">step 1", "</span>overall 2"), true);
Copy link
Member Author

Choose a reason for hiding this comment

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

Without the fix, only overall 1 gets written to the log, and you get the "failed to flush" messages in the JENKINS ticket from BuildWatcher in the log as it copies the output which calls maybeFlush.

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,12 @@ protected String lines(CharSequence... lines) {
return String.join(System.lineSeparator(), lines) + System.lineSeparator();
}

private static class MockNode extends FlowNode {
protected static class MockNode extends FlowNode {
MockNode(String id) {super(new MockFlowExecution(), id);}
@Override protected String getTypeDisplayName() {return null;}
}

private static class MockFlowExecution extends FlowExecution {
protected static class MockFlowExecution extends FlowExecution {
@Override
public void start() {
throw new UnsupportedOperationException();
Expand Down