Skip to content

Commit b7621d9

Browse files
committed
address comments
1 parent e721738 commit b7621d9

7 files changed

Lines changed: 57 additions & 100 deletions

File tree

hadoop-hdds/docs/content/design/lifecycle-task-resume.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
16-
*/
16+
*/
1717

1818
# Design for Resumable Lifecycle Scans(HDDS-8342)
1919

@@ -46,21 +46,21 @@ message LifecycleScanState {
4646
```
4747

4848
### OM DB Schema Updates
49-
Add a new table `lifecycleStateTable` to `OMMetadataManager` to store the scan states:
50-
- **Table Name:** `lifecycleStateTable`
49+
Add a new table `lifecycleScanStateTable` to `OMMetadataManager` to store the scan states:
50+
- **Table Name:** `lifecycleScanStateTable`
5151
- **Key:** `bucketKey` (String, e.g., `/volumeName/bucketName`)
5252
- **Value:** `LifecycleScanState`
5353

5454
### When to Persist the Pointer
5555
Persisting the pointer for every key would overwhelm Ratis and RocksDB. We should checkpoint periodically:
5656

57-
1. **Piggybacking on Deletes:** Add an optional `LifecycleScanState` field to `DeleteKeysRequest`. When the OM state machine applies the deletion, it atomically updates the `lifecycleStateTable` with the new pointer. This guarantees exactly-once semantics for the scan pointer relative to deletions.
57+
1. **Piggybacking on Deletes:** Add an optional `LifecycleScanState` field to `DeleteKeysRequest`. When the OM state machine applies the deletion, it atomically updates the `lifecycleScanStateTable` with the new pointer. This guarantees exactly-once semantics for the scan pointer relative to deletions.
5858
2. **Move to trash**: Since there is no `RenameKeysRequest`, rename has be called multiple times for a batch of keys. We introduce a new OM request `SaveLifecycleScanStateRequest`. After a batch of keys are moved to trash, call `SaveLifecycleScanStateRequest` explicitly to persist the state.
5959
3. **Periodic Standalone Checkpoints:** If no keys are expired (e.g., scanning millions of valid keys), we still need to save progress. The `LifecycleActionTask` will send this request periodically (e.g., every 100,000 keys iterated, or every 1 minute of execution time).
6060
3. **End of Scan:** When the scan for a bucket finishes, a `SaveLifecycleScanStateRequest` is sent to mark state as completed by recording the completion time.
6161

6262
### How to Resume the Scan
63-
When `KeyLifecycleService` schedules a `LifecycleActionTask` for a bucket, it first reads the `LifecycleScanState` from the `lifecycleStateTable`.
63+
When `KeyLifecycleService` schedules a `LifecycleActionTask` for a bucket, it first reads the `LifecycleScanState` from the `lifecycleScanStateTable`.
6464

6565
- **OBS/Legacy Resumption:**
6666
The iterator for `keyTable` is initialized to seek to `lastScannedKey` instead of the bucket prefix.

hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ public final class OMConfigKeys {
199199
"ozone.lifecycle.service.delete.cached.directory.max-count";
200200
public static final long OZONE_KEY_LIFECYCLE_SERVICE_DELETE_CACHED_DIRECTORY_MAX_COUNT_DEFAULT = 1000000;
201201

202-
// Save task state for every 5m, or evaluated keys reaches 100k
203202
public static final String OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS =
204203
"ozone.lifecycle.service.state.save.interval.ms";
205204
public static final long OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS_DEFAULT = 5 * 60 * 1000;

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeysDeleteRequest.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
import java.util.LinkedHashMap;
4141
import java.util.List;
4242
import java.util.Map;
43+
import java.util.Objects;
4344
import org.apache.commons.lang3.tuple.Pair;
4445
import org.apache.hadoop.hdds.utils.db.Table;
4546
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
4647
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
48+
import org.apache.hadoop.ozone.OmUtils;
4749
import org.apache.hadoop.ozone.OzoneConsts;
4850
import org.apache.hadoop.ozone.audit.AuditLogger;
4951
import org.apache.hadoop.ozone.om.OMMetadataManager;
@@ -93,6 +95,25 @@ public OMKeysDeleteRequest(OMRequest omRequest, BucketLayout bucketLayout) {
9395
super(omRequest, bucketLayout);
9496
}
9597

98+
@Override
99+
public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
100+
DeleteKeysRequest deleteKeysRequest = super.preExecute(ozoneManager).getDeleteKeysRequest();
101+
Objects.requireNonNull(deleteKeysRequest, "deleteKeysRequest == null");
102+
103+
if (deleteKeysRequest.getSourceType() == RequestSource.LIFECYCLE && deleteKeysRequest.hasScanState()) {
104+
if (ozoneManager.getAclsEnabled()) {
105+
UserGroupInformation ugi = createUGIForApi();
106+
if (!ozoneManager.isAdmin(ugi)) {
107+
throw new OMException("Access denied for user " + ugi + ". "
108+
+ "Superuser privilege is required to save Lifecycle Service task state.",
109+
OMException.ResultCodes.ACCESS_DENIED);
110+
}
111+
}
112+
}
113+
114+
return getOmRequest();
115+
}
116+
96117
@Override @SuppressWarnings("methodlength")
97118
public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, ExecutionContext context) {
98119
final long trxnLogIndex = context.getIndex();
@@ -164,17 +185,6 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
164185
validateBucketAndVolume(omMetadataManager, volumeName, bucketName);
165186
String volumeOwner = getVolumeOwner(omMetadataManager, volumeName);
166187

167-
if (sourceType == RequestSource.LIFECYCLE && deleteKeyRequest.hasScanState()) {
168-
if (ozoneManager.getAclsEnabled()) {
169-
UserGroupInformation ugi = createUGIForApi();
170-
if (!ozoneManager.isAdmin(ugi)) {
171-
throw new OMException("Access denied for user " + ugi + ". "
172-
+ "Superuser privilege is required to save Lifecycle Service task state.",
173-
OMException.ResultCodes.ACCESS_DENIED);
174-
}
175-
}
176-
}
177-
178188
for (indexFailed = 0; indexFailed < length; indexFailed++) {
179189
String keyName = deleteKeyArgs.getKeys(indexFailed);
180190
String objectKey =

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeysDeleteResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,8 @@ public OmBucketInfo getOmBucketInfo() {
129129
protected Map<String, OmKeyInfo> getOpenKeyInfoMap() {
130130
return openKeyInfoMap;
131131
}
132+
133+
public OmLifecycleScanState getScanState() {
134+
return scanState;
135+
}
132136
}

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeysDeleteResponseWithFSO.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ public class OMKeysDeleteResponseWithFSO extends OMKeysDeleteResponse {
4848

4949
private List<OmKeyInfo> dirsList;
5050
private long volumeId;
51-
private OmLifecycleScanState scanState;
5251

5352
public OMKeysDeleteResponseWithFSO(
5453
@Nonnull OzoneManagerProtocolProtos.OMResponse omResponse,
@@ -60,7 +59,6 @@ public OMKeysDeleteResponseWithFSO(
6059
super(omResponse, keyDeleteList, omBucketInfo, openKeyInfoMap, scanState);
6160
this.dirsList = dirDeleteList;
6261
this.volumeId = volId;
63-
this.scanState = scanState;
6462
}
6563

6664
@Override
@@ -107,6 +105,7 @@ public void addToDBBatch(OMMetadataManager omMetadataManager,
107105
}
108106
}
109107

108+
OmLifecycleScanState scanState = getScanState();
110109
if (scanState != null) {
111110
omMetadataManager.getLifecycleScanStateTable().putWithBatch(
112111
batchOperation, scanState.getBucketKey(), scanState);

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java

Lines changed: 24 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public KeyLifecycleService(OzoneManager ozoneManager,
171171
LOG.warn("Illegal value {} for Property {}. Set {} to {}", stateSaveIntervalMs,
172172
OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS, OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS,
173173
OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS_DEFAULT);
174-
maxKeysProcessedPerState = OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS_DEFAULT;
174+
stateSaveIntervalMs = OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS_DEFAULT;
175175
}
176176
this.maxKeysProcessedPerState = conf.getLong(OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_KEYS_PROCESSED,
177177
OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_KEYS_PROCESSED_DEFAULT);
@@ -181,6 +181,7 @@ public KeyLifecycleService(OzoneManager ozoneManager,
181181
OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_KEYS_PROCESSED_DEFAULT);
182182
maxKeysProcessedPerState = OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_KEYS_PROCESSED_DEFAULT;
183183
}
184+
LOG.info("stateSaveIntervalMs = {}, maxKeysProcessedPerState = {}", stateSaveIntervalMs, maxKeysProcessedPerState);
184185
this.inFlight = new ConcurrentHashMap();
185186
this.omMetadataManager = ozoneManager.getMetadataManager();
186187
int limit = (int) conf.getStorageSize(
@@ -629,40 +630,21 @@ private List<RuleListWithDirectoryList> getRuleUnion(long volumeId, OmBucketInfo
629630
return sortedConsolidatedRules;
630631
}
631632

632-
private boolean canSkipDir(OmDirectoryInfo currentDir, String currentDirTableKey, DirectoryList dirList) {
633-
// currentDir null is bucket root
634-
if (currentDir == null) {
633+
private boolean canSkipDir(String currentDirPath, String lastScannedDir) {
634+
if (lastScannedDir == null || currentDirPath.isEmpty()) {
635635
return false;
636636
}
637-
638-
int count = dirList.getSubDirCount();
639-
// if currentDir is equal to lastScannedDir
640-
if (currentDir.getObjectID() == dirList.getSubDirList().get(count - 1).getObjectID()) {
641-
return false;
642-
}
643-
644-
// if currentDir is parent of lastScannedDir
645-
long currentObjID = currentDir.getObjectID();
646-
for (int i = 0; i < count; i++) {
647-
OmDirectoryInfo dir = dirList.getSubDirList().get(i);
648-
if (dir.getObjectID() == currentObjID) {
649-
return false;
637+
String[] cur = currentDirPath.split(OM_KEY_PREFIX);
638+
String[] last = lastScannedDir.split(OM_KEY_PREFIX);
639+
int n = Math.min(cur.length, last.length);
640+
for (int i = 0; i < n; i++) {
641+
int cmp = cur[i].compareTo(last[i]);
642+
if (cmp != 0) {
643+
// current name > last name -> skip
644+
return cmp > 0;
650645
}
651646
}
652-
653-
// if currentDir and lastScannedDir has same parent
654-
do {
655-
long parentID = currentDir.getParentObjectID();
656-
for (int i = 0; i < count; i++) {
657-
OmDirectoryInfo dir = dirList.getSubDirList().get(i);
658-
if (dir.getParentObjectID() == parentID) {
659-
if (dirList.getSubDirKeyList().get(i).compareTo(currentDirTableKey) < 0) {
660-
return true;
661-
}
662-
}
663-
}
664-
return false;
665-
} while (true);
647+
return false;
666648
}
667649

668650
@SuppressWarnings({"checkstyle:parameternumber", "checkstyle:MethodLength"})
@@ -676,19 +658,6 @@ private void evaluateKeyAndDirTable(OmBucketInfo bucket, long volumeObjId, Table
676658
String lastScannedDirInState = scanStateBuilder == null ? null : scanStateBuilder.getLastScannedDir();
677659
String lastScannedDirKeyInState = scanStateBuilder == null ? null : scanStateBuilder.getLastScannedDirKey();
678660
String lastScannedKeyInState = scanStateBuilder == null ? null : scanStateBuilder.getLastScannedKey();
679-
DirectoryList lastScannedDirList = null;
680-
String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
681-
if (lastScannedDirInState != null && lastScannedDirKeyInState != null) {
682-
// find all parents of lastScannedSubDir
683-
try {
684-
lastScannedDirList = getDirList(volumeObjId, bucket, lastScannedDirInState, bucketKey);
685-
} catch (IOException e) {
686-
// Saved lastScannedDir in state could be deleted or renamed after it's saved.
687-
// Fallback to no state saved status.
688-
LOG.info("Failed to get DirList for lastScannedDirInState {}", lastScannedDirInState, e);
689-
lastScannedDirInState = null;
690-
}
691-
}
692661
try {
693662
if (dir != null) {
694663
stack.push(new PendingEvaluateDirectory(dir, dirKey, directoryPath, null));
@@ -748,8 +717,7 @@ private void evaluateKeyAndDirTable(OmBucketInfo bucket, long volumeObjId, Table
748717
* - dir3/dir8, lastScannedDir, partially scanned,
749718
* - dir3/dir9, same the same parentID as lastScannedDir, and name order > lastScannedDir, scanned, skip
750719
*/
751-
if (lastScannedDirList != null &&
752-
canSkipDir(currentDir, currentDirTableKey, lastScannedDirList)) {
720+
if (canSkipDir(currentDirPath, lastScannedDirInState)) {
753721
LOG.info("Skip {} in LifecycleActionTask for bucket {}. ", currentDirPath, bucketName);
754722
continue;
755723
}
@@ -885,33 +853,12 @@ private void evaluateKeyAndDirTable(OmBucketInfo bucket, long volumeObjId, Table
885853

886854
try (TableIterator<String, ? extends Table.KeyValue<String, OmKeyInfo>> keyTblItr =
887855
keyTable.iterator(prefix)) {
856+
boolean seekPerformed = false;
888857
if (lastScannedDirKeyInState != null && lastScannedDirKeyInState.compareTo(currentDirTableKey) == 0 &&
889858
lastScannedKeyInState != null && lastScannedKeyInState.startsWith(prefix)) {
890859
LOG.info("Seek to key {} under directory {}", scanStateBuilder.getLastScannedKey(), lastScannedDirInState);
891860
keyTblItr.seek(scanStateBuilder.getLastScannedKey());
892-
if (keyTblItr.hasNext()) {
893-
Table.KeyValue<String, OmKeyInfo> first = keyTblItr.next();
894-
if (!first.getKey().equals(scanStateBuilder.getLastScannedKey())) {
895-
OmKeyInfo key = first.getValue();
896-
String keyPath = currentDirPath.isEmpty() ? key.getKeyName() :
897-
currentDirPath + OM_KEY_PREFIX + key.getKeyName();
898-
if (!deletedKeySetInCache.remove(first.getKey()) && !keySetInCache.remove(first.getKey())) {
899-
numKeyIterated++;
900-
numKeysUnderDir++;
901-
for (OmLCRule rule : ruleList) {
902-
if (key.getParentObjectID() == currentDirObjID && rule.match(key, keyPath)) {
903-
if (keyList.isFull()) {
904-
handleAndClearFullList(bucket, keyList, false, scanStateBuilder, false);
905-
}
906-
keyList.add(keyPath, key.getReplicatedSize(), key.getUpdateID());
907-
numKeysExpired++;
908-
break;
909-
}
910-
}
911-
lastScannedKey = first.getKey();
912-
}
913-
}
914-
}
861+
seekPerformed = true;
915862
}
916863

917864
while (keyTblItr.hasNext()) {
@@ -923,6 +870,9 @@ private void evaluateKeyAndDirTable(OmBucketInfo bucket, long volumeObjId, Table
923870
OmKeyInfo key = keyValue.getValue();
924871
String keyPath = currentDirPath.isEmpty() ? key.getKeyName() :
925872
currentDirPath + OM_KEY_PREFIX + key.getKeyName();
873+
if (seekPerformed && keyValue.getKey().equals(scanStateBuilder.getLastScannedKey())) {
874+
continue;
875+
}
926876
if (deletedKeySetInCache.remove(keyValue.getKey()) || keySetInCache.remove(keyValue.getKey())) {
927877
continue;
928878
}
@@ -1066,19 +1016,10 @@ private void evaluateBucket(OmBucketInfo bucketInfo,
10661016

10671017
try (TableIterator<String, ? extends Table.KeyValue<String, OmKeyInfo>> keyTblItr =
10681018
keyTable.iterator(bucketPrefix)) {
1019+
boolean seekPerformed = false;
10691020
if (scanStateBuilder != null && scanStateBuilder.getLastScannedKey() != null) {
10701021
keyTblItr.seek(scanStateBuilder.getLastScannedKey());
1071-
// Skip the exact match since it was already processed
1072-
if (keyTblItr.hasNext()) {
1073-
Table.KeyValue<String, OmKeyInfo> first = keyTblItr.next();
1074-
if (!first.getKey().equals(scanStateBuilder.getLastScannedKey())) {
1075-
// We seeked past it, so we need to process this one.
1076-
// We can't easily "push back" in TableIterator, so we handle it here.
1077-
processKey(bucketInfo, first.getValue(), ruleList, expiredKeyList, scanStateBuilder);
1078-
numKeyIterated++;
1079-
lastScannedKey = first.getKey();
1080-
}
1081-
}
1022+
seekPerformed = true;
10821023
}
10831024

10841025
while (keyTblItr.hasNext()) {
@@ -1091,6 +1032,9 @@ private void evaluateBucket(OmBucketInfo bucketInfo,
10911032
flushAndSaveState(bucketInfo, expiredKeyList, null, scanStateBuilder);
10921033
}
10931034
Table.KeyValue<String, OmKeyInfo> keyValue = keyTblItr.next();
1035+
if (seekPerformed && keyValue.getKey().equals(scanStateBuilder.getLastScannedKey())) {
1036+
continue;
1037+
}
10941038
processKey(bucketInfo, keyValue.getValue(), ruleList, expiredKeyList, scanStateBuilder);
10951039
numKeyIterated++;
10961040
lastScannedKey = keyValue.getKey();

hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ private void createConfig(File testDir) {
219219
conf.setLong(OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_INTERVAL_MS, stateSaveInternal);
220220
conf.setLong(OZONE_KEY_LIFECYCLE_SERVICE_STATE_SAVE_KEYS_PROCESSED, maxKeysProcessedPerState);
221221
OmLCExpiration.setTest(true);
222+
KeyLifecycleService.setTest(true);
222223
}
223224

224225
private void createSubject() throws Exception {
@@ -1083,7 +1084,7 @@ void testBucketRootScannedDirResume(BucketLayout layout) throws Exception {
10831084
// So key1 and key2 are skipped, key3 is deleted.
10841085
int expectedDeleted = 1;
10851086
GenericTestUtils.waitFor(() ->
1086-
(getDeletedKeyCount() - initialDeletedKeyCount) >= expectedDeleted, WAIT_CHECK_INTERVAL, 10000);
1087+
(getDeletedKeyCount() - initialDeletedKeyCount) == expectedDeleted, WAIT_CHECK_INTERVAL, 10000);
10871088

10881089
assertEquals(2, getKeyCount(layout) - initialKeyCount);
10891090
GenericTestUtils.waitFor(() ->

0 commit comments

Comments
 (0)