From fbbe0789d0c2a4b99079584266eacbd26c6d8422 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 5 May 2026 15:56:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(document):=20break=20DocumentService=20?= =?UTF-8?q?=E2=86=94=20ThumbnailAsyncRunner=20=E2=86=94=20ThumbnailService?= =?UTF-8?q?=20cycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../document/DocumentService.java | 5 ----- .../document/ThumbnailAsyncRunner.java | 4 ++-- .../document/ThumbnailService.java | 8 +++---- .../document/ThumbnailServiceTest.java | 22 +++++++++---------- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index 3e3f1b4b..a69a77e0 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -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) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailAsyncRunner.java b/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailAsyncRunner.java index ed239ade..3b82aff1 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailAsyncRunner.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailAsyncRunner.java @@ -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 docOpt = documentService.findById(documentId); + Optional docOpt = documentRepository.findById(documentId); if (docOpt.isEmpty()) { log.warn("Thumbnail generation skipped: document not found id={}", documentId); return; diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailService.java index b937a126..26b9e67e 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/ThumbnailService.java @@ -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 diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/ThumbnailServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/ThumbnailServiceTest.java index 97611274..55f140bf 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/ThumbnailServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/ThumbnailServiceTest.java @@ -39,17 +39,17 @@ class ThumbnailServiceTest { private FileService fileService; private S3Client s3Client; - private DocumentService documentService; + private DocumentRepository documentRepository; private ThumbnailService thumbnailService; @BeforeEach void setUp() { fileService = mock(FileService.class); s3Client = mock(S3Client.class); - documentService = mock(DocumentService.class); - thumbnailService = new ThumbnailService(fileService, s3Client, documentService); + documentRepository = mock(DocumentRepository.class); + thumbnailService = new ThumbnailService(fileService, s3Client, documentRepository); ReflectionTestUtils.setField(thumbnailService, "bucketName", "test-bucket"); - when(documentService.updateThumbnailMetadata(any(Document.class))).thenAnswer(i -> i.getArgument(0)); + when(documentRepository.save(any(Document.class))).thenAnswer(i -> i.getArgument(0)); } @Test @@ -103,7 +103,7 @@ class ThumbnailServiceTest { assertThat(doc.getThumbnailKey()).isEqualTo("thumbnails/" + doc.getId() + ".jpg"); assertThat(doc.getThumbnailGeneratedAt()).isNotNull(); - verify(documentService).updateThumbnailMetadata(doc); + verify(documentRepository).save(doc); } @Test @@ -152,7 +152,7 @@ class ThumbnailServiceTest { assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED); assertThat(doc.getThumbnailKey()).isNull(); - verify(documentService, never()).updateThumbnailMetadata(any()); + verify(documentRepository, never()).save(any()); } @Test @@ -165,7 +165,7 @@ class ThumbnailServiceTest { assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED); verifyNoInteractions(s3Client); - verify(documentService, never()).updateThumbnailMetadata(any()); + verify(documentRepository, never()).save(any()); } @Test @@ -260,7 +260,7 @@ class ThumbnailServiceTest { assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED); verifyNoInteractions(s3Client); - verify(documentService, never()).updateThumbnailMetadata(any()); + verify(documentRepository, never()).save(any()); } @Test @@ -275,7 +275,7 @@ class ThumbnailServiceTest { assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED); verifyNoInteractions(s3Client); - verify(documentService, never()).updateThumbnailMetadata(any()); + verify(documentRepository, never()).save(any()); } @Test @@ -286,14 +286,14 @@ class ThumbnailServiceTest { Document doc = makeDoc("application/pdf", "documents/letter.pdf"); when(fileService.downloadFileStream(anyString())) .thenReturn(new ByteArrayInputStream(createSamplePdf())); - when(documentService.updateThumbnailMetadata(any())) + when(documentRepository.save(any())) .thenThrow(new RuntimeException("constraint violation")); ThumbnailService.Outcome outcome = thumbnailService.generate(doc); assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED); verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); - verify(documentService).updateThumbnailMetadata(any()); + verify(documentRepository).save(any()); } // ─── helpers ──────────────────────────────────────────────────────────────