# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916

## A. the port declares the bounded overload and says why it exists
  /**
   * Deletes at most {@code limit} published rows older than the cutoff.
   *
   * <p>The unbounded version deletes everything before the cutoff in one statement. On a table that
   * has been accumulating published rows since the last sweep that is a single long transaction
   * holding locks and generating WAL in proportion to the backlog, which shows up as the relay and
   * the business writes stalling behind retention. The cleanup jobs describe themselves as bounded
   * by batch size; this is the parameter that makes that true.
   *
   * @return how many rows were deleted; fewer than {@code limit} means the sweep is finished
   */
  int purgePublishedBefore(Instant publishedBefore, int limit);

## B. both JDBC repositories implement it
141:  public int purgeProcessedBefore(Instant processedBefore, int limit) {
486:  public int purgePublishedBefore(Instant publishedBefore, int limit) {
--- and the inbox implementation really is bounded
            connection.prepareStatement(
                """
                WITH expired AS (
                    SELECT message_id, consumer_id
                    FROM messaging_inbox
                    WHERE processed_at < ?
                    ORDER BY processed_at
                    LIMIT ?
                    FOR UPDATE SKIP LOCKED
                )
                DELETE FROM messaging_inbox i
                USING expired e
                WHERE i.message_id = e.message_id AND i.consumer_id = e.consumer_id
                """)) {

## C. every appearance of the two-argument form in the repository
src/messaging/messaging-inbox-jdbc-postgresql/src/main/java/dev/caskeleton/messaging/inbox/JdbcInboxRepository.java:141:  public int purgeProcessedBefore(Instant processedBefore, int limit) {
src/messaging/messaging-inbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/inbox/IdempotentConsumerTest.java:106:  public int purgeProcessedBefore(Instant processedBefore, int limit) {
src/messaging/messaging-inbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/inbox/InboxOperationsTest.java:46:    public int purgeProcessedBefore(Instant processedBefore, int limit) {
src/messaging/messaging-outbox-jdbc-postgresql/src/main/java/dev/caskeleton/messaging/outbox/JdbcOutboxRepository.java:486:  public int purgePublishedBefore(Instant publishedBefore, int limit) {
src/messaging/messaging-outbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/outbox/OutboxOperationsTest.java:125:    public int purgePublishedBefore(Instant publishedBefore, int limit) {
src/messaging/messaging-outbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/outbox/OutboxRelayTest.java:404:  public int purgePublishedBefore(Instant publishedBefore, int limit) {
src/messaging/messaging-reliability-api/src/main/java/dev/caskeleton/messaging/reliability/InboxRepository.java:52:  int purgeProcessedBefore(Instant processedBefore, int limit);
src/messaging/messaging-reliability-api/src/main/java/dev/caskeleton/messaging/reliability/OutboxRepository.java:151:  int purgePublishedBefore(Instant publishedBefore, int limit);
src/messaging/messaging-spring-boot-starter/src/test/java/dev/caskeleton/messaging/autoconfigure/MessagingOutboxRelayLifecycleTest.java:199:    public int purgePublishedBefore(Instant publishedBefore, int limit) {
(every line above is a declaration, a production implementation, or a test fake override.
 2 port declarations + 2 production implementations + 5 test fake overrides = 9.
 None of them is a call site.)

## D. what the two cleanup jobs actually call
54-    int removed = 0;
55-    for (int batch = 0; batch < maxBatches; batch++) {
56:      int deleted = inbox.purgeProcessedBefore(cutoff);
57-      removed += deleted;
58-      // A short batch means the backlog is drained; continuing would just re-scan an empty range.
48-    int removed = 0;
49-    for (int batch = 0; batch < maxBatches; batch++) {
50:      int deleted = outbox.purgePublishedBefore(cutoff);
51-      removed += deleted;
52-      if (deleted == 0) {

## E. InboxCleanupJob declares a batch size and never uses it
src/messaging/messaging-inbox-jdbc-postgresql/src/main/java/dev/caskeleton/messaging/inbox/InboxCleanupJob.java:21:  public static final int DEFAULT_BATCH_SIZE = 1_000;
(one line = the declaration only)

## F. what the unbounded implementation issues
  @Override
  public int purgeProcessedBefore(Instant processedBefore) {
    Objects.requireNonNull(processedBefore, "processedBefore must not be null");
    try (Connection connection = dataSource.getConnection();
        PreparedStatement statement =
            connection.prepareStatement("DELETE FROM messaging_inbox WHERE processed_at < ?")) {
      statement.setTimestamp(1, Timestamp.from(processedBefore));
      return statement.executeUpdate();
    } catch (SQLException exception) {
      throw new MessagingConfigurationException(
          "INBOX_PURGE_FAILED", "could not purge the inbox", exception);
    }
  }

## G. the test named for the property, and the fake it runs against
    @Override
    public int purgeProcessedBefore(Instant processedBefore, int limit) {
      return Math.min(purgeProcessedBefore(processedBefore), limit);
    }

    @Override
    public int purgeProcessedBefore(Instant processedBefore) {
      cutoffs.add(processedBefore);
      return pass < deletions.size() ? deletions.get(pass++) : 0;
    }
  }
  @Test
  void cleanupDeletesInBoundedBatches() {
    InMemoryInbox inbox = new InMemoryInbox(List.of(1000, 500));

    int removed =
        new InboxCleanupJob(inbox, policy(Duration.ofDays(7), Duration.ofDays(1)), 10).runOnce(NOW);

    assertThat(removed).isEqualTo(1500);
    assertThat(inbox.cutoffs).hasSize(3);
  }

## H. the container lane calls the unbounded form too
  @Test
  void retentionRemovesOldRows() {
    MessageId purged = MessageId.newId();
    inTransaction(() -> repository.reserve(purged, "order-projection", NOW));

    assertThat(repository.purgeProcessedBefore(NOW.plusSeconds(1))).isEqualTo(1);
  }

## verdict
Both ports declare a bounded purge, both JDBC classes implement it with LIMIT,
and no code calls it. Both cleanup jobs call the unbounded overload, which the
port's own javadoc describes as the single long transaction the bounded one exists
to avoid. The test that asserts bounded batching runs against a scripted fake whose
unbounded method returns pre-set counts, so the bound is never exercised.
