feat(backend): add ThumbnailAsyncRunner with afterCommit dispatch and timeout
Bridges @Transactional upload paths to the async thumbnail pipeline. dispatchAfterCommit registers a TransactionSynchronization so the async task only fires after the surrounding commit (and is silently skipped on rollback) — mirrors the AuditService.logAfterCommit pattern. generateAsync wraps the full ThumbnailService.generate call in a 30s watchdog so a hung PDFBox render cannot occupy a thumbnailExecutor slot indefinitely. Refs #307 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package org.raddatz.familienarchiv.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Bridges document upload paths to asynchronous thumbnail generation. Use
|
||||
* {@link #dispatchAfterCommit(UUID)} from inside {@code @Transactional} service methods —
|
||||
* it registers a post-commit hook so the async task only fires when the surrounding
|
||||
* transaction actually commits, and is silently skipped on rollback. Mirrors
|
||||
* {@link org.raddatz.familienarchiv.audit.AuditService#logAfterCommit}.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ThumbnailAsyncRunner {
|
||||
|
||||
private final DocumentRepository documentRepository;
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
/** Per-document timeout for the whole generate() call — defense against corrupt PDFs. */
|
||||
private long generateTimeoutSeconds = 30L;
|
||||
|
||||
/**
|
||||
* Registers a post-commit hook that triggers asynchronous thumbnail generation for the
|
||||
* given document. When no transaction is active the task is dispatched immediately.
|
||||
* Safe to call from inside {@code @Transactional} service methods.
|
||||
*/
|
||||
public void dispatchAfterCommit(UUID documentId) {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
generateAsync(documentId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
generateAsync(documentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs thumbnail generation on the {@code thumbnailExecutor} pool, wrapped in a watchdog
|
||||
* timeout so a hung PDFBox render cannot occupy a pool thread indefinitely. Never throws:
|
||||
* all errors and timeouts are logged and swallowed so upload paths are not affected.
|
||||
*/
|
||||
@Async("thumbnailExecutor")
|
||||
public void generateAsync(UUID documentId) {
|
||||
Optional<Document> docOpt = documentRepository.findById(documentId);
|
||||
if (docOpt.isEmpty()) {
|
||||
log.warn("Thumbnail generation skipped: document not found id={}", documentId);
|
||||
return;
|
||||
}
|
||||
Document doc = docOpt.get();
|
||||
|
||||
ExecutorService timeoutWorker = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "Thumbnail-Render-" + documentId);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
try {
|
||||
Future<ThumbnailService.Outcome> future = timeoutWorker.submit(
|
||||
() -> thumbnailService.generate(doc));
|
||||
try {
|
||||
future.get(generateTimeoutSeconds, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
log.warn("Thumbnail generation timed out after {}s for doc={}",
|
||||
generateTimeoutSeconds, documentId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Thumbnail generation errored for doc={} reason={}",
|
||||
documentId, e.getMessage());
|
||||
}
|
||||
} finally {
|
||||
timeoutWorker.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user