mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-27 06:39:24 +00:00
feat: optimize temp cleanup and add async execution
This commit is contained in:
parent
64d8ef4a39
commit
774c1f6552
@ -0,0 +1,24 @@
|
|||||||
|
package stirling.software.common.config;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class CleanupAsyncConfig {
|
||||||
|
|
||||||
|
@Bean(name = "cleanupExecutor")
|
||||||
|
public Executor cleanupExecutor() {
|
||||||
|
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
||||||
|
exec.setCorePoolSize(1);
|
||||||
|
exec.setMaxPoolSize(1);
|
||||||
|
exec.setQueueCapacity(100);
|
||||||
|
exec.setThreadNamePrefix("cleanup-");
|
||||||
|
exec.initialize();
|
||||||
|
return exec;
|
||||||
|
}
|
||||||
|
}
|
@ -328,6 +328,8 @@ public class ApplicationProperties {
|
|||||||
private long cleanupIntervalMinutes = 30;
|
private long cleanupIntervalMinutes = 30;
|
||||||
private boolean startupCleanup = true;
|
private boolean startupCleanup = true;
|
||||||
private boolean cleanupSystemTemp = false;
|
private boolean cleanupSystemTemp = false;
|
||||||
|
private int batchSize = 0;
|
||||||
|
private long pauseBetweenBatchesMs = 0;
|
||||||
|
|
||||||
public String getBaseTmpDir() {
|
public String getBaseTmpDir() {
|
||||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
||||||
|
@ -9,10 +9,10 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -121,6 +121,7 @@ public class TempFileCleanupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
|
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
|
||||||
|
@Async("cleanupExecutor")
|
||||||
@Scheduled(
|
@Scheduled(
|
||||||
fixedDelayString =
|
fixedDelayString =
|
||||||
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
||||||
@ -310,44 +311,61 @@ public class TempFileCleanupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
java.util.List<Path> subdirectories = new java.util.ArrayList<>();
|
java.util.List<Path> subdirectories = new java.util.ArrayList<>();
|
||||||
|
int batchSize = applicationProperties.getSystem().getTempFileManagement().getBatchSize();
|
||||||
|
long pauseMs =
|
||||||
|
applicationProperties
|
||||||
|
.getSystem()
|
||||||
|
.getTempFileManagement()
|
||||||
|
.getPauseBetweenBatchesMs();
|
||||||
|
int processed = 0;
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(directory)) {
|
try (java.nio.file.DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||||
pathStream.forEach(
|
for (Path path : stream) {
|
||||||
path -> {
|
try {
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
if (SHOULD_SKIP.test(fileName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
subdirectories.add(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.contains(path.toFile())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDeleteFile(path, fileName, containerMode, maxAgeMillis)) {
|
||||||
try {
|
try {
|
||||||
String fileName = path.getFileName().toString();
|
Files.deleteIfExists(path);
|
||||||
|
onDeleteCallback.accept(path);
|
||||||
if (SHOULD_SKIP.test(fileName)) {
|
} catch (IOException e) {
|
||||||
return;
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage().contains("being used by another process")) {
|
||||||
|
log.debug("File locked, skipping delete: {}", path);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to delete temp file: {}", path, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.isDirectory(path)) {
|
|
||||||
subdirectories.add(path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registry.contains(path.toFile())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldDeleteFile(path, fileName, containerMode, maxAgeMillis)) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(path);
|
|
||||||
onDeleteCallback.accept(path);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (e.getMessage() != null
|
|
||||||
&& e.getMessage()
|
|
||||||
.contains("being used by another process")) {
|
|
||||||
log.debug("File locked, skipping delete: {}", path);
|
|
||||||
} else {
|
|
||||||
log.warn("Failed to delete temp file: {}", path, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Error processing path: {}", path, e);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error processing path: {}", path, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
if (batchSize > 0 && processed >= batchSize) {
|
||||||
|
if (pauseMs > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(pauseMs);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Path subdirectory : subdirectories) {
|
for (Path subdirectory : subdirectories) {
|
||||||
|
@ -15,7 +15,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.nio.file.DirectoryStream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -142,20 +142,27 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for each directory we'll process
|
// Mock Files.newDirectoryStream for each directory we'll process
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
.thenReturn(Stream.of(
|
.thenReturn(directoryStreamOf(
|
||||||
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
ourTempFile1,
|
||||||
jettyFile1, jettyFile2, regularFile, emptyFile, nestedDir));
|
ourTempFile2,
|
||||||
|
oldTempFile,
|
||||||
|
sysTempFile1,
|
||||||
|
jettyFile1,
|
||||||
|
jettyFile2,
|
||||||
|
regularFile,
|
||||||
|
emptyFile,
|
||||||
|
nestedDir));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(customTempDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(customTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
.thenReturn(directoryStreamOf(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(libreOfficeTempDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(libreOfficeTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile5));
|
.thenReturn(directoryStreamOf(ourTempFile5));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(nestedDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(nestedDir)))
|
||||||
.thenReturn(Stream.of(nestedTempFile));
|
.thenReturn(directoryStreamOf(nestedTempFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
||||||
@ -251,9 +258,10 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.newDirectoryStream for systemTempDir
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
|
.thenReturn(directoryStreamOf(ourTempFile, sysTempFile, regularFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory
|
// Configure Files.isDirectory
|
||||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
@ -302,9 +310,10 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.newDirectoryStream for systemTempDir
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
|
.thenReturn(directoryStreamOf(emptyFile, recentEmptyFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory
|
// Configure Files.isDirectory
|
||||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
@ -369,18 +378,22 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for each directory
|
// Mock Files.newDirectoryStream for each directory
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(dir1));
|
.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
|
.thenReturn(directoryStreamOf(dir1));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir1)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(tempFile1, dir2));
|
.when(() -> Files.newDirectoryStream(eq(dir1)))
|
||||||
|
.thenReturn(directoryStreamOf(tempFile1, dir2));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir2)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(tempFile2, dir3));
|
.when(() -> Files.newDirectoryStream(eq(dir2)))
|
||||||
|
.thenReturn(directoryStreamOf(tempFile2, dir3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir3)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(tempFile3));
|
.when(() -> Files.newDirectoryStream(eq(dir3)))
|
||||||
|
.thenReturn(directoryStreamOf(tempFile3));
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||||
@ -461,4 +474,16 @@ public class TempFileCleanupServiceTest {
|
|||||||
private static Path eq(Path path) {
|
private static Path eq(Path path) {
|
||||||
return argThat(arg -> arg != null && arg.equals(path));
|
return argThat(arg -> arg != null && arg.equals(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DirectoryStream<Path> directoryStreamOf(Path... paths) {
|
||||||
|
return new DirectoryStream<>() {
|
||||||
|
@Override
|
||||||
|
public java.util.Iterator<Path> iterator() {
|
||||||
|
return java.util.Arrays.asList(paths).iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,8 @@ system:
|
|||||||
cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes)
|
cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes)
|
||||||
startupCleanup: true # Clean up old temp files on startup
|
startupCleanup: true # Clean up old temp files on startup
|
||||||
cleanupSystemTemp: false # Whether to clean broader system temp directory
|
cleanupSystemTemp: false # Whether to clean broader system temp directory
|
||||||
|
batchSize: 0 # Number of entries processed before optional pause (0 = unlimited)
|
||||||
|
pauseBetweenBatchesMs: 0 # Pause duration in milliseconds between batches
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
appName: '' # application's visible name
|
appName: '' # application's visible name
|
||||||
|
Loading…
x
Reference in New Issue
Block a user