Class OutboxRelay

java.lang.Object
com.babelqueue.outbox.OutboxRelay

public final class OutboxRelay extends Object
The read/publish side of the transactional outbox (ADR-0029): drain pending rows the Outbox writer committed and forward each onto the broker through the OutboxTransport seam, marking every row published or failed.

Run it on a short interval (a worker loop, a scheduled command) after the business transaction commits. Because the message was committed atomically with the business data, the relay is the only thing standing between "row exists" and "broker has it" — and it only ever reads already-durable rows, so it never invents work.

Semantics — at-least-once handoff:

  • A row is marked published only after OutboxTransport.publish(String, byte[]) returns; if the process dies between publish and OutboxStore.markPublished(List), the row stays pending and is published again on the next pass. That is at-least-once: a downstream consumer must dedupe on the canonical meta.id (com.babelqueue.idempotency.Idempotent is exactly that guard, the consumer-side mirror of this producer-side helper — ADR-0022).
  • A publish that throws is caught, OutboxStore.markFailed(String, String) records the error and bumps the attempt count, and the row stays pending for a later retry. One poison row never blocks the rest of the batch.
  • trace_id is preserved end-to-end (GR-4): the relay publishes the stored bytes verbatim — it never decodes, rebuilds or re-encodes the envelope — so the body that reaches the broker is byte-identical to what was stored (GR-1/GR-5).

Backoff: between a failed publish and the next attempt within the same pass the relay sleeps for a bounded, linearly-growing delay (capped), to avoid hammering a broker that is briefly down. The OutboxRelay.Sleeper is injectable so tests stay instant.

  • Field Details

    • DEFAULT_DRAIN_CEILING

      public static final int DEFAULT_DRAIN_CEILING
      Hard safety ceiling on drain(int) passes when the caller passes a non-positive value.
      See Also:
    • DEFAULT_BATCH_SIZE

      public static final int DEFAULT_BATCH_SIZE
      Default number of rows reserved and published per flush().
      See Also:
    • DEFAULT_BACKOFF_STEP_MS

      public static final int DEFAULT_BACKOFF_STEP_MS
      Default base backoff added per prior attempt, in milliseconds.
      See Also:
    • DEFAULT_BACKOFF_CAP_MS

      public static final int DEFAULT_BACKOFF_CAP_MS
      Default upper bound on a single backoff sleep, in milliseconds.
      See Also:
  • Constructor Details

    • OutboxRelay

      public OutboxRelay(OutboxTransport transport, OutboxStore store)
      A relay with the default batch size and backoff budget, using a real Thread.sleep(long).
      Parameters:
      transport - where published rows go (the publish-only seam)
      store - the outbox to drain
    • OutboxRelay

      public OutboxRelay(OutboxTransport transport, OutboxStore store, int batchSize, int backoffStepMs, int backoffCapMs, OutboxRelay.Sleeper sleeper)
      A fully configured relay.
      Parameters:
      transport - where published rows go (the same publish-only seam every framework-less producer uses)
      store - the outbox to drain
      batchSize - how many rows to reserve and publish per flush()
      backoffStepMs - base backoff added per prior attempt, in milliseconds
      backoffCapMs - upper bound on a single backoff sleep, in milliseconds
      sleeper - sleeps the backoff between attempts; null uses Thread.sleep(long). Inject a no-op (or a recorder) in tests so they stay instant.
  • Method Details

    • flush

      public OutboxRelayResult flush()
      Publish one batch of pending rows. Each row the transport accepts is marked published; each that throws is marked failed (with a backoff before continuing) and left pending. Returns a per-pass tally. Call it repeatedly (a loop / cron) to drain the outbox; drain(int) loops until it is empty.
    • drain

      public OutboxRelayResult drain(int maxPasses)
      Drain the outbox by repeatedly calling flush() while each pass keeps making progress (publishes at least one row), then return the cumulative tally. The loop stops as soon as a pass publishes nothing — the outbox is empty, or only currently-failing rows remain (those are left pending for a future drain call once the broker recovers). maxPasses is a hard safety ceiling so a degenerate store can never spin forever (a non-positive value uses DEFAULT_DRAIN_CEILING).