You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 5f14966
Browse filesBrowse the repository at this point in the historyBrowse files
- Count a batch retry's backoff from when delivery ended, as process() already did, so slow
failures (timeouts) do not eat into it.
- Postgres 004: backfill in its own changeset so ADD COLUMN holds ACCESS EXCLUSIVE only briefly.
- MySQL 004: one changeset per DDL step with preconditions, so a migration that stopped half-way
(MySQL commits each DDL) resumes; backfill FAILED rows too so a manual requeue is claimable.
- RetryPolicy: compute in nanoseconds, cap maxBackoff at 365 days, require a finite multiplier;
same checks on the Spring properties. OutboxEntry constructor is @jvmoverloads for Java callers.
- Docs: all migrations up to 004 before updated workers, clock-skew note, manual requeue recipe.
- Tests: migration tests claim through the route-aware claim (backfill, 1.x insert default, FAILED
requeue), batch backoff timing, sub-ms backoff, Spring property binding and validation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -222,7 +222,7 @@ If you use another migration tool, apply every SQL migration for your database,
222
222
-**PostgreSQL:**[001](okapi-postgres/src/main/resources/com/softwaremill/okapi/db/postgres/001__create_okapi_outbox_table.sql) table and initial indexes, [002](okapi-postgres/src/main/resources/com/softwaremill/okapi/db/postgres/002__route_aware_claim_index.sql) route-aware claim index, [003](okapi-postgres/src/main/resources/com/softwaremill/okapi/db/postgres/003__timestamptz_columns.sql)`TIMESTAMPTZ` timestamp columns, [004](okapi-postgres/src/main/resources/com/softwaremill/okapi/db/postgres/004__retry_backoff.sql) retry backoff column and claim index (its second changeset must run outside a transaction)
223
223
-**MySQL:**[001](okapi-mysql/src/main/resources/com/softwaremill/okapi/db/mysql/001__create_okapi_outbox_table.sql) table and initial indexes, [002](okapi-mysql/src/main/resources/com/softwaremill/okapi/db/mysql/002__route_aware_claim_index.sql) route-aware claim index, [003](okapi-mysql/src/main/resources/com/softwaremill/okapi/db/mysql/003__datetime_columns.sql)`DATETIME(6)` timestamp columns, [004](okapi-mysql/src/main/resources/com/softwaremill/okapi/db/mysql/004__retry_backoff.sql) retry backoff column and claim index
224
224
225
-
For an existing installation, apply `002` before starting updated workers. MySQL claims explicitly select the new index and fail if it is missing. PostgreSQL's `002`uses `CREATE INDEX CONCURRENTLY`, so run it outside a transaction when using another migration tool. The bundled Liquibase changelog handles this automatically. `CREATE INDEX CONCURRENTLY` waits for every transaction in the database that started before it, not only those on `okapi_outbox`, so long-running transactions (including old workers delivering a claimed batch) delay it, and with Liquibase they delay application startup while the changelog lock is held. Run it when no long transactions are open, or apply it ahead of the rollout.
225
+
For an existing installation, apply every new migration (up to `004`) before starting updated workers. MySQL claims explicitly select the claim index from `004`and fail if it is missing. PostgreSQL's `002`and the index changeset of `004` use `CREATE INDEX CONCURRENTLY`, so run them outside a transaction when using another migration tool. The bundled Liquibase changelog handles this automatically. `CREATE INDEX CONCURRENTLY` waits for every transaction in the database that started before it, not only those on `okapi_outbox`, so long-running transactions (including old workers delivering a claimed batch) delay it, and with Liquibase they delay application startup while the changelog lock is held. Run it when no long transactions are open, or apply it ahead of the rollout.
226
226
227
227
PostgreSQL's `003` converts the timestamp columns from `TIMESTAMP` (the okapi JVM's local wall-clock time) to `TIMESTAMPTZ`. It reads existing values in the session `TimeZone`; pgjdbc sets that to the JVM default zone, so the bundled Liquibase changelog converts them correctly when it runs inside the application. When applying it by hand, run it inside a transaction (`psql -1`) after `SET TIME ZONE '<zone your okapi JVMs run in>'` in the same session. It takes an `ACCESS EXCLUSIVE` lock: it first waits (up to 30 s, then fails and retries on the next start) for open transactions on `okapi_outbox`, queuing every other statement on the table meanwhile, and then, unless the session zone is UTC (PostgreSQL 12+), rewrites the table while reads and writes wait. 1.x instances keep working after it, apart from one failed poll per pooled connection. See UPGRADING.md for when the in-app conversion uses the wrong zone. See [UPGRADING.md](UPGRADING.md#from-1x-to-200).
Copy file name to clipboardExpand all lines: UPGRADING.md
+15-4Lines changed: 15 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -79,19 +79,30 @@ rows and replaces 002's claim index with one on `(delivery_type, next_attempt_at
79
79
80
80
-**PostgreSQL:** the column is `NOT NULL DEFAULT CURRENT_TIMESTAMP` (added without a table rewrite
81
81
on PostgreSQL 11+), so rows that 1.x instances insert during a rolling deploy are claimable at once.
82
-
The index is built in a second changeset with `CREATE INDEX CONCURRENTLY`, which must run outside a
82
+
The `ADD COLUMN` holds an `ACCESS EXCLUSIVE` lock only for itself (waiting at most 30 s for it); the
83
+
backfill of `PENDING` rows runs in a separate changeset with row locks only. The index is built in a second changeset with `CREATE INDEX CONCURRENTLY`, which must run outside a
83
84
transaction when you apply the script by hand, and which waits for every transaction that started
84
85
before it.
85
-
-**MySQL:** the column is nullable (an instant change on 8.0.12+) and only `PENDING` rows are
86
-
backfilled. 1.x instances are already stopped for 003, so nothing writes rows without it.
86
+
-**MySQL:** the column is nullable (an instant change on 8.0.12+); `PENDING` and `FAILED` rows are
87
+
backfilled, `DELIVERED` rows stay `NULL`. 1.x instances are already stopped for 003, so nothing
88
+
writes rows without it. Each step is a changeset of its own with a precondition, so a migration that
89
+
stopped half-way (e.g. on its 30 s `lock_wait_timeout`) resumes where it stopped.
87
90
-**Behaviour:** retriable failures now wait 10s, 20s, 40s, ... (capped at 10m, 20% jitter) before
88
91
the next attempt, so with `max-retries` 5 an entry becomes `FAILED` after about five minutes rather
89
92
than six seconds. Set `okapi.processor.initial-backoff`, `backoff-multiplier`, `max-backoff`,
90
93
`backoff-jitter` or `max-retries` to change that. 1.x workers still running during a rolling deploy
91
94
ignore the backoff for the rows they claim.
95
+
-**Clocks:** a new entry is due at its `createdAt`, taken from the publishing instance's clock, and
96
+
claimed once the processing instance's clock reaches it, so a publisher whose clock runs ahead of
97
+
the processors' delays its messages by the difference. Keep instance clocks NTP-synchronised.
98
+
-**Requeuing a `FAILED` row by hand** now needs a due time as well:
99
+
`UPDATE okapi_outbox SET status = 'PENDING', retries = 0, next_attempt_at = <now in UTC> WHERE ...`
100
+
(`CURRENT_TIMESTAMP` on PostgreSQL, `UTC_TIMESTAMP(6)` on MySQL). A row whose `next_attempt_at`
101
+
lies in the future is not claimed until then.
92
102
-**Code:** a custom `RouteAwareOutboxStore` must implement `claimPending(deliveryTypes, limit, now)`,
93
103
return only rows with `next_attempt_at <= now`, and write `nextAttemptAt` in `persist` and the update
94
-
methods. Tests that drive retries tick by tick need a zero backoff:
104
+
methods. `OutboxEntry` keeps a Java constructor without `nextAttemptAt` (`@JvmOverloads`). Tests
105
+
that drive retries tick by tick need a zero backoff:
Copy file name to clipboardExpand all lines: okapi-integration-tests/src/test/kotlin/com/softwaremill/okapi/test/store/PostgresTimestamptzMigrationTest.kt
+6-2Lines changed: 6 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -119,7 +119,9 @@ class PostgresTimestamptzMigrationTest : FunSpec({
119
119
}
120
120
}
121
121
val (jdbc, store) = store(url)
122
-
val entry = jdbc.withTransaction { store.claimPending(10) }.single()
122
+
// The route-aware claim, which only returns rows whose next_attempt_at (backfilled by 004) is due.
123
+
val entry = jdbc.withTransaction { store.claimPending(setOf("http"), 10, Instant.now()) }.single()
124
+
entry.nextAttemptAt shouldBe createdAt
123
125
entry.createdAt shouldBe createdAt
124
126
entry.updatedAt shouldBe createdAt
125
127
entry.lastAttempt shouldBe lastAttempt
@@ -158,6 +160,8 @@ class PostgresTimestamptzMigrationTest : FunSpec({
0 commit comments