zeroshade opened a new pull request, #2036:
URL: https://github.com/apache/iceberg-go/pull/2036

   ## Rationale for the change
   
   Concurrent `CreateTable`/`CreateView` against a SQLite catalog can fail with 
`SQLITE_BUSY` instead of the catalog sentinel (`ErrTableAlreadyExists` / 
`ErrViewAlreadyExists`).
   
   This surfaced as a flaky 
`TestSqlCatalog/TestConcurrentTableViewCollisionReturnsCatalogSentinel`:
   
   ```
   Error: Target error should be in err chain:
     expected: "view already exists"
     in chain: "failed to create table: database is locked (5) (SQLITE_BUSY)"
   ```
   
   It failed on `main` ([run 
35642756763](https://github.com/apache/iceberg-go/actions/runs/35642756763)) 
and on an unrelated dependency bump ([run 
35499928776](https://github.com/apache/iceberg-go/actions/runs/35499928776)), 
so it was blocking other PRs.
   
   ## Root cause
   
   SQLite permits one writer at a time, and the write transaction in these 
paths reads before it writes — `namespaceExistsInTx`, then 
`checkIdentifierAvailable`, then the `INSERT`. A lock conflict on that 
*upgrade* is returned immediately and **does not consult the busy handler**, so 
a `busy_timeout` cannot absorb it. I verified that directly: with 
`_pragma=busy_timeout(3000)` the writer still returns `SQLITE_BUSY`, having 
waited the full timeout.
   
   Only retrying the whole transaction helps, which `retrySerializableWriteTx` 
already does — the budget was simply too small. 3 attempts with 10ms/20ms 
backoff is 30ms of waiting, and a loaded runner holds the write lock longer 
than that. Once the budget ran out before the winning transaction committed, 
the losing caller got the driver's lock error. The post-hoc 
`checkIdentifierAvailableInCatalog` fallback also can't recover it, because at 
that point the winner still hasn't committed and there is no visible collision 
yet.
   
   ## Changes
   
   1. `serializableWriteMaxAttempts` 3 -> 6, giving roughly 310ms of retry 
budget. This costs nothing when there is no contention, since the delays only 
apply between failed attempts.
   2. A regression test that makes the contention deterministic by holding the 
database write lock for longer than the old budget.
   
   ## Are these changes tested?
   
   Yes.
   
   - The new `TestConcurrentTableViewCollisionUnderLockContention` fails 
**10/10** before the change with exactly the CI error, and passes **12/12** 
after. Reverting only the constant makes it fail again, so it pins the budget 
rather than restating the implementation.
   - The previously flaky 
`TestConcurrentTableViewCollisionReturnsCatalogSentinel` passes 40/40 under 
`-race`.
   - `go test -race ./catalog/sql/...` is green.
   
   ## Are there any user-facing changes?
   
   Yes, and an improvement: a concurrent writer that would previously fail with 
a transient `SQLITE_BUSY` now waits for the competing transaction and reports 
the real outcome. Worst-case added latency under contention is ~310ms; there is 
no change without contention.
   
   Note the same budget also covers serialization failures on other dialects 
(Postgres `40001`/`40P01`, MySQL 1205/1213), which benefit from the wider 
budget for the same reason.
   
   ## AI Disclosure
   - Model: Claude Opus 4.8
   - Platform/Tool: omp
   - Human Oversight: fully reviewed
   - Prompt Summary: Diagnose and fix a flaky SQLite catalog test failing on 
main with SQLITE_BUSY
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to