HDDS-14768. Fix lock leak during snapshot cache cleanup and handle eviction race appropriately.#9869
HDDS-14768. Fix lock leak during snapshot cache cleanup and handle eviction race appropriately.#9869SaketaChalamchala wants to merge 3 commits intoapache:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes SnapshotCache eviction/cleanup edge cases that could previously throw during cleanup, leak the snapshot DB write lock, or leave stale eviction entries behind—improving correctness and reliability of snapshot purge / checkpoint coordination in Ozone Manager.
Changes:
- Remove snapshot IDs from the pending eviction queue on invalidate, and tolerate stale eviction entries during cleanup.
- Ensure SnapshotCache write lock is released if cleanup throws (including unchecked exceptions).
- Adjust cleanup behavior so snapshot close failures are logged and retried later, with added unit tests covering these races/failures.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotCache.java | Updates eviction bookkeeping, cleanup behavior on stale/missing entries and close failures, and hardens lock() to release locks on exceptions. |
| hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotCache.java | Adds unit tests for stale-eviction cleanup, close-failure retry behavior, and write-lock release when cleanup throws. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
...ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotCache.java
Outdated
Show resolved
Hide resolved
...ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotCache.java
Outdated
Show resolved
Hide resolved
| LOG.warn("SnapshotId '" + k + "' does not exist in cache. The RocksDB " + | ||
| "instance of the Snapshot may not be closed properly."); |
There was a problem hiding this comment.
The new warning for stale eviction keys (v == null) says the RocksDB instance “may not be closed properly”, but this condition can also occur in the expected invalidate + late close race (snapshot was closed and removed from dbMap, and the callback re-queued the UUID). Consider rewording the log to reflect that this can be a benign stale-queue entry, and include guidance only if it indicates a real leak signal.
| LOG.warn("SnapshotId '" + k + "' does not exist in cache. The RocksDB " + | |
| "instance of the Snapshot may not be closed properly."); | |
| LOG.warn("SnapshotId '{}' not found in cache during cleanup. " | |
| + "This can happen if the snapshot was already closed and " | |
| + "removed from the cache, leaving a stale entry in the eviction " | |
| + "queue. If this message appears frequently for the same " | |
| + "snapshot or the cache size keeps growing, it may indicate that " | |
| + "a RocksDB instance was not closed properly."); |
| } else if (v.getTotalRefCount() > 0) { | ||
| LOG.debug("SnapshotId {} is still being referenced ({}), skipping its clean up.", k, v.getTotalRefCount()); | ||
| return v; | ||
| result = v; | ||
| } else { | ||
| LOG.debug("Closing SnapshotId {}. It is not being referenced anymore.", k); | ||
| // Close the instance, which also closes its DB handle. | ||
| try { | ||
| v.get().close(); | ||
| } catch (IOException ex) { | ||
| throw new IllegalStateException("Error while closing snapshot DB.", ex); | ||
| LOG.error("Error while closing snapshot DB.", ex); | ||
| return v; | ||
| } | ||
| omMetrics.decNumSnapshotCacheSize(); | ||
| return null; | ||
| } | ||
| pendingEvictionQueue.remove(k); | ||
| return result; | ||
| }); |
There was a problem hiding this comment.
In cleanup(evictionKey, ...), the pending eviction key is removed even when the snapshot is still referenced (totalRefCount > 0). If another thread decrements the refcount to 0 and the callback re-adds the key concurrently, this removal can win and drop the key permanently, leaving an unreferenced snapshot in dbMap that will never be retried for cleanup. Consider either not removing the key when refcount > 0, or removing it conditionally and re-adding if the refcount becomes 0 after the decision (or otherwise synchronizing queue updates).
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotCache.java
Outdated
Show resolved
Hide resolved
| private UncheckedAutoCloseableSupplier<OMLockDetails> lock(Supplier<OMLockDetails> lockFunction, | ||
| Supplier<OMLockDetails> unlockFunction, Supplier<Boolean> cleanupFunction) { | ||
| Supplier<OMLockDetails> emptyLockFunction = () -> getEmptyOmLockDetails(lockFunction.get()); | ||
| Supplier<OMLockDetails> emptyUnlockFunction = () -> getEmptyOmLockDetails(unlockFunction.get()); | ||
|
|
||
| AtomicReference<OMLockDetails> lockDetails = new AtomicReference<>(emptyLockFunction.get()); | ||
| if (lockDetails.get().isLockAcquired()) { | ||
| if (!cleanupFunction.get()) { | ||
| try { | ||
| if (!cleanupFunction.get()) { | ||
| lockDetails.set(emptyUnlockFunction.get()); | ||
| } | ||
| } catch (Throwable t) { | ||
| lockDetails.set(emptyUnlockFunction.get()); | ||
| throw t; | ||
| } |
There was a problem hiding this comment.
SnapshotCache.lock(...) can return a supplier whose OMLockDetails reports lock not acquired when cleanupFunction returns false (eg, cache not drained). The public lock() / lock(UUID) Javadocs currently describe the lock as ensuring thread-safety, but don’t mention that callers must check isLockAcquired() (some call sites use try-with-resources without checking). Consider either documenting this contract explicitly here or throwing when cleanup can’t satisfy the precondition so callers can’t proceed without the intended lock.
There was a problem hiding this comment.
Updated the patch to throw an exception when cleanup returns false so that callers do not proceed without the lock.
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotCache.java
Outdated
Show resolved
Hide resolved
| Field f = SnapshotCache.class.getDeclaredField("pendingEvictionQueue"); | ||
| f.setAccessible(true); | ||
| return (Set<UUID>) f.get(cache); |
There was a problem hiding this comment.
Using reflection to access private field in SnapshotCache is unstable. Why not just expose it via a getPendingEvictionQueue() method in SnapshotCache that has @VisibleForTesting annotation?
| * @param key SnapshotId | ||
| */ | ||
| public void invalidate(UUID key) { | ||
| dbMap.compute(key, (k, v) -> { |
There was a problem hiding this comment.
SnapshotCache.invalidate() is called during OMSnapshotPurgeResponse.
| } | ||
| omMetrics.decNumSnapshotCacheSize(); | ||
| } | ||
| pendingEvictionQueue.remove(k); |
There was a problem hiding this comment.
pendingEvictionQueue may have the entry k, if snapshot purge response is happening after, and all references of the snapshot is decremented (releasing the SnapshotCache lock of the key), but before the periodic cleanup thread kicks in.
There was a problem hiding this comment.
note: this case should not happen during checkpointing, because checkpoints holds a write lock of the snapshot cache, and once released, it invokes cleanup() immediately.
|
|
||
| AtomicReference<OMLockDetails> lockDetails = new AtomicReference<>(emptyLockFunction.get()); | ||
| if (lockDetails.get().isLockAcquired()) { | ||
| if (!cleanupFunction.get()) { |
There was a problem hiding this comment.
the lock is released by calling emptyUnlockFunction.get(), if cleanup operation was not successful or if it throws a Throwable.
|
Thanks for the review @sadanand48 , @smengcl and @jojochuang I addressed your comments and relevant copilot suggestions as well Additional updates:
|
What changes were proposed in this pull request?
Currently,
IllegalStateExceptionwhen it finds stale entries inpendingEvictionQueuefor snapshots that have already been removed from dbMapEx., say SnapshotPurge invalidates the entry right before the last thread with a reference to the snapshot just closes adding the snapshotID back to the evictionQueue
invalidateremoves snapshot entry from dbMap but does not remove it frompendingEvictionQueueif it exists.dbMapunlesssome other thread explicitly invalidates it or references it again. This means
SnapshotCache.lock()during this time cannot hold the write lock becauselock()expects the cache to be drained.cleanup(true)throws an exception write lock is not released inSnapshotCache.lock()Proposed solution:
SnapshotCache.lock()What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-14768
(Please replace this section with the link to the Apache JIRA)
How was this patch tested?
Unit tests.