Skip to content

Commit ca5fddb

Browse files
authored
Merge pull request #311 from jglick/BufferedBuildListener-JENKINS-71970
[JENKINS-71970] Memory leak involving `BufferedBuildListener`
2 parents 526907f + 34622bf commit ca5fddb

File tree

1 file changed

+39
-9
lines changed

1 file changed

+39
-9
lines changed

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@
2929
import hudson.remoting.Channel;
3030
import hudson.remoting.ChannelClosedException;
3131
import hudson.remoting.RemoteOutputStream;
32+
import hudson.util.DaemonThreadFactory;
33+
import hudson.util.NamingThreadFactory;
3234
import hudson.util.StreamTaskListener;
3335
import java.io.Closeable;
3436
import java.io.FilterOutputStream;
3537
import java.io.IOException;
3638
import java.io.OutputStream;
3739
import java.io.PrintStream;
40+
import java.lang.ref.Cleaner;
3841
import java.util.logging.Logger;
3942
import org.jenkinsci.remoting.SerializableOnlyOverRemoting;
4043

@@ -44,12 +47,27 @@
4447
*/
4548
final class BufferedBuildListener extends OutputStreamTaskListener.Default implements BuildListener, Closeable, SerializableOnlyOverRemoting {
4649

50+
private static final long serialVersionUID = 1;
51+
4752
private static final Logger LOGGER = Logger.getLogger(BufferedBuildListener.class.getName());
4853

54+
private static final Cleaner cleaner = Cleaner.create(new NamingThreadFactory(new DaemonThreadFactory(), BufferedBuildListener.class.getName() + ".cleaner"));
55+
4956
private final OutputStream out;
57+
private transient final Channel channel;
58+
private transient final Listener listener;
5059

5160
BufferedBuildListener(OutputStream out) {
5261
this.out = out;
62+
if (out instanceof CloseableOutputStream) {
63+
channel = Channel.currentOrFail();
64+
listener = new Listener((CloseableOutputStream) out, channel);
65+
channel.addListener(listener);
66+
cleaner.register(this, listener);
67+
} else {
68+
channel = null;
69+
listener = null;
70+
}
5371
}
5472

5573
@Override public OutputStream getOutputStream() {
@@ -58,12 +76,32 @@ final class BufferedBuildListener extends OutputStreamTaskListener.Default imple
5876

5977
@Override public void close() throws IOException {
6078
getLogger().close();
79+
if (listener != null) {
80+
channel.removeListener(listener);
81+
}
6182
}
6283

6384
private Object writeReplace() {
6485
return new Replacement(this);
6586
}
6687

88+
private static final class Listener extends Channel.Listener implements Runnable {
89+
private final CloseableOutputStream cos;
90+
private final Channel channel;
91+
Listener(CloseableOutputStream cos, Channel channel) {
92+
this.cos = cos;
93+
this.channel = channel;
94+
}
95+
@Override public void onClosed(Channel channel, IOException cause) {
96+
LOGGER.fine(() -> "closing " + channel.getName());
97+
cos.close(channel, cause);
98+
channel.removeListener(this);
99+
}
100+
@Override public void run() {
101+
channel.removeListener(this);
102+
}
103+
}
104+
67105
private static final class Replacement implements SerializableOnlyOverRemoting {
68106

69107
private static final long serialVersionUID = 1;
@@ -76,15 +114,7 @@ private static final class Replacement implements SerializableOnlyOverRemoting {
76114
}
77115

78116
private Object readResolve() {
79-
var cos = new CloseableOutputStream(new GCFlushedOutputStream(new DelayBufferedOutputStream(ros, tuning)));
80-
Channel.currentOrFail().addListener(new Channel.Listener() {
81-
@Override public void onClosed(Channel channel, IOException cause) {
82-
LOGGER.fine(() -> "closing " + channel.getName());
83-
cos.close(channel, cause);
84-
channel.removeListener(this);
85-
}
86-
});
87-
return new BufferedBuildListener(cos);
117+
return new BufferedBuildListener(new CloseableOutputStream(new GCFlushedOutputStream(new DelayBufferedOutputStream(ros, tuning))));
88118
}
89119

90120
}

0 commit comments

Comments
 (0)