humzahkiani opened a new pull request, #17341:
URL: https://github.com/apache/iceberg/pull/17341

   ## Problem
   
   `CachingCatalog` hands out `Table` instances pinned to the snapshot that was
   current when the table was loaded, and a commit performed through such a 
table
   is invisible to the cache. The cached entry is therefore never updated on
   commit, which allows a stale snapshot to be served after a commit:
   
   1. A writer loads a table (snapshot N) and begins a long-running write.
   2. The cache entry expires mid-write (the default TTL is 30s).
   3. Another thread (for example an OpenLineage Spark listener) calls 
`loadTable`,
      reloads snapshot N (pre-commit), and re-caches it with a fresh TTL.
   4. The writer commits snapshot N+1 through its own reference. Nothing
      invalidates the cache.
   5. A subsequent `loadTable` returns the re-cached snapshot N, a stale read.
   
   This is the race reported in #10493. It caused a production stale-read 
incident
   for us after an OpenLineage listener began issuing background `loadTable` 
calls
   during writes.
   
   ## Fix
   
   Extend the invalidate-on-commit behavior that already exists for
   `buildTable(...).replaceTransaction()` and `createOrReplaceTransaction()` to
   tables returned by `loadTable`. On a cache miss, the loaded `BaseTable` is
   rebuilt with a `TableOperations` wrapper that invalidates the table's cache
   entry (and its metadata tables) after a successful `commit`. All write paths,
   including direct operations (`newAppend()`, `newOverwrite()`, and so on),
   `newTransaction()`, and metadata-table operations that share the origin 
table's
   ops, funnel through `TableOperations.commit`, so a single interception point
   covers them.
   
   The wrapper keeps the returned object a `BaseTable`, so `instanceof 
BaseTable`
   and `(BaseTable)` consumers across core, REST, and the engines are 
unaffected.
   Only tables loaded on a cache miss are wrapped; tables served directly from 
the
   cache after `createTable` keep their current behavior, so no existing 
behavior
   or tests change. The same wrapper could be applied to the create path in a
   follow-up if desired.
   
   ### Race safety
   
   The load path uses Caffeine's atomic `Cache.get(key, loader)`, where the 
value
   is loaded and stored under the per-key compute lock, and the wrapper 
invalidates
   only after the commit is durable. Across all interleavings, a concurrent 
reload
   either has its stored entry invalidated by the commit, or (if it runs after 
the
   invalidation) reads the post-commit snapshot. A `CommitStateUnknownException`
   also triggers invalidation, since the commit may have succeeded.
   
   ## Tests
   
   Adds `testLoadTableObservesCommitAfterConcurrentReloadDuringWrite`, a
   deterministic regression test that reproduces the interleaving above (expired
   entry, concurrent reload, then commit) and asserts the post-commit 
`loadTable`
   observes the committed snapshot. It fails without the fix and passes with it.
   All existing `TestCachingCatalog` tests pass unchanged.
   
   Closes #10493
   


-- 
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