This is an automated email from the ASF dual-hosted git repository.

adoroszlai 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 17f5be015f5 HDDS-16033. Fix intermittent failures in 
TestKeyLifecycleService (#11145)
17f5be015f5 is described below

commit 17f5be015f59848858a3707ad072798fdec991c0
Author: Chi-Hsuan Huang <[email protected]>
AuthorDate: Sat Aug 29 18:47:53 2026 +0800

    HDDS-16033. Fix intermittent failures in TestKeyLifecycleService (#11145)
---
 .../apache/hadoop/ozone/om/FaultInjectorImpl.java  |  12 +
 .../ozone/om/service/TestKeyLifecycleService.java  | 368 ++++++++++++++-------
 2 files changed, 252 insertions(+), 128 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/FaultInjectorImpl.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/FaultInjectorImpl.java
index 85296b89cbf..b1c0b76b0bd 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/FaultInjectorImpl.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/FaultInjectorImpl.java
@@ -20,6 +20,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import java.io.IOException;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
 import org.apache.hadoop.hdds.utils.FaultInjector;
 import org.assertj.core.api.Fail;
@@ -66,6 +67,17 @@ public void resume() throws IOException {
     wait.countDown();
   }
 
+  /** Wait until someone is paused here, without letting them through. */
+  public void awaitPaused(long timeoutMillis) throws InterruptedException {
+    Assertions.assertTrue(ready.await(timeoutMillis, TimeUnit.MILLISECONDS),
+        "Timed out waiting for the injector to be reached");
+  }
+
+  /** Unlike {@link #resume()}, does not wait for a pause first, so a 
discarded injector cannot park a thread. */
+  public void release() {
+    wait.countDown();
+  }
+
   @Override
   public void reset() throws IOException {
     init();
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
index 5f41fb37d75..048cafd5bc0 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
@@ -77,9 +77,12 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -96,6 +99,7 @@
 import org.apache.hadoop.hdds.server.ServerUtils;
 import org.apache.hadoop.hdds.utils.BackgroundTask;
 import org.apache.hadoop.hdds.utils.BackgroundTaskQueue;
+import org.apache.hadoop.hdds.utils.FaultInjector;
 import org.apache.hadoop.hdds.utils.db.DBConfigFromFile;
 import org.apache.hadoop.hdds.utils.db.RocksDatabaseException;
 import org.apache.hadoop.hdds.utils.db.Table;
@@ -141,7 +145,6 @@
 import org.apache.hadoop.ozone.om.request.key.OMKeysDeleteRequest;
 import org.apache.hadoop.ozone.om.request.util.OMMultipartUploadUtils;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
-import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LifecycleConfiguration;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
 import org.apache.hadoop.ozone.security.acl.OzoneObj;
 import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
@@ -191,6 +194,9 @@ class TestKeyLifecycleService extends OzoneTestBase {
   private static final AtomicInteger OBJECT_ID_COUNTER = new AtomicInteger();
   private static final int KEY_COUNT = 2;
   private static final int EXPIRE_SECONDS = 2;
+  // Date-based expiration compares the key's modificationTime against the 
date, so tests that rename or
+  // update a key after it was scanned need a date that stays in the future 
for the whole test.
+  private static final int LONG_EXPIRE_SECONDS = 600;
   private static final int SERVICE_INTERVAL = 300;
   private static final int WAIT_CHECK_INTERVAL = 50;
 
@@ -205,6 +211,8 @@ class TestKeyLifecycleService extends OzoneTestBase {
   private ScmBlockLocationTestingClient scmBlockTestingClient;
   private KeyLifecycleServiceMetrics metrics;
   private long bucketObjectID;
+  private final List<FaultInjectorImpl> installedInjectors = new ArrayList<>();
+  private final Set<String> createdLifecyclePolicies = new HashSet<>();
 
   @Parameter(0)
   private long stateSaveInternal;
@@ -273,12 +281,8 @@ void setup(@TempDir File testDir) throws Exception {
     }
 
     @AfterEach
-    void resume() {
-      keyLifecycleService.setOzoneTrash(null);
-      keyLifecycleService.setMoveToTrashEnabled(true);
-      KeyLifecycleService.setInjectors(null);
-      
keyLifecycleService.setListMaxSize(conf.getInt(OZONE_KEY_LIFECYCLE_SERVICE_DELETE_BATCH_SIZE,
-          OZONE_KEY_LIFECYCLE_SERVICE_DELETE_BATCH_SIZE_DEFAULT));
+    void resume() throws Exception {
+      cleanUpService();
     }
 
     @AfterAll
@@ -400,6 +404,7 @@ void testPeriodicStateSave(BucketLayout bucketLayout) 
throws Exception {
       assertEquals(testKeyCount, keyList.size());
       GenericTestUtils.waitFor(() -> getKeyCount(bucketLayout) - 
initialKeyCount == testKeyCount,
           WAIT_CHECK_INTERVAL, 1000);
+      awaitKeyCacheDrained(bucketLayout, volumeName, bucketName);
 
       // Inject spy to LifecycleScanStateTable to count put operations
       Field tableField = 
OmMetadataManagerImpl.class.getDeclaredField("lifecycleScanStateTable");
@@ -469,6 +474,7 @@ void testBucketScanResume(BucketLayout bucketLayout, 
boolean createPrefix) throw
       assertEquals(testKeyCount, keyList.size());
       GenericTestUtils.waitFor(() -> getKeyCount(bucketLayout) - 
initialKeyCount == testKeyCount,
           WAIT_CHECK_INTERVAL, 1000);
+      awaitKeyCacheDrained(bucketLayout, volumeName, bucketName);
 
       // determine db keys
       List<String> dbKeys = new ArrayList<>();
@@ -521,18 +527,16 @@ void testBucketScanResume(BucketLayout bucketLayout, 
boolean createPrefix) throw
       assertNotNull(state);
 
       // resume the service
-      keyLifecycleService.resume();
+      runSingleLifecycleScan(volumeName, bucketName);
 
-      // wait for it to process
       // it should skip the first 3 keys (index 0, 1, 2) since we set 
lastScannedDbKey as index 2.
       // So it deletes only the last 2 keys (index 3 and 4).
       int expectedDeleted = 2;
       GenericTestUtils.waitFor(() ->
-          (getDeletedKeyCount() - initialDeletedKeyCount) >= expectedDeleted, 
WAIT_CHECK_INTERVAL, 10000);
+          (getDeletedKeyCount() - initialDeletedKeyCount) == expectedDeleted, 
WAIT_CHECK_INTERVAL, 10000);
       
       // confirm it hasn't deleted all keys
       assertEquals(testKeyCount - expectedDeleted, getKeyCount(bucketLayout) - 
initialKeyCount);
-      deleteLifecyclePolicy(volumeName, bucketName);
     }
 
     @Test
@@ -584,7 +588,8 @@ void testAbortedScanDoesNotMarkScanComplete(BucketLayout 
bucketLayout, boolean c
       int testKeyCount = 3;
 
       keyLifecycleService.setListMaxSize(1);
-      KeyLifecycleService.setInjectors(Arrays.asList(new FaultInjectorImpl()));
+      FaultInjectorImpl taskStart = new FaultInjectorImpl();
+      installInjectors(taskStart);
 
       List<OmKeyArgs> keyList =
           createKeys(volumeName, bucketName, bucketLayout, testKeyCount, 1, 
keyPrefix, null);
@@ -600,13 +605,14 @@ void testAbortedScanDoesNotMarkScanComplete(BucketLayout 
bucketLayout, boolean c
       }
 
       String bucketKey = metadataManager.getBucketKey(volumeName, bucketName);
-      GenericTestUtils.waitFor(() -> 
keyLifecycleService.status().getRunningBucketsList().contains(bucketKey),
-          WAIT_CHECK_INTERVAL, 10000);
+      // The in-flight list is filled when the task is scheduled, so wait on 
the injector instead:
+      // suspending before the task actually starts makes it skip its run and 
never pause here.
+      taskStart.awaitPaused(10000);
 
       GenericTestUtils.LogCapturer logCapturer = 
GenericTestUtils.LogCapturer.captureLogs(
           LoggerFactory.getLogger(KeyLifecycleService.class));
       keyLifecycleService.suspend();
-      KeyLifecycleService.getInjector(0).resume();
+      taskStart.release();
       GenericTestUtils.waitFor(() -> 
keyLifecycleService.status().getRunningBucketsList().isEmpty(),
           WAIT_CHECK_INTERVAL, 10000);
 
@@ -640,6 +646,7 @@ void testBucketScanWithScanEndTime(BucketLayout 
bucketLayout, boolean createPref
       assertEquals(testKeyCount, keyList.size());
       GenericTestUtils.waitFor(() -> getKeyCount(bucketLayout) - 
initialKeyCount == testKeyCount,
           WAIT_CHECK_INTERVAL, 1000);
+      awaitKeyCacheDrained(bucketLayout, volumeName, bucketName);
 
       // determine db keys
       List<String> dbKeys = new ArrayList<>();
@@ -847,6 +854,8 @@ void testNestedFSODirectoryScanResume(BucketLayout 
bucketLayout, int maxSize) th
       assertEquals(testKeyCount, keyList.size());
       GenericTestUtils.waitFor(
           () -> getKeyCount(bucketLayout) - initialKeyCount == testKeyCount, 
WAIT_CHECK_INTERVAL, 1000);
+      awaitKeyCacheDrained(bucketLayout, volumeName, bucketName);
+      awaitDirCacheDrained(volumeName, bucketName);
 
       // Create Lifecycle configuration
       ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
@@ -854,54 +863,67 @@ void testNestedFSODirectoryScanResume(BucketLayout 
bucketLayout, int maxSize) th
       createLifecyclePolicy(volumeName, bucketName, bucketLayout, prefix, 
null, date.toString(), true);
 
       // Inject to cause resume case for FSO with nested directory
+      FaultInjectorImpl firstTaskStart = new FaultInjectorImpl();
+      FaultInjectorImpl firstDelete = new FaultInjectorImpl();
       FaultInjectorImpl lastFaultInjector = new FaultInjectorImpl();
       lastFaultInjector.setException(new IOException("Injected exception for 
testing"));
-      KeyLifecycleService.setInjectors(
-          Arrays.asList(new FaultInjectorImpl(), new FaultInjectorImpl(), 
lastFaultInjector));
+      installInjectors(firstTaskStart, firstDelete, lastFaultInjector);
       // Resume the service
       keyLifecycleService.resume();
-      KeyLifecycleService.getInjector(0).resume();
-      KeyLifecycleService.getInjector(1).resume();
+      // Returns once the aborted task is past its start, so it keeps using 
the injectors below
+      firstTaskStart.awaitPaused(10000);
+      firstTaskStart.release();
+      // Hold the follow-up task at its start, otherwise it overwrites the 
scan state asserted below
+      FaultInjectorImpl nextTaskStart = new FaultInjectorImpl();
+      installInjectors(nextTaskStart, firstDelete, lastFaultInjector);
+      firstDelete.awaitPaused(10000);
+      firstDelete.release();
+      // BackgroundService waits for the previous batch, so the follow-up task 
reaching its start means
+      // the first task is done
+      nextTaskStart.awaitPaused(10000);
 
-      // wait for scanState to be updated
       String bucketKey = metadataManager.getBucketKey(volumeName, bucketName);
-      GenericTestUtils.waitFor(() -> {
-        try {
-          OmLifecycleScanState scanState = 
metadataManager.getLifecycleScanStateTable().get(bucketKey);
-          if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
-            return scanState != null && scanState.getLastScannedDir() != null 
&& scanState.getLastScannedKey() != null;
-          } else {
-            return scanState != null && scanState.getLastScannedKey() != null;
-          }
-        } catch (IOException e) {
-          return false;
-        }
-      }, WAIT_CHECK_INTERVAL, 10000);
-
-      OmLifecycleScanState scanState = 
metadataManager.getLifecycleScanStateTable().get(bucketKey);
-      if (stateSaveInternal != -1) {
-        if (maxSize == 2) {
-          if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
-            assertEquals("dir3/dir6/dir9", scanState.getLastScannedDir());
-            assertTrue(scanState.getLastScannedKey().endsWith("key8"));
-          } else {
-            assertTrue(scanState.getLastScannedKey().endsWith("key1"));
-          }
-        } else if (maxSize == 3) {
-          if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
-            assertEquals("dir3/dir6/dir8", scanState.getLastScannedDir());
-            assertTrue(scanState.getLastScannedKey().endsWith("key6"));
-          } else {
-            assertTrue(scanState.getLastScannedKey().endsWith("key2"));
+      try {
+        GenericTestUtils.waitFor(() -> {
+          try {
+            OmLifecycleScanState state = 
metadataManager.getLifecycleScanStateTable().get(bucketKey);
+            if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
+              return state != null && state.getLastScannedDir() != null && 
state.getLastScannedKey() != null;
+            } else {
+              return state != null && state.getLastScannedKey() != null;
+            }
+          } catch (IOException e) {
+            return false;
           }
-        } else {
-          if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
-            assertEquals("dir1/dir5", scanState.getLastScannedDir());
-            assertTrue(scanState.getLastScannedKey().endsWith("key3"));
+        }, WAIT_CHECK_INTERVAL, 10000);
+
+        OmLifecycleScanState scanState = 
metadataManager.getLifecycleScanStateTable().get(bucketKey);
+        if (stateSaveInternal != -1) {
+          if (maxSize == 2) {
+            if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
+              assertEquals("dir3/dir6/dir9", scanState.getLastScannedDir());
+              assertTrue(scanState.getLastScannedKey().endsWith("key8"));
+            } else {
+              assertTrue(scanState.getLastScannedKey().endsWith("key1"));
+            }
+          } else if (maxSize == 3) {
+            if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
+              assertEquals("dir3/dir6/dir8", scanState.getLastScannedDir());
+              assertTrue(scanState.getLastScannedKey().endsWith("key6"));
+            } else {
+              assertTrue(scanState.getLastScannedKey().endsWith("key2"));
+            }
           } else {
-            assertTrue(scanState.getLastScannedKey().endsWith("key7"));
+            if (bucketLayout == FILE_SYSTEM_OPTIMIZED) {
+              assertEquals("dir1/dir5", scanState.getLastScannedDir());
+              assertTrue(scanState.getLastScannedKey().endsWith("key3"));
+            } else {
+              assertTrue(scanState.getLastScannedKey().endsWith("key7"));
+            }
           }
         }
+      } finally {
+        nextTaskStart.release();
       }
 
       GenericTestUtils.waitFor(() ->
@@ -1025,9 +1047,8 @@ void testDirectorySkippedAfterResume(String 
lastScannedDir, String[] skippedDir,
       GenericTestUtils.LogCapturer logCapturer = 
GenericTestUtils.LogCapturer.captureLogs(
           LoggerFactory.getLogger(KeyLifecycleService.class));
       // Resume the service
-      keyLifecycleService.resume();
+      runSingleLifecycleScan(volumeName, bucketName);
 
-      // Wait for it to process
       GenericTestUtils.waitFor(() ->
           (getDeletedKeyCount() - initialDeletedKeyCount) == expectedDeleted, 
WAIT_CHECK_INTERVAL, 10000);
       
@@ -1038,8 +1059,6 @@ void testDirectorySkippedAfterResume(String 
lastScannedDir, String[] skippedDir,
             d -> assertTrue(logCapturer.getOutput().contains("Skip " + d)));
         logCapturer.clearOutput();
       }
-
-      deleteLifecyclePolicy(volumeName, bucketName);
     }
 
     @ParameterizedTest
@@ -1066,6 +1085,7 @@ void testLastScannedKeySeek(boolean keyBelongToDir) 
throws Exception {
       assertEquals(4, keyList.size());
       GenericTestUtils.waitFor(() -> 
getKeyCount(BucketLayout.FILE_SYSTEM_OPTIMIZED) - initialKeyCount == 4,
           WAIT_CHECK_INTERVAL, 1000);
+      awaitKeyCacheDrained(BucketLayout.FILE_SYSTEM_OPTIMIZED, volumeName, 
bucketName);
 
       long bucketId =
           
metadataManager.getBucketTable().get(metadataManager.getBucketKey(volumeName, 
bucketName)).getObjectID();
@@ -1110,22 +1130,24 @@ void testLastScannedKeySeek(boolean keyBelongToDir) 
throws Exception {
 
       GenericTestUtils.LogCapturer logCapturer = 
GenericTestUtils.LogCapturer.captureLogs(
           LoggerFactory.getLogger(KeyLifecycleService.class));
-      keyLifecycleService.resume();
+      runSingleLifecycleScan(volumeName, bucketName);
 
-      // dir1 can be fully evaluated depending on whether lastScannedKey 
belong to it (no seek) or not
-      // dir2 should be skipped dir2 > dir1
-      int expectedDeleted = 2;
+      // dir2 is skipped since dir2 > dir1. dir1 is fully evaluated only when 
lastScannedKey does not
+      // belong to it, otherwise the scan seeks past its last key and nothing 
is left to expire.
+      int expectedDeleted = keyBelongToDir ? 0 : 2;
       GenericTestUtils.waitFor(() ->
-          (getDeletedKeyCount() - initialDeletedKeyCount) >= expectedDeleted, 
WAIT_CHECK_INTERVAL, 5000);
+          (getDeletedKeyCount() - initialDeletedKeyCount) == expectedDeleted, 
WAIT_CHECK_INTERVAL, 5000);
       assertEquals(keyList.size() - expectedDeleted, 
getKeyCount(BucketLayout.FILE_SYSTEM_OPTIMIZED) - initialKeyCount);
+      // The task drops the bucket from the in-flight list before it updates 
the metrics
       GenericTestUtils.waitFor(() ->
           expectedDeleted == metrics.getNumKeyIterated().value() - 
keyIterated, WAIT_CHECK_INTERVAL, 5000);
+      // dir2 sorts after the resumed dir1, so it is skipped in both cases
+      assertTrue(logCapturer.getOutput().contains("Skip dir2"));
       if (keyBelongToDir) {
         assertTrue(logCapturer.getOutput().contains("Seek to key"));
       } else {
         assertFalse(logCapturer.getOutput().contains("Seek to key"));
       }
-      deleteLifecyclePolicy(volumeName, bucketName);
     }
 
     @ParameterizedTest
@@ -1151,6 +1173,7 @@ void testBucketRootScannedDirResume(BucketLayout layout) 
throws Exception {
       assertEquals(3, keyList.size());
       GenericTestUtils.waitFor(() -> getKeyCount(layout) - initialKeyCount == 
3,
           WAIT_CHECK_INTERVAL, 1000);
+      awaitKeyCacheDrained(layout, volumeName, bucketName);
 
       long bucketId =
           
metadataManager.getBucketTable().get(metadataManager.getBucketKey(volumeName, 
bucketName)).getObjectID();
@@ -1184,7 +1207,7 @@ void testBucketRootScannedDirResume(BucketLayout layout) 
throws Exception {
       metadataManager.getLifecycleScanStateTable().addCacheEntry(new 
CacheKey<>(bucketKey),
           CacheValue.get(1L, scanState));
           
-      keyLifecycleService.resume();
+      runSingleLifecycleScan(volumeName, bucketName);
 
       // It should seek to key2. key1 is skipped, key2 is skipped too.
       // So key1 and key2 are skipped, key3 is deleted.
@@ -1195,7 +1218,6 @@ void testBucketRootScannedDirResume(BucketLayout layout) 
throws Exception {
       assertEquals(2, getKeyCount(layout) - initialKeyCount);
       GenericTestUtils.waitFor(() ->
           expectedDeleted == metrics.getNumKeyIterated().value() - 
keyIterated, WAIT_CHECK_INTERVAL, 5000);
-      deleteLifecyclePolicy(volumeName, bucketName);
     }
 
     @ParameterizedTest
@@ -1720,8 +1742,7 @@ void testMatchedDirectoryNotDeleted(String keyPrefix, 
String rulePrefix, String
       KeyInfoWithVolumeContext keyInfo = getDirectory(volumeName, bucketName, 
dirName);
       assertFalse(keyInfo.getKeyInfo().isFile());
 
-      KeyLifecycleService.setInjectors(
-          Arrays.asList(new FaultInjectorImpl(), new FaultInjectorImpl()));
+      installInjectors(new FaultInjectorImpl(), new FaultInjectorImpl());
 
       // create Lifecycle configuration
       ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
@@ -1751,24 +1772,21 @@ void testMatchedDirectoryNotDeleted(String keyPrefix, 
String rulePrefix, String
 
     public Stream<Arguments> parameters4() {
       return Stream.of(
-          arguments("dir1/dir2/dir3//", "dir1/dir2/dir3/", 3, true, false),
-          arguments("dir1/dir2/dir3//", "dir1/dir2/dir3/", 3, false, true),
-          arguments("dir1/dir2//", "dir1/dir2/", 2, true, false),
-          arguments("dir1/dir2//", "dir1/dir2/", 2, false, true),
-          arguments("dir1//", "dir1/", 1, true, false),
-          arguments("dir1//", "dir1/", 1, false, true)
+          arguments("dir1/dir2/dir3//", "dir1/dir2/dir3/", true, false),
+          arguments("dir1/dir2/dir3//", "dir1/dir2/dir3/", false, true),
+          arguments("dir1/dir2//", "dir1/dir2/", true, false),
+          arguments("dir1/dir2//", "dir1/dir2/", false, true),
+          arguments("dir1//", "dir1/", true, false),
+          arguments("dir1//", "dir1/", false, true)
       );
     }
 
     @ParameterizedTest
     @MethodSource("parameters4")
-    void testPrefixDirectoryNotExpired(String dirName, String prefix, int 
dirDepth, boolean createPrefix,
+    void testPrefixDirectoryNotExpired(String dirName, String prefix, boolean 
createPrefix,
         boolean createFilterPrefix) throws IOException, TimeoutException, 
InterruptedException {
       final String volumeName = getTestName();
       final String bucketName = uniqueObjectName("bucket");
-      long initialDeletedDirCount = getDeletedDirectoryCount();
-      long initialDirCount = getDirCount();
-      long initialNumDeletedDir = metrics.getNumDirDeleted().value();
 
       // Create the directory
       createVolumeAndBucket(volumeName, bucketName, FILE_SYSTEM_OPTIMIZED,
@@ -1778,10 +1796,6 @@ void testPrefixDirectoryNotExpired(String dirName, 
String prefix, int dirDepth,
       assertFalse(keyInfo.getKeyInfo().isFile());
       Thread.sleep(SERVICE_INTERVAL);
 
-      GenericTestUtils.waitFor(() -> dirDepth == getDirCount() - 
initialDirCount,
-          WAIT_CHECK_INTERVAL, 5000);
-      assertEquals(0, getDeletedDirectoryCount() - initialDeletedDirCount);
-
       GenericTestUtils.LogCapturer log =
           GenericTestUtils.LogCapturer.captureLogs(
               LoggerFactory.getLogger(KeyLifecycleService.class));
@@ -1800,8 +1814,8 @@ void testPrefixDirectoryNotExpired(String dirName, String 
prefix, int dirDepth,
 
       GenericTestUtils.waitFor(() -> log.getOutput().contains("Prefix 
directory " + prefix + " doesn't get expired"),
           WAIT_CHECK_INTERVAL, 10000);
-      assertEquals(dirDepth, getDirCount() - initialDirCount);
-      assertEquals(0, metrics.getNumDirDeleted().value() - 
initialNumDeletedDir);
+      // getKeyInfo resolves the whole path, so this fails if any level of the 
directory was expired
+      assertFalse(getDirectory(volumeName, bucketName, 
dirName).getKeyInfo().isFile());
       deleteLifecyclePolicy(volumeName, bucketName);
     }
 
@@ -1934,6 +1948,7 @@ void testConsolidatedPrefixDirectoryNotExpired(String 
dir1, String dir2, String
             .build());
         keyList.add(createAndCommitKey(volumeName, bucketName, dir4 + "key4", 
1, null));
       }
+      awaitKeyCacheDrained(FILE_SYSTEM_OPTIMIZED, volumeName, bucketName);
       createLifecyclePolicy(volumeName, bucketName, FILE_SYSTEM_OPTIMIZED, 
ruleList);
 
       try {
@@ -2087,12 +2102,11 @@ void testKeyUpdatedShouldNotGetDeleted(BucketLayout 
bucketLayout)
       List<OmKeyArgs> keyList =
           createKeys(volumeName, bucketName, bucketLayout, KEY_COUNT, 1, 
keyPrefix, null);
 
-      KeyLifecycleService.setInjectors(
-          Arrays.asList(new FaultInjectorImpl(), new FaultInjectorImpl()));
+      installInjectors(new FaultInjectorImpl(), new FaultInjectorImpl());
 
       // create Lifecycle configuration
       ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
-      ZonedDateTime date = now.plusSeconds(EXPIRE_SECONDS);
+      ZonedDateTime date = now.plusSeconds(LONG_EXPIRE_SECONDS);
       createLifecyclePolicy(volumeName, bucketName, bucketLayout, rulePrefix, 
null, date.toString(), true);
       Thread.sleep(SERVICE_INTERVAL);
       KeyLifecycleService.getInjector(0).resume();
@@ -2121,10 +2135,8 @@ void testKeyUpdatedShouldNotGetDeleted(BucketLayout 
bucketLayout)
           key.getBucketName() + "/" + key.getKeyName() + " whose updateID not 
match or null";
       GenericTestUtils.waitFor(() -> 
requestLog.getOutput().contains(expectedString), WAIT_CHECK_INTERVAL, 10000);
 
-      // rename will change object's modificationTime. But since expiration 
action is an absolute timestamp, so
-      // the renamed key will expire in next evaluation task
-      GenericTestUtils.waitFor(() -> log.getOutput().contains("1 expired keys 
and 0 expired dirs found"),
-          WAIT_CHECK_INTERVAL, 10000);
+      // How many candidates a task reports depends on whether the in-flight 
delete has landed when it
+      // starts, so assert on the outcome instead.
       GenericTestUtils.waitFor(() -> getKeyCount(bucketLayout) - 
initialKeyCount == 0, WAIT_CHECK_INTERVAL, 10000);
       assertEquals(KEY_COUNT, getDeletedKeyCount() - initialDeletedKeyCount);
       deleteLifecyclePolicy(volumeName, bucketName);
@@ -2565,8 +2577,7 @@ void testMultipleDirectoriesMatched(String keyPrefix1, 
String keyPrefix2, String
       KeyInfoWithVolumeContext keyInfo = getDirectory(volumeName, bucketName, 
dirName);
       assertFalse(keyInfo.getKeyInfo().isFile());
 
-      KeyLifecycleService.setInjectors(
-          Arrays.asList(new FaultInjectorImpl(), new FaultInjectorImpl()));
+      installInjectors(new FaultInjectorImpl(), new FaultInjectorImpl());
 
       // create Lifecycle configuration
       ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
@@ -2622,8 +2633,7 @@ void testGetLifecycleServiceStatus() throws Exception {
       // Create and inject for test
       createKeys(volumeName, bucketName, FILE_SYSTEM_OPTIMIZED, KEY_COUNT, 1, 
prefix, null);
       ZonedDateTime date = 
ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(EXPIRE_SECONDS);
-      KeyLifecycleService.setInjectors(
-          Arrays.asList(new FaultInjectorImpl(), new FaultInjectorImpl()));
+      installInjectors(new FaultInjectorImpl(), new FaultInjectorImpl());
       createLifecyclePolicy(volumeName, bucketName, FILE_SYSTEM_OPTIMIZED, "", 
null, date.toString(), true);
       Thread.sleep(SERVICE_INTERVAL + 100);
       
@@ -3139,7 +3149,8 @@ void setup(@TempDir File testDir) throws Exception {
     }
 
     @AfterEach
-    void resume() {
+    void resume() throws Exception {
+      cleanUpService();
     }
 
     @AfterAll
@@ -3183,7 +3194,7 @@ void testBucketDeleted(BucketLayout bucketLayout) throws 
IOException, Interrupte
       ZonedDateTime date = now.plusSeconds(EXPIRE_SECONDS);
 
       FaultInjectorImpl injector = new FaultInjectorImpl();
-      KeyLifecycleService.setInjectors(Arrays.asList(injector));
+      installInjectors(injector);
       createLifecyclePolicy(volumeName, bucketName, bucketLayout, "", null, 
date.toString(), true);
 
       Thread.sleep(1000);
@@ -3232,12 +3243,11 @@ void testKeyDeletedOrRenamed(BucketLayout bucketLayout, 
boolean deleted)
       List<OmKeyArgs> keyList =
           createKeys(volumeName, bucketName, bucketLayout, KEY_COUNT, 1, 
keyPrefix, null);
 
-      KeyLifecycleService.setInjectors(
-          Arrays.asList(new FaultInjectorImpl(), new FaultInjectorImpl()));
+      installInjectors(new FaultInjectorImpl(), new FaultInjectorImpl());
 
       // create Lifecycle configuration
       ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
-      ZonedDateTime date = now.plusSeconds(EXPIRE_SECONDS);
+      ZonedDateTime date = now.plusSeconds(LONG_EXPIRE_SECONDS);
       createLifecyclePolicy(volumeName, bucketName, bucketLayout, rulePrefix, 
null, date.toString(), true);
       Thread.sleep(SERVICE_INTERVAL);
       KeyLifecycleService.getInjector(0).resume();
@@ -3260,11 +3270,8 @@ void testKeyDeletedOrRenamed(BucketLayout bucketLayout, 
boolean deleted)
       String expectedString = "Received a request to delete a Key does not 
exist /" + key.getVolumeName() + "/" +
           key.getBucketName() + "/" + key.getKeyName();
       GenericTestUtils.waitFor(() -> 
requestLog.getOutput().contains(expectedString), WAIT_CHECK_INTERVAL, 10000);
-      if (!deleted) {
-        // Since expiration action is an absolute timestamp, so the renamed 
key will expire in next evaluation task
-        GenericTestUtils.waitFor(() -> log.getOutput().contains("1 expired 
keys and 0 expired dirs found"),
-            SERVICE_INTERVAL, 10000);
-      }
+      // How many candidates a task reports depends on whether the in-flight 
delete has landed when it
+      // starts, so assert on the outcome instead.
       GenericTestUtils.waitFor(() -> getKeyCount(bucketLayout) - 
initialKeyCount == 0, WAIT_CHECK_INTERVAL, 10000);
       assertEquals(KEY_COUNT, getDeletedKeyCount() - initialDeletedKeyCount);
       deleteLifecyclePolicy(volumeName, bucketName);
@@ -3384,13 +3391,7 @@ private void createLifecyclePolicy(String volume, String 
bucket, BucketLayout la
       }
       throw e;
     }
-    String key = "/" + volume + "/" + bucket;
-    LifecycleConfiguration lcProto = lcc.getProtobuf();
-    OmLifecycleConfiguration canonicalLcc = 
OmLifecycleConfiguration.getFromProtobuf(lcProto);
-    canonicalLcc.valid();
-    metadataManager.getLifecycleConfigurationTable().put(key, lcc);
-    metadataManager.getLifecycleConfigurationTable().addCacheEntry(
-        new CacheKey<>(key), CacheValue.get(1L, canonicalLcc));
+    putLifecyclePolicy(volume, bucket, lcc, true);
   }
 
   private void createLifecyclePolicy(String volume, String bucket, 
BucketLayout layout, List<OmLCRule> ruleList)
@@ -3410,20 +3411,132 @@ private void createLifecyclePolicy(String volume, 
String bucket, BucketLayout la
       }
       throw e;
     }
-    String key = "/" + volume + "/" + bucket;
-    LifecycleConfiguration lcProto = lcc.getProtobuf();
-    OmLifecycleConfiguration canonicalLcc = 
OmLifecycleConfiguration.getFromProtobuf(lcProto);
+    putLifecyclePolicy(volume, bucket, lcc, false);
+  }
+
+  private void putLifecyclePolicy(String volume, String bucket, 
OmLifecycleConfiguration lcc, boolean validate)
+      throws IOException {
+    String key = metadataManager.getBucketKey(volume, bucket);
+    OmLifecycleConfiguration canonicalLcc = 
OmLifecycleConfiguration.getFromProtobuf(lcc.getProtobuf());
+    if (validate) {
+      canonicalLcc.valid();
+    }
     metadataManager.getLifecycleConfigurationTable().put(key, lcc);
     metadataManager.getLifecycleConfigurationTable().addCacheEntry(
         new CacheKey<>(key), CacheValue.get(1L, canonicalLcc));
+    createdLifecyclePolicies.add(key);
   }
 
   private void deleteLifecyclePolicy(String volume, String bucket)
       throws IOException {
-    String key = "/" + volume + "/" + bucket;
-    metadataManager.getLifecycleConfigurationTable().delete(key);
+    deleteLifecyclePolicy(metadataManager.getBucketKey(volume, bucket));
+  }
+
+  private void deleteLifecyclePolicy(String policyKey) throws IOException {
+    metadataManager.getLifecycleConfigurationTable().delete(policyKey);
     metadataManager.getLifecycleConfigurationTable().addCacheEntry(
-        new CacheKey<>(key), CacheValue.get(1L));
+        new CacheKey<>(policyKey), CacheValue.get(1L));
+    createdLifecyclePolicies.remove(policyKey);
+  }
+
+  // A scan reads keys still in the cache from an unordered map and skips them 
in the sorted table
+  // iterator without counting them, so lastScannedKey and numKeyIterated are 
unstable until they drain.
+  private void awaitKeyCacheDrained(BucketLayout layout, String volume, String 
bucket)
+      throws TimeoutException, InterruptedException {
+    Table<String, OmKeyInfo> keyTable = metadataManager.getKeyTable(layout);
+    GenericTestUtils.waitFor(() -> {
+      Iterator<Map.Entry<CacheKey<String>, CacheValue<OmKeyInfo>>> cacheIter = 
keyTable.cacheIterator();
+      while (cacheIter.hasNext()) {
+        OmKeyInfo key = cacheIter.next().getValue().getCacheValue();
+        if (key != null && volume.equals(key.getVolumeName()) && 
bucket.equals(key.getBucketName())) {
+          return false;
+        }
+      }
+      return true;
+    }, WAIT_CHECK_INTERVAL, 10000);
+  }
+
+  // getSubDirectory evaluates directories still in the cache and adds them 
again from the table iterator,
+  // so a scan sees them twice until they drain.
+  private void awaitDirCacheDrained(String volume, String bucket)
+      throws IOException, TimeoutException, InterruptedException {
+    long volumeId = 
metadataManager.getVolumeTable().get(metadataManager.getVolumeKey(volume)).getObjectID();
+    long bucketId = 
metadataManager.getBucketTable().get(metadataManager.getBucketKey(volume, 
bucket)).getObjectID();
+    String bucketPrefix = OM_KEY_PREFIX + volumeId + OM_KEY_PREFIX + bucketId 
+ OM_KEY_PREFIX;
+    Table<String, OmDirectoryInfo> dirTable = 
metadataManager.getDirectoryTable();
+    GenericTestUtils.waitFor(() -> {
+      Iterator<Map.Entry<CacheKey<String>, CacheValue<OmDirectoryInfo>>> 
cacheIter = dirTable.cacheIterator();
+      while (cacheIter.hasNext()) {
+        if (cacheIter.next().getKey().getCacheKey().startsWith(bucketPrefix)) {
+          return false;
+        }
+      }
+      return true;
+    }, WAIT_CHECK_INTERVAL, 10000);
+  }
+
+  private void awaitMultipartCacheDrained(String dbKey) throws 
TimeoutException, InterruptedException {
+    Table<String, OmMultipartKeyInfo> mpuTable = 
metadataManager.getMultipartInfoTable();
+    GenericTestUtils.waitFor(() -> {
+      Iterator<Map.Entry<CacheKey<String>, CacheValue<OmMultipartKeyInfo>>> 
cacheIter = mpuTable.cacheIterator();
+      while (cacheIter.hasNext()) {
+        if (dbKey.equals(cacheIter.next().getKey().getCacheKey())) {
+          return false;
+        }
+      }
+      return true;
+    }, WAIT_CHECK_INTERVAL, 10000);
+  }
+
+  // Tracks every injector installed, not just the current set, so a replaced 
one is still released
+  private void installInjectors(FaultInjectorImpl... injectors) {
+    installedInjectors.addAll(Arrays.asList(injectors));
+    KeyLifecycleService.setInjectors(new 
ArrayList<FaultInjector>(Arrays.asList(injectors)));
+  }
+
+  // A task parked in an injector blocks every following cycle, and a policy 
left behind keeps its bucket
+  // scanned, so both have to be cleared for the next test to start from a 
quiet service.
+  private void cleanUpService() throws Exception {
+    try {
+      // Suspending first closes the window where a cycle has already read the 
policies but has not
+      // registered its task yet, which would let that task run into the next 
test.
+      keyLifecycleService.suspend();
+      installedInjectors.forEach(FaultInjectorImpl::release);
+      GenericTestUtils.waitFor(() -> 
keyLifecycleService.status().getRunningBucketsList().isEmpty(),
+          WAIT_CHECK_INTERVAL, 30000);
+    } finally {
+      try {
+        for (String policyKey : new ArrayList<>(createdLifecyclePolicies)) {
+          deleteLifecyclePolicy(policyKey);
+        }
+      } finally {
+        installedInjectors.clear();
+        KeyLifecycleService.setInjectors(null);
+        keyLifecycleService.setOzoneTrash(null);
+        keyLifecycleService.setMoveToTrashEnabled(true);
+        
keyLifecycleService.setListMaxSize(conf.getInt(OZONE_KEY_LIFECYCLE_SERVICE_DELETE_BATCH_SIZE,
+            OZONE_KEY_LIFECYCLE_SERVICE_DELETE_BATCH_SIZE_DEFAULT));
+        keyLifecycleService.resume();
+      }
+    }
+  }
+
+  // Runs exactly one scan: without dropping the policy first, the next scan 
starts SERVICE_INTERVAL
+  // later and deletes the objects the caller expects to remain.
+  private void runSingleLifecycleScan(String volume, String bucket)
+      throws IOException, TimeoutException, InterruptedException {
+    FaultInjectorImpl taskStart = new FaultInjectorImpl();
+    FaultInjectorImpl firstDelete = new FaultInjectorImpl();
+    // The third injector is only read for an injected exception, but the scan 
expects it to exist
+    installInjectors(taskStart, firstDelete, new FaultInjectorImpl());
+    keyLifecycleService.resume();
+    // The in-flight list is filled at schedule time, so wait for the task to 
reach the injector
+    taskStart.awaitPaused(10000);
+    deleteLifecyclePolicy(volume, bucket);
+    taskStart.release();
+    firstDelete.release();
+    GenericTestUtils.waitFor(() -> 
keyLifecycleService.status().getRunningBucketsList().isEmpty(),
+        WAIT_CHECK_INTERVAL, 10000);
   }
 
   private void createVolumeAndBucket(String volumeName,
@@ -3555,16 +3668,6 @@ private long getDeletedDirectoryCount() {
     }
   }
 
-  private long getDirCount() {
-    final Table<String, OmDirectoryInfo> table = 
metadataManager.getDirectoryTable();
-    try {
-      return metadataManager.countRowsInTable(table);
-    } catch (IOException e) {
-      fail("Failed to count directories " + e.getMessage());
-      return -1;
-    }
-  }
-
   private long getKeyCount(BucketLayout layout) {
     final Table<String, OmKeyInfo> table = metadataManager.getKeyTable(layout);
     try {
@@ -3597,8 +3700,12 @@ private long getMultipartUploadCount(String volumeName, 
String bucketName) {
   }
 
   private void updateMultipartUploadCreationTime(String volumeName, String 
bucketName,
-      String keyName, String uploadId, long newCreationTime) throws 
IOException {
+      String keyName, String uploadId, long newCreationTime)
+      throws IOException, TimeoutException, InterruptedException {
     String dbKey = metadataManager.getMultipartKey(volumeName, bucketName, 
keyName, uploadId);
+    // The create is flushed to the DB asynchronously, and that flush would 
overwrite the new creation
+    // time, leaving the upload looking fresh. Wait for the entry to leave the 
cache first.
+    awaitMultipartCacheDrained(dbKey);
     OmMultipartKeyInfo existingInfo = 
metadataManager.getMultipartInfoTable().get(dbKey);
     if (existingInfo == null) {
       fail("Multipart upload not found: " + dbKey);
@@ -3750,6 +3857,11 @@ void setup(@TempDir File testDir) throws Exception {
       directoryDeletingService.suspend();
     }
 
+    @AfterEach
+    void resume() throws Exception {
+      cleanUpService();
+    }
+
     @AfterAll
     void cleanup() {
       if (om != null) {


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

Reply via email to