Skip to content

JDBC backend: nothing bounds an attempt of the transaction replay, so the retry window has to work around it #915

Description

@maximthomas

Describe the bug

JDBCStorage.write() replays a rolled-back transaction, and bounds the replays by MAX_RETRIES and by a 10 s
wall-clock window. Neither bounds an attempt, and an attempt is not short: the lock wait that precedes a
conflict is charged to the attempt that hit it, and is unbounded on three of the four engines here.

Dialect.lockTimeoutSql already carries the statement for all four engines, but it is only ever issued on the
stamp session — never on a transaction connection:

// opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java
POSTGRES("set local lock_timeout ...", ...),
MYSQL(...),
ORACLE(...),
MICROSOFT("set lock_timeout ...", ...),

So SQL Server, PostgreSQL and Oracle wait indefinitely for a row lock inside an attempt, and only MySQL bounds
it — with innodb_lock_wait_timeout, 50 s by default.

Why it matters

Everything the retry loop had to grow to work around #903 exists only because the attempt is unbounded:

  • the Conflict classification (PROMPT vs AFTER_LOCK_WAIT), and the per-hop most-specific walk that keeps it;
  • MYSQL_LOCK_WAIT_TIMEOUT, read driver-keyed only to tell a late conflict from a prompt one;
  • grantedPastTheWindow — the first replay of a prompt conflict, taken although the window is already spent,
    because no window survives a wait nothing bounds.

Bound the wait inside the attempt and every one of them goes away: each attempt becomes shorter than the window,
RETRY_WINDOW_NANOS governs both classes on its own, and the grant, the enum and the vendor number can be
deleted. The diff gets smaller, not bigger.

It also settles the two shapes the current design still leaves open, both raised in review of #904:

  1. The grant is keyed to attempt==1, which is a proxy. The invariant behind it — "no clock can bound a wait
    that nothing else bounds" — holds on attempt 2 as well. So the JDBC backend: the give-up window of the transaction replay disarms it for the deadlock it was written for #903 shape survives one attempt later: a
    SQL Server write whose attempt 1 conflicts promptly at 2 s (granted) and whose attempt 2 waits 12 s on a row
    lock before being picked as the victim is refused at 14 s < 10 s, by a clock a lock wait spent. Widening the
    grant to every attempt is the wrong answer — it leaves MAX_RETRIES as the only real cap.
  2. A MySQL lock wait timeout provably never replays at the stock innodb_lock_wait_timeout=50: it is
    reported at ~50 s, past the window on the first check, and gets no grant. Master behaved the same way, and
    one bounded wait does beat two — but it means MySQL deployments get no write retry for the conflict class
    they see most. With a lock bound on the attempt, a tuned-down timeout stops being the exception.

What it needs

Not a one-line change:

  • SQL Server converts the bounded wait into error 1222, "Lock request time out period exceeded", SQLState
    HY000. isConflict does not match it today, so issuing SET LOCK_TIMEOUT without classifying 1222 in the
    same change turns a retryable conflict into a hard failure.
  • PostgreSQL reports lock_timeout as 55P03 (lock_not_available), also unmatched today.
  • The bound has to be a configuration property, not a constant: it changes the failure every deployment sees.
  • Each engine needs container coverage of the new failure, since none of it is reachable without a real lock.

Related

Follow-up to #904 (#903), requested in review:
#904 (review)
The last section of #877 covers the same statement from the query-timeout side.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions