Skip to content

fix: reject a timer_id over 255 characters before the INSERT - #239

Merged
lesnik512 merged 1 commit into
mainfrom
fix/timer-id-length
Oct 17, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/timer-id-length

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Rejects a timer_id over 255 characters with ValueError before the INSERT, the same way queue is already rejected.

Why

timer_id is a String(255) column, but nothing checked its length before the INSERT. What happened next depended on the SQLAlchemy version:

  • SQLAlchemy 2.0.0: binds the value as $1::VARCHAR(255). Postgres truncates an explicit cast instead of raising, so the row landed with its timer_id cut to 255 characters. Two different timers sharing their first 255 characters then deduplicated against each other, and the second publish returned None. I confirmed the cast by compiling the INSERT with 2.0.0. The floors 3.11 and 3.12 jobs on refactor: move the outbox insert and its NOTIFY into the client #237 showed the truncation.
  • Newer SQLAlchemy: the bind has no length, so Postgres raised and the caller got an opaque DBAPIError.

The change

_validate_publish_args now checks timer_id length next to queue. It runs for broker.publish, publisher.publish and OutboxResponse, which raises when it's constructed, like its other checks. The message matches the one the fake client already raised: timer_id must be at most 255 characters (got N).

Tests

Written first and failing:

  • test_publish_command_rejects_oversize_timer_id and test_outbox_response_rejects_oversize_timer_id;
  • test_publish_rejects_an_oversize_timer_id_without_storing_a_row, on real Postgres. It failed on current SQLAlchemy with a DBAPIError. On 2.0.0 it would have stored a truncated row; the floors jobs run it there.

test_publish_command_accepts_timer_id_at_the_column_limit pins the boundary.

Verification

  • 741 tests pass in docker at 100% coverage.
  • ruff and ty are clean.

SQLAlchemy 2.0.x binds String(255) values as $1::VARCHAR(255), and
Postgres truncates an explicit cast instead of rejecting it, so an
over-long timer_id was stored truncated and could dedup against a
different timer sharing its first 255 characters. Newer SQLAlchemy
raised an opaque DBAPIError instead. Validate it with the queue.
@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 1442 1.000 6.69 913 242 5000 5000 10000
consumer/w1/b100 1579 1.000 7.09 1143 243 5000 5000 10000
consumer/w2/b10 1795 1.000 6.70 915 242 5000 5000 10000
consumer/w2/b100 2344 1.000 7.09 1143 243 5000 5000 10000
consumer/w4/b10 1887 1.000 6.75 947 242 5000 5000 10000
consumer/w4/b100 2251 1.000 7.09 1142 243 5000 5000 10000
consumer/w1/b100/tfbs100 8915 0.010 6.06 1117 243 5000 5000 10000
consumer/w1/b100/interval1-10 1509 1.000 7.15 1186 265 5000 5000 10000
producer/w1/b100 5851 0.000 3.04 597 1 0 0 0
producer/w1/b100/ppt1 1231 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 358c4fd into main Oct 17, 2026
13 checks passed
@lesnik512
lesnik512 deleted the fix/timer-id-length branch October 17, 2026 17:59
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