Skip to content

Commit 553cffe

Browse files
committed
Fix resource leak in Maven plugin
The resource leak increased the number of threads with every execution of Exclipse-based formatter step in a Maven module. It happened because the `SpotlessCache` wasn't properly utilized. Every cache key was based on randomized file names because Maven plugin relocates config files into the target directory with a random file name. This resulted in all cache accesses being cache misses. This commit fixes the problem by: * making the Maven plugin use non-random file names for output files. `FileLocator` is changed to construct names based on a hash of the input path. Input path can be a URL, file in a JAR, or a regular local file * changing `FileSignature` (used for `SpotlessCache` keys) to use filenames instead of paths and file hashes instead of last modified timestamps These two changes allow `FileSignature` to uniquely identify a set of files by their content and not take file paths into account. As a result, created keys for `SpotlessCache` are properly comparable which results in lots of cache hits and decreased number of created threads. Changed `FileCignature` only accepts files, not directories. NPM formatters changed to have their signatures based on package.json file instead of the whole node_modules directory.
1 parent 5e40de5 commit 553cffe

File tree

7 files changed

+161
-35
lines changed

7 files changed

+161
-35
lines changed

lib/src/main/java/com/diffplug/spotless/FileSignature.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717

1818
import static com.diffplug.spotless.MoreIterables.toNullHostileList;
1919
import static com.diffplug.spotless.MoreIterables.toSortedSet;
20+
import static java.util.Comparator.comparing;
2021

