-
Notifications
You must be signed in to change notification settings - Fork 0
make the scheduled attachment cleanup multi pod save #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
15c22d4
first draft for concurrency using an approach at the database level
J0nasMayr cc401e8
Revert "first draft for concurrency using an approach at the database…
J0nasMayr b25377f
refactor Email entity by embedding EmailContent and adding new fields
J0nasMayr 6fa769d
solve concurrency issue between pods using atomic compare-and-set; Ad…
J0nasMayr 7d6526f
add test for concurrency and schedule functionality
J0nasMayr e429e20
update readme.md
J0nasMayr be9d60e
test fixes
J0nasMayr 38db610
truncate OffsetDateTime to MICROS for pipeline to run successfully
J0nasMayr f60de88
remove unnecessary errorMessage set
J0nasMayr 1c03524
requested changes
J0nasMayr 259f4e5
consider all error cases that can happen on a schedule pass
J0nasMayr 63ff658
remove the first try catch since it is overkill
J0nasMayr 5cd6e65
make the scheduled attachment cleanup multip pod save
J0nasMayr 357c405
add tests
J0nasMayr 5e999cc
Merge branch 'main' into make-cleanup-scheduler-multi-pod-save
J0nasMayr f2615ea
requested changes
J0nasMayr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
...java/it/aboutbits/springboot/emailservice/lib/application/CleanupAttachmentFilesTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package it.aboutbits.springboot.emailservice.lib.application; | ||
|
|
||
| import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; | ||
| import it.aboutbits.springboot.emailservice.lib.EmailState; | ||
| import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; | ||
| import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; | ||
| import it.aboutbits.springboot.emailservice.lib.model.Email; | ||
| import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; | ||
| import it.aboutbits.springboot.emailservice.support.database.WithPostgres; | ||
| import it.aboutbits.springboot.emailservice.support.database.factory.EmailFactory; | ||
| import org.jspecify.annotations.NullMarked; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
| import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||
|
|
||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.stream.IntStream; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.mockito.ArgumentMatchers.anyLong; | ||
| import static org.mockito.Mockito.doThrow; | ||
| import static org.mockito.Mockito.times; | ||
| import static org.mockito.Mockito.verify; | ||
|
|
||
| @SpringBootTest(properties = { | ||
| "aboutbits.emailservice.scheduling.interval=30000" | ||
| }) | ||
| @WithPostgres | ||
| @NullMarked | ||
| class CleanupAttachmentFilesTest { | ||
| @MockitoBean | ||
| AttachmentDataSource attachmentDataSource; | ||
|
|
||
| @Autowired | ||
| EmailRepository emailRepository; | ||
|
|
||
| @Autowired | ||
| CleanupAttachmentFiles cleanupAttachmentFiles; | ||
|
|
||
| @Test | ||
| void givenSentUncleanedEmails_cleanupAttachments_shouldReleaseAndMarkCleaned() throws AttachmentException { | ||
| persistCleanableEmail(100L); | ||
| persistCleanableEmail(101L); | ||
| persistCleanableEmail(102L); | ||
|
|
||
| cleanupAttachmentFiles.cleanupAttachments(); | ||
|
|
||
| assertThat(emailRepository.findAll()) | ||
| .hasSize(3) | ||
| .allMatch(Email::isAttachmentsCleaned); | ||
| verify(attachmentDataSource, times(1)).releaseAttachment(100L); | ||
| verify(attachmentDataSource, times(1)).releaseAttachment(101L); | ||
| verify(attachmentDataSource, times(1)).releaseAttachment(102L); | ||
| } | ||
|
|
||
| @Test | ||
| void givenNonSentOrAlreadyCleanedRows_cleanupAttachments_shouldSkip() throws AttachmentException { | ||
| persistEmail(200L, EmailState.PENDING, false); | ||
| persistEmail(201L, EmailState.SENDING, false); | ||
| persistEmail(202L, EmailState.ERROR, false); | ||
| persistEmail(203L, EmailState.SENT, true); | ||
|
|
||
| cleanupAttachmentFiles.cleanupAttachments(); | ||
|
|
||
| verify(attachmentDataSource, times(0)).releaseAttachment(anyLong()); | ||
| assertThat(emailRepository.findAll()) | ||
| .filteredOn(email -> email.getState() != EmailState.SENT) | ||
| .allMatch(email -> !email.isAttachmentsCleaned()); | ||
| } | ||
|
|
||
| @Test | ||
| void givenReleaseFails_cleanupAttachments_shouldResetFlagForRetry() throws AttachmentException { | ||
| var email = persistCleanableEmail(300L); | ||
| doThrow(new AttachmentException()).when(attachmentDataSource).releaseAttachment(300L); | ||
|
|
||
| cleanupAttachmentFiles.cleanupAttachments(); | ||
|
|
||
| assertThat(emailRepository.findById(email.getId())) | ||
| .get() | ||
| .satisfies(reloaded -> assertThat(reloaded.isAttachmentsCleaned()).isFalse()); | ||
|
J0nasMayr marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| @Test | ||
| void givenConcurrentPasses_cleanupAttachments_shouldReleaseEachEmailExactlyOnce() throws Exception { | ||
| var totalEmails = 25; | ||
| var workerThreads = 4; | ||
| var passesPerWorker = 10; | ||
| for (var i = 0; i < totalEmails; i++) { | ||
| persistCleanableEmail(1000L + i); | ||
| } | ||
|
|
||
| var startGate = new CountDownLatch(1); | ||
| var executor = Executors.newFixedThreadPool(workerThreads); | ||
| try { | ||
| IntStream.range(0, workerThreads).forEach(_ -> executor.submit(() -> { | ||
| startGate.await(); | ||
| for (var i = 0; i < passesPerWorker; i++) { | ||
| cleanupAttachmentFiles.cleanupAttachments(); | ||
| } | ||
| return null; | ||
| })); | ||
| startGate.countDown(); | ||
| executor.shutdown(); | ||
| assertThat(executor.awaitTermination(60, TimeUnit.SECONDS)).isTrue(); | ||
| } finally { | ||
| if (!executor.isTerminated()) { | ||
| executor.shutdownNow(); | ||
| } | ||
| } | ||
|
|
||
| assertThat(emailRepository.findAll()) | ||
| .hasSize(totalEmails) | ||
| .allMatch(Email::isAttachmentsCleaned); | ||
| // Exactly one release per file reference | ||
| verify(attachmentDataSource, times(totalEmails)).releaseAttachment(anyLong()); | ||
| } | ||
|
|
||
| private Email persistCleanableEmail(long fileReference) { | ||
| return persistEmail(fileReference, EmailState.SENT, false); | ||
| } | ||
|
|
||
| private Email persistEmail(long fileReference, EmailState state, boolean attachmentsCleaned) { | ||
| var email = EmailFactory.once() | ||
| .state(state) | ||
| .attachmentsCleaned(attachmentsCleaned) | ||
| .build(); | ||
|
|
||
| var attachment = new EmailAttachment(); | ||
| attachment.setEmail(email); | ||
| attachment.setFileName("file.png"); | ||
| attachment.setContentType("image/png"); | ||
| attachment.setFileReference(fileReference); | ||
| email.setAttachments(new HashSet<>(Set.of(attachment))); | ||
|
|
||
| return emailRepository.save(email); | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.