Skip to content

fix: align the fake client with the real one and return fetched rows oldest first - #233

Merged
lesnik512 merged 1 commit into
mainfrom
fix/fake-client-parity
Oct 17, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/fake-client-parity

Conversation

@lesnik512

Copy link
Copy Markdown
Member

For 0.15.1, no issue: four places where FakeOutboxClient and TestOutboxBroker behaved differently from the real client. Only one changes the real client: fetch now returns rows oldest first.

1. fetch return order (real client)

OutboxClient.fetch returned the claimed rows in whatever order UPDATE ... RETURNING produced. Postgres gives no order there. The fetch statement picks the oldest rows, but they could come back in any order. The fake always sorted them, so a test like "FIFO within a batch with max_workers=1" passed under TestOutboxBroker and could fail in production.

fetch now sorts the returned rows by (next_attempt_at, id), the same key the statement selects on. The new contract test test_fetch_returns_rows_oldest_first seeds 41 rows across two queues, including an expired lease, with ids running against age. It failed on the real client before the change: the first row back was id 1, while id 40 was the oldest.

Perf: sorting a fetched batch takes 0.9 µs at the default batch size of 10 and 13.5 µs at 100, against a fetch round trip of roughly 0.1–1 ms. just bench-check passes.

2. dlq_payload without a DLQ table

The real client raises RuntimeError when it gets a dlq_payload but has no dlq_table, rather than dropping the audit row. The fake always wrote the audit row.

  • FakeOutboxClient(dlq_enabled=...) is a new keyword argument. It defaults to True, so existing direct uses keep their behaviour.
  • TestOutboxBroker sets it from the broker's dlq_table.
  • The real client and the fake share the error message.
  • A new contract_without_dlq fixture runs the case against both clients.

The subscriber only sends a dlq_payload when the broker has a DLQ table, so this isn't reachable through normal use. It keeps the two clients from drifting.

3. feed() accepted rows Postgres rejects

The fake stored queue names and timer_ids over 255 characters, and headers that JSONB can't hold. Postgres rejects all three at INSERT. feed() now raises ValueError for them.

The batch path checks every row before inserting any of them, so a bad body leaves no partial batch, the same as the real single INSERT.

4. The fake's published metric

The test broker never emitted published{status="error"} when encoding failed, and duration_seconds was always 0.0. It now emits the same tags as OutboxProducer, including exception_type, for single and batch publishes, with a measured duration. A row feed() rejects counts as an error, the way a failed INSERT does in production. The observability page said the test-mode duration was 0.0; that paragraph is updated.

Tests

Every behaviour change has a test that failed first:

  • the return-order contract test failed on the real client;
  • the missing-DLQ contract case failed on the fake;
  • the three feed() cases, the encode-failure metric for single and batch, the measured duration, and the TestOutboxBroker DLQ wiring all failed on the fake.

Verification

  • 704 tests pass in docker at 100% coverage.
  • ruff and ty are clean, and just bench-check passes.

@github-actions

Copy link
Copy Markdown

Benchmark gate

✅ gate passed

scenario msg/s delete/msg WALrec/msg WALB/msg fpi upd del dead_tup
consumer/w1/b10 1415 1.000 6.70 915 242 5000 5000 10000
consumer/w1/b100 1644 1.000 7.09 1143 243 5000 5000 10000
consumer/w2/b10 1747 1.000 6.70 916 242 5000 5000 10000
consumer/w2/b100 2317 1.000 7.09 1143 243 5000 5000 10000
consumer/w4/b10 1892 1.000 6.74 945 242 5000 5000 10000
consumer/w4/b100 2148 1.000 7.09 1143 243 5000 5000 10000
consumer/w1/b100/tfbs100 8632 0.010 6.06 1117 243 5000 5000 10000
consumer/w1/b100/interval1-10 1606 1.000 7.15 1186 265 5000 5000 10000
producer/w1/b100 5291 0.000 3.04 597 1 0 0 0
producer/w1/b100/ppt1 961 0.000 4.04 637 1 0 0 0

Gated (fails the build): delete_calls + tuple counters (upd/del/ins) + the producer's insert_calls, exact; select_calls and other_calls within +2; wal_records within a 10% band; fetch_calls within 2x. msg/s, WAL bytes and total calls are informational (timing/FPI noise).

@lesnik512
lesnik512 merged commit 08da536 into main Oct 17, 2026
13 checks passed
@lesnik512
lesnik512 deleted the fix/fake-client-parity branch October 17, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant