WIP tests for lock file cleanup

This commit is contained in:
dotasek 2024-09-09 17:03:56 -04:00
parent 4cbf1860d7
commit e001d781b9
3 changed files with 111 additions and 4 deletions

View File

@ -215,12 +215,31 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple
createIniFile();
} else {
deleteOldTempDirectories();
cleanUpCorruptPackages();
}
}
return null;
});
}
/*
Look for .lock files that are not actively held by a process. If found, delete the lock file, and the package
referenced.
*/
protected void cleanUpCorruptPackages() throws IOException {
for (File file : Objects.requireNonNull(cacheFolder.listFiles())) {
if (file.getName().endsWith(".lock")) {
String packageDirectoryName = file.getName().substring(0, file.getName().length() - 5);
File packageDirectory = ManagedFileAccess.file(Utilities.path(cacheFolder, packageDirectoryName));
if (packageDirectory.exists()) {
Utilities.clearDirectory(packageDirectory.getAbsolutePath());
packageDirectory.delete();
}
file.delete();
}
}
}
private boolean isCacheFolderValid() throws IOException {
String iniPath = getPackagesIniPath();
File iniFile = ManagedFileAccess.file(iniPath);

View File

@ -118,6 +118,20 @@ public class FilesystemPackageCacheManagerLocks {
if (!lockFile.exists()) {
return;
}
// Check if the file is locked by a process. If it is not, it is likely an incomplete package cache install, and
// we should throw an exception.
if (lockFile.isFile()) {
try (FileChannel channel = new RandomAccessFile(lockFile, "rw").getChannel()) {
FileLock fileLock = channel.tryLock(0, Long.MAX_VALUE, true);
if (fileLock != null) {
fileLock.release();
channel.close();
throw new IOException("Lock file exists, but is not locked by a process: " + lockFile.getName());
}
}
}
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
Path dir = lockFile.getParentFile().toPath();
dir.register(watchService, StandardWatchEventKinds.ENTRY_DELETE);

View File

@ -1,22 +1,30 @@
package org.hl7.fhir.utilities.npm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledOnOs;
@ -111,6 +119,67 @@ public class FilesystemPackageManagerTests {
assertEquals( System.getenv("ProgramData") + "\\.fhir\\packages", folder.getAbsolutePath());
}
@Test
public void testFailureForUnlockedLockFiles() throws IOException, InterruptedException {
String pcmPath = ManagedFileAccess.fromPath(Files.createTempDirectory("fpcm-multithreadingTest")).getAbsolutePath();
final FilesystemPackageCacheManager pcm = new FilesystemPackageCacheManager.Builder().withCacheFolder(pcmPath).build();
Assertions.assertTrue(pcm.listPackages().isEmpty());
//Now sneak in a new lock file and directory:
File lockFile = ManagedFileAccess.file(pcmPath, "example.fhir.uv.myig#1.2.3.lock");
lockFile.createNewFile();
File directory = ManagedFileAccess.file(pcmPath, "example.fhir.uv.myig#1.2.3" );
directory.mkdir();
IOException exception = assertThrows(IOException.class, () -> pcm.loadPackageFromCacheOnly("example.fhir.uv.myig", "1.2.3"));
assertThat(exception.getMessage()).contains("Lock file exists, but is not locked by a process");
}
@Test
//@EnabledOnOs(OS.LINUX)
public void testCacheCleanupForUnlockedLockFiles() throws IOException, InterruptedException {
String pcmPath = ManagedFileAccess.fromPath(Files.createTempDirectory("fpcm-multithreadingTest")).getAbsolutePath();
final FilesystemPackageCacheManager pcm = new FilesystemPackageCacheManager.Builder().withCacheFolder(pcmPath).build();
Assertions.assertTrue(pcm.listPackages().isEmpty());
String packageAndVersion = "example.fhir.uv.myig#1.2.3";
String lockFileName = packageAndVersion + ".lock";
//Now sneak in a new lock file and directory:
File lockFile = ManagedFileAccess.file(pcmPath, lockFileName);
lockFile.createNewFile();
File directory = ManagedFileAccess.file(pcmPath, packageAndVersion);
directory.mkdir();
// We can't create a lock file from within the same JVM, so we have to use the flock utility, which is OS dependent.
// The following works for Linux only.
ProcessBuilder processBuilder = new ProcessBuilder("flock", lockFileName, "--command", "sleep 10");
processBuilder.directory(new File(pcmPath));
processBuilder.start();
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService.schedule(()->{
try {
Utilities.clearDirectory(directory.getAbsolutePath());
directory.delete();
} catch (IOException e) {
throw new RuntimeException(e);
}
lockFile.delete();
}, 15, TimeUnit.SECONDS);
IOException ioException = assertThrows(IOException.class, () -> { pcm.loadPackageFromCacheOnly("example.fhir.uv.myig", "1.2.3"); });
ioException.printStackTrace();
}
/**
We repeat the same tests multiple times here, in order to catch very rare edge cases.
*/
@ -133,7 +202,7 @@ public class FilesystemPackageManagerTests {
Random rand = new Random();
final AtomicInteger totalSuccessful = new AtomicInteger();
final ConcurrentHashMap successfulThreads = new ConcurrentHashMap();
final ConcurrentHashMap<Long, Integer> successfulThreads = new ConcurrentHashMap<>();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < threadTotal; i++) {
final int index = i;
@ -142,22 +211,27 @@ public class FilesystemPackageManagerTests {
System.out.println("Thread #" + index + ": " + Thread.currentThread().getId() + " started");
final int randomPCM = rand.nextInt(packageCacheManagerTotal);
final int randomOperation = rand.nextInt(4);
final String operationName;
if (packageCacheManagers[randomPCM] == null) {
packageCacheManagers[randomPCM] = new FilesystemPackageCacheManager.Builder().withCacheFolder(pcmPath).build();
}
FilesystemPackageCacheManager pcm = packageCacheManagers[randomPCM];
if (randomOperation == 0) {
operationName = "addPackageToCache";
pcm.addPackageToCache("example.fhir.uv.myig", "1.2.3", this.getClass().getResourceAsStream("/npm/dummy-package.tgz"), "https://packages.fhir.org/example.fhir.uv.myig/1.2.3");
} else if (randomOperation == 1) {
operationName = "clear";
pcm.clear();
} else if (randomOperation == 2) {
operationName = "loadPackageFromCacheOnly";
pcm.loadPackageFromCacheOnly("example.fhir.uv.myig", "1.2.3");
} else {
operationName = "removePackage";
pcm.removePackage("example.fhir.uv.myig", "1.2.3");
}
totalSuccessful.incrementAndGet();
successfulThreads.put(Thread.currentThread().getId(), index);
System.out.println("Thread #" + index + ": " + Thread.currentThread().getId() + " completed");
System.out.println("Thread #" + index + ": " + Thread.currentThread().getId() + " completed. Ran: " + operationName);
} catch (Exception e) {
e.printStackTrace();
System.err.println("Thread #" + index + ": " + Thread.currentThread().getId() + " failed");