From 7175f2cff28a10fde1cfd8f586e690a8dccb4eb4 Mon Sep 17 00:00:00 2001 From: ACX <8075870+acx10@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:27:22 -0700 Subject: [PATCH] Fix bookdrop race condition processing files before fully written (#2267) (#2785) Co-authored-by: acx10 --- .../bookdrop/BookdropEventHandlerService.java | 47 +++++++++++++++++++ .../bookdrop/BookdropMetadataService.java | 8 ++++ .../metadata/MetadataRefreshService.java | 5 ++ 3 files changed, 60 insertions(+) diff --git a/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropEventHandlerService.java b/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropEventHandlerService.java index 404006291..0b6ff6816 100644 --- a/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropEventHandlerService.java +++ b/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropEventHandlerService.java @@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; @@ -35,6 +36,10 @@ public class BookdropEventHandlerService { private final AppSettingService appSettingService; private final BookdropMetadataService bookdropMetadataService; + private static final long STABILITY_CHECK_INTERVAL_MS = 500; + private static final int STABILITY_REQUIRED_CHECKS = 3; + private static final long STABILITY_MAX_WAIT_MS = 30_000; + private final BlockingQueue fileQueue = new LinkedBlockingQueue<>(); private volatile boolean running = true; private Thread workerThread; @@ -100,6 +105,11 @@ public class BookdropEventHandlerService { return; } + if (!waitForFileStability(file)) { + log.warn("File did not stabilize within timeout, skipping: {}", file); + return; + } + log.info("Handling new bookdrop file: {}", file); int queueSize = fileQueue.size(); @@ -158,4 +168,41 @@ public class BookdropEventHandlerService { bookdropNotificationService.sendBookdropFileSummaryNotification(); } } + + private boolean waitForFileStability(Path file) { + long startTime = System.currentTimeMillis(); + long lastSize = -1; + int stableCount = 0; + + while (System.currentTimeMillis() - startTime < STABILITY_MAX_WAIT_MS) { + try { + if (!Files.exists(file)) { + return false; + } + + long currentSize = Files.size(file); + + if (currentSize == lastSize && currentSize > 0) { + stableCount++; + if (stableCount >= STABILITY_REQUIRED_CHECKS) { + return true; + } + } else { + stableCount = 0; + } + + lastSize = currentSize; + Thread.sleep(STABILITY_CHECK_INTERVAL_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (IOException e) { + log.warn("Error checking file size for stability: {}", file, e); + return false; + } + } + + log.warn("File size did not stabilize after {}ms: {}", STABILITY_MAX_WAIT_MS, file); + return false; + } } \ No newline at end of file diff --git a/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropMetadataService.java b/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropMetadataService.java index 1b63bf737..3bc7e78c7 100644 --- a/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropMetadataService.java +++ b/booklore-api/src/main/java/org/booklore/service/bookdrop/BookdropMetadataService.java @@ -21,6 +21,8 @@ import org.springframework.transaction.annotation.Transactional; import tools.jackson.core.JacksonException; import tools.jackson.databind.ObjectMapper; +import org.apache.commons.io.FilenameUtils; + import java.io.File; import java.io.IOException; import java.time.Instant; @@ -46,6 +48,12 @@ public class BookdropMetadataService { public BookdropFileEntity attachInitialMetadata(Long bookdropFileId) throws JacksonException { BookdropFileEntity entity = getOrThrow(bookdropFileId); BookMetadata initial = extractInitialMetadata(entity); + if (initial == null) { + log.warn("Metadata extraction returned null for file: {}. Using filename as fallback.", entity.getFileName()); + initial = BookMetadata.builder() + .title(FilenameUtils.getBaseName(entity.getFileName())) + .build(); + } extractAndSaveCover(entity); String initialJson = objectMapper.writeValueAsString(initial); entity.setOriginalMetadata(initialJson); diff --git a/booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java b/booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java index 9aecf10cf..b1847a993 100644 --- a/booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java +++ b/booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java @@ -413,6 +413,11 @@ public class MetadataRefreshService { private FetchMetadataRequest buildFetchMetadataRequestFromBook(Book book) { BookMetadata metadata = book.getMetadata(); + if (metadata == null) { + return FetchMetadataRequest.builder() + .bookId(book.getId()) + .build(); + } String isbn = metadata.getIsbn13(); if (isbn == null || isbn.isBlank()) { isbn = metadata.getIsbn10();