Skip to content

Commit 2ead3bb

Browse files
committed
Serialize dir deletion. Improve directory tidiness.
- Serialization is better so that parallelization threads aren't contending to delete the same directories. (File deletion is still parallelized.) - Improve directory cleanup by finding eligible, file-less child directories to delete. The existing and former logic only cleaned up parents, but testing with Homebrew/brew as a sample project showed empty directories still remaining.
1 parent 0a625ce commit 2ead3bb

File tree

1 file changed

+147
-6
lines changed

1 file changed

+147
-6
lines changed

src/org/opensolaris/opengrok/index/PendingFileCompleter.java

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,18 @@
2525

2626
import java.io.File;
2727
import java.io.IOException;
28+
import java.nio.file.DirectoryStream;
2829
import java.nio.file.Files;
30+
import java.nio.file.Path;
2931
import java.nio.file.Paths;
3032
import java.nio.file.StandardCopyOption;
33+
import java.util.ArrayList;
34+
import java.util.Comparator;
3135
import java.util.HashSet;
3236
import java.util.List;
3337
import java.util.Map;
3438
import java.util.Set;
39+
import java.util.TreeSet;
3540
import java.util.logging.Level;
3641
import java.util.logging.Logger;
3742
import java.util.stream.Collectors;
@@ -53,6 +58,25 @@ public class PendingFileCompleter {
5358
private static final Logger LOGGER =
5459
LoggerFactory.getLogger(PendingFileCompleter.class);
5560

61+
/**
62+
* Descending path segment length comparator
63+
*/
64+
private static final Comparator<File> DESC_PATHLEN_COMPARATOR =
65+
(File f1, File f2) -> {
66+
String s1 = f1.getAbsolutePath();
67+
String s2 = f2.getAbsolutePath();
68+
int n1 = countPathSegments(s1);
69+
int n2 = countPathSegments(s2);
70+
// DESC: s2 no. of segments <=> s1 no. of segments
71+
int cmp = Integer.compare(n2, n1);
72+
if (cmp != 0) return cmp;
73+
74+
// the Comparator must also be "consistent with equals", so check
75+
// string contents too when (length)cmp == 0. (ASC: s1 <=> s2.)
76+
cmp = s1.compareTo(s2);
77+
return cmp;
78+
};
79+
5680
private final Set<PendingFileDeletion> deletions = new HashSet<>();
5781

5882
private final Set<PendingFileRenaming> renames = new HashSet<>();
@@ -125,6 +149,8 @@ private int completeRenamings() throws IOException {
125149
int numPending = renames.size();
126150
int numFailures = 0;
127151

152+
if (numPending < 1) return 0;
153+
128154
List<PendingFileRenamingExec> pendingExecs = renames.
129155
parallelStream().map(f ->
130156
new PendingFileRenamingExec(f.getTransientPath(),
@@ -168,6 +194,8 @@ private int completeDeletions() throws IOException {
168194
int numPending = deletions.size();
169195
int numFailures = 0;
170196

197+
if (numPending < 1) return 0;
198+
171199
List<PendingFileDeletionExec> pendingExecs = deletions.
172200
parallelStream().map(f ->
173201
new PendingFileDeletionExec(f.getAbsolutePath())).collect(
@@ -186,6 +214,10 @@ private int completeDeletions() throws IOException {
186214
}));
187215
deletions.clear();
188216

217+
List<PendingFileDeletionExec> successes = bySuccess.getOrDefault(
218+
Boolean.TRUE, null);
219+
if (successes != null) tryDeleteParents(successes);
220+
189221
List<PendingFileDeletionExec> failures = bySuccess.getOrDefault(
190222
Boolean.FALSE, null);
191223
if (failures != null && failures.size() > 0) {
@@ -202,15 +234,12 @@ private int completeDeletions() throws IOException {
202234

203235
private void doDelete(PendingFileDeletionExec del) throws IOException {
204236
File f = new File(del.absolutePath + PENDING_EXTENSION);
237+
File parent = f.getParentFile();
238+
del.absoluteParent = parent;
239+
205240
doDelete(f);
206241
f = new File(del.absolutePath);
207242
doDelete(f);
208-
209-
File parent = f.getParentFile();
210-
if (parent.delete()) {
211-
LOGGER.log(Level.FINE, "Removed empty parent dir: {0}",
212-
parent.getAbsolutePath());
213-
}
214243
}
215244

216245
private void doDelete(File f) {
@@ -237,8 +266,111 @@ private void doRename(PendingFileRenamingExec ren) throws IOException {
237266
}
238267
}
239268

269+
private void tryDeleteParents(List<PendingFileDeletionExec> dels) {
270+
Set<File> parents = new TreeSet<>(DESC_PATHLEN_COMPARATOR);
271+
dels.forEach((del) -> { parents.add(del.absoluteParent); });
272+
273+
for (File dir : parents) {
274+
Set<File> children = findFilelessChildren(dir);
275+
children.forEach((childDir) -> {
276+
tryDeleteDirectory(childDir);
277+
});
278+
279+
tryDeleteDirectory(dir);
280+
}
281+
}
282+
283+
private void tryDeleteDirectory(File dir) {
284+
if (dir.delete()) {
285+
LOGGER.log(Level.FINE, "Removed empty parent dir: {0}",
286+
dir.getAbsolutePath());
287+
}
288+
}
289+
290+
/**
291+
* Determines a DESC ordered set of eligible file-less child directories
292+
* for cleaning up.
293+
*/
294+
private Set<File> findFilelessChildren(File directory) {
295+
SkeletonDirs ret = new SkeletonDirs();
296+
findFilelessChildrenDeep(ret, directory);
297+
298+
Set<File> parents = new TreeSet<>(DESC_PATHLEN_COMPARATOR);
299+
// N.b. the `ineligible' field is not relevant here but only during
300+
// recursion. `childDirs' contains eligible directories.
301+
parents.addAll(ret.childDirs);
302+
return parents;
303+
}
304+
305+
/**
306+
* Recursive method used by {@link #findFilelessChildren(java.io.File)}.
307+
*/
308+
private void findFilelessChildrenDeep(SkeletonDirs ret, File directory) {
309+
310+
if (!directory.exists()) return;
311+
String dirPath = directory.getAbsolutePath();
312+
boolean topLevelIneligible = false;
313+
boolean didLogFileTopLevelIneligible = false;
314+
315+
try {
316+
DirectoryStream<Path> directoryStream = Files.newDirectoryStream(
317+
Paths.get(dirPath));
318+
for (Path path : directoryStream) {
319+
File f = path.toFile();
320+
if (f.isFile()) {
321+
topLevelIneligible = true;
322+
if (!didLogFileTopLevelIneligible && LOGGER.isLoggable(
323+
Level.FINEST)) {
324+
didLogFileTopLevelIneligible = true; // just once is OK
325+
LOGGER.log(Level.FINEST, "not file-less due to: {0}",
326+
f.getAbsolutePath());
327+
}
328+
} else {
329+
findFilelessChildrenDeep(ret, f);
330+
if (!ret.ineligible) {
331+
ret.childDirs.add(f);
332+
} else {
333+
topLevelIneligible = true;
334+
if (LOGGER.isLoggable(Level.FINEST)) {
335+
LOGGER.log(Level.FINEST,
336+
"its children prevent delete: {0}",
337+
f.getAbsolutePath());
338+
}
339+
}
340+
341+
// Reset this flag so that other potential, eligible
342+
// children are evaluated.
343+
ret.ineligible = false;
344+
}
345+
}
346+
} catch (IOException ex) {
347+
topLevelIneligible = true;
348+
if (LOGGER.isLoggable(Level.FINEST)) {
349+
LOGGER.log(Level.FINEST, "Failed to stream directory:" +
350+
directory, ex);
351+
}
352+
}
353+
354+
ret.ineligible = topLevelIneligible;
355+
}
356+
357+
/**
358+
* Counts segments arising from {@code File.separatorChar} or '\\'
359+
* @param path
360+
* @return a natural number
361+
*/
362+
private static int countPathSegments(String path) {
363+
int n = 1;
364+
for (int i = 0; i < path.length(); ++i) {
365+
char c = path.charAt(i);
366+
if (c == File.separatorChar || c == '\\') ++n;
367+
}
368+
return n;
369+
}
370+
240371
private class PendingFileDeletionExec {
241372
public String absolutePath;
373+
public File absoluteParent;
242374
public IOException exception;
243375
public PendingFileDeletionExec(String absolutePath) {
244376
this.absolutePath = absolutePath;
@@ -254,4 +386,13 @@ public PendingFileRenamingExec(String source, String target) {
254386
this.target = target;
255387
}
256388
}
389+
390+
/**
391+
* Represents a collection of file-less directories which should also be
392+
* deleted for cleanliness.
393+
*/
394+
private class SkeletonDirs {
395+
public boolean ineligible; // a flag used during recursion
396+
public List<File> childDirs = new ArrayList<>();
397+
}
257398
}

0 commit comments

Comments
 (0)