Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions docs/email-async/01-async-send-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,6 @@ parses CSV ([parseCSV.ts](../../js/src/features/emails/api/parseCSV.ts)) and pos
committed, the rows are already visible — no `AFTER_COMMIT` event is needed. **There is no automatic
enqueue-time trigger** (the accepted tradeoff of #6: if the kick is never issued, the batch waits for the next
kick or a restart).
2. **On startup** — `@EventListener(ApplicationReadyEvent.class)` first resets `PROCESSING → ERROR`
(at-most-once recovery), then calls `trigger()` once (covers rows left `PENDING` before shutdown). This is the
only safety net for a missed kick.
- **Drain job** (runs on the executor thread, loops until no rows, then the thread idles):
1. **Claim** up to 50 `PENDING` rows atomically:
```sql
Expand Down
17 changes: 0 additions & 17 deletions src/main/java/org/patinanetwork/patchats/email/EmailDrainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
import org.patinanetwork.patchats.email.db.repos.EmailRepo;
import org.patinanetwork.patchats.email.db.repos.EmailTemplateRepo;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
Expand All @@ -27,7 +24,6 @@
*/
@Component
@Slf4j
@Profile("!ci")
public class EmailDrainer {

private static final int BATCH_SIZE = 50;
Expand Down Expand Up @@ -113,17 +109,4 @@ private void sendOne(final Email email, final Map<UUID, EmailTemplate> templateC
emailRepo.markError(email.getId(), ex.getMessage());
}
}

/**
* On boot: reset orphaned {@code PROCESSING} rows to {@code ERROR} (at-most-once recovery, decision #9), then kick
* one drain to cover rows left {@code PENDING} before shutdown — the only safety net for a missed kick.
*/
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
final int reset = emailRepo.resetProcessingToError();
Comment thread
isabellalam12 marked this conversation as resolved.
if (reset > 0) {
log.warn("Reset {} orphaned PROCESSING email(s) to ERROR on startup", reset);
}
trigger();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ public interface EmailRepo {
/** Marks a row {@code ERROR} with the given message (no retry — terminal, decision #8). */
void markError(UUID id, String errorMessage);

/**
* At-most-once crash recovery (decision #9): flips any orphaned {@code PROCESSING} rows to {@code ERROR}. Returns
* the number of rows reset.
*/
int resetProcessingToError();

/** Per-status row counts for one batch ({@code GROUP BY status}); statuses with no rows are absent. */
Map<EmailStatus, Integer> countByStatus(UUID requestId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,6 @@ public void markError(final UUID id, final String errorMessage) {
.update();
}

@Override
public int resetProcessingToError() {
return jdbc.sql("""
UPDATE emails
SET status = 'ERROR',
error_message = 'Reset on startup: orphaned PROCESSING row (at-most-once recovery)',
updated_at = now()
WHERE status = 'PROCESSING'
""").update();
}

@Override
public Map<EmailStatus, Integer> countByStatus(final UUID requestId) {
final Map<EmailStatus, Integer> counts = new EnumMap<>(EmailStatus.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,6 @@ void renderFailureIsTerminalError() {
verify(emailRepo, never()).markSent(any());
}

@Test
void startupResetsProcessingToErrorThenDrains() {
when(emailRepo.resetProcessingToError()).thenReturn(2);
when(emailRepo.claimBatch(50)).thenReturn(List.of());

drainer(SYNC).onApplicationReady();

verify(emailRepo).resetProcessingToError();
verify(emailRepo).claimBatch(50); // the startup kick still runs a drain pass
}

@Test
void overlappingTriggersCoalesceToOneDrain() {
// A manual executor that captures jobs without running them, to observe submission count.
Expand Down
Loading