2122
import java.io.File;
2223
import java.io.IOException;
2324
import java.io.Serializable;
25+
import java.nio.file.Files;
26+
import java.security.MessageDigest;
27+
import java.security.NoSuchAlgorithmException;
2428
import java.util.Arrays;
2529
import java.util.Collection;
2630
import java.util.Collections;
@@ -42,7 +46,7 @@ public final class FileSignature implements Serializable {
4246

4347
private final String[] filenames;
4448
private final long[] filesizes;
45-
private final long[] lastModified;
49+
private final byte[][] fileHashes;
4650

4751
/** Method has been renamed to {@link FileSignature#signAsSet}.
4852
* In case no sorting and removal of duplicates is required,
@@ -77,21 +81,21 @@ public static FileSignature signAsSet(File... files) throws IOException {
7781

7882
/** Creates file signature insensitive to the order of the files. */
7983
public static FileSignature signAsSet(Iterable<File> files) throws IOException {
80-
return new FileSignature(toSortedSet(files));
84+
return new FileSignature(toSortedSet(files, comparing(File::getName)));
8185
}
8286

8387
private FileSignature(final List<File> files) throws IOException {
84-
this.files = files;
88+
this.files = validateInputFiles(files);
8589

8690
filenames = new String[this.files.size()];
8791
filesizes = new long[this.files.size()];
88-
lastModified = new long[this.files.size()];
92+
fileHashes = new byte[this.files.size()][];
8993

9094
int i = 0;
9195
for (File file : this.files) {
92-
filenames[i] = file.getCanonicalPath();
96+
filenames[i] = file.getName();
9397
filesizes[i] = file.length();
94-
lastModified[i] = file.lastModified();
98+
fileHashes[i] = hash(file);
9599
++i;
96100
}
97101
}
@@ -119,4 +123,27 @@ public static String pathNativeToUnix(String pathNative) {
119123
public static String pathUnixToNative(String pathUnix) {
120124
return LineEnding.nativeIsWin() ? pathUnix.replace('/', '\\') : pathUnix;
121125
}
126+
127+
private static List<File> validateInputFiles(List<File> files) {
128+
for (File file : files) {
129+
if (!file.isFile()) {
130+
throw new IllegalArgumentException(
131+
"File signature can only be created for existing regular files, given: "
132+
+ file);
133+
}
134+
}
135+
return files;
136+
}
137+
138+
private static byte[] hash(File file) throws IOException {
139+
MessageDigest messageDigest;
140+
try {
141+
messageDigest = MessageDigest.getInstance("SHA-256");
142+
} catch (NoSuchAlgorithmException e) {
143+
throw new IllegalStateException("SHA-256 digest algorithm not available", e);
144+
}
145+
byte[] fileContent = Files.readAllBytes(file.toPath());
146+
messageDigest.update(fileContent);
147+
return messageDigest.digest();
148+
}
122149
}

lib/src/main/java/com/diffplug/spotless/MoreIterables.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,14 +20,17 @@
2020
import java.util.ArrayList;
2121
import java.util.Collection;
2222
import java.util.Collections;
23+
import java.util.Comparator;
2324
import java.util.Iterator;
2425
import java.util.List;
2526

2627
final class MoreIterables {
2728
// prevent direct instantiation
2829
private MoreIterables() {}
2930

30-
/** Returns a shallow copy of input elements, throwing on null elements. */
31+
/**
32+
* Returns a shallow copy of input elements, throwing on null elements.
33+
*/
3134
static <T> List<T> toNullHostileList(Iterable<T> input) {
3235
requireElementsNonNull(input);
3336
List<T> shallowCopy = (input instanceof Collection)
@@ -37,18 +40,27 @@ static <T> List<T> toNullHostileList(Iterable<T> input) {
3740
return shallowCopy;
3841
}
3942

40-
/** Sorts "raw" and removes duplicates, throwing on null elements. */
43+
/**
44+
* Sorts "raw" using {@link Comparator#naturalOrder()} and removes duplicates, throwing on null elements.
45+
*/
4146
static <T extends Comparable<T>> List<T> toSortedSet(Iterable<T> raw) {
47+
return toSortedSet(raw, Comparator.naturalOrder());
48+
}
49+
50+
/**
51+
* Sorts "raw" and removes duplicates, throwing on null elements.
52+
*/
53+
static <T> List<T> toSortedSet(Iterable<T> raw, Comparator<T> comparator) {
4254
List<T> toBeSorted = toNullHostileList(raw);
4355
// sort it
44-
Collections.sort(toBeSorted);
56+
Collections.sort(toBeSorted, comparator);
4557
// remove any duplicates (normally there won't be any)
4658
if (toBeSorted.size() > 1) {
4759
Iterator<T> iter = toBeSorted.iterator();
4860
T last = iter.next();
4961
while (iter.hasNext()) {
5062
T next = iter.next();
51-
if (next.compareTo(last) == 0) {
63+
if (comparator.compare(next, last) == 0) {
5264
iter.remove();
5365
} else {
5466
last = next;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2020 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.npm;
17+
18+
import java.io.File;
19+
20+
class NodeServerLayout {
21+
22+
private final File nodeModulesDir;
23+
private final File packageJsonFile;
24+
private final File serveJsFile;
25+
26+
NodeServerLayout(File buildDir, String stepName) {
27+
this.nodeModulesDir = new File(buildDir, "spotless-node-modules-" + stepName);
28+
this.packageJsonFile = new File(nodeModulesDir, "package.json");
29+
this.serveJsFile = new File(nodeModulesDir, "serve.js");
30+
}
31+
32+
File nodeModulesDir() {
33+
return nodeModulesDir;
34+
}
35+
36+
File packageJsonFile() {
37+
return packageJsonFile;
38+
}
39+
40+
File serveJsFile() {
41+
return serveJsFile;
42+
}
43+
}

lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ abstract class NpmFormatterStepStateBase implements Serializable {
4444
private static final long serialVersionUID = 1460749955865959948L;
4545

4646
@SuppressWarnings("unused")
47-
private final FileSignature nodeModulesSignature;
47+
private final FileSignature packageJsonSignature;
4848

4949
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
5050
public final transient File nodeModulesDir;
@@ -56,22 +56,26 @@ abstract class NpmFormatterStepStateBase implements Serializable {
5656

5757
private final String stepName;
5858

59-
protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir, @Nullable File npm) throws IOException {
59+
protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir,
60+
@Nullable File npm) throws IOException {
6061
this.stepName = requireNonNull(stepName);
6162
this.npmConfig = requireNonNull(npmConfig);
6263
this.npmExecutable = resolveNpm(npm);
6364

64-
this.nodeModulesDir = prepareNodeServer(buildDir);
65-
this.nodeModulesSignature = FileSignature.signAsList(this.nodeModulesDir);
65+
NodeServerLayout layout = prepareNodeServer(buildDir);
66+
this.nodeModulesDir = layout.nodeModulesDir();
67+
this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile());
6668
}
6769

68-
private File prepareNodeServer(File buildDir) throws IOException {
69-
File targetDir = new File(buildDir, "spotless-node-modules-" + stepName);
70-
NpmResourceHelper.assertDirectoryExists(targetDir);
71-
NpmResourceHelper.writeUtf8StringToFile(targetDir, "package.json", this.npmConfig.getPackageJsonContent());
72-
NpmResourceHelper.writeUtf8StringToFile(targetDir, "serve.js", this.npmConfig.getServeScriptContent());
73-
runNpmInstall(targetDir);
74-
return targetDir;
70+
private NodeServerLayout prepareNodeServer(File buildDir) throws IOException {
71+
NodeServerLayout layout = new NodeServerLayout(buildDir, stepName);
72+
NpmResourceHelper.assertDirectoryExists(layout.nodeModulesDir());
73+
NpmResourceHelper.writeUtf8StringToFile(layout.packageJsonFile(),
74+
this.npmConfig.getPackageJsonContent());
75+
NpmResourceHelper
76+
.writeUtf8StringToFile(layout.serveJsFile(), this.npmConfig.getServeScriptContent());
77+
runNpmInstall(layout.nodeModulesDir());
78+
return layout;
7579
}
7680

7781
private void runNpmInstall(File npmProjectDir) throws IOException {

lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
*/
1616
package com.diffplug.spotless.npm;
1717

18-
import java.io.*;
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.OutputStream;
1923
import java.nio.charset.StandardCharsets;
2024
import java.nio.file.Files;
2125
import java.time.Duration;
@@ -24,16 +28,17 @@
2428
import com.diffplug.spotless.ThrowingEx;
2529

2630
final class NpmResourceHelper {
31+
2732
private NpmResourceHelper() {
2833
// no instance required
2934
}
3035

31-
static void writeUtf8StringToFile(File targetDir, String fileName, String stringToWrite) throws IOException {
32-
File packageJsonFile = new File(targetDir, fileName);
33-
Files.write(packageJsonFile.toPath(), stringToWrite.getBytes(StandardCharsets.UTF_8));
36+
static void writeUtf8StringToFile(File file, String stringToWrite) throws IOException {
37+
Files.write(file.toPath(), stringToWrite.getBytes(StandardCharsets.UTF_8));
3438
}
3539

36-
static void writeUtf8StringToOutputStream(String stringToWrite, OutputStream outputStream) throws IOException {
40+
static void writeUtf8StringToOutputStream(String stringToWrite, OutputStream outputStream)
41+
throws IOException {
3742
final byte[] bytes = stringToWrite.getBytes(StandardCharsets.UTF_8);
3843
outputStream.write(bytes);
3944
}

plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
1616
package com.diffplug.spotless.maven;
1717

1818
import static com.diffplug.common.base.Strings.isNullOrEmpty;
19+
import static java.nio.charset.StandardCharsets.UTF_8;
1920

2021
import java.io.File;
22+
import java.security.MessageDigest;
23+
import java.security.NoSuchAlgorithmException;
24+
import java.util.Base64;
2125
import java.util.Objects;
22-
import java.util.UUID;
2326

2427
import org.codehaus.plexus.resource.ResourceManager;
2528
import org.codehaus.plexus.resource.loader.FileResourceCreationException;
@@ -63,16 +66,29 @@ public File locateFile(String path) {
6366
}
6467
}
6568

66-
private static String tmpOutputFileName(String path) {
67-
String extension = FileUtils.extension(path);
68-
return TMP_RESOURCE_FILE_PREFIX + UUID.randomUUID() + '.' + extension;
69-
}
70-
7169
public File getBaseDir() {
7270
return baseDir;
7371
}
7472

7573
public File getBuildDir() {
7674
return buildDir;
7775
}
76+
77+
private static String tmpOutputFileName(String path) {
78+
String extension = FileUtils.extension(path);
79+
byte[] pathHash = hash(path);
80+
String pathBase64 = Base64.getEncoder().encodeToString(pathHash);
81+
return TMP_RESOURCE_FILE_PREFIX + pathBase64 + '.' + extension;
82+
}
83+
84+
private static byte[] hash(String value) {
85+
MessageDigest messageDigest;
86+
try {
87+
messageDigest = MessageDigest.getInstance("SHA-256");
88+
} catch (NoSuchAlgorithmException e) {
89+
throw new IllegalStateException("SHA-256 digest algorithm not available", e);
90+
}
91+
messageDigest.update(value.getBytes(UTF_8));
92+
return messageDigest.digest();
93+
}
7894
}

testlib/src/test/java/com/diffplug/spotless/FileSignatureTest.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,13 @@
1616
package com.diffplug.spotless;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1920

2021
import java.io.File;
2122
import java.io.IOException;
2223
import java.util.ArrayList;
2324
import java.util.Collection;
25+
import java.util.Collections;
2426
import java.util.List;
2527

2628
import org.junit.Test;
@@ -48,6 +50,23 @@ public void testFromSet() throws IOException {
4850
assertThat(outputFiles).containsExactlyElementsOf(expectedFiles);
4951
}
5052

53+
@Test
54+
public void testFromDirectory() {
55+
File dir = new File(rootFolder(), "dir");
56+
assertThatThrownBy(() -> FileSignature.signAsList(dir))
57+
.isInstanceOf(IllegalArgumentException.class);
58+
}
59+
60+
@Test
61+
public void testFromFilesAndDirectory() throws IOException {
62+
File dir = new File(rootFolder(), "dir");
63+
List<File> files = getTestFiles(inputPaths);
64+
files.add(dir);
65+
Collections.shuffle(files);
66+
assertThatThrownBy(() -> FileSignature.signAsList(files))
67+
.isInstanceOf(IllegalArgumentException.class);
68+
}
69+
5170
private List<File> getTestFiles(final String[] paths) throws IOException {
5271
final List<File> result = new ArrayList<>(paths.length);
5372
for (String path : paths) {

0 commit comments

Comments
 (0)