Skip to content

Commit 259f4e5

Browse files
committed
consider all error cases that can happen on a schedule pass
1 parent 1c03524 commit 259f4e5

1 file changed

Lines changed: 45 additions & 9 deletions

File tree

src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33

44
import it.aboutbits.springboot.emailservice.lib.EmailSchedulerCallback;
5+
import it.aboutbits.springboot.emailservice.lib.model.Email;
56
import lombok.extern.log4j.Log4j2;
67
import org.jspecify.annotations.NullMarked;
78
import org.springframework.scheduling.annotation.Scheduled;
89

910
import java.time.Duration;
1011
import java.time.OffsetDateTime;
1112
import java.util.List;
13+
import java.util.Optional;
1214

1315
@Log4j2
1416
@NullMarked
@@ -43,21 +45,55 @@ void sendEmails() {
4345
var staleSendingBefore = OffsetDateTime.now().minus(stuckSendingRecoveryThreshold);
4446
var candidateIds = queryEmail.candidateIdsToSend(staleSendingBefore);
4547

48+
var countClaimed = 0;
4649
var countSent = 0;
4750
var countError = 0;
4851
for (var id : candidateIds) {
49-
var claimed = manageEmail.tryClaimForSend(id, staleSendingBefore);
52+
Optional<Email> claimed;
53+
try {
54+
claimed = manageEmail.tryClaimForSend(id, staleSendingBefore);
55+
} catch (Exception e) {
56+
// Claim failed and rolled back: nothing was sent and the row is unchanged, so it stays claimable
57+
// Another pod may pick it up in this same pass, or it is retried on a later pass.
58+
log.error(
59+
JOB_DESCRIPTION + " | Failed to claim email for sending: {}. "
60+
+ "Nothing was sent and the row is unchanged; it stays claimable and may be picked up "
61+
+ "by another pod in this cycle or retried on a later pass.",
62+
id,
63+
e
64+
);
65+
continue;
66+
}
67+
5068
if (claimed.isEmpty()) {
5169
// Lost race to another pod; Skip
5270
continue;
5371
}
54-
var updated = manageEmail.completeClaimedSend(claimed.get());
55-
switch (updated.getState()) {
56-
case ERROR, PENDING -> countError++;
57-
case SENT -> countSent++;
58-
default -> log.warn(
59-
JOB_DESCRIPTION + " | Job produced an invalid notification result state: {}.",
60-
updated.getState().name()
72+
countClaimed++;
73+
74+
try {
75+
var updated = manageEmail.completeClaimedSend(claimed.get());
76+
switch (updated.getState()) {
77+
// We do not count PENDING emails as errors since they will
78+
// be retried and eventually end up in one of the two buckets.
79+
case ERROR -> countError++;
80+
case SENT -> countSent++;
81+
default -> log.warn(
82+
JOB_DESCRIPTION + " | Job produced an invalid notification result state: {}.",
83+
updated.getState().name()
84+
);
85+
}
86+
} catch (Exception e) {
87+
// Best-effort visibility only: we cannot reliably recover here. The email may already have been
88+
// sent but persisting the result failed, so the row stays in SENDING and will be re-claimed and
89+
// re-sent after the recovery threshold (possible duplicate delivery).
90+
countError++;
91+
log.error(
92+
JOB_DESCRIPTION + " | Failed to complete send for email: {}. It may have been sent already; "
93+
+ "if the result could not be persisted the row stays in SENDING and will be re-sent "
94+
+ "after the recovery threshold (possible duplicate delivery).",
95+
id,
96+
e
6197
);
6298
}
6399
}
@@ -66,7 +102,7 @@ void sendEmails() {
66102

67103
for (var callback : callbacks) {
68104
callback.report(new EmailSchedulerCallback.Report(
69-
candidateIds.size(),
105+
countClaimed,
70106
countSent,
71107
countError
72108
));

0 commit comments

Comments
 (0)