# V2 가 더한 컬럼과 제약 ALTER TABLE messaging_outbox ADD COLUMN lease_owner VARCHAR(160), ADD COLUMN lease_token BIGINT NOT NULL DEFAULT 0, ADD COLUMN next_attempt_at TIMESTAMPTZ; -- Backfill is unnecessary for correctness — the default is 0 and the first claim increments it — -- but the constraint states the invariant the code depends on. ALTER TABLE messaging_outbox ADD CONSTRAINT ck_messaging_outbox_lease_token CHECK (lease_token >= 0); # 다섯 상태에서 여섯으로 23: CHECK (status IN ('PENDING', 'IN_FLIGHT', 'PUBLISHED', 'AMBIGUOUS', 'FAILED')), CHECK (status IN ('PENDING', 'IN_FLIGHT', 'PUBLISHED', 'AMBIGUOUS', 'FAILED', 'EXHAUSTED')); # 청구가 소유자와 토큰을 같은 문장에서 쓴다 UPDATE messaging_outbox o SET status = 'IN_FLIGHT', lease_expires_at = ?, lease_owner = ?, lease_token = o.lease_token + 1 # 그 청구가 읽는 술어 WHERE status IN ('PENDING', 'AMBIGUOUS', 'IN_FLIGHT') AND (lease_expires_at IS NULL OR lease_expires_at <= ?) -- The retry clock lives in the row, not in the relay's memory. Without these two -- predicates an AMBIGUOUS row became claimable again on the very next pass, so a -- broker outage meant the whole backlog was republished every poll interval and the -- configured attempt budget was a number nothing consulted. AND (next_attempt_at IS NULL OR next_attempt_at <= ?) AND attempts < ? # V2 가 그 술어에 맞춰 만든 부분 인덱스 CREATE INDEX ix_messaging_outbox_next_attempt ON messaging_outbox (next_attempt_at, created_at) WHERE status IN ('PENDING', 'AMBIGUOUS', 'IN_FLIGHT'); # 최종 쓰기는 소유자와 토큰을 조건으로 걸고, 0행을 삼키지 않는다 354: + "WHERE message_id = ? AND status = 'IN_FLIGHT' AND lease_owner = ? AND lease_token = ?"; 403: + "WHERE message_id = ? AND status = 'IN_FLIGHT' AND lease_owner = ? AND lease_token = ?"; * past its lease writes nothing: another relay's claim incremented the token, and this update * matches zero rows. Zero is reported rather than swallowed — a stale write means this worker may * have produced a duplicate publication, which is exactly what an operator needs to see. # 릴레이가 부르는 것은 전부 신세대다 OutboxRelay.java:158: repository.claimBatch(owner, batchSize, leaseDuration, now, scheduler.maxAttempts()); OutboxRelay.java:171: if (repository.markPublished(lease, now) == OutboxTransitionResult.APPLIED) { OutboxRelay.java:189: .map(reason -> repository.markExhausted(lease, reason, now)) OutboxRelay.java:192: repository.markAmbiguous( OutboxRelay.java:205: if (repository.markFailed( # 펜싱 없는 옛 경로는 인터페이스에 남아 있다 36: List leaseBatch(int batchSize, Duration leaseDuration, Instant now); # 그 메서드 자신의 javadoc 은 아직 안전을 주장한다 *

Leasing rather than simply selecting is what makes multiple relay instances safe: a record * claimed by one relay is invisible to the others until its lease expires, so the same message is * not published concurrently by two processes. # 폐기를 적은 것은 신세대 쪽 javadoc 이다 *

The token is what a terminal write is checked against. {@link #leaseBatch} returns records * without one, so its callers cannot prove a write belongs to their claim; it remains for * inspection paths and is deprecated for the relay's use. * # messaging 트리 전체의 @Deprecated 매치: 0 # .leaseBatch( 를 부르는 main 코드: 0 # 두 세대가 AMBIGUOUS 에서 남기는 것이 다르다 "UPDATE messaging_outbox SET status = 'AMBIGUOUS', lease_expires_at = NULL, " + "lease_owner = NULL, last_failure_code = ?, attempts = attempts + 1, " + "next_attempt_at = ? ", ---구세대 "UPDATE messaging_outbox SET status = 'AMBIGUOUS', lease_expires_at = NULL, " + "last_failure_code = ?, attempts = attempts + 1 WHERE message_id = ?", # 같은 형태가 다른 곳에도 있었다고 파일서버 마이그레이션이 적는다 12:-- part-way through — the same fenced-lease problem the notification dispatcher had.