fix(document): break DocumentService ↔ ThumbnailAsyncRunner ↔ ThumbnailService cycle
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m29s
CI / OCR Service Tests (push) Successful in 38s
CI / Backend Unit Tests (push) Failing after 1m54s
CI / Unit & Component Tests (pull_request) Failing after 28s
CI / OCR Service Tests (pull_request) Successful in 36s
CI / Backend Unit Tests (pull_request) Failing after 1m49s

Spring Framework 7 prohibits constructor injection cycles even with @Lazy.
Replace DocumentService dependencies in ThumbnailAsyncRunner and ThumbnailService
with direct DocumentRepository calls — both are intra-domain reads/saves.
Update ThumbnailServiceTest to mock DocumentRepository accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-05 15:56:05 +02:00
parent 7e6e809aa4
commit fbbe0789d0
4 changed files with 17 additions and 22 deletions

View File

@@ -29,7 +29,6 @@ import org.raddatz.familienarchiv.ocr.TrainingLabel;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.document.DocumentRepository;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -77,10 +76,6 @@ public class DocumentService {
private final AuditService auditService;
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
private final AuditLogQueryService auditLogQueryService;
// @Lazy breaks the DocumentService ↔ ThumbnailAsyncRunner cycle: the runner
// now reaches Document data through DocumentService (per the layering rule),
// and Spring needs a proxy here to defer the back-edge until both beans exist.
@Lazy
private final ThumbnailAsyncRunner thumbnailAsyncRunner;
public record StoreResult(Document document, boolean isNew) {}

View File

@@ -28,7 +28,7 @@ import java.util.concurrent.TimeoutException;
@Slf4j
public class ThumbnailAsyncRunner {
private final DocumentService documentService;
private final DocumentRepository documentRepository;
private final ThumbnailService thumbnailService;
/** Per-document timeout for the whole generate() call — defense against corrupt PDFs. */
@@ -59,7 +59,7 @@ public class ThumbnailAsyncRunner {
*/
@Async("thumbnailExecutor")
public void generateAsync(UUID documentId) {
Optional<Document> docOpt = documentService.findById(documentId);
Optional<Document> docOpt = documentRepository.findById(documentId);
if (docOpt.isEmpty()) {
log.warn("Thumbnail generation skipped: document not found id={}", documentId);
return;

View File

@@ -62,16 +62,16 @@ public class ThumbnailService {
private final FileService fileService;
private final S3Client s3Client;
private final DocumentService documentService;
private final DocumentRepository documentRepository;
@Value("${app.s3.bucket}")
private String bucketName;
public ThumbnailService(FileService fileService, S3Client s3Client,
DocumentService documentService) {
DocumentRepository documentRepository) {
this.fileService = fileService;
this.s3Client = s3Client;
this.documentService = documentService;
this.documentRepository = documentRepository;
}
public Outcome generate(Document doc) {
@@ -167,7 +167,7 @@ public class ThumbnailService {
doc.setThumbnailGeneratedAt(LocalDateTime.now());
doc.setThumbnailAspect(result.aspect());
doc.setPageCount(result.pageCount());
documentService.updateThumbnailMetadata(doc);
documentRepository.save(doc);
return Outcome.SUCCESS;
} catch (Exception e) {
// Thumbnail is already in S3 but the entity update failed. Because the S3