This is an automated email from the ASF dual-hosted git repository.
smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new bc21296f297 HDDS-16118. Release snapshot DB handles before
KeyDeletingService submits OM requests (#10987)
bc21296f297 is described below
commit bc21296f297dddc3207d052149ba14f9a44ba745
Author: Siyao Meng <[email protected]>
AuthorDate: Wed Aug 12 16:05:42 2026 -0700
HDDS-16118. Release snapshot DB handles before KeyDeletingService submits
OM requests (#10987)
---
.../ozone/om/service/KeyDeletingService.java | 15 +-
.../om/snapshot/filter/ReclaimableFilter.java | 15 +-
.../ozone/om/service/TestKeyDeletingService.java | 245 +++++++++++++++++++++
3 files changed, 271 insertions(+), 4 deletions(-)
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyDeletingService.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyDeletingService.java
index e999a9e3ad8..8518a02b5cf 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyDeletingService.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyDeletingService.java
@@ -560,7 +560,8 @@ private
OzoneManagerProtocolProtos.SetSnapshotPropertyRequest getSetSnapshotRequ
* @param keyManager KeyManager of the underlying store.
*/
private void processDeletedKeysForStore(SnapshotInfo currentSnapshotInfo,
KeyManager keyManager,
- int remainNum) throws IOException, InterruptedException {
+ UncheckedAutoCloseableSupplier<OmSnapshot> currentSnapshot, int
remainNum)
+ throws IOException, InterruptedException {
String volume = null, bucket = null, snapshotTableKey = null;
if (currentSnapshotInfo != null) {
volume = currentSnapshotInfo.getVolumeName();
@@ -603,6 +604,16 @@ private void processDeletedKeysForStore(SnapshotInfo
currentSnapshotInfo, KeyMan
? keyManager.getPendingDeletionKeys(reclaimableKeyFilter,
remainNum)
: keyManager.getPendingDeletionKeys(volume, bucket, null,
reclaimableKeyFilter, remainNum);
Map<String, PurgedKey> purgedKeys =
pendingKeysDeletion.getPurgedKeys();
+
+ // Keep the snapshot GC locks until the OM requests below complete,
but release all snapshot DB read locks
+ // first. Otherwise the synchronous Ratis submission can wait for
the double buffer while the double buffer
+ // waits for a colliding snapshot DB write lock.
+ reclaimableKeyFilter.closeSnapshotDbHandles();
+ renameEntryFilter.closeSnapshotDbHandles();
+ if (currentSnapshot != null) {
+ currentSnapshot.close();
+ }
+
//submit purge requests if there are renamed entries to be purged or
keys to be purged.
if (!renamedTableEntries.isEmpty() || purgedKeys != null &&
!purgedKeys.isEmpty()) {
// Validating if the previous snapshot is still the same before
purging the blocks.
@@ -702,7 +713,7 @@ public BackgroundTaskResult call() {
snapInfo.getName())) {
KeyManager keyManager = snapInfo == null ?
getOzoneManager().getKeyManager()
: omSnapshot.get().getKeyManager();
- processDeletedKeysForStore(snapInfo, keyManager, remainNum);
+ processDeletedKeysForStore(snapInfo, keyManager, omSnapshot,
remainNum);
}
} catch (IOException e) {
LOG.error("Error while running delete files background task for
store {}. Will retry at next run.",
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/filter/ReclaimableFilter.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/filter/ReclaimableFilter.java
index 76d2ddb4b8d..6d5e64b23d7 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/filter/ReclaimableFilter.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/filter/ReclaimableFilter.java
@@ -216,10 +216,21 @@ public void close() throws IOException {
this.cleanup();
}
- private void cleanup() {
- this.snapshotIdLocks.releaseLock();
+ /**
+ * Closes snapshot DB handles while retaining the snapshot GC locks.
+ *
+ * The GC locks must remain held until the corresponding OM request is
+ * applied, but a synchronous request must not retain snapshot DB read locks
+ * since the double buffer may need a snapshot DB write lock to flush it.
+ */
+ public void closeSnapshotDbHandles() {
IOUtils.close(LOG, previousOmSnapshots);
previousOmSnapshots.clear();
+ }
+
+ private void cleanup() {
+ this.snapshotIdLocks.releaseLock();
+ closeSnapshotDbHandles();
previousSnapshotInfos.clear();
lockedSnapshotIds.clear();
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyDeletingService.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyDeletingService.java
index 4c2efd47668..411cd65bebc 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyDeletingService.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyDeletingService.java
@@ -20,6 +20,7 @@
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL;
import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.THREE;
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_BLOCK_DELETING_SERVICE_INTERVAL;
+import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_MANAGER_STRIPED_LOCK_SIZE_PREFIX;
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SNAPSHOT_DELETING_SERVICE_INTERVAL;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_DIR_DELETING_SERVICE_INTERVAL;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_DEEP_CLEANING_ENABLED;
@@ -30,6 +31,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -45,6 +47,7 @@
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.ServiceException;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -56,8 +59,14 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -69,6 +78,7 @@
import org.apache.hadoop.hdds.client.StandaloneReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.conf.StorageUnit;
+import org.apache.hadoop.hdds.scm.HddsWhiteboxTestUtils;
import org.apache.hadoop.hdds.scm.container.common.helpers.ExcludeList;
import org.apache.hadoop.hdds.server.ServerUtils;
import org.apache.hadoop.hdds.utils.db.DBConfigFromFile;
@@ -103,6 +113,7 @@
import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import org.apache.hadoop.ozone.om.ratis.OzoneManagerDoubleBuffer;
import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
import org.apache.hadoop.ozone.om.snapshot.filter.ReclaimableKeyFilter;
@@ -110,6 +121,7 @@
import org.apache.ozone.test.GenericTestUtils;
import org.apache.ozone.test.OzoneTestBase;
import org.apache.ozone.test.tag.Flaky;
+import org.apache.ratis.protocol.ClientId;
import org.apache.ratis.util.ExitUtils;
import org.apache.ratis.util.function.UncheckedAutoCloseableSupplier;
import org.junit.jupiter.api.AfterAll;
@@ -849,6 +861,239 @@ void testSnapshotExclusiveSize() throws Exception {
}
}
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class SnapshotDbHandleLifecycle {
+
+ @BeforeAll
+ void setup(@TempDir File testDir) throws Exception {
+ scmBlockTestingClient = new ScmBlockLocationTestingClient(null, null, 0);
+ createConfig(testDir);
+ createSubject();
+ keyDeletingService.shutdown();
+ }
+
+ @AfterAll
+ void cleanup() {
+ if (om.stop()) {
+ om.join();
+ }
+ }
+
+ @Test
+ @DisplayName("KeyDeletingService should close the snapshot DB handle
before submitting an OM request")
+ void testSnapshotDbHandleClosedBeforeSubmit() throws Exception {
+ String volumeName = getTestName();
+ String bucketName = uniqueObjectName("bucket");
+ String snapshotName = uniqueObjectName("snap");
+ createVolumeAndBucket(volumeName, bucketName, false);
+ writeClient.createSnapshot(volumeName, bucketName, snapshotName);
+ om.awaitDoubleBufferFlush();
+
+ String snapshotKey = SnapshotInfo.getTableKey(volumeName, bucketName,
snapshotName);
+ SnapshotInfo snapshotInfo =
metadataManager.getSnapshotInfoTable().get(snapshotKey);
+ GenericTestUtils.waitFor(() -> {
+ try {
+ return
OmSnapshotManager.areSnapshotChangesFlushedToDB(metadataManager, snapshotInfo);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }, 100, 10000);
+ snapshotInfo.setDeepCleanedDeletedDir(true);
+ metadataManager.getSnapshotInfoTable().put(snapshotKey, snapshotInfo);
+
+ OzoneManager ozoneManager = Mockito.spy(om);
+ OmSnapshotManager omSnapshotManager =
Mockito.spy(om.getOmSnapshotManager());
+ when(ozoneManager.getOmSnapshotManager()).thenReturn(omSnapshotManager);
+
+ AtomicBoolean snapshotDbHandleClosed = new AtomicBoolean();
+ doAnswer(invocation -> {
+ UncheckedAutoCloseableSupplier<OmSnapshot> delegate =
+ (UncheckedAutoCloseableSupplier<OmSnapshot>)
invocation.callRealMethod();
+ return new UncheckedAutoCloseableSupplier<OmSnapshot>() {
+ @Override
+ public OmSnapshot get() {
+ return delegate.get();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ snapshotDbHandleClosed.set(true);
+ }
+ };
+ }).when(omSnapshotManager).getActiveSnapshot(volumeName, bucketName,
snapshotName);
+
+ AtomicBoolean requestSubmitted = new AtomicBoolean();
+ KeyDeletingService service = new KeyDeletingService(ozoneManager,
scmBlockTestingClient, 10000,
+ 100000, conf, 1, true) {
+ @Override
+ protected OzoneManagerProtocolProtos.OMResponse submitRequest(
+ OzoneManagerProtocolProtos.OMRequest omRequest) {
+ assertTrue(snapshotDbHandleClosed.get(), "Snapshot DB handle must be
closed before submitting to Ratis");
+ requestSubmitted.set(true);
+ return OzoneManagerProtocolProtos.OMResponse.newBuilder()
+ .setCmdType(omRequest.getCmdType())
+ .setStatus(OzoneManagerProtocolProtos.Status.OK)
+ .setSuccess(true)
+ .build();
+ }
+ };
+ service.shutdown();
+
+ service.new KeyDeletingTask(snapshotInfo.getSnapshotId()).call();
+
+ assertTrue(requestSubmitted.get());
+ }
+ }
+
+ /**
+ * Regression test for HDDS-16118: {@link KeyDeletingService} must not hold
a snapshot DB read lock
+ * while it submits a synchronous OM request. If it does, a snapshot purge
being applied by the
+ * OMDoubleBuffer flush thread blocks on the colliding snapshot DB write
lock, never releases its
+ * double-buffer permit, and the deep-clean submission then blocks forever
waiting for that permit.
+ *
+ * <p>The scenario is forced deterministically by shrinking two knobs: a
single double-buffer permit
+ * ({@code ozone.om.unflushed.transaction.max.count = 1}) and a single
snapshot DB lock stripe
+ * ({@code ozone.om.lock.stripes.snapshot_db_lock = 1}), so any snapshot
read lock collides with any
+ * snapshot write lock (the "different UUIDs, same stripe" case from the
fix).
+ */
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class SnapshotDbHandlePermitDeadlock {
+
+ @BeforeAll
+ void setup(@TempDir File testDir) throws Exception {
+ scmBlockTestingClient = new ScmBlockLocationTestingClient(null, null, 0);
+ createConfig(testDir);
+ // A single double-buffer permit: one in-flight transaction exhausts the
semaphore, so a stalled
+ // flush immediately blocks the next transaction application.
+ conf.setInt(OMConfigKeys.OZONE_OM_UNFLUSHED_TRANSACTION_MAX_COUNT, 1);
+ // A single snapshot DB lock stripe: a read lock on one snapshot and a
write lock on another map
+ // to the same lock, guaranteeing the collision without hand-picking
colliding UUIDs.
+ conf.setInt(OZONE_MANAGER_STRIPED_LOCK_SIZE_PREFIX + "snapshot_db_lock",
1);
+ // Keep background deletion services out of the way; the test drives the
flow by hand.
+ conf.setTimeDuration(OZONE_BLOCK_DELETING_SERVICE_INTERVAL, 1,
TimeUnit.HOURS);
+ conf.setTimeDuration(OZONE_SNAPSHOT_DELETING_SERVICE_INTERVAL, 1,
TimeUnit.HOURS);
+ conf.setTimeDuration(OZONE_DIR_DELETING_SERVICE_INTERVAL, 1,
TimeUnit.HOURS);
+ createSubject();
+ keyDeletingService.shutdown();
+ }
+
+ @AfterAll
+ void cleanup() {
+ if (om.stop()) {
+ om.join();
+ }
+ }
+
+ @Test
+ @DisplayName("KeyDeletingService must not deadlock while a snapshot purge
holds the double-buffer permit")
+ void testNoPermitExhaustionDeadlock() throws Exception {
+ String volumeName = getTestName();
+ String bucketName = uniqueObjectName("bucket");
+ createVolumeAndBucket(volumeName, bucketName, false);
+
+ // deepCleanSnapshot: KeyDeletingService deep-cleans it and holds its
SNAPSHOT_DB read lock while
+ // it submits. purgeSnapshot: purged by the flush thread, which needs
the colliding write lock.
+ String deepCleanSnapshotName = uniqueObjectName("snap");
+ writeClient.createSnapshot(volumeName, bucketName,
deepCleanSnapshotName);
+ String purgeSnapshotName = uniqueObjectName("snap");
+ writeClient.createSnapshot(volumeName, bucketName, purgeSnapshotName);
+ om.awaitDoubleBufferFlush();
+
+ String deepCleanKey = SnapshotInfo.getTableKey(volumeName, bucketName,
deepCleanSnapshotName);
+ SnapshotInfo deepCleanInfo =
metadataManager.getSnapshotInfoTable().get(deepCleanKey);
+ GenericTestUtils.waitFor(() -> {
+ try {
+ return
OmSnapshotManager.areSnapshotChangesFlushedToDB(metadataManager, deepCleanInfo);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }, 100, 10000);
+ // Make the snapshot eligible for deleted-key deep cleaning so the task
reaches submitRequest.
+ deepCleanInfo.setDeepCleanedDeletedDir(true);
+ metadataManager.getSnapshotInfoTable().put(deepCleanKey, deepCleanInfo);
+
+ // Delete the purge snapshot so it can be purged. Background services
are suspended, so it is not
+ // auto-purged; the test submits the purge itself.
+ writeClient.deleteSnapshot(volumeName, bucketName, purgeSnapshotName);
+ om.awaitDoubleBufferFlush();
+ String purgeKey = SnapshotInfo.getTableKey(volumeName, bucketName,
purgeSnapshotName);
+ SnapshotInfo purgeInfo =
metadataManager.getSnapshotInfoTable().get(purgeKey);
+ assertNotNull(purgeInfo);
+
+ CountDownLatch kdsAtSubmit = new CountDownLatch(1);
+ CountDownLatch kdsMayProceed = new CountDownLatch(1);
+ AtomicBoolean paused = new AtomicBoolean();
+ KeyDeletingService service = new KeyDeletingService(om,
scmBlockTestingClient, 10000,
+ 100000, conf, 1, true) {
+ @Override
+ protected OzoneManagerProtocolProtos.OMResponse submitRequest(
+ OzoneManagerProtocolProtos.OMRequest omRequest) throws
ServiceException {
+ // Park on the first submission, while snapshot DB read locks
acquired during the metadata
+ // scan are (pre-fix) still held. Post-fix the fix has already
released them by now.
+ if (paused.compareAndSet(false, true)) {
+ kdsAtSubmit.countDown();
+ try {
+ kdsMayProceed.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new ServiceException(e);
+ }
+ }
+ return super.submitRequest(omRequest);
+ }
+ };
+ service.shutdown();
+
+ OzoneManagerDoubleBuffer doubleBuffer =
+
om.getOmRatisServer().getOmStateMachine().getOzoneManagerDoubleBuffer();
+ Semaphore permits = (Semaphore)
HddsWhiteboxTestUtils.getInternalState(doubleBuffer, "unFlushedTransactions");
+
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try {
+ // 1. Run the deep-clean task. It parks in submitRequest holding the
snapshot DB read lock (pre-fix).
+ Future<?> kdsFuture = executor.submit(
+ () -> service.new
KeyDeletingTask(deepCleanInfo.getSnapshotId()).call());
+ assertTrue(kdsAtSubmit.await(30, TimeUnit.SECONDS), "KeyDeletingTask
never reached submitRequest");
+
+ // 2. Submit a snapshot purge. Applying it takes the single
double-buffer permit; on the pre-fix
+ // code its flush then blocks on the read lock still held above and
never releases the permit.
+ AtomicBoolean purgeDone = new AtomicBoolean();
+ OzoneManagerProtocolProtos.OMRequest purgeOmRequest =
OzoneManagerProtocolProtos.OMRequest.newBuilder()
+ .setCmdType(OzoneManagerProtocolProtos.Type.SnapshotPurge)
+
.setSnapshotPurgeRequest(OzoneManagerProtocolProtos.SnapshotPurgeRequest.newBuilder()
+ .addSnapshotDBKeys(purgeKey))
+ .setClientId(ClientId.randomId().toString())
+ .build();
+ Future<?> purgeFuture = executor.submit(() -> {
+ OzoneManagerRatisUtils.submitRequest(om, purgeOmRequest,
ClientId.randomId(), 1L);
+ purgeDone.set(true);
+ return null;
+ });
+ // Wait until the purge has taken the permit (pre-fix it stays taken)
or already completed (post-fix).
+ GenericTestUtils.waitFor(() -> permits.availablePermits() == 0 ||
purgeDone.get(), 50, 30000);
+
+ // 3. Release the deep-clean submission. It now needs the single
permit.
+ kdsMayProceed.countDown();
+
+ // Pre-fix: the permit is held by the stalled purge, so the task
blocks forever (deadlock).
+ // Post-fix: the purge released the permit, so the task completes.
+ try {
+ kdsFuture.get(60, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ fail("Permit-exhaustion deadlock reproduced: KeyDeletingService held
a snapshot DB read lock "
+ + "across its synchronous OM request while a snapshot purge held
the only double-buffer permit.");
+ }
+ purgeFuture.get(60, TimeUnit.SECONDS);
+ } finally {
+ kdsMayProceed.countDown();
+ executor.shutdownNow();
+ }
+ }
+ }
+
/**
* Tests failure scenarios.
*/
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]