Skip to content
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

Migrate from deprecated finalization API to Java 9+ API #8486

Merged
merged 2 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
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
52 changes: 37 additions & 15 deletions core/src/main/java/hudson/util/AtomicFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.ref.Cleaner;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
Expand All @@ -55,6 +56,9 @@

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

private static final Cleaner CLEANER = Cleaner.create(
new NamingThreadFactory(new DaemonThreadFactory(), AtomicFileWriter.class.getName() + ".cleaner"));

private static /* final */ boolean DISABLE_FORCED_FLUSH = SystemProperties.getBoolean(
AtomicFileWriter.class.getName() + ".DISABLE_FORCED_FLUSH");

Expand All @@ -64,7 +68,7 @@
}
}

private final Writer core;
private final FileChannelWriter core;
private final Path tmpPath;
private final Path destPath;

Expand Down Expand Up @@ -151,6 +155,8 @@
}

core = new FileChannelWriter(tmpPath, charset, integrityOnFlush, integrityOnClose, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

CLEANER.register(this, new CleanupChecker(core, tmpPath, destPath));
}

@Override
Expand Down Expand Up @@ -185,7 +191,12 @@
* the {@link #commit()} is called, to simplify coding.
*/
public void abort() throws IOException {
closeAndDeleteTempFile();
// One way or another, the temporary file should be deleted.
try {
close();
} finally {
Files.deleteIfExists(tmpPath);
}
}

public void commit() throws IOException {
Expand Down Expand Up @@ -225,21 +236,32 @@
}
}

@Override
protected void finalize() throws Throwable {
try {
closeAndDeleteTempFile();
} finally {
super.finalize();
private static final class CleanupChecker implements Runnable {
private final FileChannelWriter core;
private final Path tmpPath;
private final Path destPath;

CleanupChecker(final FileChannelWriter core, final Path tmpPath, final Path destPath) {
this.core = core;
this.tmpPath = tmpPath;
this.destPath = destPath;
}
}

private void closeAndDeleteTempFile() throws IOException {
// one way or the other, temporary file should be deleted.
try {
close();
} finally {
Files.deleteIfExists(tmpPath);
@Override
public void run() {
if (core.isOpen()) {

Check warning on line 252 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 252 is only partially covered, one branch is missing
LOGGER.log(Level.WARNING, "AtomicFileWriter for " + destPath + " was not closed before being released");

Check warning on line 253 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 253 is not covered by tests
try {
core.close();

Check warning on line 255 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 255 is not covered by tests
} catch (IOException e) {

Check warning on line 256 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 256 is not covered by tests
LOGGER.log(Level.WARNING, "Failed to close " + tmpPath + " for destination file " + destPath, e);

Check warning on line 257 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 257 is not covered by tests
}

Check warning on line 258 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 258 is not covered by tests
}
try {
Files.deleteIfExists(tmpPath);
} catch (IOException e) {

Check warning on line 262 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 262 is not covered by tests
LOGGER.log(Level.WARNING, "Failed to delete temporary file " + tmpPath + " for destination file " + destPath, e);

Check warning on line 263 in core/src/main/java/hudson/util/AtomicFileWriter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 263 is not covered by tests
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion core/src/main/java/hudson/util/FileChannelWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.OpenOption;
Expand All @@ -26,7 +27,7 @@
* @see <a href="https://github.com/jenkinsci/jenkins/pull/2548">PR-2548</a>
*/
@Restricted(NoExternalUse.class)
public class FileChannelWriter extends Writer {
public class FileChannelWriter extends Writer implements Channel {

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

Expand Down Expand Up @@ -82,6 +83,11 @@ public void flush() throws IOException {
}
}

@Override
public boolean isOpen() {
return channel.isOpen();
}

@Override
public void close() throws IOException {
if (channel.isOpen()) {
Expand Down
8 changes: 7 additions & 1 deletion core/src/test/java/hudson/util/AtomicFileWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
Expand Down Expand Up @@ -72,6 +73,11 @@ public void setUp() throws IOException {
afw = new AtomicFileWriter(af.toPath(), Charset.defaultCharset());
}

@After
public void tearDown() throws IOException {
afw.abort();
}

@Test
public void symlinkToDirectory() throws Exception {
assumeFalse(Functions.isWindows());
Expand All @@ -83,7 +89,7 @@ public void symlinkToDirectory() throws Exception {

final Path childFileInSymlinkToDir = Paths.get(zeSymlink.toString(), "childFileInSymlinkToDir");

new AtomicFileWriter(childFileInSymlinkToDir, StandardCharsets.UTF_8);
new AtomicFileWriter(childFileInSymlinkToDir, StandardCharsets.UTF_8).abort();
}

@Test
Expand Down
63 changes: 63 additions & 0 deletions test/src/test/java/hudson/util/AtomicFileWriterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package hudson.util;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.MemoryAssert;

public class AtomicFileWriterTest {
@Rule
public TemporaryFolder tmp = new TemporaryFolder();

@Rule
public LoggerRule logging =
new LoggerRule().record(AtomicFileWriter.class, Level.WARNING).capture(100);

@Test
public void noResourceLeak() throws IOException {
Path destPath = tmp.newFolder().toPath().resolve("file");
AtomicFileWriter writer = new AtomicFileWriter(destPath, StandardCharsets.UTF_8);
Path tmpPath = writer.getTemporaryPath();
assertTrue(Files.exists(tmpPath));
assertFalse(Files.exists(destPath));
try {
writer.commit();
} finally {
writer.abort();
}
assertFalse(Files.exists(tmpPath));
assertTrue(Files.exists(destPath));
assertThat(logging.getMessages(), empty());
}

@Test
public void resourceLeak() throws IOException {
Path destPath = tmp.newFolder().toPath().resolve("file");
WeakReference<AtomicFileWriter> ref =
new WeakReference<>(new AtomicFileWriter(destPath, StandardCharsets.UTF_8));
Path tmpPath = ref.get().getTemporaryPath();
assertTrue(Files.exists(tmpPath));
assertFalse(Files.exists(destPath));
MemoryAssert.assertGC(ref, false);
await().atMost(30, TimeUnit.SECONDS).until(() -> !Files.exists(tmpPath));
assertFalse(Files.exists(destPath));
assertThat(
logging.getMessages(),
contains("AtomicFileWriter for " + destPath + " was not closed before being released"));
}
}
Loading