tags;
private Long expectedDataGeneration = null;
+ private Long versionId = null;
+ private boolean isDeleteMarker;
+ private boolean isNullVersion;
public Builder() {
this.acls = AclListBuilder.empty();
@@ -533,6 +580,9 @@ public Builder(OmKeyInfo obj) {
this.fileChecksum = obj.fileChecksum;
this.isFile = obj.isFile;
this.expectedDataGeneration = obj.expectedDataGeneration;
+ this.versionId = obj.versionId;
+ this.isDeleteMarker = obj.isDeleteMarker;
+ this.isNullVersion = obj.isNullVersion;
this.tags = MapBuilder.of(obj.tags);
obj.keyLocationVersions.forEach(keyLocationVersion ->
this.omKeyLocationInfoGroups.add(
@@ -704,6 +754,21 @@ public Builder setExpectedDataGeneration(Long existingGeneration) {
return this;
}
+ public Builder setVersionId(Long versionId) {
+ this.versionId = versionId;
+ return this;
+ }
+
+ public Builder setDeleteMarker(boolean deleteMarker) {
+ this.isDeleteMarker = deleteMarker;
+ return this;
+ }
+
+ public Builder setNullVersion(boolean nullVersion) {
+ this.isNullVersion = nullVersion;
+ return this;
+ }
+
@Override
protected void validate() {
super.validate();
@@ -855,6 +920,16 @@ private KeyInfo getProtobuf(boolean ignorePipeline, String fullKeyName,
if (ownerName != null) {
kb.setOwnerName(ownerName);
}
+ if (versionId != null) {
+ kb.setVersionId(versionId);
+ }
+ // only persisted when set, to keep records without versioning unchanged
+ if (isDeleteMarker) {
+ kb.setIsDeleteMarker(true);
+ }
+ if (isNullVersion) {
+ kb.setIsNullVersion(true);
+ }
return kb.build();
}
@@ -909,6 +984,15 @@ public static Builder builderFromProtobuf(KeyInfo keyInfo) {
if (keyInfo.hasOwnerName()) {
builder.setOwnerName(keyInfo.getOwnerName());
}
+ if (keyInfo.hasVersionId()) {
+ builder.setVersionId(keyInfo.getVersionId());
+ }
+ if (keyInfo.hasIsDeleteMarker()) {
+ builder.setDeleteMarker(keyInfo.getIsDeleteMarker());
+ }
+ if (keyInfo.hasIsNullVersion()) {
+ builder.setNullVersion(keyInfo.getIsNullVersion());
+ }
return builder;
}
@@ -946,7 +1030,10 @@ public boolean isKeyInfoSame(OmKeyInfo omKeyInfo, boolean checkPath,
Objects.equals(getMetadata(), omKeyInfo.getMetadata()) &&
Objects.equals(acls, omKeyInfo.acls) &&
Objects.equals(getTags(), omKeyInfo.getTags()) &&
- getObjectID() == omKeyInfo.getObjectID();
+ getObjectID() == omKeyInfo.getObjectID() &&
+ Objects.equals(versionId, omKeyInfo.versionId) &&
+ isDeleteMarker == omKeyInfo.isDeleteMarker &&
+ isNullVersion == omKeyInfo.isNullVersion;
if (isEqual && checkUpdateID) {
isEqual = getUpdateID() == omKeyInfo.getUpdateID();
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/PinnedFirstVersionIdGenerator.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/PinnedFirstVersionIdGenerator.java
new file mode 100644
index 000000000000..061c467651c9
--- /dev/null
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/PinnedFirstVersionIdGenerator.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Pins the first version of every key to {@link #FIRST_VERSION_ID} and uses the
+ * committing transaction's index for every later version, exactly like
+ * {@link TransactionIndexVersionIdGenerator}. Clusters configured with this
+ * generator can reference the first version of a key without listing it first.
+ *
+ * Holds no allocator state either: a key is on its first version exactly
+ * when keyTable holds no current version for it, which the write path looks up
+ * anyway.
+ *
+ *
The sentinel is smaller than any transaction index, so the first version
+ * sorts at the old end of the key's version sequence, as the versionedKeyTable
+ * layout requires.
+ *
+ *
Known trade-off: once every version of a key is permanently deleted, a
+ * recreated key takes the sentinel again, so an external reference to the first
+ * version resolves to the new content. Later versions are transaction indices
+ * and are never reused.
+ */
+public class PinnedFirstVersionIdGenerator implements VersionIdGenerator {
+
+ @Override
+ public long generateVersionId(long transactionLogIndex, boolean hasCurrentVersion) {
+ if (!hasCurrentVersion) {
+ return FIRST_VERSION_ID;
+ }
+ Preconditions.checkArgument(transactionLogIndex > FIRST_VERSION_ID,
+ "Transaction index " + transactionLogIndex
+ + " is a reserved versionId, expected greater than " + FIRST_VERSION_ID);
+ return transactionLogIndex;
+ }
+}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/TransactionIndexVersionIdGenerator.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/TransactionIndexVersionIdGenerator.java
new file mode 100644
index 000000000000..c94a7679e042
--- /dev/null
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/TransactionIndexVersionIdGenerator.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Uses the index of the committing transaction as the versionId, the default
+ * generator. Holds no allocator state, so a version costs no read or write
+ * beyond the commit itself.
+ */
+public class TransactionIndexVersionIdGenerator implements VersionIdGenerator {
+
+ @Override
+ public long generateVersionId(long transactionLogIndex, boolean hasCurrentVersion) {
+ Preconditions.checkArgument(transactionLogIndex > FIRST_VERSION_ID,
+ "Transaction index " + transactionLogIndex
+ + " is a reserved versionId, expected greater than " + FIRST_VERSION_ID);
+ return transactionLogIndex;
+ }
+}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/VersionIdGenerator.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/VersionIdGenerator.java
new file mode 100644
index 000000000000..083b423a6677
--- /dev/null
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/VersionIdGenerator.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.apache.hadoop.util.ReflectionUtils;
+
+/**
+ * Assigns the versionId of an object version when the version is committed.
+ *
+ *
Deployments differ in whether they need a version identity that can be
+ * constructed without listing, so the implementation is chosen per cluster
+ * through {@link OMConfigKeys#OZONE_OM_VERSIONING_VERSION_ID_GENERATOR}.
+ * Implementations must be public, have a public no-argument constructor, and
+ * satisfy the constraints that the versionedKeyTable layout and version
+ * promotion rely on:
+ *
+ *
+ * - ids strictly increase within a key: for one generator, the id of a
+ * version created later is always greater than the id of every version of
+ * that key created before it, never equal and never smaller. The
+ * versionedKeyTable ordering and version promotion depend on this;
+ * - an id is assigned once when the version is created and never changes
+ * afterwards, so that external references stay valid;
+ * - {@link #UNSET_VERSION_ID} and {@link #FIRST_VERSION_ID} are reserved
+ * and are never generated.
+ *
+ *
+ * The first constraint binds one generator, not a sequence of them: the
+ * generator is cluster-wide and may be changed on a running cluster, and the
+ * new one knows nothing of the ids the old one handed out.
+ * {@code VersionIdAllocator} enforces the constraint at commit time and refuses
+ * a write whose id does not come after the key's current version, so a change
+ * of generator fails loudly on affected keys instead of corrupting their
+ * version order.
+ */
+public interface VersionIdGenerator {
+
+ /**
+ * Unset value of the optional versionId field, carried by records written
+ * before versioning existed. Reserved, and never returned by a generator.
+ *
+ *
This is not the id of the null version: a null version carries a
+ * normally generated id like any other version and is identified by the
+ * {@code isNullVersion} attribute instead. Pinning it to a fixed low value
+ * would misorder a null created between two versioned writes, which is the
+ * middle version of the key rather than its oldest.
+ */
+ long UNSET_VERSION_ID = 0L;
+
+ /**
+ * Reserved id of the pinned first version of a key. Only assigned by
+ * generators that pin the first version of a key; it is smaller than any
+ * transaction index, so such a version sorts at the old end of the key's
+ * version sequence.
+ */
+ long FIRST_VERSION_ID = 1L;
+
+ /**
+ * Generates the versionId to freeze on a version being committed.
+ *
+ * @param transactionLogIndex index of the committing OM Ratis transaction
+ * @param hasCurrentVersion whether keyTable already holds a current version
+ * of the key being committed. The write path looks the current version up
+ * anyway, so generators that treat the first version of a key specially
+ * need no read of their own.
+ * @return the versionId of the new version
+ */
+ long generateVersionId(long transactionLogIndex, boolean hasCurrentVersion);
+
+ /**
+ * Instantiates the generator configured for this cluster.
+ *
+ * @throws RuntimeException if the configured class cannot be instantiated
+ */
+ static VersionIdGenerator fromConfiguration(ConfigurationSource conf) {
+ Class extends VersionIdGenerator> generatorClass = conf.getClass(
+ OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR,
+ OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR_DEFAULT,
+ VersionIdGenerator.class);
+ return ReflectionUtils.newInstance(generatorClass, null);
+ }
+}
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketArgs.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketArgs.java
index 147255b3b573..29715e577f35 100644
--- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketArgs.java
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketArgs.java
@@ -65,6 +65,57 @@ public void testQuotaIsSetFlagsAreCorrectlySet() {
assertTrue(argsFromProto.hasQuotaInNamespace());
}
+ @Test
+ public void testVersioningStatusIsSetCorrectly() {
+ OmBucketArgs bucketArgs = OmBucketArgs.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("volume")
+ .build();
+
+ OmBucketArgs argsFromProto = OmBucketArgs.getFromProtobuf(
+ bucketArgs.getProtobuf());
+
+ // absent means "not being changed"
+ assertNull(argsFromProto.getVersioningStatus());
+
+ bucketArgs = OmBucketArgs.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("volume")
+ .setVersioningStatus(BucketVersioningStatus.SUSPENDED)
+ .build();
+
+ argsFromProto = OmBucketArgs.getFromProtobuf(
+ bucketArgs.getProtobuf());
+
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ argsFromProto.getVersioningStatus());
+ }
+
+ @Test
+ public void testMaxVersionsIsSetCorrectly() {
+ OmBucketArgs bucketArgs = OmBucketArgs.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("volume")
+ .build();
+
+ OmBucketArgs argsFromProto = OmBucketArgs.getFromProtobuf(
+ bucketArgs.getProtobuf());
+
+ // absent means "not being changed"
+ assertNull(argsFromProto.getMaxVersions());
+
+ bucketArgs = OmBucketArgs.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("volume")
+ .setMaxVersions(0)
+ .build();
+
+ argsFromProto = OmBucketArgs.getFromProtobuf(bucketArgs.getProtobuf());
+
+ // 0 means unlimited, which is a change like any other
+ assertEquals(0, argsFromProto.getMaxVersions());
+ }
+
@Test
public void testDefaultReplicationConfigIsSetCorrectly() {
OmBucketArgs bucketArgs = OmBucketArgs.newBuilder()
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketInfo.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketInfo.java
index 857103a20c0d..e20fc3997d81 100644
--- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketInfo.java
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmBucketInfo.java
@@ -29,6 +29,7 @@
import org.apache.hadoop.hdds.client.ReplicationConfig;
import org.apache.hadoop.hdds.client.ReplicationType;
import org.apache.hadoop.hdds.protocol.StorageType;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.ozone.OzoneAcl;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
@@ -54,6 +55,133 @@ public void protobufConversion() {
OmBucketInfo.getFromProtobuf(bucket.getProtobuf()));
}
+ @Test
+ public void legacyFlagDoesNotImplyAnS3VersioningStatus() {
+ // Records written before the versioningStatus field existed deserialize
+ // unchanged and keep carrying the legacy flag alone. The legacy flag
+ // selects the in-record block version list, which is a different feature
+ // from S3 versioning, so it must not be promoted to a status.
+ OzoneManagerProtocolProtos.BucketInfo oldRecord =
+ OzoneManagerProtocolProtos.BucketInfo.newBuilder()
+ .setVolumeName("vol1")
+ .setBucketName("bucket")
+ .setIsVersionEnabled(false)
+ .setStorageType(HddsProtos.StorageTypeProto.DISK)
+ .build();
+ OmBucketInfo bucket = OmBucketInfo.getFromProtobuf(oldRecord);
+ assertFalse(bucket.hasVersioningStatus());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ bucket.getVersioningStatus());
+ assertFalse(bucket.getIsVersionEnabled());
+ assertEquals(bucket, OmBucketInfo.getFromProtobuf(bucket.getProtobuf()));
+
+ oldRecord = oldRecord.toBuilder().setIsVersionEnabled(true).build();
+ bucket = OmBucketInfo.getFromProtobuf(oldRecord);
+ assertFalse(bucket.hasVersioningStatus());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ bucket.getVersioningStatus());
+ assertTrue(bucket.getIsVersionEnabled());
+ // the absent status survives the round trip, so a re-serialized legacy
+ // record is still distinguishable from an S3-versioned one
+ assertFalse(bucket.getProtobuf().hasVersioningStatus());
+ assertEquals(bucket, OmBucketInfo.getFromProtobuf(bucket.getProtobuf()));
+ }
+
+ @Test
+ public void versioningStatusProtobufConversion() {
+ // SUSPENDED is not representable by the legacy flag alone, so it must
+ // survive a proto round trip via the new field.
+ OmBucketInfo bucket = OmBucketInfo.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("vol1")
+ .setVersioningStatus(BucketVersioningStatus.SUSPENDED)
+ .build();
+ assertFalse(bucket.getIsVersionEnabled());
+
+ OmBucketInfo recovered = OmBucketInfo.getFromProtobuf(bucket.getProtobuf());
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ recovered.getVersioningStatus());
+ assertFalse(recovered.getIsVersionEnabled());
+ assertEquals(bucket, recovered);
+ }
+
+ @Test
+ public void builderKeepsVersioningStatusAndLegacyFlagInSync() {
+ OmBucketInfo.Builder builder = OmBucketInfo.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("vol1");
+
+ // no status set at all
+ assertFalse(builder.build().hasVersioningStatus());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ builder.build().getVersioningStatus());
+
+ // the legacy flag sets only itself: no status is derived from it
+ builder.setIsVersionEnabled(true);
+ assertFalse(builder.build().hasVersioningStatus());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ builder.build().getVersioningStatus());
+ assertTrue(builder.build().getIsVersionEnabled());
+
+ // an explicit status is authoritative and drives the legacy flag
+ builder.setVersioningStatus(BucketVersioningStatus.SUSPENDED);
+ assertTrue(builder.build().hasVersioningStatus());
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ builder.build().getVersioningStatus());
+ assertFalse(builder.build().getIsVersionEnabled());
+
+ // ENABLED shows up as true to clients that only know the legacy flag
+ builder.setVersioningStatus(BucketVersioningStatus.ENABLED);
+ assertTrue(builder.build().getIsVersionEnabled());
+
+ // a null status is a no-op (records without the new field)
+ builder.setVersioningStatus(null);
+ assertEquals(BucketVersioningStatus.ENABLED,
+ builder.build().getVersioningStatus());
+ }
+
+ @Test
+ public void maxVersionsProtobufConversion() {
+ // A bucket that sets no limit of its own stays distinguishable from one
+ // that set a limit explicitly, since the cluster default applies only to
+ // the former.
+ OmBucketInfo bucket = OmBucketInfo.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("vol1")
+ .build();
+ assertNull(bucket.getMaxVersions());
+ assertFalse(bucket.getProtobuf().hasMaxVersions());
+ assertNull(OmBucketInfo.getFromProtobuf(bucket.getProtobuf())
+ .getMaxVersions());
+
+ bucket = bucket.toBuilder().setMaxVersions(5).build();
+ OmBucketInfo recovered = OmBucketInfo.getFromProtobuf(bucket.getProtobuf());
+ assertEquals(5, recovered.getMaxVersions());
+ assertEquals(bucket, recovered);
+
+ // 0 is "unlimited", which is a set value rather than an absent one
+ bucket = bucket.toBuilder().setMaxVersions(0).build();
+ recovered = OmBucketInfo.getFromProtobuf(bucket.getProtobuf());
+ assertEquals(0, recovered.getMaxVersions());
+ assertTrue(bucket.getProtobuf().hasMaxVersions());
+ }
+
+ @Test
+ public void noncurrentVersionExpirationProtobufConversion() {
+ OmBucketInfo bucket = OmBucketInfo.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("vol1")
+ .build();
+ // expiration is opt-in, so an unset bucket retains versions forever
+ assertNull(bucket.getNoncurrentVersionExpirationDays());
+ assertFalse(bucket.getProtobuf().hasNoncurrentVersionExpirationDays());
+
+ bucket = bucket.toBuilder().setNoncurrentVersionExpirationDays(30).build();
+ OmBucketInfo recovered = OmBucketInfo.getFromProtobuf(bucket.getProtobuf());
+ assertEquals(30, recovered.getNoncurrentVersionExpirationDays());
+ assertEquals(bucket, recovered);
+ }
+
@Test
public void protobufConversionOfBucketLink() {
OmBucketInfo bucket = OmBucketInfo.newBuilder()
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmKeyInfo.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmKeyInfo.java
index 285853a3a766..12846bece1c4 100644
--- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmKeyInfo.java
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmKeyInfo.java
@@ -25,6 +25,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
@@ -116,6 +117,40 @@ public void getProtobufMessageEC() throws IOException {
assertEquals(2, config.getParity());
}
+ @Test
+ public void protobufConversionWithVersioningFields() {
+ // records without versioning fields keep them unset after a round trip
+ OmKeyInfo key = createOmKeyInfo(
+ RatisReplicationConfig.getInstance(ReplicationFactor.THREE));
+ OzoneManagerProtocolProtos.KeyInfo proto = key.getProtobuf(ClientVersion.CURRENT_VERSION);
+ assertFalse(proto.hasVersionId());
+ assertFalse(proto.hasIsDeleteMarker());
+ assertFalse(proto.hasIsNullVersion());
+ OmKeyInfo recovered = OmKeyInfo.getFromProtobuf(proto);
+ assertNull(recovered.getVersionId());
+ assertFalse(recovered.isDeleteMarker());
+ assertFalse(recovered.isNullVersion());
+
+ // versioning fields survive a round trip and the copy constructor
+ key = createOmKeyInfo(RatisReplicationConfig.getInstance(ReplicationFactor.THREE))
+ .toBuilder()
+ .setVersionId(4242L)
+ .setDeleteMarker(true)
+ .setNullVersion(true)
+ .build();
+ recovered = OmKeyInfo.getFromProtobuf(key.getProtobuf(ClientVersion.CURRENT_VERSION));
+ assertEquals(4242L, recovered.getVersionId());
+ assertTrue(recovered.isDeleteMarker());
+ assertTrue(recovered.isNullVersion());
+
+ // records differing only in a versioning field must not compare equal
+ OmKeyInfo plain = createOmKeyInfo(
+ RatisReplicationConfig.getInstance(ReplicationFactor.THREE));
+ assertNotEquals(plain, plain.toBuilder().setVersionId(1L).build());
+ assertNotEquals(plain, plain.toBuilder().setDeleteMarker(true).build());
+ assertNotEquals(plain, plain.toBuilder().setNullVersion(true).build());
+ }
+
private OmKeyInfo createOmKeyInfo(ReplicationConfig replicationConfig) {
return new Builder()
.setKeyName("key1")
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestVersionIdGenerator.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestVersionIdGenerator.java
new file mode 100644
index 000000000000..612b42872927
--- /dev/null
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestVersionIdGenerator.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import static org.apache.hadoop.ozone.om.helpers.VersionIdGenerator.FIRST_VERSION_ID;
+import static org.apache.hadoop.ozone.om.helpers.VersionIdGenerator.UNSET_VERSION_ID;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.stream.Stream;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests the constraints every {@link VersionIdGenerator} has to satisfy, and
+ * how the cluster-wide generator is selected.
+ */
+public class TestVersionIdGenerator {
+
+ /** The first transaction index that is not a reserved versionId. */
+ private static final long FIRST_USABLE_INDEX = FIRST_VERSION_ID + 1;
+
+ /** Every generator shipped with Ozone; extended as generators are added. */
+ static Stream generators() {
+ return Stream.of(new TransactionIndexVersionIdGenerator(),
+ new PinnedFirstVersionIdGenerator());
+ }
+
+ @ParameterizedTest
+ @MethodSource("generators")
+ void generatedIdsStrictlyIncreaseWithinAKey(VersionIdGenerator generator) {
+ // The contract every generator owes VersionIdAllocator: over the life of a
+ // key, ids only ever go up, starting from the key's first version.
+ long previous = generator.generateVersionId(FIRST_USABLE_INDEX, false);
+ for (long index = FIRST_USABLE_INDEX + 1; index < 100; index++) {
+ long current = generator.generateVersionId(index, true);
+ assertTrue(previous < current,
+ "versionId " + current + " generated for transaction " + index
+ + " does not exceed " + previous);
+ previous = current;
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("generators")
+ void generatedIdsNeverCollideWithReservedIds(VersionIdGenerator generator) {
+ for (long index = FIRST_USABLE_INDEX; index < 100; index++) {
+ assertNotEquals(UNSET_VERSION_ID, generator.generateVersionId(index, true));
+ assertNotEquals(UNSET_VERSION_ID, generator.generateVersionId(index, false));
+ }
+ // A transaction index that lands on a reserved id is a misconfiguration of
+ // the Ratis log rather than something to silently work around.
+ assertThrows(IllegalArgumentException.class,
+ () -> generator.generateVersionId(UNSET_VERSION_ID, true));
+ assertThrows(IllegalArgumentException.class,
+ () -> generator.generateVersionId(FIRST_VERSION_ID, true));
+ }
+
+ @ParameterizedTest
+ @MethodSource("generators")
+ void generationIsDeterministic(VersionIdGenerator generator) {
+ assertEquals(generator.generateVersionId(4242, true),
+ generator.generateVersionId(4242, true));
+ assertEquals(generator.generateVersionId(4242, false),
+ generator.generateVersionId(4242, false));
+ }
+
+ @Test
+ void reservedIdsDoNotCollide() {
+ assertNotEquals(UNSET_VERSION_ID, FIRST_VERSION_ID);
+ }
+
+ @Test
+ void transactionIndexGeneratorIsTheDefault() {
+ assertInstanceOf(TransactionIndexVersionIdGenerator.class,
+ VersionIdGenerator.fromConfiguration(new OzoneConfiguration()));
+ }
+
+ @Test
+ void transactionIndexIgnoresWhetherTheKeyHasACurrentVersion() {
+ VersionIdGenerator generator = new TransactionIndexVersionIdGenerator();
+
+ assertEquals(7, generator.generateVersionId(7, false));
+ assertEquals(7, generator.generateVersionId(7, true));
+ }
+
+ @Test
+ void pinnedFirstPinsOnlyTheFirstVersionOfAKey() {
+ VersionIdGenerator generator = new PinnedFirstVersionIdGenerator();
+
+ assertEquals(FIRST_VERSION_ID, generator.generateVersionId(7, false));
+ assertEquals(7, generator.generateVersionId(7, true));
+ }
+
+ @Test
+ void pinnedFirstSentinelIsOlderThanEveryTransactionIndex() {
+ VersionIdGenerator generator = new PinnedFirstVersionIdGenerator();
+ long first = generator.generateVersionId(FIRST_USABLE_INDEX, false);
+
+ for (long index = FIRST_USABLE_INDEX; index < 100; index++) {
+ assertTrue(first < generator.generateVersionId(index, true),
+ "sentinel " + first + " is not older than the version at transaction " + index);
+ }
+ }
+
+ @Test
+ void pinnedFirstSentinelIsNotTheUnsetId() {
+ assertNotEquals(UNSET_VERSION_ID,
+ new PinnedFirstVersionIdGenerator().generateVersionId(7, false));
+ }
+
+ @Test
+ void pinnedFirstGeneratorIsSelectableByConfiguration() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR,
+ PinnedFirstVersionIdGenerator.class.getName());
+
+ assertInstanceOf(PinnedFirstVersionIdGenerator.class,
+ VersionIdGenerator.fromConfiguration(conf));
+ }
+
+ @Test
+ void generatorClassIsReadFromConfiguration() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR,
+ TransactionIndexVersionIdGenerator.class.getName());
+
+ assertInstanceOf(TransactionIndexVersionIdGenerator.class,
+ VersionIdGenerator.fromConfiguration(conf));
+ }
+
+ @Test
+ void unknownGeneratorClassIsRejected() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR,
+ "org.apache.hadoop.ozone.om.helpers.NoSuchVersionIdGenerator");
+
+ assertThrows(RuntimeException.class, () -> VersionIdGenerator.fromConfiguration(conf));
+ }
+
+ @Test
+ void generatorClassNotImplementingTheInterfaceIsRejected() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR,
+ String.class.getName());
+
+ assertThrows(RuntimeException.class, () -> VersionIdGenerator.fromConfiguration(conf));
+ }
+}
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index bb8f54c79c56..89723e436e2b 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -162,6 +162,9 @@ enum Type {
PutBucketTagging = 144;
GetBucketTagging = 145;
DeleteBucketTagging = 146;
+
+ // Submitted by VersionCleanupService to reclaim noncurrent object versions.
+ ReclaimObjectVersions = 147;
}
enum SafeMode {
@@ -321,6 +324,8 @@ message OMRequest {
optional GetBucketTaggingRequest getBucketTaggingRequest = 146;
// TODO: DeleteBucketTagging — clears tags on target bucket (link resolves in OM).
optional DeleteBucketTaggingRequest deleteBucketTaggingRequest = 147;
+
+ optional ReclaimObjectVersionsRequest reclaimObjectVersionsRequest = 148;
}
message OMResponse {
@@ -463,6 +468,8 @@ message OMResponse {
optional GetBucketTaggingResponse getBucketTaggingResponse = 145;
// TODO: Empty ack after OM clears BucketInfo.tags.
optional DeleteBucketTaggingResponse deleteBucketTaggingResponse = 146;
+
+ optional ReclaimObjectVersionsResponse reclaimObjectVersionsResponse = 147;
}
enum Status {
@@ -598,6 +605,10 @@ enum Status {
ETAG_NOT_AVAILABLE = 100;
ATOMIC_WRITE_CONFLICT = 101;
+
+ // The addressed version exists but is a delete marker. Distinct from
+ // KEY_NOT_FOUND: the S3 Gateway maps it to 405, not 404.
+ KEY_IS_DELETE_MARKER = 102;
}
/**
@@ -808,6 +819,22 @@ message BucketInfo {
optional uint64 snapshotUsedNamespace = 22;
// TODO: S3 bucket tags persisted in OM DB; set by PutBucketTagging, read by GetBucketTagging.
repeated hadoop.hdds.KeyValue tags = 23;
+ // S3-compatible object versioning status. When absent, derived from
+ // isVersionEnabled (true -> VERSIONING_ENABLED, false -> UNVERSIONED).
+ optional BucketVersioningStatusProto versioningStatus = 24;
+ // Maximum number of versions retained per key, counting the current
+ // version and delete markers. 0 means unlimited; when absent the cluster
+ // default applies. Versions beyond the limit are reclaimed oldest-first by
+ // VersionCleanupService.
+ optional uint32 maxVersions = 25;
+ // Days a version is retained after becoming noncurrent, counted from the
+ // moment the version that superseded it was committed, as S3 lifecycle's
+ // NoncurrentDays is. 0 or absent means versions are retained forever.
+ optional uint32 noncurrentVersionExpirationDays = 26;
+ // Whether a key whose only remaining version is a delete marker is removed
+ // entirely. Such a marker is invisible to reads and cannot be addressed by
+ // versionId, so nothing else can ever remove it. Enabled when absent.
+ optional bool expiredDeleteMarkerCleanup = 27;
}
enum BucketLayoutProto {
@@ -816,6 +843,17 @@ enum BucketLayoutProto {
OBJECT_STORE = 3;
}
+/**
+ * S3-compatible bucket versioning state machine:
+ * UNVERSIONED -> VERSIONING_ENABLED <-> VERSIONING_SUSPENDED.
+ * Once enabled, a bucket can never return to UNVERSIONED.
+ */
+enum BucketVersioningStatusProto {
+ UNVERSIONED = 1;
+ VERSIONING_ENABLED = 2;
+ VERSIONING_SUSPENDED = 3;
+}
+
/**
* Cipher suite.
*/
@@ -883,6 +921,15 @@ message BucketArgs {
optional BucketEncryptionInfoProto bekInfo = 12;
// TODO: Tag payload for PutBucketTagging only.
repeated hadoop.hdds.KeyValue tags = 13;
+ // S3-compatible object versioning status. Takes precedence over
+ // isVersionEnabled when both are set.
+ optional BucketVersioningStatusProto versioningStatus = 14;
+ // Maximum number of versions retained per key; 0 means unlimited.
+ optional uint32 maxVersions = 15;
+ // Days a version is retained after becoming noncurrent; 0 means forever.
+ optional uint32 noncurrentVersionExpirationDays = 16;
+ // Whether keys left with only a delete marker are removed entirely.
+ optional bool expiredDeleteMarkerCleanup = 17;
}
message PrefixInfo {
@@ -1124,6 +1171,14 @@ message KeyArgs {
// the given ETag for the operation to succeed. This is used for
// S3 conditional writes with the If-Match header.
optional string expectedETag = 24;
+
+ // S3-compatible object versioning: addresses one specific version of the key
+ // instead of its current version. nullVersion selects the key's null version
+ // slot, which cannot be addressed by id because a null version carries a
+ // normally generated versionId like any other version. At most one of the
+ // two may be set.
+ optional uint64 versionId = 25;
+ optional bool nullVersion = 26;
}
message KeyLocation {
@@ -1217,6 +1272,15 @@ message KeyInfo {
// This allows a key to be created an committed atomically if the original has not
// been modified.
optional uint64 expectedDataGeneration = 22;
+ // S3-compatible object versioning fields. versionId identifies an object
+ // version: assigned once from the committing transaction's index when the
+ // version is created, then frozen. A delete marker is a record with
+ // isDeleteMarker set and no data blocks. isNullVersion marks the single
+ // overwritable "null version" slot per key (writes while versioning is
+ // suspended, or objects that predate enabling versioning).
+ optional uint64 versionId = 23;
+ optional bool isDeleteMarker = 24;
+ optional bool isNullVersion = 25;
}
// KeyInfoProtoLight is a lightweight subset of KeyInfo message containing
@@ -1537,6 +1601,27 @@ message OpenKey {
optional uint64 clientID = 2 [deprecated=true];
}
+/**
+ * Noncurrent object versions of one bucket that VersionCleanupService selected
+ * for reclamation, addressed by their versionedKeyTable dbKeys.
+ */
+message ObjectVersionsBucket {
+ required string volumeName = 1;
+ required string bucketName = 2;
+ repeated string versionKeys = 3;
+ // keyTable dbKeys of keys whose only remaining version is a delete marker.
+ // These leave the keyTable rather than the versionedKeyTable, so the key
+ // disappears entirely.
+ repeated string markerKeys = 4;
+}
+
+message ReclaimObjectVersionsRequest {
+ repeated ObjectVersionsBucket versionsPerBucket = 1;
+}
+
+message ReclaimObjectVersionsResponse {
+}
+
message OMTokenProto {
enum Type {
DELEGATION_TOKEN = 1;
diff --git a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java
index be66ffc195b5..98c385e50239 100644
--- a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java
+++ b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java
@@ -58,6 +58,7 @@
import org.apache.hadoop.ozone.om.lock.HierarchicalResourceLockManager;
import org.apache.hadoop.ozone.om.lock.IOzoneManagerLock;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
import org.apache.hadoop.ozone.storage.proto.OzoneManagerStorageProtos.PersistedUserVolumeInfo;
@@ -172,6 +173,24 @@ public interface OMMetadataManager extends DBStoreHAManager, AutoCloseable {
*/
String getOzoneKey(String volume, String bucket, String key);
+ /**
+ * Given a volume, bucket, key and versionId, return the corresponding
+ * versionedKeyTable DB key: the versionId is appended as fixed-width hex of
+ * (Long.MAX_VALUE - versionId), so all versions of a key are adjacent and
+ * ordered newest first.
+ */
+ String getVersionedOzoneKey(String volume, String bucket, String key, long versionId);
+
+ /**
+ * Prefix under which all noncurrent versions of the given key are stored in
+ * the versionedKeyTable. The key name is separated from the versionId suffix
+ * by OM_VERSIONED_KEY_SEPARATOR rather than OM_KEY_PREFIX, so that a key's
+ * versions stay contiguous under this prefix and sort before any key nested
+ * under it: seeking this prefix yields exactly that key's versions, newest
+ * first.
+ */
+ String getVersionedOzoneKeyPrefix(String volume, String bucket, String key);
+
/**
* Get DB key for a key or prefix in an FSO bucket given existing
* volume and bucket names.
@@ -359,6 +378,68 @@ List listVolumes(String userName, String prefix,
ExpiredOpenKeys getExpiredOpenKeys(Duration expireThreshold, int count,
BucketLayout bucketLayout, Duration leaseThreshold) throws IOException;
+ /**
+ * Returns the noncurrent object versions that exceed their bucket's
+ * maxVersions, oldest first, grouped by volume and bucket. The limit counts a
+ * key's current version and its delete markers along with the noncurrent
+ * ones, so a key with maxVersions {@code n} keeps at most {@code n - 1}
+ * noncurrent versions.
+ *
+ * Only buckets that have ever been versioned are examined, and a bucket
+ * whose effective limit is 0 (unlimited) is skipped entirely.
+ *
+ * @param defaultMaxVersions the limit applied to a bucket that sets none of
+ * its own; 0 means unlimited.
+ * @param limitPerTask the maximum number of versions to return.
+ * @return a {@link List} of {@link ObjectVersionsBucket}, the versions to
+ * reclaim, grouped by volume and bucket.
+ */
+ List getVersionsToReclaim(int defaultMaxVersions,
+ int limitPerTask) throws IOException;
+
+ /**
+ * Result of one pass of {@link #getExpiredDeleteMarkers}: the markers found,
+ * and where the next pass should resume.
+ */
+ class ExpiredDeleteMarkers {
+ private final List markersPerBucket;
+ private final String nextStartKey;
+
+ public ExpiredDeleteMarkers(List markersPerBucket,
+ String nextStartKey) {
+ this.markersPerBucket = markersPerBucket;
+ this.nextStartKey = nextStartKey;
+ }
+
+ public List getMarkersPerBucket() {
+ return markersPerBucket;
+ }
+
+ /** Where to resume, or null when the table was walked to the end. */
+ public String getNextStartKey() {
+ return nextStartKey;
+ }
+ }
+
+ /**
+ * Returns the keys whose only remaining version is a delete marker, grouped
+ * by volume and bucket. Such a key is invisible to reads and its marker
+ * carries no versionId to address it by, so nothing other than this removes
+ * it.
+ *
+ * There is no index of delete markers, so this walks the keyTable. The
+ * walk is bounded by {@code scanBudget} and resumes from {@code startKey},
+ * so one run cannot iterate an unbounded number of keys and successive runs
+ * still make progress; passing a null {@code startKey} starts from the
+ * beginning.
+ *
+ * @param startKey where to resume the walk, or null to start over.
+ * @param scanBudget the maximum number of keyTable entries to examine.
+ * @param limit the maximum number of markers to return.
+ */
+ ExpiredDeleteMarkers getExpiredDeleteMarkers(String startKey, int scanBudget,
+ int limit) throws IOException;
+
/**
* Returns the names of up to {@code count} MPU key whose age is greater
* than or equal to {@code expireThreshold}.
@@ -405,6 +486,14 @@ List getExpiredMultipartUploads(
Table getKeyTable(BucketLayout bucketLayout);
+ /**
+ * Returns the versionedKeyTable holding noncurrent object versions
+ * (including noncurrent delete markers) of versioning-enabled buckets.
+ *
+ * @return versionedKeyTable.
+ */
+ Table getVersionedKeyTable();
+
/**
* Returns the FileTable.
*
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMSystemAction.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMSystemAction.java
index 3fbceeeae9c7..177e6e889f54 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMSystemAction.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMSystemAction.java
@@ -25,6 +25,7 @@ public enum OMSystemAction implements AuditAction {
STARTUP,
LEADER_CHANGE,
OPEN_KEY_CLEANUP,
+ OBJECT_VERSION_CLEANUP,
DB_CHECKPOINT_INSTALL,
DIRECTORY_DELETION,
KEY_DELETION,
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/DeletingServiceMetrics.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/DeletingServiceMetrics.java
index 56ccdac79b6d..42419dd0799f 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/DeletingServiceMetrics.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/DeletingServiceMetrics.java
@@ -70,6 +70,15 @@ public final class DeletingServiceMetrics {
private MutableGaugeLong numKeysPurged;
@Metric("Total no. of rename entries purged")
private MutableGaugeLong numRenameEntriesPurged;
+ /*
+ * Object version reclamation metrics. The submitted and reclaimed counts
+ * differ by the versions that were permanently deleted or promoted between
+ * VersionCleanupService selecting them and the request being applied.
+ */
+ @Metric("Total no. of object versions sent for reclamation")
+ private MutableGaugeLong numObjectVersionsSentForReclaim;
+ @Metric("Total no. of object versions reclaimed")
+ private MutableGaugeLong numObjectVersionsReclaimed;
/*
* Key deletion metrics in the last 24 hours.
@@ -198,6 +207,14 @@ public void incrNumKeysSentForPurge(long keysPurge) {
this.numKeysSentForPurge.incr(keysPurge);
}
+ public void incrNumObjectVersionsSentForReclaim(long versions) {
+ this.numObjectVersionsSentForReclaim.incr(versions);
+ }
+
+ public void incrNumObjectVersionsReclaimed(long versions) {
+ this.numObjectVersionsReclaimed.incr(versions);
+ }
+
public void incrNumDirPurged(long dirPurged) {
this.numDirsPurged.incr(dirPurged);
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManager.java
index fdf4172c71b2..6c84311c29e6 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManager.java
@@ -43,6 +43,7 @@
import org.apache.hadoop.ozone.om.service.SnapshotDeletingService;
import org.apache.hadoop.ozone.om.snapshot.defrag.SnapshotDefragService;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
import org.apache.ratis.util.function.CheckedFunction;
/**
@@ -226,6 +227,30 @@ ExpiredOpenKeys getExpiredOpenKeys(Duration expireThreshold, int count,
List getExpiredMultipartUploads(
Duration expireThreshold, int maxParts) throws IOException;
+ /**
+ * Returns the noncurrent object versions that exceed their bucket's
+ * maxVersions, oldest first, grouped by volume and bucket.
+ *
+ * @param defaultMaxVersions the limit applied to a bucket that sets none of
+ * its own; 0 means unlimited.
+ * @param limitPerTask the maximum number of versions to return.
+ * @return a {@link List} of {@link ObjectVersionsBucket}, the versions to
+ * reclaim, grouped by volume and bucket.
+ */
+ List getVersionsToReclaim(int defaultMaxVersions,
+ int limitPerTask) throws IOException;
+
+ /**
+ * Returns the keys whose only remaining version is a delete marker, grouped
+ * by volume and bucket, together with where the next pass should resume.
+ *
+ * @param startKey where to resume the keyTable walk, or null to start over.
+ * @param scanBudget the maximum number of keyTable entries to examine.
+ * @param limit the maximum number of markers to return.
+ */
+ OMMetadataManager.ExpiredDeleteMarkers getExpiredDeleteMarkers(
+ String startKey, int scanBudget, int limit) throws IOException;
+
/**
* Look up an existing key from the OM table and retrieve the tags from
* the key info.
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
index 3232f9b1ff33..ff3c1cd613c7 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
@@ -58,6 +58,10 @@
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_OPEN_KEY_CLEANUP_SERVICE_INTERVAL_DEFAULT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_OPEN_KEY_CLEANUP_SERVICE_TIMEOUT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_OPEN_KEY_CLEANUP_SERVICE_TIMEOUT_DEFAULT;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_VERSION_CLEANUP_SERVICE_INTERVAL;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_VERSION_CLEANUP_SERVICE_INTERVAL_DEFAULT;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_VERSION_CLEANUP_SERVICE_TIMEOUT;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_VERSION_CLEANUP_SERVICE_TIMEOUT_DEFAULT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_DEEP_CLEANING_ENABLED;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_DEEP_CLEANING_ENABLED_DEFAULT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_DEFRAG_SERVICE_INTERVAL;
@@ -178,8 +182,10 @@
import org.apache.hadoop.ozone.om.service.MultipartUploadCleanupService;
import org.apache.hadoop.ozone.om.service.OpenKeyCleanupService;
import org.apache.hadoop.ozone.om.service.SnapshotDeletingService;
+import org.apache.hadoop.ozone.om.service.VersionCleanupService;
import org.apache.hadoop.ozone.om.snapshot.defrag.SnapshotDefragService;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.PartKeyInfo;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.hadoop.ozone.security.acl.OzoneObj;
@@ -220,6 +226,7 @@ public class KeyManagerImpl implements KeyManager {
private BackgroundService openKeyCleanupService;
private BackgroundService multipartUploadCleanupService;
+ private BackgroundService versionCleanupService;
private DNSToSwitchMapping dnsToSwitchMapping;
private CompactionService compactionService;
@@ -361,6 +368,20 @@ public void start(OzoneConfiguration configuration) {
multipartUploadCleanupService.start();
}
+ if (versionCleanupService == null) {
+ long serviceInterval = configuration.getTimeDuration(
+ OZONE_OM_VERSION_CLEANUP_SERVICE_INTERVAL,
+ OZONE_OM_VERSION_CLEANUP_SERVICE_INTERVAL_DEFAULT,
+ TimeUnit.MILLISECONDS);
+ long serviceTimeout = configuration.getTimeDuration(
+ OZONE_OM_VERSION_CLEANUP_SERVICE_TIMEOUT,
+ OZONE_OM_VERSION_CLEANUP_SERVICE_TIMEOUT_DEFAULT,
+ TimeUnit.MILLISECONDS);
+ versionCleanupService = new VersionCleanupService(serviceInterval,
+ TimeUnit.MILLISECONDS, serviceTimeout, ozoneManager, configuration);
+ versionCleanupService.start();
+ }
+
Class extends DNSToSwitchMapping> dnsToSwitchMappingClass =
configuration.getClass(
ScmConfigKeys.NET_TOPOLOGY_NODE_SWITCH_MAPPING_IMPL_KEY,
@@ -512,6 +533,10 @@ public void stop() {
multipartUploadCleanupService.shutdown();
multipartUploadCleanupService = null;
}
+ if (versionCleanupService != null) {
+ versionCleanupService.shutdown();
+ versionCleanupService = null;
+ }
if (compactionService != null) {
compactionService.shutdown();
compactionService = null;
@@ -601,15 +626,31 @@ private OmKeyInfo readKeyInfo(OmKeyArgs args, BucketLayout bucketLayout)
bucketLayout);
if (bucketLayout.isFileSystemOptimized()) {
+ if (args.addressesVersion()) {
+ throw new OMException("Object versioning is only supported on "
+ + BucketLayout.OBJECT_STORE + " buckets",
+ ResultCodes.NOT_SUPPORTED_OPERATION);
+ }
value = getOmKeyInfoFSO(volumeName, bucketName, keyName);
} else {
value = getOmKeyInfo(volumeName, bucketName, keyName, bucketLayout);
+ if (args.addressesVersion()) {
+ value = getAddressedVersion(args, volumeName, bucketName, keyName,
+ value);
+ }
if (value != null) {
// For Legacy & OBS buckets, any key is a file by default. This is to
// keep getKeyInfo compatible with OFS clients.
value.setFile(true);
}
}
+ if (value != null && value.isDeleteMarker()) {
+ // Addressing a delete marker by version is answered with 405 by S3,
+ // while a request that lands on a current marker is a plain 404.
+ throw new OMException("Key: " + keyName + " is a delete marker",
+ args.addressesVersion()
+ ? ResultCodes.KEY_IS_DELETE_MARKER : ResultCodes.KEY_NOT_FOUND);
+ }
} catch (IOException ex) {
if (ex instanceof OMException) {
throw ex;
@@ -661,6 +702,48 @@ private OmKeyInfo getOmKeyInfo(String volumeName, String bucketName,
.get(keyBytes);
}
+ /**
+ * Resolves the version addressed by {@code args} for a key whose current
+ * version is {@code current}. The current version is checked first, so a
+ * request naming the current version costs no extra read; otherwise the
+ * version is looked up in the versionedKeyTable.
+ *
+ * @param current the key's current version, or null when the key has none
+ * @return the addressed version, or null when it does not exist
+ */
+ private OmKeyInfo getAddressedVersion(OmKeyArgs args, String volumeName,
+ String bucketName, String keyName, OmKeyInfo current) throws IOException {
+ if (args.isNullVersion()) {
+ if (current != null && current.isNullVersionRecord()) {
+ return current;
+ }
+ // The null version carries a normally generated versionId, so it can only
+ // be found by scanning the key's versions. The scan is bounded by the
+ // number of versions the key has and stops at the first match, since a key
+ // has at most one null version.
+ String prefix = metadataManager
+ .getVersionedOzoneKeyPrefix(volumeName, bucketName, keyName);
+ try (Table.KeyValueIterator versions =
+ metadataManager.getVersionedKeyTable().iterator(prefix)) {
+ while (versions.hasNext()) {
+ OmKeyInfo version = versions.next().getValue();
+ if (version.isNullVersionRecord()) {
+ return version;
+ }
+ }
+ }
+ return null;
+ }
+
+ long versionId = args.getVersionId();
+ if (current != null && current.getVersionId() != null
+ && current.getVersionId() == versionId) {
+ return current;
+ }
+ return metadataManager.getVersionedKeyTable().get(metadataManager
+ .getVersionedOzoneKey(volumeName, bucketName, keyName, versionId));
+ }
+
/**
* Look up will return only closed fileInfo. This will return null if the
* keyName is a directory or if the keyName is still open for writing.
@@ -1001,6 +1084,19 @@ public List getExpiredMultipartUploads(
maxParts);
}
+ @Override
+ public List getVersionsToReclaim(
+ int defaultMaxVersions, int limitPerTask) throws IOException {
+ return metadataManager.getVersionsToReclaim(defaultMaxVersions,
+ limitPerTask);
+ }
+
+ @Override
+ public OMMetadataManager.ExpiredDeleteMarkers getExpiredDeleteMarkers(
+ String startKey, int scanBudget, int limit) throws IOException {
+ return metadataManager.getExpiredDeleteMarkers(startKey, scanBudget, limit);
+ }
+
@Override
public Map getObjectTagging(OmKeyArgs args, ResolvedBucket bucket) throws IOException {
Objects.requireNonNull(args, "args == null");
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
index 283bb4933580..e482d49bad9e 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
@@ -22,6 +22,7 @@
import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME;
import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
import static org.apache.hadoop.ozone.OzoneConsts.OM_SNAPSHOT_CHECKPOINT_DIR;
+import static org.apache.hadoop.ozone.OzoneConsts.OM_VERSIONED_KEY_SEPARATOR;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_MAX_OPEN_FILES;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_MAX_OPEN_FILES_DEFAULT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SNAPSHOT_DB_MAX_OPEN_FILES;
@@ -63,6 +64,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@@ -70,6 +72,7 @@
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
@@ -133,6 +136,7 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadInfo;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadsBucket;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
import org.apache.hadoop.ozone.storage.proto.OzoneManagerStorageProtos.PersistedUserVolumeInfo;
@@ -159,6 +163,7 @@ public class OmMetadataManagerImpl implements OMMetadataManager,
private Table volumeTable;
private Table bucketTable;
private Table keyTable;
+ private Table versionedKeyTable;
private Table openKeyTable;
private Table multipartInfoTable;
@@ -376,6 +381,11 @@ public Table getKeyTable(BucketLayout bucketLayout) {
return keyTable;
}
+ @Override
+ public Table getVersionedKeyTable() {
+ return versionedKeyTable;
+ }
+
@Override
public Table getFileTable() {
return fileTable;
@@ -494,6 +504,7 @@ protected void initializeOmTables(CacheType cacheType,
volumeTable = initializer.get(OMDBDefinition.VOLUME_TABLE_DEF, cacheType);
bucketTable = initializer.get(OMDBDefinition.BUCKET_TABLE_DEF, cacheType);
keyTable = initializer.get(OMDBDefinition.KEY_TABLE_DEF);
+ versionedKeyTable = initializer.get(OMDBDefinition.VERSIONED_KEY_TABLE_DEF);
openKeyTable = initializer.get(OMDBDefinition.OPEN_KEY_TABLE_DEF);
multipartInfoTable = initializer.get(OMDBDefinition.MULTIPART_INFO_TABLE_DEF);
@@ -649,6 +660,17 @@ public String getOzoneKey(String volume, String bucket, String key) {
return builder.toString();
}
+ @Override
+ public String getVersionedOzoneKey(String volume, String bucket, String key, long versionId) {
+ return getVersionedOzoneKeyPrefix(volume, bucket, key)
+ + String.format("%016x", Long.MAX_VALUE - versionId);
+ }
+
+ @Override
+ public String getVersionedOzoneKeyPrefix(String volume, String bucket, String key) {
+ return getOzoneKey(volume, bucket, key) + OM_VERSIONED_KEY_SEPARATOR;
+ }
+
@Override
public String getOzoneKeyFSO(String volumeName,
String bucketName,
@@ -1517,6 +1539,200 @@ public ExpiredOpenKeys getExpiredOpenKeys(Duration expireThreshold,
return expiredKeys;
}
+ @Override
+ public List getVersionsToReclaim(
+ int defaultMaxVersions, int limitPerTask) throws IOException {
+ List reclaimable = new ArrayList<>();
+ int collected = 0;
+
+ try (Table.KeyValueIterator buckets =
+ getBucketTable().iterator()) {
+ while (collected < limitPerTask && buckets.hasNext()) {
+ OmBucketInfo bucketInfo = buckets.next().getValue();
+ // Only a bucket that keeps versions has a versionedKeyTable range, and
+ // a limit of 0 means unlimited: both skip without a single seek.
+ if (!bucketInfo.hasEverBeenVersioned()) {
+ continue;
+ }
+ int maxVersions = bucketInfo.getMaxVersions() != null
+ ? bucketInfo.getMaxVersions() : defaultMaxVersions;
+ Integer expirationDays = bucketInfo.getNoncurrentVersionExpirationDays();
+ // Expiration is opt-in, so a bucket with neither control set has
+ // nothing to reclaim and is skipped without a single seek.
+ final boolean expires = expirationDays != null && expirationDays > 0;
+ if (maxVersions <= 0 && !expires) {
+ continue;
+ }
+
+ ObjectVersionsBucket.Builder bucketBuilder = null;
+ // The current version lives in the keyTable and counts toward the
+ // limit, so this is how many noncurrent versions a key may keep.
+ // A limit of 0 is unlimited, and only expiration applies.
+ final int allowedNoncurrent =
+ maxVersions <= 0 ? Integer.MAX_VALUE : maxVersions - 1;
+ final long expiredBefore = expires
+ ? Time.now() - TimeUnit.DAYS.toMillis(expirationDays) : 0L;
+ final String bucketPrefix = getBucketKey(bucketInfo.getVolumeName(),
+ bucketInfo.getBucketName()) + OM_KEY_PREFIX;
+ String currentKeyPrefix = null;
+ int seenInKey = 0;
+ // When the version that superseded this one was committed, which is
+ // when this one became noncurrent. Only read when expiration applies.
+ long becameNoncurrentAt = 0L;
+
+ try (Table.KeyValueIterator versions =
+ getVersionedKeyTable().iterator(bucketPrefix)) {
+ while (collected < limitPerTask && versions.hasNext()) {
+ Table.KeyValue entry = versions.next();
+ String dbKey = entry.getKey();
+ // All versions of a key are contiguous and ordered newest first,
+ // so a change of prefix starts a new key and restarts the count.
+ int separator = dbKey.indexOf(OM_VERSIONED_KEY_SEPARATOR);
+ String keyPrefix = dbKey.substring(0, separator + 1);
+ if (!keyPrefix.equals(currentKeyPrefix)) {
+ currentKeyPrefix = keyPrefix;
+ seenInKey = 0;
+ if (expires) {
+ // The newest noncurrent version was superseded by the key's
+ // current version, which is the one the keyTable holds.
+ becameNoncurrentAt = supersedingTime(bucketInfo,
+ dbKey.substring(bucketPrefix.length(), separator));
+ }
+ }
+
+ boolean overLimit = seenInKey++ >= allowedNoncurrent;
+ boolean expired = false;
+ if (expires) {
+ expired = becameNoncurrentAt > 0
+ && becameNoncurrentAt <= expiredBefore;
+ // Walking a key newest first, the version just visited is the
+ // one that superseded the next one.
+ becameNoncurrentAt = entry.getValue().getModificationTime();
+ }
+ if (!overLimit && !expired) {
+ continue;
+ }
+ if (bucketBuilder == null) {
+ bucketBuilder = ObjectVersionsBucket.newBuilder()
+ .setVolumeName(bucketInfo.getVolumeName())
+ .setBucketName(bucketInfo.getBucketName());
+ }
+ bucketBuilder.addVersionKeys(dbKey);
+ collected++;
+ }
+ }
+
+ if (bucketBuilder != null) {
+ reclaimable.add(bucketBuilder.build());
+ }
+ }
+ }
+
+ return reclaimable;
+ }
+
+ @Override
+ public ExpiredDeleteMarkers getExpiredDeleteMarkers(String startKey,
+ int scanBudget, int limit) throws IOException {
+ Map markers = new LinkedHashMap<>();
+ // Buckets resolved during this pass. The keyTable is ordered by bucket, so
+ // in practice this holds one entry at a time, but caching by key keeps the
+ // lookup correct if that ever stops holding.
+ Map bucketCache = new HashMap<>();
+ String nextStartKey = null;
+ int scanned = 0;
+ int collected = 0;
+
+ // OBJECT_STORE and LEGACY buckets share the keyTable; only a bucket that
+ // keeps versions can hold a delete marker, and the lookup below drops the
+ // rest.
+ try (Table.KeyValueIterator keys =
+ getKeyTable(BucketLayout.OBJECT_STORE).iterator()) {
+ if (startKey != null) {
+ keys.seek(startKey);
+ }
+ while (keys.hasNext()) {
+ Table.KeyValue entry = keys.next();
+ if (scanned >= scanBudget || collected >= limit) {
+ // Resume at this entry rather than walking the table from the start
+ // again, so that successive runs make progress.
+ nextStartKey = entry.getKey();
+ break;
+ }
+ scanned++;
+ OmKeyInfo keyInfo = entry.getValue();
+ if (!keyInfo.isDeleteMarker()) {
+ continue;
+ }
+
+ String bucketKey = getBucketKey(keyInfo.getVolumeName(),
+ keyInfo.getBucketName());
+ OmBucketInfo bucketInfo = bucketCache.get(bucketKey);
+ if (bucketInfo == null) {
+ // Read past the table cache, so that this walk and the versionedKey
+ // walk in getVersionsToReclaim judge a bucket by the same state. A
+ // bucket property changed but not yet flushed applies on the next
+ // run, which is soon enough for a background reclaimer.
+ bucketInfo = getBucketTable().getSkipCache(bucketKey);
+ if (bucketInfo == null) {
+ continue;
+ }
+ bucketCache.put(bucketKey, bucketInfo);
+ }
+ if (!bucketInfo.hasEverBeenVersioned()
+ || !bucketInfo.isExpiredDeleteMarkerCleanupEnabled()) {
+ continue;
+ }
+
+ // The marker has only expired once nothing is left under it: while a
+ // noncurrent version survives, the marker is what makes the key read
+ // as deleted, and removing it would resurrect that version.
+ if (hasNoncurrentVersion(keyInfo)) {
+ continue;
+ }
+
+ markers.computeIfAbsent(bucketKey, k ->
+ ObjectVersionsBucket.newBuilder()
+ .setVolumeName(keyInfo.getVolumeName())
+ .setBucketName(keyInfo.getBucketName()))
+ .addMarkerKeys(entry.getKey());
+ collected++;
+ }
+ }
+
+ List result = markers.values().stream()
+ .map(ObjectVersionsBucket.Builder::build)
+ .collect(Collectors.toList());
+ return new ExpiredDeleteMarkers(result, nextStartKey);
+ }
+
+ /** Whether the key has any version left in the versionedKeyTable. */
+ private boolean hasNoncurrentVersion(OmKeyInfo keyInfo) throws IOException {
+ try (Table.KeyValueIterator versions =
+ getVersionedKeyTable().iterator(getVersionedOzoneKeyPrefix(
+ keyInfo.getVolumeName(), keyInfo.getBucketName(),
+ keyInfo.getKeyName()))) {
+ return versions.hasNext();
+ }
+ }
+
+ /**
+ * When the key's current version was committed, which is the moment the
+ * newest noncurrent version stopped being current. Returns 0 when the key
+ * has no current version, which leaves its versions unexpired: the keyTable
+ * holds the current version of every key that still has one, so this only
+ * happens if that invariant is broken, and expiring on a broken invariant
+ * would destroy data.
+ */
+ private long supersedingTime(OmBucketInfo bucketInfo, String keyName)
+ throws IOException {
+ // S3 versioning is supported on OBJECT_STORE buckets only.
+ OmKeyInfo current = getKeyTable(BucketLayout.OBJECT_STORE).get(
+ getOzoneKey(bucketInfo.getVolumeName(), bucketInfo.getBucketName(),
+ keyName));
+ return current == null ? 0L : current.getModificationTime();
+ }
+
@Override
public List getExpiredMultipartUploads(
Duration expireThreshold, int maxParts) throws IOException {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 5d1e33f7cd84..b85802f26e88 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -416,6 +416,7 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
private BucketManager bucketManager;
private KeyManager keyManager;
private PrefixManagerImpl prefixManager;
+ private final VersionIdAllocator versionIdAllocator;
private final UpgradeFinalizer upgradeFinalizer;
private ExecutorService edekCacheLoader = null;
@@ -565,6 +566,7 @@ private OzoneManager(OzoneConfiguration conf, StartupOption startupOption)
versionManager = new OMLayoutVersionManager(omStorage.getLayoutVersion());
upgradeFinalizer = new OMUpgradeFinalizer(versionManager);
+ versionIdAllocator = new VersionIdAllocator(conf);
replicationConfigValidator =
conf.getObject(ReplicationConfigValidator.class);
@@ -2387,6 +2389,15 @@ public long getObjectIdFromTxId(long trxnId) {
trxnId);
}
+ /**
+ * Assigns the versionId of a version being committed, using the
+ * {@link org.apache.hadoop.ozone.om.helpers.VersionIdGenerator} configured
+ * for this cluster.
+ */
+ public VersionIdAllocator getVersionIdAllocator() {
+ return versionIdAllocator;
+ }
+
/**
*
* @return Gets the stored layout version from the DB meta table.
@@ -3060,6 +3071,11 @@ public OmBucketInfo getBucketInfo(String volume, String bucket)
.setDefaultReplicationConfig(
realBucket.getDefaultReplicationConfig())
.setIsVersionEnabled(realBucket.getIsVersionEnabled())
+ // Only when the real bucket actually carries a status: copying the
+ // value getVersioningStatus() derives for a legacy bucket would give
+ // the link an explicit status the real bucket does not have.
+ .setVersioningStatus(realBucket.hasVersioningStatus()
+ ? realBucket.getVersioningStatus() : null)
.setStorageType(realBucket.getStorageType())
.setQuotaInBytes(realBucket.getQuotaInBytes())
.setQuotaInNamespace(realBucket.getQuotaInNamespace())
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/VersionIdAllocator.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/VersionIdAllocator.java
new file mode 100644
index 000000000000..bc2f776254a2
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/VersionIdAllocator.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om;
+
+import java.io.IOException;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.VersionIdGenerator;
+
+/**
+ * Assigns the versionId of a version being committed, using the generator
+ * configured for this cluster.
+ *
+ * The generator is cluster-wide and can be changed by reconfiguring the OMs,
+ * so ids generated before and after a change are not guaranteed to be distinct.
+ * Rather than constrain the change, a commit whose versionId already exists on
+ * the key is rejected: the operator or the client deletes the existing version
+ * first, and the write can then be retried.
+ */
+public class VersionIdAllocator {
+
+ private final VersionIdGenerator generator;
+
+ public VersionIdAllocator(ConfigurationSource conf) {
+ this(VersionIdGenerator.fromConfiguration(conf));
+ }
+
+ public VersionIdAllocator(VersionIdGenerator generator) {
+ this.generator = generator;
+ }
+
+ public VersionIdGenerator getGenerator() {
+ return generator;
+ }
+
+ /**
+ * Returns the versionId to freeze on the version being committed.
+ *
+ *
The id must be greater than the one on the key's current version: a
+ * generator has to hand out increasing ids for a key, and the versionedKeyTable
+ * ordering and version promotion depend on it. An id that is not greater is
+ * refused rather than written, because it would either overwrite an existing
+ * version or sort into the wrong place. In practice this only happens after the
+ * cluster's generator is changed, or if the Ratis log index went backwards.
+ *
+ * @param currentVersion the key's current version, or null if the key has
+ * none. The write path holds it already, so no extra read is needed here.
+ * @throws OMException INVALID_REQUEST if the generated id does not exceed the
+ * current version's id, KEY_ALREADY_EXISTS if it is already taken
+ */
+ public long allocate(OMMetadataManager metadataManager, String volumeName,
+ String bucketName, String keyName, long transactionLogIndex,
+ OmKeyInfo currentVersion) throws IOException {
+
+ long versionId =
+ generator.generateVersionId(transactionLogIndex, currentVersion != null);
+
+ if (currentVersion == null) {
+ // No current version means the key has no versions at all, so nothing can
+ // be taken and nothing constrains the id.
+ return versionId;
+ }
+
+ Long currentVersionId = currentVersion.getVersionId();
+ if (currentVersionId == null) {
+ // A current version written before versioning was enabled carries no id,
+ // so there is nothing to order against; fall back to looking the id up.
+ if (isTaken(metadataManager, volumeName, bucketName, keyName, versionId)) {
+ throw alreadyExists(volumeName, bucketName, keyName, versionId);
+ }
+ return versionId;
+ }
+
+ if (versionId <= currentVersionId) {
+ throw new OMException("Version " + versionId + " of key /" + volumeName + "/"
+ + bucketName + "/" + keyName + " does not come after the current version "
+ + currentVersionId + ". " + generator.getClass().getName()
+ + " must generate increasing versionIds for a key; an id that goes backwards can "
+ + "happen after the cluster's " + OMConfigKeys.OZONE_OM_VERSIONING_VERSION_ID_GENERATOR
+ + " is changed. Delete the key's versions before writing with the new generator.",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+
+ // Strictly greater than the largest id on the key, so no lookup is needed:
+ // every noncurrent version of the key has a smaller id than the current one.
+ return versionId;
+ }
+
+ private static OMException alreadyExists(String volumeName, String bucketName,
+ String keyName, long versionId) {
+ return new OMException("Version " + versionId + " of key /" + volumeName + "/"
+ + bucketName + "/" + keyName + " already exists. Delete that version before "
+ + "writing this one.", OMException.ResultCodes.KEY_ALREADY_EXISTS);
+ }
+
+ private boolean isTaken(OMMetadataManager metadataManager, String volumeName,
+ String bucketName, String keyName, long versionId) throws IOException {
+ return metadataManager.getVersionedKeyTable().isExist(
+ metadataManager.getVersionedOzoneKey(volumeName, bucketName, keyName, versionId));
+ }
+
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
index 02e32edec464..49d24f9d6138 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
@@ -86,6 +86,7 @@
* | Column Family | Mapping |
* |----------------------------------------------------------------------------------|
* | keyTable | /volume/bucket/key :- KeyInfo |
+ * | versionedKeyTable | /volume/bucket/key\0revVersionId :- KeyInfo |
* | deletedTable | /volume/bucket/key :- RepeatedKeyInfo |
* | openKeyTable | /volume/bucket/key/id :- KeyInfo |
* | multipartInfoTable | /volume/bucket/key/uploadId :- parts |
@@ -210,6 +211,21 @@ public final class OMDBDefinition extends DBDefinition.WithMap {
StringCodec.get(),
OmKeyInfo.getKeyTableCodec());
+ public static final String VERSIONED_KEY_TABLE = "versionedKeyTable";
+ /**
+ * versionedKeyTable: /volume/bucket/key\0revVersionId :- KeyInfo.
+ * Noncurrent object versions (including noncurrent delete markers) of
+ * versioning-enabled buckets; the current version stays in keyTable.
+ * revVersionId is the fixed-width hex of (Long.MAX_VALUE - versionId), so
+ * versions of a key are adjacent and ordered newest first. The separator is
+ * OM_VERSIONED_KEY_SEPARATOR (0x00), not '/', because OBJECT_STORE key names
+ * contain '/' verbatim.
+ */
+ public static final DBColumnFamilyDefinition VERSIONED_KEY_TABLE_DEF
+ = new DBColumnFamilyDefinition<>(VERSIONED_KEY_TABLE,
+ StringCodec.get(),
+ OmKeyInfo.getKeyTableCodec());
+
public static final String DELETED_TABLE = "deletedTable";
/** deletedTable: /volume/bucket/key :- RepeatedKeyInfo (excludes fields only used in openKeyTable). */
public static final DBColumnFamilyDefinition DELETED_TABLE_DEF
@@ -353,6 +369,7 @@ public final class OMDBDefinition extends DBDefinition.WithMap {
TENANT_STATE_TABLE_DEF,
TRANSACTION_INFO_TABLE_DEF,
USER_TABLE_DEF,
+ VERSIONED_KEY_TABLE_DEF,
VOLUME_TABLE_DEF);
private static final OMDBDefinition INSTANCE = new OMDBDefinition();
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
index 0a46c589af29..e9b8de48c78e 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
@@ -53,6 +53,7 @@
import org.apache.hadoop.ozone.om.request.file.OMRecoverLeaseRequest;
import org.apache.hadoop.ozone.om.request.key.OMDirectoriesPurgeRequestWithFSO;
import org.apache.hadoop.ozone.om.request.key.OMKeyPurgeRequest;
+import org.apache.hadoop.ozone.om.request.key.OMObjectVersionsReclaimRequest;
import org.apache.hadoop.ozone.om.request.key.OMOpenKeysDeleteRequest;
import org.apache.hadoop.ozone.om.request.key.acl.OMKeyAddAclRequest;
import org.apache.hadoop.ozone.om.request.key.acl.OMKeyAddAclRequestWithFSO;
@@ -332,6 +333,8 @@ public static OMClientRequest createClientRequest(OMRequest omRequest,
return new OMEchoRPCWriteRequest(omRequest);
case AbortExpiredMultiPartUploads:
return new S3ExpiredMultipartUploadsAbortRequest(omRequest);
+ case ReclaimObjectVersions:
+ return new OMObjectVersionsReclaimRequest(omRequest);
case QuotaRepair:
return new OMQuotaRepairRequest(omRequest);
case PutObjectTagging:
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketCreateRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketCreateRequest.java
index 718f329aaaff..e549102e97f0 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketCreateRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketCreateRequest.java
@@ -101,6 +101,23 @@ public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
// FSO and LEGACY buckets are not strictly bound to S3 naming semantics.
OmUtils.validateBucketName(bucketInfo.getBucketName(), strict);
+ if (bucketInfo.hasVersioningStatus()
+ && bucketLayout != BucketLayout.OBJECT_STORE) {
+ throw new OMException("S3 object versioning is only supported on "
+ + BucketLayout.OBJECT_STORE + " buckets, but bucket "
+ + bucketInfo.getBucketName() + " is created with layout "
+ + bucketLayout + ".",
+ OMException.ResultCodes.NOT_SUPPORTED_OPERATION);
+ }
+
+ if (bucketInfo.hasMaxVersions()) {
+ validateMaxVersions(bucketInfo.getMaxVersions());
+ }
+ if (bucketInfo.hasNoncurrentVersionExpirationDays()) {
+ validateNoncurrentVersionExpirationDays(
+ bucketInfo.getNoncurrentVersionExpirationDays());
+ }
+
// ACL check during preExecute
if (ozoneManager.getAclsEnabled()) {
try {
@@ -369,6 +386,31 @@ private void checkQuotaInNamespace(OmVolumeArgs omVolumeArgs,
}
}
+ /**
+ * The wire type is uint32, so a value above Integer.MAX_VALUE arrives as a
+ * negative int. 0 means unlimited; anything else has to be a usable count.
+ */
+ static void validateMaxVersions(int maxVersions) throws OMException {
+ if (maxVersions < 0) {
+ throw new OMException("maxVersions " + Integer.toUnsignedString(maxVersions)
+ + " is out of range; it must be between 0 and " + Integer.MAX_VALUE
+ + ", where 0 means unlimited.",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+ }
+
+ /** Same uint32 range check as maxVersions; 0 means versions never expire. */
+ static void validateNoncurrentVersionExpirationDays(int days)
+ throws OMException {
+ if (days < 0) {
+ throw new OMException("noncurrentVersionExpirationDays "
+ + Integer.toUnsignedString(days) + " is out of range; it must be"
+ + " between 0 and " + Integer.MAX_VALUE + ", where 0 means versions"
+ + " are retained forever.",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+ }
+
public boolean checkQuotaBytesValid(OMMetadataManager metadataManager,
OmVolumeArgs omVolumeArgs, OmBucketInfo omBucketInfo, String volumeKey)
throws IOException {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketSetPropertyRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketSetPropertyRequest.java
index a88e5fb73334..77ebe561348d 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketSetPropertyRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketSetPropertyRequest.java
@@ -38,6 +38,8 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
import org.apache.hadoop.ozone.om.helpers.BucketEncryptionKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.BucketVersioningStatus;
import org.apache.hadoop.ozone.om.helpers.KeyValueUtil;
import org.apache.hadoop.ozone.om.helpers.OmBucketArgs;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
@@ -174,10 +176,67 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
//Check Versioning to update
Boolean versioning = omBucketArgs.getIsVersionEnabled();
+ BucketVersioningStatus newVersioningStatus = omBucketArgs.getVersioningStatus();
if (versioning != null) {
+ // Apply the legacy flag on its own; setVersioningStatus below overrides
+ // it when a status is also being set.
bucketInfoBuilder.setIsVersionEnabled(versioning);
- LOG.debug("Updating bucket versioning for bucket: {} in volume: {}",
- bucketName, volumeName);
+ }
+ if (newVersioningStatus == null && versioning != null
+ && dbBucketInfo.hasVersioningStatus()) {
+ // Legacy flag from an older client against a bucket that already has an
+ // S3 versioning status: keep the two consistent. Disabling maps to
+ // SUSPENDED, since the S3 state machine forbids returning to
+ // UNVERSIONED once versioning has been enabled.
+ //
+ // On a bucket without a status the flag is left alone: it selects the
+ // legacy in-record block version list, and an old client must not be
+ // able to opt a bucket into S3 versioning semantics.
+ newVersioningStatus = versioning
+ ? BucketVersioningStatus.ENABLED : BucketVersioningStatus.SUSPENDED;
+ }
+ if (newVersioningStatus != null) {
+ if (dbBucketInfo.getBucketLayout() != BucketLayout.OBJECT_STORE) {
+ throw new OMException("S3 object versioning is only supported on "
+ + BucketLayout.OBJECT_STORE + " buckets, but bucket " + bucketName
+ + " has layout " + dbBucketInfo.getBucketLayout() + ".",
+ OMException.ResultCodes.NOT_SUPPORTED_OPERATION);
+ }
+ if (!dbBucketInfo.getVersioningStatus().canTransitionTo(newVersioningStatus)) {
+ throw new OMException("Bucket versioning cannot be changed from "
+ + dbBucketInfo.getVersioningStatus() + " to " + newVersioningStatus
+ + "; once enabled, versioning can only be suspended.",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+ bucketInfoBuilder.setVersioningStatus(newVersioningStatus);
+ LOG.debug("Updating bucket versioning to {} for bucket: {} in volume: {}",
+ newVersioningStatus, bucketName, volumeName);
+ }
+
+ // Check maxVersions to update. It is accepted on any bucket, so that the
+ // retention limit can be set before versioning is enabled; it only takes
+ // effect once the bucket keeps versions.
+ Integer newMaxVersions = omBucketArgs.getMaxVersions();
+ if (newMaxVersions != null) {
+ OMBucketCreateRequest.validateMaxVersions(newMaxVersions);
+ bucketInfoBuilder.setMaxVersions(newMaxVersions);
+ LOG.debug("Updating maxVersions to {} for bucket: {} in volume: {}",
+ newMaxVersions, bucketName, volumeName);
+ }
+
+ Integer newExpirationDays = omBucketArgs.getNoncurrentVersionExpirationDays();
+ if (newExpirationDays != null) {
+ OMBucketCreateRequest.validateNoncurrentVersionExpirationDays(newExpirationDays);
+ bucketInfoBuilder.setNoncurrentVersionExpirationDays(newExpirationDays);
+ LOG.debug("Updating noncurrentVersionExpirationDays to {} for bucket: {} in volume: {}",
+ newExpirationDays, bucketName, volumeName);
+ }
+
+ Boolean newMarkerCleanup = omBucketArgs.getExpiredDeleteMarkerCleanup();
+ if (newMarkerCleanup != null) {
+ bucketInfoBuilder.setExpiredDeleteMarkerCleanup(newMarkerCleanup);
+ LOG.debug("Updating expiredDeleteMarkerCleanup to {} for bucket: {} in volume: {}",
+ newMarkerCleanup, bucketName, volumeName);
}
//Check quotaInBytes and quotaInNamespace to update
@@ -376,4 +435,37 @@ public static OMRequest disallowSetBucketPropertyWithECReplicationConfig(
}
return req;
}
+
+ @RequestFeatureValidator(
+ conditions = ValidationCondition.CLUSTER_NEEDS_FINALIZATION,
+ processingPhase = RequestProcessingPhase.PRE_PROCESS,
+ requestType = Type.SetBucketProperty
+ )
+ public static OMRequest disallowSetBucketPropertyWithVersioningStatus(
+ OMRequest req, ValidationContext ctx) throws OMException {
+ if (!ctx.versionManager()
+ .isAllowed(OMLayoutFeature.OBJECT_VERSIONING)) {
+ SetBucketPropertyRequest propReq =
+ req.getSetBucketPropertyRequest();
+ if (propReq.hasBucketArgs()
+ && propReq.getBucketArgs().hasVersioningStatus()) {
+ throw new OMException("Cluster does not have the object versioning"
+ + " feature finalized yet, but the request contains a bucket"
+ + " versioning status. Rejecting the request, please finalize the"
+ + " cluster upgrade and then try again.",
+ OMException.ResultCodes.NOT_SUPPORTED_OPERATION_PRIOR_FINALIZATION);
+ }
+ if (propReq.hasBucketArgs()
+ && (propReq.getBucketArgs().hasMaxVersions()
+ || propReq.getBucketArgs().hasNoncurrentVersionExpirationDays()
+ || propReq.getBucketArgs().hasExpiredDeleteMarkerCleanup())) {
+ throw new OMException("Cluster does not have the object versioning"
+ + " feature finalized yet, but the request contains version"
+ + " retention settings. Rejecting the request, please finalize the"
+ + " cluster upgrade and then try again.",
+ OMException.ResultCodes.NOT_SUPPORTED_OPERATION_PRIOR_FINALIZATION);
+ }
+ }
+ return req;
+ }
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCommitRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCommitRequest.java
index 6c34443058b5..7d6f7a6abed4 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCommitRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCommitRequest.java
@@ -34,6 +34,8 @@
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
import org.apache.hadoop.ozone.OmUtils;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.OzoneManagerVersion;
@@ -52,6 +54,7 @@
import org.apache.hadoop.ozone.om.helpers.OzoneFSUtils;
import org.apache.hadoop.ozone.om.helpers.QuotaUtil;
import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.VersionIdGenerator;
import org.apache.hadoop.ozone.om.helpers.WithMetadata;
import org.apache.hadoop.ozone.om.request.util.OmKeyHSyncUtil;
import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
@@ -303,14 +306,36 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
}
validateAtomicRewrite(keyToDelete, omKeyInfo, auditMap);
+ final boolean versioningEnabled = omBucketInfo.isS3VersioningEnabled();
+ final boolean keepsVersions = omBucketInfo.hasEverBeenVersioned();
+ // A write while versioning is suspended creates no version of its own: it
+ // takes the key's null version slot, replacing whatever held it. The
+ // record still carries a generated versionId, so that it sorts among the
+ // key's versions by the time it was written.
+ final boolean suspendedWrite = omBucketInfo.isS3VersioningSuspended();
// Set the UpdateID to current transactionLogIndex
- omKeyInfo = omKeyInfo.toBuilder()
+ OmKeyInfo.Builder committedKeyBuilder = omKeyInfo.toBuilder()
.setExpectedDataGeneration(null)
.addAllMetadata(KeyValueUtil.getFromProtobuf(
commitKeyArgs.getMetadataList()))
.setUpdateID(trxnLogIndex)
- .setDataSize(commitKeyArgs.getDataSize())
- .build();
+ .setDataSize(commitKeyArgs.getDataSize());
+ if (keepsVersions) {
+ // The version identity is frozen when the version is created: an hsync
+ // re-commit keeps updating the same version, so it keeps its versionId.
+ committedKeyBuilder.setVersionId(isSameHsyncKey
+ ? keyToDelete.getVersionId()
+ : ozoneManager.getVersionIdAllocator().allocate(omMetadataManager,
+ volumeName, bucketName, keyName, trxnLogIndex, keyToDelete));
+ committedKeyBuilder.setNullVersion(suspendedWrite);
+ }
+ omKeyInfo = committedKeyBuilder.build();
+
+ // The version a write supersedes is kept as a noncurrent version, except
+ // for the null version, which a suspended write replaces outright.
+ final boolean supersededVersionRetained = keyToDelete != null
+ && keepsVersions
+ && (versioningEnabled || !keyToDelete.isNullVersionRecord());
// Update the block length for each block, return the allocated but
// uncommitted blocks
@@ -325,7 +350,11 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
correctedSpace -= keyToDelete.getReplicatedSize();
checkBucketQuotaInBytes(omMetadataManager, omBucketInfo,
correctedSpace);
- } else if (keyToDelete != null && !omBucketInfo.getIsVersionEnabled()) {
+ } else if (keyToDelete != null && !omBucketInfo.getIsVersionEnabled()
+ && !supersededVersionRetained) {
+ // A retained version keeps its blocks: it lives on in the
+ // versionedKeyTable. What reaches this branch under S3 versioning is
+ // the null version being replaced by a suspended write.
RepeatedOmKeyInfo oldVerKeyInfo = getOldVersionsToCleanUp(
keyToDelete, omBucketInfo.getObjectID(), trxnLogIndex);
// using pseudoObjId as objectId can be same in case of overwrite key
@@ -401,6 +430,47 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
dbOpenKey, newOpenKeyInfo, trxnLogIndex);
}
+ // With S3-compatible versioning the overwritten current version is kept
+ // as a noncurrent version instead of being reclaimed. A record written
+ // before versioning was enabled carries no versionId and becomes the
+ // key's null version.
+ String dbVersionedKey = null;
+ OmKeyInfo versionedKeyInfo = null;
+ if (supersededVersionRetained && !isSameHsyncKey) {
+ versionedKeyInfo = keyToDelete.getVersionId() != null ? keyToDelete
+ : keyToDelete.toBuilder()
+ .setVersionId(VersionIdGenerator.UNSET_VERSION_ID)
+ .setNullVersion(true)
+ .build();
+ dbVersionedKey = omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionedKeyInfo.getVersionId());
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ dbVersionedKey, versionedKeyInfo, trxnLogIndex);
+ }
+
+ // A suspended write replaces the key's null version wherever it is. It is
+ // the current version when the previous write was also suspended, and a
+ // noncurrent version when versioning was enabled in between - the version
+ // that demoted it is still current, and is retained above.
+ String replacedNullVersionKey = null;
+ if (suspendedWrite && !isSameHsyncKey && supersededVersionRetained) {
+ Pair nullVersion = getNoncurrentNullVersion(
+ omMetadataManager, volumeName, bucketName, keyName);
+ if (nullVersion != null) {
+ replacedNullVersionKey = nullVersion.getKey();
+ oldKeyVersionsToDeleteMap = addKeyInfoToDeleteMap(ozoneManager,
+ trxnLogIndex, dbOzoneKey, omBucketInfo.getObjectID(),
+ nullVersion.getValue().withCommittedKeyDeletedFlag(true),
+ oldKeyVersionsToDeleteMap);
+ omBucketInfo.decrUsedBytes(
+ sumBlockLengths(nullVersion.getValue()), true);
+ omBucketInfo.decrUsedNamespace(1L, true);
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(replacedNullVersionKey),
+ CacheValue.get(trxnLogIndex));
+ }
+ }
+
omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
dbOzoneKey, omKeyInfo, trxnLogIndex);
@@ -408,7 +478,9 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
omClientResponse = new OMKeyCommitResponse(omResponse.build(),
omKeyInfo, dbOzoneKey, dbOpenKey, omBucketInfo.copyObject(),
- oldKeyVersionsToDeleteMap, isHSync, newOpenKeyInfo, dbOpenKeyToDeleteKey, openKeyToDelete);
+ oldKeyVersionsToDeleteMap, isHSync, newOpenKeyInfo, dbOpenKeyToDeleteKey, openKeyToDelete)
+ .withVersionedKey(dbVersionedKey, versionedKeyInfo)
+ .withReplacedNullVersion(replacedNullVersionKey);
result = Result.SUCCESS;
} catch (IOException | InvalidPathException ex) {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
index 26287ca66d26..5c9ee1848781 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyDeleteRequest.java
@@ -19,13 +19,20 @@
import static org.apache.hadoop.ozone.OzoneConsts.DELETED_HSYNC_KEY;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_NOT_FOUND;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.BUCKET_LOCK;
import static org.apache.hadoop.ozone.util.MetricUtil.captureLatencyNs;
import java.io.IOException;
import java.nio.file.InvalidPathException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
@@ -42,12 +49,17 @@
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfoGroup;
+import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.VersionIdGenerator;
import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
import org.apache.hadoop.ozone.om.request.validation.RequestFeatureValidator;
import org.apache.hadoop.ozone.om.request.validation.ValidationCondition;
import org.apache.hadoop.ozone.om.request.validation.ValidationContext;
import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.key.OMKeyDeleteMarkerResponse;
import org.apache.hadoop.ozone.om.response.key.OMKeyDeleteResponse;
+import org.apache.hadoop.ozone.om.response.key.OMKeyVersionDeleteResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeyRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeyResponse;
@@ -128,6 +140,15 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
boolean acquiredLock = false;
OMClientResponse omClientResponse = null;
Result result = null;
+ // whether the request removed a key that was visible to plain reads; a
+ // delete marker supersedes the current version without removing anything
+ boolean visibleKeyRemoved = false;
+ // whether the request took the delete marker path, and so may have left
+ // versionedKeyTable entries in the table cache for this transaction
+ boolean insertingDeleteMarker = false;
+ // whether the request took the permanent version delete path, and so may
+ // have left versionedKeyTable entries in the table cache
+ boolean deletingVersion = false;
long startNanos = Time.monotonicNowNanos();
try {
String objectKey =
@@ -142,57 +163,88 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
OmKeyInfo omKeyInfo =
omMetadataManager.getKeyTable(getBucketLayout()).get(objectKey);
- if (omKeyInfo == null) {
- throw new OMException("Key not found", KEY_NOT_FOUND);
- }
-
- validateIfMatchETag(keyArgs, omKeyInfo);
-
- // Set the UpdateID to current transactionLogIndex
- omKeyInfo = omKeyInfo.toBuilder()
- .setUpdateID(trxnLogIndex)
- .build();
-
- // Update table cache. Put a tombstone entry
- omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
- new CacheKey<>(
- omMetadataManager.getOzoneKey(volumeName, bucketName, keyName)),
- CacheValue.get(trxnLogIndex));
OmBucketInfo omBucketInfo =
getBucketInfo(omMetadataManager, volumeName, bucketName);
- long quotaReleased = sumBlockLengths(omKeyInfo);
- // Empty entries won't be added to deleted table so this key shouldn't get added to snapshotUsed space.
- boolean isKeyNonEmpty = !OmKeyInfo.isKeyEmpty(omKeyInfo);
- omBucketInfo.decrUsedBytes(quotaReleased, isKeyNonEmpty);
- omBucketInfo.decrUsedNamespace(1L, isKeyNonEmpty);
- OmKeyInfo deletedOpenKeyInfo = null;
-
- // If omKeyInfo has hsync metadata, delete its corresponding open key as well
- String dbOpenKey = null;
- String hsyncClientId = omKeyInfo.getMetadata().get(OzoneConsts.HSYNC_CLIENT_ID);
- if (hsyncClientId != null) {
- Table openKeyTable = omMetadataManager.getOpenKeyTable(getBucketLayout());
- dbOpenKey = omMetadataManager.getOpenKey(volumeName, bucketName, keyName, hsyncClientId);
- OmKeyInfo openKeyInfo = openKeyTable.get(dbOpenKey);
- if (openKeyInfo != null) {
- openKeyInfo = openKeyInfo.withMetadataMutations(
- metadata -> metadata.put(DELETED_HSYNC_KEY, "true"));
- openKeyTable.addCacheEntry(dbOpenKey, openKeyInfo, trxnLogIndex);
- deletedOpenKeyInfo = openKeyInfo;
- } else {
- LOG.warn("Potentially inconsistent DB state: open key not found with dbOpenKey '{}'", dbOpenKey);
+ if (keyArgs.hasVersionId() || keyArgs.getNullVersion()) {
+ // DELETE ?versionId= permanently removes one version. It is the only
+ // delete that destroys data on a versioned bucket.
+ if (!omBucketInfo.hasEverBeenVersioned()) {
+ throw new OMException("Bucket " + bucketName
+ + " does not have S3 versioning enabled",
+ NOT_SUPPORTED_OPERATION);
+ }
+ deletingVersion = true;
+ omClientResponse = deleteVersion(omMetadataManager, omBucketInfo,
+ omKeyInfo, keyArgs, volumeName, bucketName, keyName, trxnLogIndex,
+ omResponse);
+ // Noncurrent versions are invisible to plain reads, so removing one
+ // does not change the visible key count.
+ } else if (omBucketInfo.hasEverBeenVersioned()) {
+ // A delete without a versionId removes no data: a delete marker
+ // becomes the current version and the version it supersedes moves to
+ // the versionedKeyTable. Like S3, the marker is inserted even when the
+ // key does not exist. While versioning is suspended the marker takes
+ // the key's null version slot instead of creating a version.
+ insertingDeleteMarker = true;
+ omClientResponse = insertDeleteMarker(ozoneManager, omMetadataManager,
+ omBucketInfo, omKeyInfo, objectKey, keyArgs, trxnLogIndex,
+ omResponse);
+ // The key stays in the keyTable as a marker, so nothing is released;
+ // only superseding a visible object is one fewer visible key.
+ visibleKeyRemoved = omKeyInfo != null && !omKeyInfo.isDeleteMarker();
+ } else {
+ if (omKeyInfo == null) {
+ throw new OMException("Key not found", KEY_NOT_FOUND);
}
- }
- omClientResponse = new OMKeyDeleteResponse(
- omResponse.setDeleteKeyResponse(DeleteKeyResponse.newBuilder())
- .build(), omKeyInfo,
- omBucketInfo.copyObject(), deletedOpenKeyInfo);
- if (omKeyInfo.isFile()) {
- auditMap.put(OzoneConsts.DATA_SIZE, String.valueOf(omKeyInfo.getDataSize()));
- auditMap.put(OzoneConsts.REPLICATION_CONFIG, omKeyInfo.getReplicationConfig().toString());
+ validateIfMatchETag(keyArgs, omKeyInfo);
+
+ // Set the UpdateID to current transactionLogIndex
+ omKeyInfo = omKeyInfo.toBuilder()
+ .setUpdateID(trxnLogIndex)
+ .build();
+
+ // Update table cache. Put a tombstone entry
+ omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
+ new CacheKey<>(
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName)),
+ CacheValue.get(trxnLogIndex));
+
+ long quotaReleased = sumBlockLengths(omKeyInfo);
+ // Empty entries won't be added to deleted table so this key shouldn't get added to snapshotUsed space.
+ boolean isKeyNonEmpty = !OmKeyInfo.isKeyEmpty(omKeyInfo);
+ omBucketInfo.decrUsedBytes(quotaReleased, isKeyNonEmpty);
+ omBucketInfo.decrUsedNamespace(1L, isKeyNonEmpty);
+ OmKeyInfo deletedOpenKeyInfo = null;
+
+ // If omKeyInfo has hsync metadata, delete its corresponding open key as well
+ String dbOpenKey = null;
+ String hsyncClientId = omKeyInfo.getMetadata().get(OzoneConsts.HSYNC_CLIENT_ID);
+ if (hsyncClientId != null) {
+ Table openKeyTable = omMetadataManager.getOpenKeyTable(getBucketLayout());
+ dbOpenKey = omMetadataManager.getOpenKey(volumeName, bucketName, keyName, hsyncClientId);
+ OmKeyInfo openKeyInfo = openKeyTable.get(dbOpenKey);
+ if (openKeyInfo != null) {
+ openKeyInfo = openKeyInfo.withMetadataMutations(
+ metadata -> metadata.put(DELETED_HSYNC_KEY, "true"));
+ openKeyTable.addCacheEntry(dbOpenKey, openKeyInfo, trxnLogIndex);
+ deletedOpenKeyInfo = openKeyInfo;
+ } else {
+ LOG.warn("Potentially inconsistent DB state: open key not found with dbOpenKey '{}'", dbOpenKey);
+ }
+ }
+
+ omClientResponse = new OMKeyDeleteResponse(
+ omResponse.setDeleteKeyResponse(DeleteKeyResponse.newBuilder())
+ .build(), omKeyInfo,
+ omBucketInfo.copyObject(), deletedOpenKeyInfo);
+ if (omKeyInfo.isFile()) {
+ auditMap.put(OzoneConsts.DATA_SIZE, String.valueOf(omKeyInfo.getDataSize()));
+ auditMap.put(OzoneConsts.REPLICATION_CONFIG, omKeyInfo.getReplicationConfig().toString());
+ }
+ visibleKeyRemoved = true;
}
result = Result.SUCCESS;
@@ -201,9 +253,22 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
} catch (IOException | InvalidPathException ex) {
result = Result.FAILURE;
exception = ex;
- omClientResponse =
- new OMKeyDeleteResponse(createErrorOMResponse(omResponse, exception),
- getBucketLayout());
+ // The failure response has to declare the same tables as the successful
+ // one: the double buffer cleans up the table cache from the response's
+ // CleanupTableInfo, so a delete marker request that failed after
+ // touching the versionedKeyTable cache would otherwise leave an entry
+ // behind that is in no DB and is never cleaned up.
+ OMResponse errorResponse = createErrorOMResponse(omResponse, exception);
+ if (insertingDeleteMarker) {
+ omClientResponse =
+ new OMKeyDeleteMarkerResponse(errorResponse, getBucketLayout());
+ } else if (deletingVersion) {
+ omClientResponse =
+ new OMKeyVersionDeleteResponse(errorResponse, getBucketLayout());
+ } else {
+ omClientResponse =
+ new OMKeyDeleteResponse(errorResponse, getBucketLayout());
+ }
long endNanosDeleteKeyFailureLatencyNs = Time.monotonicNowNanos();
perfMetrics.setDeleteKeyFailureLatencyNs(endNanosDeleteKeyFailureLatencyNs - startNanos);
} finally {
@@ -222,7 +287,9 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
switch (result) {
case SUCCESS:
- omMetrics.decNumKeys();
+ if (visibleKeyRemoved) {
+ omMetrics.decNumKeys();
+ }
LOG.debug("Key deleted. Volume:{}, Bucket:{}, Key:{}", volumeName,
bucketName, keyName);
break;
@@ -239,6 +306,213 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
return omClientResponse;
}
+ /**
+ * Supersedes the key's current version with a delete marker: a record with
+ * no data blocks that makes plain reads of the key return KEY_NOT_FOUND
+ * while every existing version stays readable by versionId. The superseded
+ * current version becomes a noncurrent version; a record written before
+ * versioning was enabled carries no versionId and becomes the key's null
+ * version. When the key does not exist the marker is still inserted, as S3
+ * does.
+ */
+ /**
+ * Permanently removes the addressed version. Only noncurrent versions are
+ * handled here: removing the current version has to promote the next-newest
+ * one to keep the keyTable authoritative, which T4.3 adds.
+ *
+ * @param currentVersion the key's current version, or null if it has none
+ */
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ private OMClientResponse deleteVersion(OMMetadataManager omMetadataManager,
+ OmBucketInfo omBucketInfo, OmKeyInfo currentVersion,
+ OzoneManagerProtocolProtos.KeyArgs keyArgs, String volumeName,
+ String bucketName, String keyName, long trxnLogIndex,
+ OMResponse.Builder omResponse) throws IOException {
+
+ boolean nullVersion = keyArgs.getNullVersion();
+ boolean deletingCurrent = currentVersion != null && (nullVersion
+ ? currentVersion.isNullVersionRecord()
+ : Long.valueOf(keyArgs.getVersionId()).equals(
+ currentVersion.getVersionId()));
+
+ // Everything that can fail runs before the first cache entry is added, so
+ // that a failed request leaves no versionedKeyTable cache entry behind.
+ String versionedKey;
+ OmKeyInfo version;
+ if (deletingCurrent) {
+ versionedKey = null;
+ version = currentVersion;
+ } else if (nullVersion) {
+ Pair nullSlot = getNoncurrentNullVersion(
+ omMetadataManager, volumeName, bucketName, keyName);
+ versionedKey = nullSlot == null ? null : nullSlot.getKey();
+ version = nullSlot == null ? null : nullSlot.getValue();
+ } else {
+ versionedKey = omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, keyArgs.getVersionId());
+ version = omMetadataManager.getVersionedKeyTable().get(versionedKey);
+ }
+ if (version == null) {
+ throw new OMException("Version not found for key " + keyName,
+ KEY_NOT_FOUND);
+ }
+
+ // Removing the current version leaves the key without one, so the newest
+ // noncurrent version is promoted to keep the invariant that keyTable holds
+ // the current version of every key that still has one. The record moves
+ // unchanged: promotion is positional, the version keeps its identity.
+ String promotedKey = null;
+ OmKeyInfo promoted = null;
+ if (deletingCurrent) {
+ Pair newest = getNewestNoncurrentVersion(
+ omMetadataManager, volumeName, bucketName, keyName);
+ if (newest != null) {
+ promotedKey = newest.getKey();
+ promoted = newest.getValue();
+ }
+ }
+
+ version = version.toBuilder().setUpdateID(trxnLogIndex).build();
+
+ String objectKey =
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName);
+ if (deletingCurrent) {
+ if (promoted != null) {
+ omMetadataManager.getKeyTable(getBucketLayout())
+ .addCacheEntry(objectKey, promoted, trxnLogIndex);
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(promotedKey), CacheValue.get(trxnLogIndex));
+ } else {
+ // no version survives, so the key disappears entirely
+ omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
+ new CacheKey<>(objectKey), CacheValue.get(trxnLogIndex));
+ }
+ } else {
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(versionedKey), CacheValue.get(trxnLogIndex));
+ }
+
+ // A delete marker holds no blocks, so it releases namespace but no space.
+ long quotaReleased = sumBlockLengths(version);
+ boolean isVersionNonEmpty = !OmKeyInfo.isKeyEmpty(version);
+ omBucketInfo.decrUsedBytes(quotaReleased, isVersionNonEmpty);
+ omBucketInfo.decrUsedNamespace(1L, isVersionNonEmpty);
+
+ return new OMKeyVersionDeleteResponse(
+ omResponse.setDeleteKeyResponse(DeleteKeyResponse.newBuilder()).build(),
+ version, deletingCurrent ? objectKey : versionedKey, deletingCurrent,
+ promotedKey, promoted, omBucketInfo.copyObject());
+ }
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ private OMClientResponse insertDeleteMarker(OzoneManager ozoneManager,
+ OMMetadataManager omMetadataManager, OmBucketInfo omBucketInfo,
+ OmKeyInfo currentVersion, String objectKey,
+ OzoneManagerProtocolProtos.KeyArgs keyArgs, long trxnLogIndex,
+ OMResponse.Builder omResponse) throws IOException {
+
+ String volumeName = omBucketInfo.getVolumeName();
+ String bucketName = omBucketInfo.getBucketName();
+ String keyName = keyArgs.getKeyName();
+ // While versioning is suspended the marker is the key's null version, so
+ // it replaces whatever held that slot instead of superseding it.
+ final boolean suspended = omBucketInfo.isS3VersioningSuspended();
+ final boolean replacesCurrent = suspended && currentVersion != null
+ && currentVersion.isNullVersionRecord();
+
+ // Everything that can fail runs before the first cache entry is added: a
+ // request that throws here is answered with an OMKeyDeleteResponse, whose
+ // cleanup does not cover the versionedKeyTable, so a cache entry left
+ // behind would never be removed and would outlive the failed request.
+ long markerVersionId = ozoneManager.getVersionIdAllocator().allocate(
+ omMetadataManager, volumeName, bucketName, keyName, trxnLogIndex,
+ currentVersion);
+ // the marker is a record of its own; it holds no blocks, so it consumes
+ // namespace but no space
+ checkBucketQuotaInNamespace(omBucketInfo, 1L);
+
+ OmKeyInfo.Builder markerBuilder;
+ if (currentVersion != null) {
+ markerBuilder = currentVersion.toBuilder()
+ .setMetadata(new HashMap<>())
+ .setTags(new HashMap<>())
+ .setFileChecksum(null);
+ } else {
+ markerBuilder = new OmKeyInfo.Builder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setReplicationConfig(RatisReplicationConfig.getInstance(
+ ReplicationFactor.ONE))
+ .setObjectID(ozoneManager.getObjectIdFromTxId(trxnLogIndex))
+ .setOwnerName(omBucketInfo.getOwner())
+ .setFile(true);
+ }
+ OmKeyInfo deleteMarker = markerBuilder
+ .setOmKeyLocationInfos(Collections.singletonList(
+ new OmKeyLocationInfoGroup(0, new ArrayList<>())))
+ .setDataSize(0L)
+ .setCreationTime(keyArgs.getModificationTime())
+ .setModificationTime(keyArgs.getModificationTime())
+ .setUpdateID(trxnLogIndex)
+ .setVersionId(markerVersionId)
+ .setDeleteMarker(true)
+ .setNullVersion(suspended)
+ .build();
+
+ String movedVersionedKeyName = null;
+ OmKeyInfo movedVersionedKeyInfo = null;
+ if (currentVersion != null && !replacesCurrent) {
+ movedVersionedKeyInfo = currentVersion.getVersionId() != null
+ ? currentVersion
+ : currentVersion.toBuilder()
+ .setVersionId(VersionIdGenerator.UNSET_VERSION_ID)
+ .setNullVersion(true)
+ .build();
+ movedVersionedKeyName = omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName,
+ movedVersionedKeyInfo.getVersionId());
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ movedVersionedKeyName, movedVersionedKeyInfo, trxnLogIndex);
+ }
+
+ // The null version the marker replaces is removed: the current one when
+ // the last write was also suspended, and a noncurrent one when versioning
+ // was enabled in between.
+ OmKeyInfo replacedNullVersion = replacesCurrent ? currentVersion : null;
+ String replacedNullVersionKey = null;
+ if (suspended && !replacesCurrent) {
+ Pair nullVersion = getNoncurrentNullVersion(
+ omMetadataManager, volumeName, bucketName, keyName);
+ if (nullVersion != null) {
+ replacedNullVersionKey = nullVersion.getKey();
+ replacedNullVersion = nullVersion.getValue();
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(replacedNullVersionKey),
+ CacheValue.get(trxnLogIndex));
+ }
+ }
+ Map keysToDelete = null;
+ if (replacedNullVersion != null) {
+ keysToDelete = addKeyInfoToDeleteMap(ozoneManager, trxnLogIndex,
+ objectKey, omBucketInfo.getObjectID(),
+ replacedNullVersion.withCommittedKeyDeletedFlag(true), null);
+ omBucketInfo.decrUsedBytes(sumBlockLengths(replacedNullVersion), true);
+ omBucketInfo.decrUsedNamespace(1L, true);
+ }
+
+ omBucketInfo.incrUsedNamespace(1L);
+
+ omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
+ objectKey, deleteMarker, trxnLogIndex);
+
+ return new OMKeyDeleteMarkerResponse(
+ omResponse.setDeleteKeyResponse(DeleteKeyResponse.newBuilder()).build(),
+ deleteMarker, objectKey, movedVersionedKeyName, movedVersionedKeyInfo,
+ omBucketInfo.copyObject())
+ .withReplacedNullVersion(replacedNullVersionKey, keysToDelete);
+ }
+
/**
* Validates key delete requests.
* We do not want to allow older clients to delete keys in buckets which use
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
index d12b8fa05257..42eeeace25f6 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
@@ -49,6 +49,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.EncryptedKeyVersion;
@@ -63,6 +64,7 @@
import org.apache.hadoop.hdds.scm.container.common.helpers.ExcludeList;
import org.apache.hadoop.hdds.scm.exceptions.SCMException;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
+import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
import org.apache.hadoop.ipc_.Server;
@@ -896,6 +898,107 @@ public static long sumBlockLengths(OmKeyInfo omKeyInfo) {
return bytesUsed;
}
+ /**
+ * Returns the newest noncurrent version of the given key as a
+ * (dbKey, keyInfo) pair, or null if the key has no noncurrent version.
+ */
+ protected Pair getNewestNoncurrentVersion(
+ OMMetadataManager omMetadataManager, String volumeName,
+ String bucketName, String keyName) throws IOException {
+ return findNoncurrentVersion(omMetadataManager, volumeName, bucketName,
+ keyName, keyInfo -> true);
+ }
+
+ /**
+ * Returns the key's null version as a (dbKey, keyInfo) pair, or null if the
+ * key has no noncurrent null version. A key has at most one.
+ */
+ protected Pair getNoncurrentNullVersion(
+ OMMetadataManager omMetadataManager, String volumeName,
+ String bucketName, String keyName) throws IOException {
+ return findNoncurrentVersion(omMetadataManager, volumeName, bucketName,
+ keyName, OmKeyInfo::isNullVersionRecord);
+ }
+
+ /**
+ * Returns the newest noncurrent version of the key that satisfies
+ * {@code filter}, or null if there is none. Versions of a key are adjacent
+ * and ordered newest first, so the smallest matching dbKey wins.
+ *
+ * Both the table cache and the DB are searched, and neither can stand in
+ * for the other:
+ *
+ *
+ * - {@link Table#iterator} reads RocksDB directly and does not consult
+ * the cache, so a version written by a transaction that the double
+ * buffer has not flushed yet is invisible to it, and a version removed
+ * by such a transaction still appears in it;
+ * - the cache only holds the current flush window, so every older
+ * version of the key exists in the DB only.
+ *
+ *
+ * The search does not stop at the first cache match either. Versions are
+ * demoted into the versionedKeyTable in increasing versionId order, so in
+ * practice a cached version is newer than every flushed one, but that is a
+ * property of the write paths rather than something enforced here, and
+ * relying on it would silently promote the wrong version if a later write
+ * path broke it. The DB search costs one seek, since it stops at the first
+ * match.
+ */
+ private Pair findNoncurrentVersion(
+ OMMetadataManager omMetadataManager, String volumeName,
+ String bucketName, String keyName, Predicate filter)
+ throws IOException {
+ final String prefix = omMetadataManager.getVersionedOzoneKeyPrefix(
+ volumeName, bucketName, keyName);
+ final Table table =
+ omMetadataManager.getVersionedKeyTable();
+
+ String bestKey = null;
+ OmKeyInfo bestValue = null;
+
+ // Entries of transactions that are not flushed yet. The cache is not
+ // sorted, so it has to be scanned; it only holds this table's writes from
+ // the current flush window.
+ Iterator, CacheValue>> cacheIterator =
+ table.cacheIterator();
+ while (cacheIterator.hasNext()) {
+ Map.Entry, CacheValue> entry = cacheIterator.next();
+ String dbKey = entry.getKey().getCacheKey();
+ OmKeyInfo value = entry.getValue().getCacheValue();
+ // a null value is a tombstone: the version is deleted but not flushed
+ if (value == null || !dbKey.startsWith(prefix) || !filter.test(value)) {
+ continue;
+ }
+ if (bestKey == null || dbKey.compareTo(bestKey) < 0) {
+ bestKey = dbKey;
+ bestValue = value;
+ }
+ }
+
+ try (Table.KeyValueIterator versions = table.iterator(prefix)) {
+ while (versions.hasNext()) {
+ Table.KeyValue entry = versions.next();
+ String dbKey = entry.getKey();
+ CacheValue cached = table.getCacheValue(new CacheKey<>(dbKey));
+ if (cached != null && cached.getCacheValue() == null) {
+ continue;
+ }
+ OmKeyInfo value = cached != null ? cached.getCacheValue() : entry.getValue();
+ if (!filter.test(value)) {
+ continue;
+ }
+ if (bestKey == null || dbKey.compareTo(bestKey) < 0) {
+ bestKey = dbKey;
+ bestValue = value;
+ }
+ // DB entries ascend, so the first match is the smallest one in the DB
+ break;
+ }
+ }
+ return bestKey == null ? null : Pair.of(bestKey, bestValue);
+ }
+
/**
* Return bucket info for the specified bucket.
*/
@@ -959,11 +1062,15 @@ protected OmKeyInfo prepareFileInfo(
if (dbKeyInfo != null) {
// The key already exist, the new blocks will replace old ones
// as new versions unless the bucket does not have versioning
- // turned on.
- dbKeyInfo.addNewVersion(locations, false,
- omBucketInfo.getIsVersionEnabled());
+ // turned on. With S3-compatible versioning the previous current version
+ // is kept as its own record in the versionedKeyTable at commit time, so
+ // the in-record block version list is not used to accumulate object
+ // versions and always holds a single version.
+ boolean keepInRecordVersions = omBucketInfo.getIsVersionEnabled()
+ && !omBucketInfo.hasEverBeenVersioned();
+ dbKeyInfo.addNewVersion(locations, false, keepInRecordVersions);
long newSize = size;
- if (omBucketInfo.getIsVersionEnabled()) {
+ if (keepInRecordVersions) {
newSize += dbKeyInfo.getDataSize();
}
// The modification time is set in preExecute. Use the same
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMObjectVersionsReclaimRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMObjectVersionsReclaimRequest.java
new file mode 100644
index 000000000000..a721587178b4
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMObjectVersionsReclaimRequest.java
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.request.key;
+
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.BUCKET_LOCK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.audit.AuditLogger;
+import org.apache.hadoop.ozone.audit.AuditLoggerType;
+import org.apache.hadoop.ozone.audit.OMSystemAction;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.key.OMObjectVersionsReclaimResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ReclaimObjectVersionsRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ReclaimObjectVersionsResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles the reclamation of noncurrent object versions that exceed their
+ * bucket's maxVersions, as selected by VersionCleanupService. The versions
+ * leave the versionedKeyTable and their blocks are queued in the deletedTable;
+ * nothing here reclaims blocks directly.
+ *
+ * The service selects versions from the DB as of its own scan, so a version
+ * may already be gone by the time this request applies - permanently deleted,
+ * or promoted into the keyTable because the current version was deleted. Such
+ * a version is simply skipped: whatever is no longer in the versionedKeyTable
+ * is not this request's to remove, and a later run reselects if the key is
+ * still over its limit.
+ */
+public class OMObjectVersionsReclaimRequest extends OMKeyRequest {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(OMObjectVersionsReclaimRequest.class);
+
+ private static final AuditLogger AUDIT =
+ new AuditLogger(AuditLoggerType.OMSYSTEMLOGGER);
+ private static final String AUDIT_PARAM_NUM_VERSIONS =
+ "numObjectVersionsReclaimed";
+
+ public OMObjectVersionsReclaimRequest(OMRequest omRequest) {
+ // S3 object versioning is supported on OBJECT_STORE buckets only, so every
+ // version this request touches lives in the OBJECT_STORE keyTable.
+ super(omRequest, BucketLayout.OBJECT_STORE);
+ }
+
+ @Override
+ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager,
+ ExecutionContext context) {
+ final long trxnLogIndex = context.getIndex();
+
+ ReclaimObjectVersionsRequest reclaimRequest =
+ getOmRequest().getReclaimObjectVersionsRequest();
+ List versionsPerBucket =
+ reclaimRequest.getVersionsPerBucketList();
+
+ long numSubmittedVersions = 0;
+ for (ObjectVersionsBucket bucket : versionsPerBucket) {
+ numSubmittedVersions +=
+ bucket.getVersionKeysCount() + bucket.getMarkerKeysCount();
+ }
+
+ OMResponse.Builder omResponse =
+ OmResponseUtil.getOMResponseBuilder(getOmRequest());
+ omResponse.setReclaimObjectVersionsResponse(
+ ReclaimObjectVersionsResponse.newBuilder());
+
+ OMClientResponse omClientResponse = null;
+ List reclaimedVersionKeys = new ArrayList<>();
+ List reclaimedMarkerKeys = new ArrayList<>();
+ Map keysToDelete = new HashMap<>();
+ List updatedBuckets = new ArrayList<>();
+ Map auditParams = new LinkedHashMap<>();
+ try {
+ for (ObjectVersionsBucket bucket : versionsPerBucket) {
+ reclaimBucketVersions(ozoneManager, trxnLogIndex, bucket,
+ reclaimedVersionKeys, reclaimedMarkerKeys, keysToDelete,
+ updatedBuckets);
+ }
+
+ omClientResponse = new OMObjectVersionsReclaimResponse(
+ omResponse.build(), reclaimedVersionKeys, reclaimedMarkerKeys,
+ keysToDelete, updatedBuckets);
+
+ int reclaimed = reclaimedVersionKeys.size() + reclaimedMarkerKeys.size();
+ ozoneManager.getDeletionMetrics()
+ .incrNumObjectVersionsReclaimed(reclaimed);
+ auditParams.put(AUDIT_PARAM_NUM_VERSIONS, String.valueOf(reclaimed));
+ AUDIT.logWriteSuccess(ozoneManager.buildAuditMessageForSuccess(
+ OMSystemAction.OBJECT_VERSION_CLEANUP, auditParams));
+ LOG.debug("Reclaimed {} object versions and {} expired delete markers "
+ + "out of {} submitted.", reclaimedVersionKeys.size(),
+ reclaimedMarkerKeys.size(), numSubmittedVersions);
+ } catch (IOException ex) {
+ AUDIT.logWriteFailure(ozoneManager.buildAuditMessageForFailure(
+ OMSystemAction.OBJECT_VERSION_CLEANUP, auditParams, ex));
+ LOG.error("Failed to reclaim {} submitted object versions.",
+ numSubmittedVersions, ex);
+ omClientResponse = new OMObjectVersionsReclaimResponse(
+ createErrorOMResponse(omResponse, ex));
+ } finally {
+ if (omClientResponse != null) {
+ omClientResponse.setOmLockDetails(getOmLockDetails());
+ }
+ }
+
+ return omClientResponse;
+ }
+
+ /** Whether the key has any version left in the versionedKeyTable. */
+ private boolean hasNoncurrentVersion(OMMetadataManager omMetadataManager,
+ OmKeyInfo marker) throws IOException {
+ try (Table.KeyValueIterator versions =
+ omMetadataManager.getVersionedKeyTable().iterator(
+ omMetadataManager.getVersionedOzoneKeyPrefix(
+ marker.getVolumeName(), marker.getBucketName(),
+ marker.getKeyName()))) {
+ return versions.hasNext();
+ }
+ }
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ private void reclaimBucketVersions(OzoneManager ozoneManager,
+ long trxnLogIndex, ObjectVersionsBucket versionsBucket,
+ List reclaimedVersionKeys, List reclaimedMarkerKeys,
+ Map keysToDelete,
+ List updatedBuckets) throws IOException {
+
+ String volumeName = versionsBucket.getVolumeName();
+ String bucketName = versionsBucket.getBucketName();
+ OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
+
+ boolean acquiredLock = false;
+ try {
+ mergeOmLockDetails(omMetadataManager.getLock()
+ .acquireWriteLock(BUCKET_LOCK, volumeName, bucketName));
+ acquiredLock = getOmLockDetails().isLockAcquired();
+
+ OmBucketInfo omBucketInfo =
+ getBucketInfo(omMetadataManager, volumeName, bucketName);
+ if (omBucketInfo == null) {
+ LOG.debug("Bucket {}/{} no longer exists, skipping the {} object "
+ + "versions and {} delete markers submitted for it.", volumeName,
+ bucketName, versionsBucket.getVersionKeysCount(),
+ versionsBucket.getMarkerKeysCount());
+ return;
+ }
+
+ boolean reclaimedAny = false;
+ for (String versionKey : versionsBucket.getVersionKeysList()) {
+ OmKeyInfo version =
+ omMetadataManager.getVersionedKeyTable().get(versionKey);
+ if (version == null) {
+ // Already reclaimed, permanently deleted, or promoted into the
+ // keyTable since the service selected it.
+ continue;
+ }
+ if (trxnLogIndex < version.getUpdateID()) {
+ LOG.warn("Transaction log index {} is smaller than the current "
+ + "updateID {} of version {}, skipping reclamation.",
+ trxnLogIndex, version.getUpdateID(), versionKey);
+ continue;
+ }
+
+ version = version.toBuilder().setUpdateID(trxnLogIndex).build();
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(versionKey), CacheValue.get(trxnLogIndex));
+ reclaimedVersionKeys.add(versionKey);
+
+ // A delete marker holds no blocks, so it releases namespace but no
+ // space, and there is nothing to reclaim for it: an empty record is
+ // not queued in the deletedTable at all, as every other delete path
+ // does through AbstractOMKeyDeleteResponse.
+ boolean isVersionNonEmpty = !OmKeyInfo.isKeyEmpty(version);
+ if (isVersionNonEmpty) {
+ // Versions of one key share a deletedTable entry: it holds a
+ // RepeatedOmKeyInfo list that KeyDeletingService evaluates one
+ // record at a time.
+ String ozoneKey = omMetadataManager.getOzoneKey(volumeName,
+ bucketName, version.getKeyName());
+ addKeyInfoToDeleteMap(ozoneManager, trxnLogIndex, ozoneKey,
+ omBucketInfo.getObjectID(),
+ version.withCommittedKeyDeletedFlag(true), keysToDelete);
+ }
+ omBucketInfo.decrUsedBytes(sumBlockLengths(version), isVersionNonEmpty);
+ omBucketInfo.decrUsedNamespace(1L, isVersionNonEmpty);
+ reclaimedAny = true;
+ }
+
+ for (String markerKey : versionsBucket.getMarkerKeysList()) {
+ OmKeyInfo marker =
+ omMetadataManager.getKeyTable(getBucketLayout()).get(markerKey);
+ if (marker == null) {
+ continue;
+ }
+ // A write since the scan supersedes the marker: the key's current
+ // version is then a real object, and there is nothing expired here.
+ if (!marker.isDeleteMarker()) {
+ continue;
+ }
+ if (trxnLogIndex < marker.getUpdateID()) {
+ LOG.warn("Transaction log index {} is smaller than the current "
+ + "updateID {} of marker {}, skipping reclamation.",
+ trxnLogIndex, marker.getUpdateID(), markerKey);
+ continue;
+ }
+ // Re-checked here and not only in the scan: removing the marker while
+ // a noncurrent version survives would make that version current again,
+ // resurrecting an object the user deleted.
+ if (hasNoncurrentVersion(omMetadataManager, marker)) {
+ continue;
+ }
+
+ omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
+ new CacheKey<>(markerKey), CacheValue.get(trxnLogIndex));
+ reclaimedMarkerKeys.add(markerKey);
+
+ // The marker holds no blocks, so it never reaches the deletedTable; it
+ // does hold a namespace slot of its own.
+ omBucketInfo.decrUsedNamespace(1L, false);
+ reclaimedAny = true;
+ }
+
+ if (reclaimedAny) {
+ updatedBuckets.add(omBucketInfo.copyObject());
+ }
+ } finally {
+ if (acquiredLock) {
+ mergeOmLockDetails(omMetadataManager.getLock()
+ .releaseWriteLock(BUCKET_LOCK, volumeName, bucketName));
+ }
+ }
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartUploadCompleteRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartUploadCompleteRequest.java
index 841ced7dacce..0a6a224ea828 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartUploadCompleteRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartUploadCompleteRequest.java
@@ -34,6 +34,7 @@
import java.util.function.BiFunction;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.hdds.client.ReplicationConfig;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
@@ -55,6 +56,7 @@
import org.apache.hadoop.ozone.om.helpers.OmMultipartPartInfo;
import org.apache.hadoop.ozone.om.helpers.OmMultipartPartKey;
import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.VersionIdGenerator;
import org.apache.hadoop.ozone.om.request.file.OMFileRequest;
import org.apache.hadoop.ozone.om.request.key.OMKeyRequest;
import org.apache.hadoop.ozone.om.request.util.OMMultipartUploadUtils;
@@ -331,7 +333,23 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
OmKeyInfo keyToDelete =
omMetadataManager.getKeyTable(getBucketLayout()).get(dbOzoneKey);
boolean isNamespaceUpdate = false;
- if (keyToDelete != null && !omBucketInfo.getIsVersionEnabled()) {
+ // Completing a multipart upload creates a version like any other
+ // write: the version it supersedes is kept instead of reclaimed,
+ // except for the null version, which a suspended write replaces.
+ final boolean supersededVersionRetained = keyToDelete != null
+ && omBucketInfo.hasEverBeenVersioned()
+ && (omBucketInfo.isS3VersioningEnabled()
+ || !keyToDelete.isNullVersionRecord());
+ if (omBucketInfo.hasEverBeenVersioned()) {
+ omKeyInfo = omKeyInfo.toBuilder()
+ .setVersionId(ozoneManager.getVersionIdAllocator().allocate(
+ omMetadataManager, volumeName, bucketName, keyName,
+ trxnLogIndex, keyToDelete))
+ .setNullVersion(omBucketInfo.isS3VersioningSuspended())
+ .build();
+ }
+ if (keyToDelete != null && !omBucketInfo.getIsVersionEnabled()
+ && !supersededVersionRetained) {
RepeatedOmKeyInfo oldKeyVersionsToDelete = getOldVersionsToCleanUp(
keyToDelete, omBucketInfo.getObjectID(), trxnLogIndex);
allKeyInfoToRemove.addAll(oldKeyVersionsToDelete.getOmKeyInfoList());
@@ -342,6 +360,41 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
isNamespaceUpdate = true;
}
+ // The superseded version becomes a noncurrent version. Without this the
+ // record would be dropped from the keyTable, kept out of the
+ // deletedTable by the check above, and leak.
+ String dbVersionedKey = null;
+ OmKeyInfo versionedKeyInfo = null;
+ if (supersededVersionRetained) {
+ versionedKeyInfo = keyToDelete.getVersionId() != null ? keyToDelete
+ : keyToDelete.toBuilder()
+ .setVersionId(VersionIdGenerator.UNSET_VERSION_ID)
+ .setNullVersion(true)
+ .build();
+ dbVersionedKey = omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionedKeyInfo.getVersionId());
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ dbVersionedKey, versionedKeyInfo, trxnLogIndex);
+ }
+
+ // A suspended write replaces the key's null version wherever it is.
+ String replacedNullVersionKey = null;
+ if (omBucketInfo.isS3VersioningSuspended()
+ && supersededVersionRetained) {
+ Pair nullVersion = getNoncurrentNullVersion(
+ omMetadataManager, volumeName, bucketName, keyName);
+ if (nullVersion != null) {
+ replacedNullVersionKey = nullVersion.getKey();
+ allKeyInfoToRemove.add(nullVersion.getValue()
+ .withCommittedKeyDeletedFlag(true));
+ usedBytesDiff -= nullVersion.getValue().getReplicatedSize();
+ omBucketInfo.decrUsedNamespace(1L, true);
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(replacedNullVersionKey),
+ CacheValue.get(trxnLogIndex));
+ }
+ }
+
String dbBucketKey = omMetadataManager.getBucketKey(
omBucketInfo.getVolumeName(), omBucketInfo.getBucketName());
if (usedBytesDiff != 0) {
@@ -366,10 +419,12 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
long volumeId = omMetadataManager.getVolumeId(volumeName);
long bucketId = omMetadataManager.getBucketId(volumeName, bucketName);
omClientResponse =
- getOmClientResponse(multipartKey, omResponse, dbMultipartOpenKey,
+ ((S3MultipartUploadCompleteResponse) getOmClientResponse(multipartKey, omResponse, dbMultipartOpenKey,
omKeyInfo, allKeyInfoToRemove, omBucketInfo,
volumeId, bucketId, missingParentInfos, multipartKeyInfo,
- multipartPartKeysToDelete);
+ multipartPartKeysToDelete))
+ .withVersionedKey(dbVersionedKey, versionedKeyInfo,
+ replacedNullVersionKey);
result = Result.SUCCESS;
} else {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyCommitResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyCommitResponse.java
index 425c4f63ac5e..ed17907b4fa0 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyCommitResponse.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyCommitResponse.java
@@ -21,6 +21,7 @@
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.DELETED_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.OPEN_KEY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VERSIONED_KEY_TABLE;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nonnull;
@@ -39,7 +40,7 @@
* Response for CommitKey request.
*/
@CleanupTableInfo(cleanupTables = {OPEN_KEY_TABLE, KEY_TABLE, DELETED_TABLE,
- BUCKET_TABLE})
+ BUCKET_TABLE, VERSIONED_KEY_TABLE})
public class OMKeyCommitResponse extends OmKeyResponse {
private OmKeyInfo omKeyInfo;
@@ -51,6 +52,9 @@ public class OMKeyCommitResponse extends OmKeyResponse {
private OmKeyInfo newOpenKeyInfo;
private OmKeyInfo openKeyToUpdate;
private String openKeyNameToUpdate;
+ private String versionedKeyName;
+ private OmKeyInfo versionedKeyInfo;
+ private String replacedNullVersionKey;
@SuppressWarnings("checkstyle:ParameterNumber")
public OMKeyCommitResponse(
@@ -82,6 +86,27 @@ public OMKeyCommitResponse(@Nonnull OMResponse omResponse, @Nonnull
checkStatusNotOK();
}
+ /**
+ * The version this commit overwrote, to be kept in the versionedKeyTable as
+ * a noncurrent version. Null for buckets without S3-compatible versioning.
+ */
+ public OMKeyCommitResponse withVersionedKey(String dbVersionedKey,
+ OmKeyInfo keyInfo) {
+ this.versionedKeyName = dbVersionedKey;
+ this.versionedKeyInfo = keyInfo;
+ return this;
+ }
+
+ /**
+ * The noncurrent null version this commit replaced, to be removed from the
+ * versionedKeyTable. Null unless a suspended write replaced a null version
+ * that was not the current one.
+ */
+ public OMKeyCommitResponse withReplacedNullVersion(String dbVersionedKey) {
+ this.replacedNullVersionKey = dbVersionedKey;
+ return this;
+ }
+
@Override
public void addToDBBatch(OMMetadataManager omMetadataManager,
BatchOperation batchOperation) throws IOException {
@@ -98,6 +123,16 @@ public void addToDBBatch(OMMetadataManager omMetadataManager,
omMetadataManager.getKeyTable(getBucketLayout())
.putWithBatch(batchOperation, ozoneKeyName, omKeyInfo);
+ if (versionedKeyInfo != null) {
+ omMetadataManager.getVersionedKeyTable()
+ .putWithBatch(batchOperation, versionedKeyName, versionedKeyInfo);
+ }
+
+ if (replacedNullVersionKey != null) {
+ omMetadataManager.getVersionedKeyTable()
+ .deleteWithBatch(batchOperation, replacedNullVersionKey);
+ }
+
updateDeletedTable(omMetadataManager, batchOperation);
handleOpenKeyToUpdate(omMetadataManager, batchOperation);
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyDeleteMarkerResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyDeleteMarkerResponse.java
new file mode 100644
index 000000000000..50d4821b13e7
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyDeleteMarkerResponse.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.response.key;
+
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.BUCKET_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.DELETED_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VERSIONED_KEY_TABLE;
+
+import com.google.common.annotations.VisibleForTesting;
+import jakarta.annotation.Nonnull;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/**
+ * Response for a DeleteKey request on a bucket with S3-compatible versioning
+ * enabled: no data is removed. A delete marker becomes the current version in
+ * the keyTable, and the version it supersedes (if the key existed) moves to
+ * the versionedKeyTable.
+ */
+@CleanupTableInfo(cleanupTables = {KEY_TABLE, VERSIONED_KEY_TABLE, BUCKET_TABLE,
+ DELETED_TABLE})
+public class OMKeyDeleteMarkerResponse extends OmKeyResponse {
+
+ private OmKeyInfo deleteMarker;
+ private String ozoneKeyName;
+ private String movedVersionedKeyName;
+ private OmKeyInfo movedVersionedKeyInfo;
+ private OmBucketInfo omBucketInfo;
+ private String replacedNullVersionKey;
+ private Map keysToDelete;
+
+ public OMKeyDeleteMarkerResponse(@Nonnull OMResponse omResponse,
+ @Nonnull OmKeyInfo deleteMarker, @Nonnull String ozoneKeyName,
+ String movedVersionedKeyName, OmKeyInfo movedVersionedKeyInfo,
+ @Nonnull OmBucketInfo omBucketInfo) {
+ super(omResponse, omBucketInfo.getBucketLayout());
+ this.deleteMarker = deleteMarker;
+ this.ozoneKeyName = ozoneKeyName;
+ this.movedVersionedKeyName = movedVersionedKeyName;
+ this.movedVersionedKeyInfo = movedVersionedKeyInfo;
+ this.omBucketInfo = omBucketInfo;
+ }
+
+ /**
+ * For when the request is not successful.
+ * For a successful request, the other constructor should be used.
+ */
+ public OMKeyDeleteMarkerResponse(@Nonnull OMResponse omResponse,
+ @Nonnull BucketLayout bucketLayout) {
+ super(omResponse, bucketLayout);
+ checkStatusNotOK();
+ }
+
+ /**
+ * The null version the marker replaced, when versioning is suspended: the
+ * versionedKeyTable entry to remove, if it had one, and its blocks to queue
+ * for reclamation.
+ */
+ public OMKeyDeleteMarkerResponse withReplacedNullVersion(
+ String dbVersionedKey, Map deleteMap) {
+ this.replacedNullVersionKey = dbVersionedKey;
+ this.keysToDelete = deleteMap;
+ return this;
+ }
+
+ @VisibleForTesting
+ public Map getKeysToDelete() {
+ return keysToDelete;
+ }
+
+ @Override
+ public void addToDBBatch(OMMetadataManager omMetadataManager,
+ BatchOperation batchOperation) throws IOException {
+ omMetadataManager.getKeyTable(getBucketLayout())
+ .putWithBatch(batchOperation, ozoneKeyName, deleteMarker);
+
+ if (movedVersionedKeyInfo != null) {
+ omMetadataManager.getVersionedKeyTable().putWithBatch(batchOperation,
+ movedVersionedKeyName, movedVersionedKeyInfo);
+ }
+
+ if (replacedNullVersionKey != null) {
+ omMetadataManager.getVersionedKeyTable()
+ .deleteWithBatch(batchOperation, replacedNullVersionKey);
+ }
+
+ if (keysToDelete != null) {
+ for (Map.Entry entry
+ : keysToDelete.entrySet()) {
+ omMetadataManager.getDeletedTable().putWithBatch(batchOperation,
+ entry.getKey(), entry.getValue());
+ }
+ }
+
+ omMetadataManager.getBucketTable().putWithBatch(batchOperation,
+ omMetadataManager.getBucketKey(omBucketInfo.getVolumeName(),
+ omBucketInfo.getBucketName()), omBucketInfo);
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyVersionDeleteResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyVersionDeleteResponse.java
new file mode 100644
index 000000000000..c76ef0aceaf4
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyVersionDeleteResponse.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.response.key;
+
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.BUCKET_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.DELETED_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VERSIONED_KEY_TABLE;
+
+import jakarta.annotation.Nonnull;
+import java.io.IOException;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/**
+ * Response for {@code DELETE ?versionId=}: the version leaves the table that
+ * held it and its blocks go to the deletedTable, which is the single path
+ * through which version blocks are reclaimed. Removing the current version
+ * promotes the newest remaining one into the keyTable in the same batch, so the
+ * key never lacks a current version; if none remains the key disappears.
+ */
+@CleanupTableInfo(cleanupTables = {KEY_TABLE, VERSIONED_KEY_TABLE, DELETED_TABLE, BUCKET_TABLE})
+public class OMKeyVersionDeleteResponse extends AbstractOMKeyDeleteResponse {
+
+ private final OmKeyInfo deletedVersion;
+ private final String deletedKeyName;
+ private final boolean deletedCurrent;
+ private final String promotedKeyName;
+ private final OmKeyInfo promoted;
+ private final OmBucketInfo omBucketInfo;
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ public OMKeyVersionDeleteResponse(@Nonnull OMResponse omResponse,
+ @Nonnull OmKeyInfo deletedVersion, @Nonnull String deletedKeyName,
+ boolean deletedCurrent, String promotedKeyName, OmKeyInfo promoted,
+ @Nonnull OmBucketInfo omBucketInfo) {
+ super(omResponse, omBucketInfo.getBucketLayout());
+ this.deletedVersion = deletedVersion;
+ this.deletedKeyName = deletedKeyName;
+ this.deletedCurrent = deletedCurrent;
+ this.promotedKeyName = promotedKeyName;
+ this.promoted = promoted;
+ this.omBucketInfo = omBucketInfo;
+ }
+
+ /**
+ * For when the request is not successful.
+ * For a successful request, the other constructor should be used.
+ */
+ public OMKeyVersionDeleteResponse(@Nonnull OMResponse omResponse,
+ @Nonnull BucketLayout bucketLayout) {
+ super(omResponse, bucketLayout);
+ this.deletedVersion = null;
+ this.deletedKeyName = null;
+ this.deletedCurrent = false;
+ this.promotedKeyName = null;
+ this.promoted = null;
+ this.omBucketInfo = null;
+ checkStatusNotOK();
+ }
+
+ @Override
+ public void addToDBBatch(OMMetadataManager omMetadataManager,
+ BatchOperation batchOperation) throws IOException {
+ // The version leaves whichever table held it and its blocks go to the
+ // deletedTable; when it was the current version the successor takes its
+ // place in the keyTable, so the delete and the promotion land in one batch.
+ addDeletionToBatch(omMetadataManager, batchOperation,
+ deletedCurrent ? omMetadataManager.getKeyTable(getBucketLayout())
+ : omMetadataManager.getVersionedKeyTable(),
+ deletedKeyName, deletedVersion, omBucketInfo.getObjectID(), true);
+
+ if (promoted != null) {
+ omMetadataManager.getKeyTable(getBucketLayout())
+ .putWithBatch(batchOperation, deletedKeyName, promoted);
+ omMetadataManager.getVersionedKeyTable()
+ .deleteWithBatch(batchOperation, promotedKeyName);
+ }
+
+ omMetadataManager.getBucketTable().putWithBatch(batchOperation,
+ omMetadataManager.getBucketKey(omBucketInfo.getVolumeName(),
+ omBucketInfo.getBucketName()), omBucketInfo);
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMObjectVersionsReclaimResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMObjectVersionsReclaimResponse.java
new file mode 100644
index 000000000000..91cdeffb0dcf
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMObjectVersionsReclaimResponse.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.response.key;
+
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.BUCKET_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.DELETED_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VERSIONED_KEY_TABLE;
+
+import com.google.common.annotations.VisibleForTesting;
+import jakarta.annotation.Nonnull;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/**
+ * Response for the reclamation of noncurrent object versions that exceed their
+ * bucket's maxVersions. The versions leave the versionedKeyTable and their
+ * blocks go to the deletedTable, which is the single path through which
+ * version blocks are reclaimed: KeyDeletingService decides from there whether
+ * a snapshot still needs them.
+ */
+@CleanupTableInfo(cleanupTables = {KEY_TABLE, VERSIONED_KEY_TABLE,
+ DELETED_TABLE, BUCKET_TABLE})
+public class OMObjectVersionsReclaimResponse extends OmKeyResponse {
+
+ private final List reclaimedVersionKeys;
+ private final List reclaimedMarkerKeys;
+ private final Map keysToDelete;
+ private final List updatedBuckets;
+
+ public OMObjectVersionsReclaimResponse(@Nonnull OMResponse omResponse,
+ @Nonnull List reclaimedVersionKeys,
+ @Nonnull List reclaimedMarkerKeys,
+ @Nonnull Map keysToDelete,
+ @Nonnull List updatedBuckets) {
+ super(omResponse, BucketLayout.OBJECT_STORE);
+ this.reclaimedVersionKeys = reclaimedVersionKeys;
+ this.reclaimedMarkerKeys = reclaimedMarkerKeys;
+ this.keysToDelete = keysToDelete;
+ this.updatedBuckets = updatedBuckets;
+ }
+
+ /**
+ * For when the request is not successful.
+ * For a successful request, the other constructor should be used.
+ */
+ public OMObjectVersionsReclaimResponse(@Nonnull OMResponse omResponse) {
+ super(omResponse, BucketLayout.OBJECT_STORE);
+ this.reclaimedVersionKeys = Collections.emptyList();
+ this.reclaimedMarkerKeys = Collections.emptyList();
+ this.keysToDelete = Collections.emptyMap();
+ this.updatedBuckets = Collections.emptyList();
+ checkStatusNotOK();
+ }
+
+ @VisibleForTesting
+ public Map getKeysToDelete() {
+ return keysToDelete;
+ }
+
+ @Override
+ public void addToDBBatch(OMMetadataManager omMetadataManager,
+ BatchOperation batchOperation) throws IOException {
+ for (String versionKey : reclaimedVersionKeys) {
+ omMetadataManager.getVersionedKeyTable()
+ .deleteWithBatch(batchOperation, versionKey);
+ }
+
+ // An expired marker leaves the keyTable and the key disappears with it.
+ // It holds no blocks, so nothing goes to the deletedTable for it.
+ for (String markerKey : reclaimedMarkerKeys) {
+ omMetadataManager.getKeyTable(getBucketLayout())
+ .deleteWithBatch(batchOperation, markerKey);
+ }
+
+ for (Map.Entry entry : keysToDelete.entrySet()) {
+ omMetadataManager.getDeletedTable().putWithBatch(batchOperation,
+ entry.getKey(), entry.getValue());
+ }
+
+ for (OmBucketInfo omBucketInfo : updatedBuckets) {
+ omMetadataManager.getBucketTable().putWithBatch(batchOperation,
+ omMetadataManager.getBucketKey(omBucketInfo.getVolumeName(),
+ omBucketInfo.getBucketName()), omBucketInfo);
+ }
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/multipart/S3MultipartUploadCompleteResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/multipart/S3MultipartUploadCompleteResponse.java
index a1dfe4ed317d..b9da28f2bbb4 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/multipart/S3MultipartUploadCompleteResponse.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/multipart/S3MultipartUploadCompleteResponse.java
@@ -23,6 +23,7 @@
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.MULTIPART_INFO_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.MULTIPART_PARTS_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.OPEN_KEY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VERSIONED_KEY_TABLE;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
@@ -48,7 +49,8 @@
* 3) Delete unused parts.
*/
@CleanupTableInfo(cleanupTables = {OPEN_KEY_TABLE, KEY_TABLE, DELETED_TABLE,
- MULTIPART_INFO_TABLE, MULTIPART_PARTS_TABLE, BUCKET_TABLE})
+ MULTIPART_INFO_TABLE, MULTIPART_PARTS_TABLE, BUCKET_TABLE,
+ VERSIONED_KEY_TABLE})
public class S3MultipartUploadCompleteResponse extends OmKeyResponse {
private String multipartKey;
private String multipartOpenKey;
@@ -57,6 +59,9 @@ public class S3MultipartUploadCompleteResponse extends OmKeyResponse {
private List multipartPartKeysToDelete;
private OmBucketInfo omBucketInfo;
private long bucketId;
+ private String versionedKeyName;
+ private OmKeyInfo versionedKeyInfo;
+ private String replacedNullVersionKey;
@SuppressWarnings("parameternumber")
public S3MultipartUploadCompleteResponse(
@@ -128,6 +133,19 @@ public void addToDBBatch(OMMetadataManager omMetadataManager,
}
}
+ /**
+ * The version this upload superseded, to be kept in the versionedKeyTable as
+ * a noncurrent version, and the null version it replaced, if any. Both null
+ * for buckets that have never been versioned.
+ */
+ public S3MultipartUploadCompleteResponse withVersionedKey(
+ String dbVersionedKey, OmKeyInfo keyInfo, String replacedNullVersion) {
+ this.versionedKeyName = dbVersionedKey;
+ this.versionedKeyInfo = keyInfo;
+ this.replacedNullVersionKey = replacedNullVersion;
+ return this;
+ }
+
protected String addToKeyTable(OMMetadataManager omMetadataManager,
BatchOperation batchOperation) throws IOException {
@@ -135,6 +153,15 @@ protected String addToKeyTable(OMMetadataManager omMetadataManager,
omKeyInfo.getBucketName(), omKeyInfo.getKeyName());
omMetadataManager.getKeyTable(getBucketLayout())
.putWithBatch(batchOperation, ozoneKey, omKeyInfo);
+
+ if (versionedKeyInfo != null) {
+ omMetadataManager.getVersionedKeyTable()
+ .putWithBatch(batchOperation, versionedKeyName, versionedKeyInfo);
+ }
+ if (replacedNullVersionKey != null) {
+ omMetadataManager.getVersionedKeyTable()
+ .deleteWithBatch(batchOperation, replacedNullVersionKey);
+ }
return ozoneKey;
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/VersionCleanupService.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/VersionCleanupService.java
new file mode 100644
index 000000000000..23e26f96ff2b
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/VersionCleanupService.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.service;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.ServiceException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.utils.BackgroundService;
+import org.apache.hadoop.hdds.utils.BackgroundTask;
+import org.apache.hadoop.hdds.utils.BackgroundTaskQueue;
+import org.apache.hadoop.hdds.utils.BackgroundTaskResult;
+import org.apache.hadoop.ozone.om.KeyManager;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ReclaimObjectVersionsRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.apache.hadoop.util.Time;
+import org.apache.ratis.protocol.ClientId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Background service that reclaims the noncurrent object versions a key has
+ * accumulated beyond its bucket's maxVersions, oldest first. Trimming runs
+ * here rather than inside the write transaction, so the cost of a version
+ * write does not grow with the number of versions the key already has.
+ *
+ * The versions it selects are handed to OM as a ReclaimObjectVersions
+ * request, which moves them to the deletedTable; the blocks themselves are
+ * reclaimed by KeyDeletingService, which is where snapshot-awareness lives.
+ */
+public class VersionCleanupService extends BackgroundService {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(VersionCleanupService.class);
+
+ // Similar to OpenKeyCleanupService, use a single thread.
+ private static final int VERSION_CLEANUP_CORE_POOL_SIZE = 1;
+
+ private final OzoneManager ozoneManager;
+ private final KeyManager keyManager;
+ // Dummy client ID to use for response.
+ private final ClientId clientId = ClientId.randomId();
+ private final int defaultMaxVersions;
+ private final int versionLimitPerTask;
+ // There is no index of delete markers, so finding them walks the keyTable.
+ // The walk is bounded per run and resumes where the last one stopped, so no
+ // single run iterates an unbounded number of keys. The resume point is held
+ // in memory only: a restart or a failover just starts the walk over, which
+ // costs a pass and loses nothing.
+ private final int markerScanBudget;
+ private String markerScanStartKey;
+ private final AtomicLong submittedVersionCount;
+ private final AtomicLong runCount;
+ private final AtomicBoolean suspended;
+
+ public VersionCleanupService(long interval, TimeUnit unit, long timeout,
+ OzoneManager ozoneManager, ConfigurationSource conf) {
+ super("VersionCleanupService", interval, unit,
+ VERSION_CLEANUP_CORE_POOL_SIZE, timeout,
+ ozoneManager.getThreadNamePrefix());
+ this.ozoneManager = ozoneManager;
+ this.keyManager = ozoneManager.getKeyManager();
+
+ this.defaultMaxVersions = conf.getInt(
+ OMConfigKeys.OZONE_OM_VERSIONING_MAX_VERSIONS,
+ OMConfigKeys.OZONE_OM_VERSIONING_MAX_VERSIONS_DEFAULT);
+
+ this.versionLimitPerTask = conf.getInt(
+ OMConfigKeys.OZONE_OM_VERSION_CLEANUP_LIMIT_PER_TASK,
+ OMConfigKeys.OZONE_OM_VERSION_CLEANUP_LIMIT_PER_TASK_DEFAULT);
+
+ this.markerScanBudget = conf.getInt(
+ OMConfigKeys.OZONE_OM_VERSION_CLEANUP_MARKER_SCAN_BUDGET,
+ OMConfigKeys.OZONE_OM_VERSION_CLEANUP_MARKER_SCAN_BUDGET_DEFAULT);
+
+ this.markerScanStartKey = null;
+ this.submittedVersionCount = new AtomicLong(0);
+ this.runCount = new AtomicLong(0);
+ this.suspended = new AtomicBoolean(false);
+ }
+
+ /**
+ * Returns the number of times this Background service has run.
+ *
+ * @return Long, run count.
+ */
+ @VisibleForTesting
+ public long getRunCount() {
+ return runCount.get();
+ }
+
+ /**
+ * Suspend the service (for testing).
+ */
+ @VisibleForTesting
+ public void suspend() {
+ suspended.set(true);
+ }
+
+ /**
+ * Resume the service if suspended (for testing).
+ */
+ @VisibleForTesting
+ public void resume() {
+ suspended.set(false);
+ }
+
+ /**
+ * Returns the number of object versions that were submitted for reclamation
+ * by this service. A version that is permanently deleted or promoted between
+ * being submitted and the request being applied is not reclaimed here.
+ *
+ * @return long count.
+ */
+ @VisibleForTesting
+ public long getSubmittedVersionCount() {
+ return submittedVersionCount.get();
+ }
+
+ @Override
+ public BackgroundTaskQueue getTasks() {
+ BackgroundTaskQueue queue = new BackgroundTaskQueue();
+ queue.add(new VersionCleanupTask());
+ return queue;
+ }
+
+ private boolean shouldRun() {
+ return !suspended.get() && ozoneManager.isLeaderReady();
+ }
+
+ private class VersionCleanupTask implements BackgroundTask {
+
+ @Override
+ public int getPriority() {
+ return 0;
+ }
+
+ @Override
+ public BackgroundTaskResult call() throws Exception {
+ if (!shouldRun()) {
+ return BackgroundTaskResult.EmptyTaskResult.newResult();
+ }
+
+ runCount.incrementAndGet();
+ long startTime = Time.monotonicNow();
+ List versionsToReclaim;
+ try {
+ versionsToReclaim = new ArrayList<>(keyManager.getVersionsToReclaim(
+ defaultMaxVersions, versionLimitPerTask));
+ versionsToReclaim.addAll(scanExpiredDeleteMarkers());
+ } catch (IOException e) {
+ LOG.error("Unable to get the object versions to reclaim, retry in "
+ + "next interval", e);
+ return BackgroundTaskResult.EmptyTaskResult.newResult();
+ }
+
+ if (!versionsToReclaim.isEmpty()) {
+ int numVersions = versionsToReclaim.stream()
+ .mapToInt(bucket -> bucket.getVersionKeysCount()
+ + bucket.getMarkerKeysCount())
+ .sum();
+
+ submitRequest(createRequest(versionsToReclaim));
+
+ LOG.debug("Number of object versions submitted for reclamation: {}, "
+ + "elapsed time: {}ms", numVersions,
+ Time.monotonicNow() - startTime);
+ submittedVersionCount.addAndGet(numVersions);
+ ozoneManager.getDeletionMetrics()
+ .incrNumObjectVersionsSentForReclaim(numVersions);
+ }
+ return BackgroundTaskResult.EmptyTaskResult.newResult();
+ }
+
+ /**
+ * One bounded pass over the keyTable looking for keys left with only a
+ * delete marker, resuming where the previous pass stopped.
+ */
+ private List scanExpiredDeleteMarkers()
+ throws IOException {
+ OMMetadataManager.ExpiredDeleteMarkers markers =
+ keyManager.getExpiredDeleteMarkers(markerScanStartKey,
+ markerScanBudget, versionLimitPerTask);
+ // null means the walk reached the end of the table, so the next pass
+ // starts over from the beginning.
+ markerScanStartKey = markers.getNextStartKey();
+ return markers.getMarkersPerBucket();
+ }
+
+ private OMRequest createRequest(
+ List versionsPerBucket) {
+ ReclaimObjectVersionsRequest request =
+ ReclaimObjectVersionsRequest.newBuilder()
+ .addAllVersionsPerBucket(versionsPerBucket)
+ .build();
+
+ return OMRequest.newBuilder()
+ .setCmdType(Type.ReclaimObjectVersions)
+ .setReclaimObjectVersionsRequest(request)
+ .setClientId(clientId.toString())
+ .build();
+ }
+
+ private void submitRequest(OMRequest omRequest) {
+ try {
+ OzoneManagerRatisUtils.submitRequest(ozoneManager, omRequest, clientId, runCount.get());
+ } catch (ServiceException e) {
+ LOG.error("Object version reclamation request failed. "
+ + "Will retry at next run.", e);
+ }
+ }
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMLayoutFeature.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMLayoutFeature.java
index ef99b453b7f0..b832d6efc9ec 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMLayoutFeature.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMLayoutFeature.java
@@ -44,7 +44,10 @@ public enum OMLayoutFeature implements LayoutFeature {
QUOTA(6, "Ozone quota re-calculate"),
HBASE_SUPPORT(7, "Full support of hsync, lease recovery and listOpenFiles APIs for HBase"),
DELEGATION_TOKEN_SYMMETRIC_SIGN(8, "Delegation token signed by symmetric key"),
- SNAPSHOT_DEFRAG(9, "Supporting defragmentation of snapshot");
+ SNAPSHOT_DEFRAG(9, "Supporting defragmentation of snapshot"),
+
+ OBJECT_VERSIONING(10, "S3-compatible object versioning: bucket versioning"
+ + " state machine and the versionedKeyTable for noncurrent versions");
/////////////////////////////// /////////////////////////////
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 7359065986ab..0e6a589f66db 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -645,6 +645,8 @@ private LookupKeyResponse lookupKey(LookupKeyRequest request,
.setLatestVersionLocation(keyArgs.getLatestVersionLocation())
.setSortDatanodesInPipeline(keyArgs.getSortDatanodes())
.setHeadOp(keyArgs.getHeadOp())
+ .setVersionId(keyArgs.hasVersionId() ? keyArgs.getVersionId() : null)
+ .setNullVersion(keyArgs.getNullVersion())
.build();
OmKeyInfo keyInfo = impl.lookupKey(omKeyArgs);
@@ -666,6 +668,8 @@ private GetKeyInfoResponse getKeyInfo(GetKeyInfoRequest request,
.setForceUpdateContainerCacheFromSCM(
keyArgs.getForceUpdateContainerCacheFromSCM())
.setMultipartUploadPartNumber(keyArgs.getMultipartNumber())
+ .setVersionId(keyArgs.hasVersionId() ? keyArgs.getVersionId() : null)
+ .setNullVersion(keyArgs.getNullVersion())
.build();
KeyInfoWithVolumeContext keyInfo = impl.getKeyInfo(omKeyArgs,
request.getAssumeS3Context());
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerUnit.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerUnit.java
index 9b1844212073..ace48d8ffea2 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerUnit.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerUnit.java
@@ -23,6 +23,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.anySet;
import static org.mockito.Mockito.mock;
@@ -64,6 +66,7 @@
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
@@ -579,6 +582,234 @@ public void testGetKeyInfo() throws IOException {
.getContainerWithPipelineBatch(containerIDs);
}
+ private OmKeyInfo versionedKeyInfo(String volume, String bucket, String key,
+ long versionId, boolean nullVersion, boolean deleteMarker) {
+ return new OmKeyInfo.Builder()
+ .setVolumeName(volume)
+ .setBucketName(bucket)
+ .setKeyName(key)
+ .setOmKeyLocationInfos(Collections.emptyList())
+ .setCreationTime(Time.now())
+ .setModificationTime(Time.now())
+ .setDataSize(0)
+ .setReplicationConfig(RatisReplicationConfig.getInstance(ReplicationFactor.ONE))
+ .setVersionId(versionId)
+ .setNullVersion(nullVersion)
+ .setDeleteMarker(deleteMarker)
+ .build();
+ }
+
+ /**
+ * Enabling versioning does not rewrite the objects a bucket already holds:
+ * they keep no versionId at all, and S3 reports their version as "null".
+ */
+ @Test
+ public void testLookupPreVersioningKeyAsNullVersion() throws Exception {
+ String volume = "vol-legacy";
+ String bucket = "buck-legacy";
+ String key = "obj";
+ OMRequestTestUtils.addVolumeAndBucketToDB(volume, bucket, metadataManager,
+ BucketLayout.OBJECT_STORE);
+
+ // written before versioning was enabled: no versionId, no null flag
+ OmKeyInfo legacy = new OmKeyInfo.Builder()
+ .setVolumeName(volume)
+ .setBucketName(bucket)
+ .setKeyName(key)
+ .setOmKeyLocationInfos(Collections.emptyList())
+ .setCreationTime(Time.now())
+ .setModificationTime(Time.now())
+ .setDataSize(0)
+ .setReplicationConfig(
+ RatisReplicationConfig.getInstance(ReplicationFactor.ONE))
+ .build();
+ metadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ metadataManager.getOzoneKey(volume, bucket, key), legacy);
+
+ OmKeyArgs.Builder base = new OmKeyArgs.Builder()
+ .setVolumeName(volume).setBucketName(bucket).setKeyName(key)
+ .setHeadOp(true);
+
+ // a plain read still returns it
+ OmKeyArgs args = base.build();
+ assertNull(keyManager.lookupKey(args, resolveBucket(args), null)
+ .getVersionId());
+
+ // and version "null" addresses it, without the record having been rewritten
+ args = base.setNullVersion(true).build();
+ OmKeyInfo nullVersion =
+ keyManager.lookupKey(args, resolveBucket(args), null);
+ assertNull(nullVersion.getVersionId());
+ assertTrue(nullVersion.isNullVersionRecord());
+ assertEquals(legacy, metadataManager
+ .getKeyTable(BucketLayout.OBJECT_STORE)
+ .get(metadataManager.getOzoneKey(volume, bucket, key)));
+ }
+
+ @Test
+ public void testLookupKeyByVersionId() throws Exception {
+ String volume = "vol-ver";
+ String bucket = "buck-ver";
+ String key = "obj";
+ OMRequestTestUtils.addVolumeAndBucketToDB(volume, bucket, metadataManager,
+ BucketLayout.OBJECT_STORE);
+
+ // current version 30, one noncurrent version 20, and a null version 10
+ metadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ metadataManager.getOzoneKey(volume, bucket, key),
+ versionedKeyInfo(volume, bucket, key, 30L, false, false));
+ metadataManager.getVersionedKeyTable().put(
+ metadataManager.getVersionedOzoneKey(volume, bucket, key, 20L),
+ versionedKeyInfo(volume, bucket, key, 20L, false, false));
+ metadataManager.getVersionedKeyTable().put(
+ metadataManager.getVersionedOzoneKey(volume, bucket, key, 10L),
+ versionedKeyInfo(volume, bucket, key, 10L, true, false));
+
+ OmKeyArgs.Builder base = new OmKeyArgs.Builder()
+ .setVolumeName(volume).setBucketName(bucket).setKeyName(key)
+ .setHeadOp(true);
+
+ // no versionId addresses the current version
+ OmKeyArgs args = base.build();
+ assertEquals(30L, keyManager.lookupKey(args, resolveBucket(args), null)
+ .getVersionId());
+
+ // the current version can also be addressed by its id
+ args = base.setVersionId(30L).build();
+ assertEquals(30L, keyManager.lookupKey(args, resolveBucket(args), null)
+ .getVersionId());
+
+ // a noncurrent version resolves through the versionedKeyTable
+ args = base.setVersionId(20L).build();
+ assertEquals(20L, keyManager.lookupKey(args, resolveBucket(args), null)
+ .getVersionId());
+
+ // the null version slot is found by attribute, not by id
+ args = base.setNullVersion(true).build();
+ OmKeyInfo nullVersion =
+ keyManager.lookupKey(args, resolveBucket(args), null);
+ assertEquals(10L, nullVersion.getVersionId());
+ assertTrue(nullVersion.isNullVersion());
+
+ // an unknown versionId is a plain not-found
+ OmKeyArgs unknown = base.setVersionId(999L).build();
+ OMException ex = assertThrows(OMException.class,
+ () -> keyManager.lookupKey(unknown, resolveBucket(unknown), null));
+ assertEquals(OMException.ResultCodes.KEY_NOT_FOUND, ex.getResult());
+ }
+
+ @Test
+ public void testLookupOfDeleteMarkerIsDistinguishable() throws Exception {
+ String volume = "vol-marker";
+ String bucket = "buck-marker";
+ String key = "obj";
+ OMRequestTestUtils.addVolumeAndBucketToDB(volume, bucket, metadataManager,
+ BucketLayout.OBJECT_STORE);
+
+ // current version is a marker, with a readable version behind it
+ metadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ metadataManager.getOzoneKey(volume, bucket, key),
+ versionedKeyInfo(volume, bucket, key, 40L, false, true));
+ metadataManager.getVersionedKeyTable().put(
+ metadataManager.getVersionedOzoneKey(volume, bucket, key, 20L),
+ versionedKeyInfo(volume, bucket, key, 20L, false, false));
+
+ OmKeyArgs.Builder base = new OmKeyArgs.Builder()
+ .setVolumeName(volume).setBucketName(bucket).setKeyName(key)
+ .setHeadOp(true);
+
+ // without a versionId a current marker reads as absent
+ OmKeyArgs current = base.build();
+ OMException ex = assertThrows(OMException.class,
+ () -> keyManager.lookupKey(current, resolveBucket(current), null));
+ assertEquals(OMException.ResultCodes.KEY_NOT_FOUND, ex.getResult());
+
+ // naming the marker's version is a different condition: S3 answers 405
+ OmKeyArgs marker = base.setVersionId(40L).build();
+ ex = assertThrows(OMException.class,
+ () -> keyManager.lookupKey(marker, resolveBucket(marker), null));
+ assertEquals(OMException.ResultCodes.KEY_IS_DELETE_MARKER, ex.getResult());
+
+ // the version behind the marker stays readable
+ OmKeyArgs behind = base.setVersionId(20L).build();
+ assertEquals(20L,
+ keyManager.lookupKey(behind, resolveBucket(behind), null).getVersionId());
+ }
+
+ @Test
+ public void testCurrentVersionIsResolvedWithoutReadingVersionedKeyTable()
+ throws Exception {
+ String volume = "vol-cur";
+ String bucket = "buck-cur";
+ String key = "obj";
+ OMRequestTestUtils.addVolumeAndBucketToDB(volume, bucket, metadataManager,
+ BucketLayout.OBJECT_STORE);
+
+ // The current version and a decoy stored under the dbKey that version would
+ // occupy in the versionedKeyTable. Resolving to the current record proves
+ // the lookup answers from keyTable and never falls through to the scan.
+ metadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ metadataManager.getOzoneKey(volume, bucket, key),
+ versionedKeyInfo(volume, bucket, key, 30L, false, false));
+ metadataManager.getVersionedKeyTable().put(
+ metadataManager.getVersionedOzoneKey(volume, bucket, key, 30L),
+ versionedKeyInfo(volume, bucket, key, 30L, false, true));
+
+ OmKeyArgs args = new OmKeyArgs.Builder()
+ .setVolumeName(volume).setBucketName(bucket).setKeyName(key)
+ .setHeadOp(true).setVersionId(30L).build();
+
+ // the decoy is a delete marker, so reading it would have thrown
+ assertEquals(30L, keyManager.lookupKey(args, resolveBucket(args), null)
+ .getVersionId());
+ }
+
+ @Test
+ public void testNullVersionSlotCanBeTheCurrentVersion() throws Exception {
+ String volume = "vol-null";
+ String bucket = "buck-null";
+ String key = "obj";
+ OMRequestTestUtils.addVolumeAndBucketToDB(volume, bucket, metadataManager,
+ BucketLayout.OBJECT_STORE);
+
+ // A suspended PUT leaves the null version as the current one, with the
+ // versions accumulated while enabled behind it.
+ metadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ metadataManager.getOzoneKey(volume, bucket, key),
+ versionedKeyInfo(volume, bucket, key, 50L, true, false));
+ metadataManager.getVersionedKeyTable().put(
+ metadataManager.getVersionedOzoneKey(volume, bucket, key, 20L),
+ versionedKeyInfo(volume, bucket, key, 20L, false, false));
+
+ OmKeyArgs args = new OmKeyArgs.Builder()
+ .setVolumeName(volume).setBucketName(bucket).setKeyName(key)
+ .setHeadOp(true).setNullVersion(true).build();
+
+ OmKeyInfo nullVersion = keyManager.lookupKey(args, resolveBucket(args), null);
+ assertEquals(50L, nullVersion.getVersionId());
+ assertTrue(nullVersion.isNullVersion());
+ }
+
+ @Test
+ public void testVersionIdRejectedOnFileSystemOptimizedBucket()
+ throws Exception {
+ String volume = "vol-fso";
+ String bucket = "buck-fso";
+ OMRequestTestUtils.addVolumeAndBucketToDB(volume, bucket, metadataManager,
+ BucketLayout.FILE_SYSTEM_OPTIMIZED);
+
+ OmKeyArgs args = new OmKeyArgs.Builder()
+ .setVolumeName(volume).setBucketName(bucket).setKeyName("obj")
+ .setHeadOp(true).setVersionId(1L).build();
+ ResolvedBucket fso = new ResolvedBucket(volume, bucket, volume, bucket, "",
+ BucketLayout.FILE_SYSTEM_OPTIMIZED);
+
+ OMException ex = assertThrows(OMException.class,
+ () -> keyManager.lookupKey(args, fso, null));
+ assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION,
+ ex.getResult());
+ }
+
private ResolvedBucket resolveBucket(OmKeyArgs keyArgs) {
return new ResolvedBucket(keyArgs.getVolumeName(), keyArgs.getBucketName(),
keyArgs.getVolumeName(), keyArgs.getBucketName(), "",
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
index ec241f9dcb3e..ce639da0894c 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
@@ -46,6 +46,7 @@
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.TENANT_STATE_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.TRANSACTION_INFO_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.USER_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VERSIONED_KEY_TABLE;
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VOLUME_TABLE;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.BUCKET_NOT_FOUND;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLUME_NOT_FOUND;
@@ -53,6 +54,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -84,6 +86,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.BucketVersioningStatus;
import org.apache.hadoop.ozone.om.helpers.ListOpenFilesResult;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
@@ -97,6 +100,7 @@
import org.apache.hadoop.ozone.om.request.util.OMMultipartUploadUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadInfo;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ExpiredMultipartUploadsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OpenKey;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OpenKeyBucket;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.PartKeyInfo;
@@ -120,6 +124,7 @@ public class TestOmMetadataManager {
VOLUME_TABLE,
BUCKET_TABLE,
KEY_TABLE,
+ VERSIONED_KEY_TABLE,
DELETED_TABLE,
OPEN_KEY_TABLE,
MULTIPART_INFO_TABLE,
@@ -172,6 +177,415 @@ public void testTransactionTable() throws Exception {
assertEquals(250, transactionInfo.getTransactionIndex());
}
+ @Test
+ public void testVersionedOzoneKeyOrdering() {
+ String prefix = omMetadataManager.getVersionedOzoneKeyPrefix("vol", "buck", "key");
+ assertEquals("/vol/buck/key\0", prefix);
+
+ // newer versions (larger versionId) must sort before older ones, and all
+ // versioned keys must sort under the key's prefix
+ String v1 = omMetadataManager.getVersionedOzoneKey("vol", "buck", "key", 1L);
+ String v2 = omMetadataManager.getVersionedOzoneKey("vol", "buck", "key", 42L);
+ String v3 = omMetadataManager.getVersionedOzoneKey("vol", "buck", "key", Long.MAX_VALUE - 1);
+ assertThat(v3).startsWith(prefix).isLessThan(v2);
+ assertThat(v2).startsWith(prefix).isLessThan(v1);
+
+ // fixed-width suffix: identical length regardless of versionId magnitude
+ assertEquals(v1.length(), v3.length());
+ }
+
+ @Test
+ public void testVersionedOzoneKeyIsolatedFromNestedKeys() {
+ // OBJECT_STORE key names contain '/' verbatim, so "key" and "key/001" are two
+ // unrelated keys. Every version of "key" must sort under "key"'s prefix and
+ // ahead of anything belonging to "key/001", otherwise a prefix seek for the
+ // newest noncurrent version of "key" would land on a version of "key/001".
+ String prefix = omMetadataManager.getVersionedOzoneKeyPrefix("vol", "buck", "key");
+ String nestedPrefix = omMetadataManager.getVersionedOzoneKeyPrefix("vol", "buck", "key/001");
+ assertThat(nestedPrefix).doesNotStartWith(prefix);
+
+ String oldest = omMetadataManager.getVersionedOzoneKey("vol", "buck", "key", 1L);
+ String nestedNewest =
+ omMetadataManager.getVersionedOzoneKey("vol", "buck", "key/001", Long.MAX_VALUE - 1);
+ assertThat(oldest).isLessThan(nestedNewest);
+
+ // the nested key's own current entry in keyTable also sorts after all of them
+ assertThat(oldest).isLessThan(omMetadataManager.getOzoneKey("vol", "buck", "key/001"));
+ }
+
+ @Test
+ public void testGetVersionsToReclaimAppliesTheBucketLimit() throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 3);
+ // versions 1..5, all noncurrent; the current version lives in the keyTable
+ // and counts toward the limit, so a limit of 3 keeps 2 noncurrent ones
+ addNoncurrentVersionsToDB(volumeName, bucketName, "key1", 1, 2, 3, 4, 5);
+
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(100, 1000);
+
+ assertEquals(1, reclaimable.size());
+ ObjectVersionsBucket bucket = reclaimable.get(0);
+ assertEquals(volumeName, bucket.getVolumeName());
+ assertEquals(bucketName, bucket.getBucketName());
+ // versions 1, 2 and 3 go and 4 and 5 stay; the scan walks a key newest
+ // first, so the doomed ones come back in descending versionId order
+ assertEquals(versionKeys(volumeName, bucketName, "key1", 3, 2, 1),
+ bucket.getVersionKeysList());
+ }
+
+ @Test
+ public void testGetVersionsToReclaimCountsEachKeySeparately()
+ throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 2);
+ addNoncurrentVersionsToDB(volumeName, bucketName, "key1", 1, 2, 3);
+ // a key within its limit contributes nothing, and does not carry the
+ // running count of the previous key into its own
+ addNoncurrentVersionsToDB(volumeName, bucketName, "key2", 7);
+
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(100, 1000);
+
+ assertEquals(1, reclaimable.size());
+ assertEquals(versionKeys(volumeName, bucketName, "key1", 2, 1),
+ reclaimable.get(0).getVersionKeysList());
+ }
+
+ @Test
+ public void testGetVersionsToReclaimSkipsUnlimitedAndUnversioned()
+ throws Exception {
+ // 0 means unlimited, on the bucket itself and as the cluster default
+ addVersionedBucketToDB("vol1", "unlimited", 0);
+ addNoncurrentVersionsToDB("vol1", "unlimited", "key1", 1, 2, 3);
+
+ addVersionedBucketToDB("vol1", "clusterdefault", null);
+ addNoncurrentVersionsToDB("vol1", "clusterdefault", "key1", 1, 2, 3);
+
+ // a bucket that never had versioning enabled is not examined at all
+ omMetadataManager.getBucketTable().put(
+ omMetadataManager.getBucketKey("vol1", "unversioned"),
+ OmBucketInfo.newBuilder()
+ .setVolumeName("vol1")
+ .setBucketName("unversioned")
+ .setStorageType(StorageType.DISK)
+ .setMaxVersions(1)
+ .build());
+ addNoncurrentVersionsToDB("vol1", "unversioned", "key1", 1, 2, 3);
+
+ assertThat(omMetadataManager.getVersionsToReclaim(0, 1000)).isEmpty();
+
+ // the cluster default applies only to the bucket that sets no limit
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(1, 1000);
+ assertEquals(1, reclaimable.size());
+ assertEquals("clusterdefault", reclaimable.get(0).getBucketName());
+ assertEquals(versionKeys("vol1", "clusterdefault", "key1", 3, 2, 1),
+ reclaimable.get(0).getVersionKeysList());
+ }
+
+ @Test
+ public void testGetVersionsToReclaimHonoursTheTaskLimit() throws Exception {
+ String volumeName = "vol1";
+ addVersionedBucketToDB(volumeName, "buck1", 1);
+ addNoncurrentVersionsToDB(volumeName, "buck1", "key1", 1, 2, 3);
+ addVersionedBucketToDB(volumeName, "buck2", 1);
+ addNoncurrentVersionsToDB(volumeName, "buck2", "key1", 1, 2, 3);
+
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(100, 4);
+
+ int collected = reclaimable.stream()
+ .mapToInt(ObjectVersionsBucket::getVersionKeysCount).sum();
+ assertEquals(4, collected);
+ }
+
+ /**
+ * A version becomes noncurrent when the version that superseded it was
+ * committed, as S3 lifecycle's NoncurrentDays does. The record itself does
+ * not carry that moment, so the scan takes it from the next-newer version -
+ * the key's current version for the newest noncurrent one.
+ */
+ @Test
+ public void testGetVersionsToReclaimExpiresFromWhenSuperseded()
+ throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 0, 10);
+
+ long now = Time.now();
+ long longAgo = now - TimeUnit.DAYS.toMillis(30);
+ // version 1 was superseded long ago by version 2, so it is expired;
+ // version 2 was superseded just now by the current version, so it is not,
+ // even though version 2 itself is just as old as version 1
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 1L, longAgo);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 2L, longAgo);
+ addCurrentVersionWithTime(volumeName, bucketName, "key1", 3L, now);
+
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(0, 1000);
+
+ assertEquals(1, reclaimable.size());
+ assertEquals(versionKeys(volumeName, bucketName, "key1", 1),
+ reclaimable.get(0).getVersionKeysList());
+ }
+
+ @Test
+ public void testGetVersionsToReclaimKeepsUnexpiredVersions()
+ throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 0, 10);
+
+ long now = Time.now();
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 1L, now);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 2L, now);
+ addCurrentVersionWithTime(volumeName, bucketName, "key1", 3L, now);
+
+ assertThat(omMetadataManager.getVersionsToReclaim(0, 1000)).isEmpty();
+ }
+
+ /**
+ * Expiring on a missing current version would destroy data on a broken
+ * invariant, so a key without one keeps its versions.
+ */
+ @Test
+ public void testGetVersionsToReclaimSkipsExpiryWithoutCurrentVersion()
+ throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 0, 1);
+
+ long longAgo = Time.now() - TimeUnit.DAYS.toMillis(30);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 1L, longAgo);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 2L, longAgo);
+ // no keyTable entry for key1
+
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(0, 1000);
+
+ // version 1 is still expired: version 2 superseded it long ago. Only the
+ // newest one, whose superseding time is unknown, is kept.
+ assertEquals(1, reclaimable.size());
+ assertEquals(versionKeys(volumeName, bucketName, "key1", 1),
+ reclaimable.get(0).getVersionKeysList());
+ }
+
+ /** Either control on its own is enough to select a version. */
+ @Test
+ public void testGetVersionsToReclaimCombinesCountAndExpiry()
+ throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 3, 10);
+
+ long now = Time.now();
+ long longAgo = now - TimeUnit.DAYS.toMillis(30);
+ // key1 is within its count limit but version 1 expired
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 1L, longAgo);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key1", 2L, longAgo);
+ addCurrentVersionWithTime(volumeName, bucketName, "key1", 3L, now);
+ // key2 has nothing expired but exceeds maxVersions of 3
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key2", 1L, now);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key2", 2L, now);
+ addNoncurrentVersionWithTime(volumeName, bucketName, "key2", 3L, now);
+ addCurrentVersionWithTime(volumeName, bucketName, "key2", 4L, now);
+
+ List reclaimable =
+ omMetadataManager.getVersionsToReclaim(0, 1000);
+
+ assertEquals(1, reclaimable.size());
+ List expected = new ArrayList<>();
+ expected.addAll(versionKeys(volumeName, bucketName, "key1", 1));
+ expected.addAll(versionKeys(volumeName, bucketName, "key2", 1));
+ assertEquals(expected, reclaimable.get(0).getVersionKeysList());
+ }
+
+ @Test
+ public void testGetExpiredDeleteMarkers() throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 0, null);
+
+ // only a marker left: the key is invisible and nothing else can remove it
+ addDeleteMarkerToDB(volumeName, bucketName, "expired");
+ // a marker over surviving versions still makes the key read as deleted
+ addDeleteMarkerToDB(volumeName, bucketName, "hasVersions");
+ addNoncurrentVersionsToDB(volumeName, bucketName, "hasVersions", 1);
+ // a live key is not a marker at all
+ addCurrentVersionWithTime(volumeName, bucketName, "live", 5L, Time.now());
+
+ OMMetadataManager.ExpiredDeleteMarkers markers =
+ omMetadataManager.getExpiredDeleteMarkers(null, 1000, 1000);
+
+ assertNull(markers.getNextStartKey());
+ assertEquals(1, markers.getMarkersPerBucket().size());
+ assertEquals(Collections.singletonList(
+ omMetadataManager.getOzoneKey(volumeName, bucketName, "expired")),
+ markers.getMarkersPerBucket().get(0).getMarkerKeysList());
+ }
+
+ @Test
+ public void testGetExpiredDeleteMarkersRespectsBucketSettings()
+ throws Exception {
+ // cleanup turned off on the bucket
+ addVersionedBucketToDB("vol1", "optedout", 0, null, false);
+ addDeleteMarkerToDB("vol1", "optedout", "key1");
+
+ // a bucket that never had versioning enabled cannot hold a real marker,
+ // and is not examined either way
+ omMetadataManager.getBucketTable().put(
+ omMetadataManager.getBucketKey("vol1", "unversioned"),
+ OmBucketInfo.newBuilder()
+ .setVolumeName("vol1")
+ .setBucketName("unversioned")
+ .setStorageType(StorageType.DISK)
+ .build());
+ addDeleteMarkerToDB("vol1", "unversioned", "key1");
+
+ assertThat(omMetadataManager.getExpiredDeleteMarkers(null, 1000, 1000)
+ .getMarkersPerBucket()).isEmpty();
+ }
+
+ /**
+ * The walk is bounded so one run cannot iterate the whole keyTable, and it
+ * resumes where it stopped so successive runs still make progress.
+ */
+ @Test
+ public void testGetExpiredDeleteMarkersResumesAfterTheScanBudget()
+ throws Exception {
+ String volumeName = "vol1";
+ String bucketName = "buck1";
+ addVersionedBucketToDB(volumeName, bucketName, 0, null);
+ for (int i = 0; i < 5; i++) {
+ addDeleteMarkerToDB(volumeName, bucketName, "key" + i);
+ }
+
+ OMMetadataManager.ExpiredDeleteMarkers first =
+ omMetadataManager.getExpiredDeleteMarkers(null, 2, 1000);
+ assertEquals(2, first.getMarkersPerBucket().get(0).getMarkerKeysCount());
+ assertNotNull(first.getNextStartKey());
+
+ OMMetadataManager.ExpiredDeleteMarkers second =
+ omMetadataManager.getExpiredDeleteMarkers(first.getNextStartKey(), 2,
+ 1000);
+ assertEquals(2, second.getMarkersPerBucket().get(0).getMarkerKeysCount());
+ // the second pass starts where the first stopped, so nothing is revisited
+ assertThat(second.getMarkersPerBucket().get(0).getMarkerKeysList())
+ .doesNotContainAnyElementsOf(
+ first.getMarkersPerBucket().get(0).getMarkerKeysList());
+
+ OMMetadataManager.ExpiredDeleteMarkers third =
+ omMetadataManager.getExpiredDeleteMarkers(second.getNextStartKey(), 2,
+ 1000);
+ assertEquals(1, third.getMarkersPerBucket().get(0).getMarkerKeysCount());
+ assertNull(third.getNextStartKey());
+ }
+
+ private void addDeleteMarkerToDB(String volumeName, String bucketName,
+ String keyName) throws Exception {
+ OmKeyInfo marker = new OmKeyInfo.Builder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setReplicationConfig(RatisReplicationConfig.getInstance(ONE))
+ .setVersionId(1L)
+ .setDeleteMarker(true)
+ .build();
+ omMetadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName), marker);
+ }
+
+ private void addVersionedBucketToDB(String volumeName, String bucketName,
+ Integer maxVersions) throws Exception {
+ addVersionedBucketToDB(volumeName, bucketName, maxVersions, null);
+ }
+
+ private void addVersionedBucketToDB(String volumeName, String bucketName,
+ Integer maxVersions, Integer expirationDays, boolean markerCleanup)
+ throws Exception {
+ addVersionedBucketToDB(volumeName, bucketName, maxVersions, expirationDays);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+ omMetadataManager.getBucketTable().put(bucketKey,
+ omMetadataManager.getBucketTable().getSkipCache(bucketKey).toBuilder()
+ .setExpiredDeleteMarkerCleanup(markerCleanup).build());
+ }
+
+ private void addVersionedBucketToDB(String volumeName, String bucketName,
+ Integer maxVersions, Integer expirationDays) throws Exception {
+ OmBucketInfo.Builder builder = OmBucketInfo.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setStorageType(StorageType.DISK)
+ .setBucketLayout(BucketLayout.OBJECT_STORE)
+ .setVersioningStatus(BucketVersioningStatus.ENABLED);
+ if (maxVersions != null) {
+ builder.setMaxVersions(maxVersions);
+ }
+ if (expirationDays != null) {
+ builder.setNoncurrentVersionExpirationDays(expirationDays);
+ }
+ omMetadataManager.getBucketTable().put(
+ omMetadataManager.getBucketKey(volumeName, bucketName), builder.build());
+ }
+
+ private void addNoncurrentVersionWithTime(String volumeName,
+ String bucketName, String keyName, long versionId,
+ long modificationTime) throws Exception {
+ omMetadataManager.getVersionedKeyTable().put(
+ omMetadataManager.getVersionedOzoneKey(volumeName, bucketName, keyName,
+ versionId),
+ versionWithTime(volumeName, bucketName, keyName, versionId,
+ modificationTime));
+ }
+
+ private void addCurrentVersionWithTime(String volumeName, String bucketName,
+ String keyName, long versionId, long modificationTime) throws Exception {
+ omMetadataManager.getKeyTable(BucketLayout.OBJECT_STORE).put(
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName),
+ versionWithTime(volumeName, bucketName, keyName, versionId,
+ modificationTime));
+ }
+
+ private OmKeyInfo versionWithTime(String volumeName, String bucketName,
+ String keyName, long versionId, long modificationTime) {
+ return new OmKeyInfo.Builder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setReplicationConfig(RatisReplicationConfig.getInstance(ONE))
+ .setVersionId(versionId)
+ .setCreationTime(modificationTime)
+ .setModificationTime(modificationTime)
+ .build();
+ }
+
+ private void addNoncurrentVersionsToDB(String volumeName, String bucketName,
+ String keyName, long... versionIds) throws Exception {
+ for (long versionId : versionIds) {
+ OmKeyInfo version = new OmKeyInfo.Builder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setReplicationConfig(RatisReplicationConfig.getInstance(ONE))
+ .setVersionId(versionId)
+ .build();
+ omMetadataManager.getVersionedKeyTable().put(
+ omMetadataManager.getVersionedOzoneKey(volumeName, bucketName,
+ keyName, versionId), version);
+ }
+ }
+
+ private List versionKeys(String volumeName, String bucketName,
+ String keyName, long... versionIds) {
+ return Arrays.stream(versionIds)
+ .mapToObj(versionId -> omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionId))
+ .collect(Collectors.toList());
+ }
+
@Test
public void testListVolumes() throws Exception {
String ownerName = "owner";
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestVersionIdAllocator.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestVersionIdAllocator.java
new file mode 100644
index 000000000000..17f3b6e41b0e
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestVersionIdAllocator.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.TransactionIndexVersionIdGenerator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link VersionIdAllocator}: which versionId a commit gets, and the
+ * rejection of ids that are already taken on the key.
+ */
+public class TestVersionIdAllocator {
+
+ private static final String VOLUME = "vol1";
+ private static final String BUCKET = "bucket1";
+ private static final String KEY = "key1";
+
+ private OMMetadataManager metadataManager;
+ private Set versionedKeys;
+ private int lookups;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ versionedKeys = new HashSet<>();
+ lookups = 0;
+
+ Table versionedKeyTable = mock(Table.class);
+ when(versionedKeyTable.isExist(anyString())).thenAnswer(invocation -> {
+ lookups++;
+ return versionedKeys.contains(invocation.getArgument(0));
+ });
+
+ metadataManager = mock(OMMetadataManager.class);
+ when(metadataManager.getVersionedKeyTable()).thenReturn(versionedKeyTable);
+ when(metadataManager.getVersionedOzoneKey(eq(VOLUME), eq(BUCKET), eq(KEY), anyLong()))
+ .thenAnswer(invocation -> dbKey(invocation.getArgument(3)));
+ }
+
+ private static String dbKey(long versionId) {
+ return "/" + VOLUME + "/" + BUCKET + "/" + KEY + "/" + versionId;
+ }
+
+ private VersionIdAllocator allocator() {
+ return new VersionIdAllocator(new TransactionIndexVersionIdGenerator());
+ }
+
+ private static OmKeyInfo keyWithVersionId(Long versionId) {
+ return new OmKeyInfo.Builder()
+ .setVolumeName(VOLUME)
+ .setBucketName(BUCKET)
+ .setKeyName(KEY)
+ .setVersionId(versionId)
+ .build();
+ }
+
+ @Test
+ void allocatesTheTransactionIndexForTheFirstVersion() throws Exception {
+ assertEquals(7, allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 7, null));
+ }
+
+ @Test
+ void allocatesTheTransactionIndexForALaterVersion() throws Exception {
+ assertEquals(9, allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 9,
+ keyWithVersionId(7L)));
+ }
+
+ @Test
+ void rejectsAnIdEqualToTheCurrentVersion() {
+ OMException e = assertThrows(OMException.class,
+ () -> allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 7,
+ keyWithVersionId(7L)));
+
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, e.getResult());
+ }
+
+ @Test
+ void rejectsAnIdOlderThanTheCurrentVersion() {
+ // Refused even though no version holds this id: writing it would sort the
+ // new version before versions that predate it.
+ OMException e = assertThrows(OMException.class,
+ () -> allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 5,
+ keyWithVersionId(9L)));
+
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, e.getResult());
+ }
+
+ @Test
+ void skipsTheLookupWhenTheKeyHasNoCurrentVersion() throws Exception {
+ assertEquals(7, allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 7, null));
+ assertEquals(0, lookups);
+ }
+
+ @Test
+ void skipsTheLookupWhenTheGeneratedIdIsNewerThanTheCurrentVersion() throws Exception {
+ // The steady-state path: the current version holds the key's largest id, so
+ // an id above it cannot be taken and costs no read.
+ assertEquals(9, allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 9,
+ keyWithVersionId(7L)));
+ assertEquals(0, lookups);
+ }
+
+ @Test
+ void skipsTheLookupWhenTheIdIsRefusedForGoingBackwards() {
+ assertThrows(OMException.class,
+ () -> allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 5,
+ keyWithVersionId(9L)));
+
+ assertEquals(0, lookups);
+ }
+
+ @Test
+ void looksUpTheTableForACurrentVersionPredatingVersioning() throws Exception {
+ // Keys written before versioning was enabled carry no versionId, so there
+ // is nothing to order against and the id has to be looked up.
+ assertEquals(7, allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 7,
+ keyWithVersionId(null)));
+
+ assertEquals(1, lookups);
+ }
+
+ @Test
+ void rejectsATakenIdForACurrentVersionPredatingVersioning() {
+ versionedKeys.add(dbKey(7));
+
+ OMException e = assertThrows(OMException.class,
+ () -> allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 7,
+ keyWithVersionId(null)));
+
+ assertEquals(OMException.ResultCodes.KEY_ALREADY_EXISTS, e.getResult());
+ }
+
+ @Test
+ void allowsAnIdHeldByAnotherKeysVersion() throws Exception {
+ // Ids are only unique within a key, so another key holding it is fine.
+ versionedKeys.add("/" + VOLUME + "/" + BUCKET + "/otherKey/7");
+
+ assertEquals(7, allocator().allocate(metadataManager, VOLUME, BUCKET, KEY, 7,
+ keyWithVersionId(null)));
+ }
+
+ @Test
+ void usesTheGeneratorConfiguredForTheCluster() {
+ assertInstanceOf(TransactionIndexVersionIdGenerator.class,
+ new VersionIdAllocator(new OzoneConfiguration()).getGenerator());
+ }
+
+}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/bucket/TestOMBucketSetPropertyRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/bucket/TestOMBucketSetPropertyRequest.java
index 2e41d4c8b173..a9dbe9624fd5 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/bucket/TestOMBucketSetPropertyRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/bucket/TestOMBucketSetPropertyRequest.java
@@ -24,19 +24,28 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import java.util.UUID;
import org.apache.hadoop.hdds.client.DefaultReplicationConfig;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.BucketEncryptionKeyInfo;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.BucketVersioningStatus;
import org.apache.hadoop.ozone.om.helpers.OmBucketArgs;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
+import org.apache.hadoop.ozone.om.request.validation.ValidationContext;
import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.upgrade.OMLayoutFeature;
+import org.apache.hadoop.ozone.om.upgrade.OMLayoutVersionManager;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.BucketArgs;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
@@ -422,6 +431,375 @@ public void testValidateAndUpdateCacheWithQuotaNamespaceUsed()
"is less than used namespaceQuota");
}
+ /**
+ * S3 object versioning is only defined for OBJECT_STORE buckets: combining
+ * it with the directory and rename semantics of the other layouts is out of
+ * scope, so the status cannot be set on them at all.
+ */
+ @Test
+ public void testVersioningStatusRejectedOnNonObjectStoreBucket()
+ throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.FILE_SYSTEM_OPTIMIZED);
+
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.ENABLED)).validateAndUpdateCache(
+ ozoneManager, 1);
+
+ assertFalse(response.getOMResponse().getSuccess());
+ assertEquals(OzoneManagerProtocolProtos.Status.NOT_SUPPORTED_OPERATION,
+ response.getOMResponse().getStatus());
+ assertFalse(omMetadataManager.getBucketTable()
+ .get(omMetadataManager.getBucketKey(volumeName, bucketName))
+ .hasVersioningStatus());
+ }
+
+ @Test
+ public void testVersioningStatusTransitions() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ omMetadataManager.getBucketTable().get(bucketKey).getVersioningStatus());
+
+ // UNVERSIONED -> ENABLED
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.ENABLED)).validateAndUpdateCache(ozoneManager, 1);
+ assertTrue(response.getOMResponse().getSuccess());
+ OmBucketInfo dbBucketInfo = omMetadataManager.getBucketTable().get(bucketKey);
+ assertEquals(BucketVersioningStatus.ENABLED, dbBucketInfo.getVersioningStatus());
+ assertTrue(dbBucketInfo.getIsVersionEnabled());
+
+ // ENABLED -> SUSPENDED
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.SUSPENDED)).validateAndUpdateCache(ozoneManager, 2);
+ assertTrue(response.getOMResponse().getSuccess());
+ dbBucketInfo = omMetadataManager.getBucketTable().get(bucketKey);
+ assertEquals(BucketVersioningStatus.SUSPENDED, dbBucketInfo.getVersioningStatus());
+ assertFalse(dbBucketInfo.getIsVersionEnabled());
+
+ // SUSPENDED -> UNVERSIONED is rejected
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.UNVERSIONED)).validateAndUpdateCache(ozoneManager, 3);
+ assertFalse(response.getOMResponse().getSuccess());
+ assertEquals(OzoneManagerProtocolProtos.Status.INVALID_REQUEST,
+ response.getOMResponse().getStatus());
+ assertThat(response.getOMResponse().getMessage())
+ .contains("once enabled, versioning can only be suspended");
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ omMetadataManager.getBucketTable().get(bucketKey).getVersioningStatus());
+
+ // SUSPENDED -> ENABLED
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.ENABLED)).validateAndUpdateCache(ozoneManager, 4);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(BucketVersioningStatus.ENABLED,
+ omMetadataManager.getBucketTable().get(bucketKey).getVersioningStatus());
+ }
+
+ @Test
+ public void testLegacyVersioningFlagMapsToStateMachine() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ // legacy false on a never-enabled bucket stays UNVERSIONED
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetVersioningFlagRequest(volumeName, bucketName, false))
+ .validateAndUpdateCache(ozoneManager, 1);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ omMetadataManager.getBucketTable().get(bucketKey).getVersioningStatus());
+
+ // legacy true does NOT opt the bucket into S3 versioning: it only sets the
+ // legacy flag, which selects the in-record block version list
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningFlagRequest(volumeName, bucketName, true))
+ .validateAndUpdateCache(ozoneManager, 2);
+ assertTrue(response.getOMResponse().getSuccess());
+ OmBucketInfo dbBucketInfo = omMetadataManager.getBucketTable().get(bucketKey);
+ assertFalse(dbBucketInfo.hasVersioningStatus());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ dbBucketInfo.getVersioningStatus());
+ assertTrue(dbBucketInfo.getIsVersionEnabled());
+
+ // once a status exists, an old client's flag is kept consistent with it:
+ // disabling maps to SUSPENDED rather than back to UNVERSIONED
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.ENABLED))
+ .validateAndUpdateCache(ozoneManager, 3);
+ assertTrue(response.getOMResponse().getSuccess());
+
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningFlagRequest(volumeName, bucketName, false))
+ .validateAndUpdateCache(ozoneManager, 4);
+ assertTrue(response.getOMResponse().getSuccess());
+ dbBucketInfo = omMetadataManager.getBucketTable().get(bucketKey);
+ assertEquals(BucketVersioningStatus.SUSPENDED, dbBucketInfo.getVersioningStatus());
+ assertFalse(dbBucketInfo.getIsVersionEnabled());
+ }
+
+ @Test
+ public void testVersioningStatusRejectedBeforeFinalization()
+ throws Exception {
+ OMRequest request = createSetVersioningStatusRequest(
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ BucketVersioningStatus.ENABLED);
+
+ OMLayoutVersionManager preFinalizedVersionManager =
+ mock(OMLayoutVersionManager.class);
+ when(preFinalizedVersionManager
+ .isAllowed(OMLayoutFeature.OBJECT_VERSIONING)).thenReturn(false);
+ ValidationContext preFinalizedContext = ValidationContext.of(
+ preFinalizedVersionManager, omMetadataManager);
+
+ OMException omException = assertThrows(OMException.class,
+ () -> OMBucketSetPropertyRequest
+ .disallowSetBucketPropertyWithVersioningStatus(
+ request, preFinalizedContext));
+ assertEquals(OMException.ResultCodes
+ .NOT_SUPPORTED_OPERATION_PRIOR_FINALIZATION,
+ omException.getResult());
+
+ // requests without a versioningStatus pass through untouched
+ OMRequest legacyRequest = createSetVersioningFlagRequest(
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(), true);
+ assertSame(legacyRequest, OMBucketSetPropertyRequest
+ .disallowSetBucketPropertyWithVersioningStatus(
+ legacyRequest, preFinalizedContext));
+
+ // after finalization the request passes through untouched
+ OMLayoutVersionManager finalizedVersionManager =
+ mock(OMLayoutVersionManager.class);
+ when(finalizedVersionManager
+ .isAllowed(OMLayoutFeature.OBJECT_VERSIONING)).thenReturn(true);
+ ValidationContext finalizedContext = ValidationContext.of(
+ finalizedVersionManager, omMetadataManager);
+ assertSame(request, OMBucketSetPropertyRequest
+ .disallowSetBucketPropertyWithVersioningStatus(
+ request, finalizedContext));
+ }
+
+ @Test
+ public void testSetMaxVersions() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ // a bucket that never set a limit carries none, so the cluster default
+ // applies to it
+ assertNull(omMetadataManager.getBucketTable().get(bucketKey)
+ .getMaxVersions());
+
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetMaxVersionsRequest(volumeName, bucketName, 5))
+ .validateAndUpdateCache(ozoneManager, 1);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(5,
+ omMetadataManager.getBucketTable().get(bucketKey).getMaxVersions());
+
+ // 0 is unlimited, and is a set value rather than a reset to the default
+ response = new OMBucketSetPropertyRequest(
+ createSetMaxVersionsRequest(volumeName, bucketName, 0))
+ .validateAndUpdateCache(ozoneManager, 2);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(0,
+ omMetadataManager.getBucketTable().get(bucketKey).getMaxVersions());
+
+ // a request that does not carry maxVersions leaves it alone
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningStatusRequest(volumeName, bucketName,
+ BucketVersioningStatus.ENABLED))
+ .validateAndUpdateCache(ozoneManager, 3);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(0,
+ omMetadataManager.getBucketTable().get(bucketKey).getMaxVersions());
+ }
+
+ /**
+ * maxVersions is accepted on a bucket that is not versioned yet, so that the
+ * retention limit can be in place before versioning is enabled.
+ */
+ @Test
+ public void testSetMaxVersionsOnUnversionedBucket() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetMaxVersionsRequest(volumeName, bucketName, 3))
+ .validateAndUpdateCache(ozoneManager, 1);
+
+ assertTrue(response.getOMResponse().getSuccess());
+ OmBucketInfo dbBucketInfo = omMetadataManager.getBucketTable().get(bucketKey);
+ assertEquals(3, dbBucketInfo.getMaxVersions());
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ dbBucketInfo.getVersioningStatus());
+ }
+
+ /**
+ * The wire type is uint32, so a value above Integer.MAX_VALUE arrives as a
+ * negative int and has to be refused rather than stored.
+ */
+ @Test
+ public void testMaxVersionsOutOfRangeRejected() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetMaxVersionsRequest(volumeName, bucketName, -1))
+ .validateAndUpdateCache(ozoneManager, 1);
+
+ assertFalse(response.getOMResponse().getSuccess());
+ assertEquals(OzoneManagerProtocolProtos.Status.INVALID_REQUEST,
+ response.getOMResponse().getStatus());
+ assertNull(omMetadataManager.getBucketTable().get(bucketKey)
+ .getMaxVersions());
+ }
+
+ @Test
+ public void testMaxVersionsRejectedBeforeFinalization() throws Exception {
+ OMRequest request = createSetMaxVersionsRequest(
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(), 5);
+
+ OMLayoutVersionManager preFinalizedVersionManager =
+ mock(OMLayoutVersionManager.class);
+ when(preFinalizedVersionManager
+ .isAllowed(OMLayoutFeature.OBJECT_VERSIONING)).thenReturn(false);
+ ValidationContext preFinalizedContext = ValidationContext.of(
+ preFinalizedVersionManager, omMetadataManager);
+
+ OMException omException = assertThrows(OMException.class,
+ () -> OMBucketSetPropertyRequest
+ .disallowSetBucketPropertyWithVersioningStatus(
+ request, preFinalizedContext));
+ assertEquals(OMException.ResultCodes
+ .NOT_SUPPORTED_OPERATION_PRIOR_FINALIZATION,
+ omException.getResult());
+
+ OMLayoutVersionManager finalizedVersionManager =
+ mock(OMLayoutVersionManager.class);
+ when(finalizedVersionManager
+ .isAllowed(OMLayoutFeature.OBJECT_VERSIONING)).thenReturn(true);
+ ValidationContext finalizedContext = ValidationContext.of(
+ finalizedVersionManager, omMetadataManager);
+ assertSame(request, OMBucketSetPropertyRequest
+ .disallowSetBucketPropertyWithVersioningStatus(
+ request, finalizedContext));
+ }
+
+ @Test
+ public void testSetNoncurrentVersionExpirationDays() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ // expiration is opt-in: a bucket that never set it retains versions forever
+ assertNull(omMetadataManager.getBucketTable().get(bucketKey)
+ .getNoncurrentVersionExpirationDays());
+
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetExpirationRequest(volumeName, bucketName, 30))
+ .validateAndUpdateCache(ozoneManager, 1);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(30, omMetadataManager.getBucketTable().get(bucketKey)
+ .getNoncurrentVersionExpirationDays());
+
+ // setting it back to 0 turns expiration off again
+ response = new OMBucketSetPropertyRequest(
+ createSetExpirationRequest(volumeName, bucketName, 0))
+ .validateAndUpdateCache(ozoneManager, 2);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(0, omMetadataManager.getBucketTable().get(bucketKey)
+ .getNoncurrentVersionExpirationDays());
+ }
+
+ @Test
+ public void testNoncurrentVersionExpirationOutOfRangeRejected()
+ throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager, BucketLayout.OBJECT_STORE);
+ String bucketKey = omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ OMClientResponse response = new OMBucketSetPropertyRequest(
+ createSetExpirationRequest(volumeName, bucketName, -1))
+ .validateAndUpdateCache(ozoneManager, 1);
+
+ assertFalse(response.getOMResponse().getSuccess());
+ assertEquals(OzoneManagerProtocolProtos.Status.INVALID_REQUEST,
+ response.getOMResponse().getStatus());
+ assertNull(omMetadataManager.getBucketTable().get(bucketKey)
+ .getNoncurrentVersionExpirationDays());
+ }
+
+ private OMRequest createSetExpirationRequest(String volumeName,
+ String bucketName, int days) {
+ return OMRequest.newBuilder().setSetBucketPropertyRequest(
+ SetBucketPropertyRequest.newBuilder().setBucketArgs(
+ BucketArgs.newBuilder().setBucketName(bucketName)
+ .setVolumeName(volumeName)
+ .setNoncurrentVersionExpirationDays(days).build()))
+ .setCmdType(OzoneManagerProtocolProtos.Type.SetBucketProperty)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
+ private OMRequest createSetMaxVersionsRequest(String volumeName,
+ String bucketName, int maxVersions) {
+ return OMRequest.newBuilder().setSetBucketPropertyRequest(
+ SetBucketPropertyRequest.newBuilder().setBucketArgs(
+ BucketArgs.newBuilder().setBucketName(bucketName)
+ .setVolumeName(volumeName)
+ .setMaxVersions(maxVersions).build()))
+ .setCmdType(OzoneManagerProtocolProtos.Type.SetBucketProperty)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
+ private OMRequest createSetVersioningStatusRequest(String volumeName,
+ String bucketName, BucketVersioningStatus status) {
+ return OMRequest.newBuilder().setSetBucketPropertyRequest(
+ SetBucketPropertyRequest.newBuilder().setBucketArgs(
+ BucketArgs.newBuilder().setBucketName(bucketName)
+ .setVolumeName(volumeName)
+ .setVersioningStatus(status.toProto()).build()))
+ .setCmdType(OzoneManagerProtocolProtos.Type.SetBucketProperty)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
+ private OMRequest createSetVersioningFlagRequest(String volumeName,
+ String bucketName, boolean isVersionEnabled) {
+ return OMRequest.newBuilder().setSetBucketPropertyRequest(
+ SetBucketPropertyRequest.newBuilder().setBucketArgs(
+ BucketArgs.newBuilder().setBucketName(bucketName)
+ .setVolumeName(volumeName)
+ .setIsVersionEnabled(isVersionEnabled).build()))
+ .setCmdType(OzoneManagerProtocolProtos.Type.SetBucketProperty)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
@Test
public void testSettingQuotaRetainsReplication() throws Exception {
String volumeName1 = UUID.randomUUID().toString();
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequestTests.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequestTests.java
index 167fbc354a3c..f219f23f4077 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequestTests.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequestTests.java
@@ -74,6 +74,7 @@
import org.apache.hadoop.ozone.om.OzoneManagerPrepareState;
import org.apache.hadoop.ozone.om.ResolvedBucket;
import org.apache.hadoop.ozone.om.ScmClient;
+import org.apache.hadoop.ozone.om.VersionIdAllocator;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
@@ -154,6 +155,8 @@ public void setup() throws Exception {
when(ozoneManager.getMetadataManager()).thenReturn(omMetadataManager);
when(ozoneManager.getConfiguration()).thenReturn(ozoneConfiguration);
when(ozoneManager.getConfig()).thenReturn(ozoneConfiguration.getObject(OmConfig.class));
+ when(ozoneManager.getVersionIdAllocator())
+ .thenReturn(new VersionIdAllocator(ozoneConfiguration));
OMLayoutVersionManager lvm = mock(OMLayoutVersionManager.class);
when(lvm.isAllowed(anyString())).thenReturn(true);
when(ozoneManager.getVersionManager()).thenReturn(lvm);
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyVersioningRequests.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyVersioningRequests.java
new file mode 100644
index 000000000000..1cab35d136e6
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyVersioningRequests.java
@@ -0,0 +1,857 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.request.key;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.UUID;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.BucketVersioningStatus;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.QuotaUtil;
+import org.apache.hadoop.ozone.om.helpers.VersionIdGenerator;
+import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.key.OMKeyCommitResponse;
+import org.apache.hadoop.ozone.om.response.key.OMKeyDeleteMarkerResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CommitKeyRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeyRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.util.Time;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the S3-compatible versioning behaviour of key writes on a
+ * versioning-enabled OBJECT_STORE bucket: a commit freezes a versionId on the
+ * new current version and keeps the version it overwrote as a noncurrent
+ * version in the versionedKeyTable instead of reclaiming it.
+ */
+public class TestOMKeyVersioningRequests extends OMKeyRequestTests {
+
+ @Override
+ public BucketLayout getBucketLayout() {
+ return BucketLayout.OBJECT_STORE;
+ }
+
+ private void setupVersionedBucket() throws Exception {
+ setupVersionedBucket(OzoneConsts.QUOTA_RESET, OzoneConsts.QUOTA_RESET);
+ }
+
+ private void setupVersionedBucket(long quotaInBytes, long quotaInNamespace)
+ throws Exception {
+ setupVersionedBucket(quotaInBytes, quotaInNamespace, 0L);
+ }
+
+ private void setupSuspendedBucket() throws Exception {
+ setupVersionedBucket(OzoneConsts.QUOTA_RESET, OzoneConsts.QUOTA_RESET, 0L,
+ BucketVersioningStatus.SUSPENDED);
+ }
+
+ private void setupVersionedBucket(long quotaInBytes, long quotaInNamespace,
+ long usedNamespace) throws Exception {
+ setupVersionedBucket(quotaInBytes, quotaInNamespace, usedNamespace,
+ BucketVersioningStatus.ENABLED);
+ }
+
+ private void setupVersionedBucket(long quotaInBytes, long quotaInNamespace,
+ long usedNamespace, BucketVersioningStatus status) throws Exception {
+ OMRequestTestUtils.addVolumeToDB(volumeName, omMetadataManager);
+ OmBucketInfo bucketInfo = OmBucketInfo.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setBucketLayout(BucketLayout.OBJECT_STORE)
+ .setVersioningStatus(status)
+ .setQuotaInBytes(quotaInBytes)
+ .setQuotaInNamespace(quotaInNamespace)
+ .setUsedNamespace(usedNamespace)
+ .setCreationTime(Time.now())
+ .build();
+ omMetadataManager.getBucketTable().addCacheEntry(
+ new CacheKey<>(omMetadataManager.getBucketKey(volumeName, bucketName)),
+ CacheValue.get(1L, bucketInfo));
+ }
+
+ /** Puts a current version into keyTable, as an earlier write would have. */
+ private String seedCurrentVersion(Long versionId) throws Exception {
+ return seedCurrentVersion(versionId, false);
+ }
+
+ private String seedCurrentVersion(Long versionId, boolean deleteMarker)
+ throws Exception {
+ return seedCurrentVersion(versionId, deleteMarker, false);
+ }
+
+ private String seedCurrentVersion(Long versionId, boolean deleteMarker,
+ boolean nullVersion) throws Exception {
+ return seedCurrentVersion(versionId, deleteMarker, nullVersion, false);
+ }
+
+ private String seedCurrentVersion(Long versionId, boolean deleteMarker,
+ boolean nullVersion, boolean withBlocks) throws Exception {
+ OmKeyInfo keyInfo = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(versionId)
+ .setDeleteMarker(deleteMarker)
+ .setNullVersion(nullVersion)
+ .build();
+ if (withBlocks) {
+ OMRequestTestUtils.addKeyLocationInfo(keyInfo, 0L, 1000L);
+ }
+ String ozoneKey = omMetadataManager.getOzoneKey(
+ volumeName, bucketName, keyName);
+ omMetadataManager.getKeyTable(getBucketLayout()).put(ozoneKey, keyInfo);
+ return ozoneKey;
+ }
+
+ private OMRequest deleteRequest() {
+ KeyArgs keyArgs = KeyArgs.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setModificationTime(Time.now())
+ .build();
+ return OMRequest.newBuilder()
+ .setDeleteKeyRequest(DeleteKeyRequest.newBuilder().setKeyArgs(keyArgs))
+ .setCmdType(OzoneManagerProtocolProtos.Type.DeleteKey)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
+ private OMClientResponse deleteAt(long trxnLogIndex) throws Exception {
+ OMClientResponse response = new OMKeyDeleteRequest(deleteRequest(),
+ getBucketLayout()).validateAndUpdateCache(ozoneManager, trxnLogIndex);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ return response;
+ }
+
+ private OMRequest deleteVersionRequest(Long versionId, boolean nullVersion) {
+ KeyArgs.Builder keyArgs = KeyArgs.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setModificationTime(Time.now());
+ if (versionId != null) {
+ keyArgs.setVersionId(versionId);
+ }
+ if (nullVersion) {
+ keyArgs.setNullVersion(true);
+ }
+ return OMRequest.newBuilder()
+ .setDeleteKeyRequest(DeleteKeyRequest.newBuilder().setKeyArgs(keyArgs))
+ .setCmdType(OzoneManagerProtocolProtos.Type.DeleteKey)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
+ private OMClientResponse deleteVersionAt(Long versionId, boolean nullVersion,
+ long trxnLogIndex) throws Exception {
+ return new OMKeyDeleteRequest(deleteVersionRequest(versionId, nullVersion),
+ getBucketLayout()).validateAndUpdateCache(ozoneManager, trxnLogIndex);
+ }
+
+ private void seedNoncurrentVersion(long versionId, boolean nullVersion)
+ throws Exception {
+ OmKeyInfo version = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(versionId)
+ .setNullVersion(nullVersion)
+ .build();
+ omMetadataManager.getVersionedKeyTable().put(
+ omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionId), version);
+ }
+
+ /** A noncurrent version that only exists in the table cache, not in the DB. */
+ private void cacheOnlyNoncurrentVersion(long versionId) throws Exception {
+ OmKeyInfo version = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(versionId)
+ .build();
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionId), version, 350L);
+ }
+
+ @Test
+ public void testPermanentDeleteRemovesOnlyTheAddressedVersion()
+ throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, false);
+ seedNoncurrentVersion(200L, false);
+
+ OMClientResponse response = deleteVersionAt(100L, false, 400L);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+
+ assertNull(noncurrentVersion(100L));
+ assertNotNull(noncurrentVersion(200L));
+ assertEquals(300L, currentVersion().getVersionId());
+ }
+
+ @Test
+ public void testPermanentDeleteReleasesQuota() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, false);
+
+ OmBucketInfo before = omMetadataManager.getBucketTable()
+ .get(omMetadataManager.getBucketKey(volumeName, bucketName));
+ long usedNamespaceBefore = before.getUsedNamespace();
+
+ deleteVersionAt(100L, false, 400L);
+
+ OmBucketInfo after = omMetadataManager.getBucketTable()
+ .get(omMetadataManager.getBucketKey(volumeName, bucketName));
+ assertEquals(usedNamespaceBefore - 1, after.getUsedNamespace());
+ }
+
+ @Test
+ public void testPermanentDeleteOfNullVersion() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, true);
+ seedNoncurrentVersion(200L, false);
+
+ OMClientResponse response = deleteVersionAt(null, true, 400L);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+
+ assertNull(noncurrentVersion(100L));
+ assertNotNull(noncurrentVersion(200L));
+ }
+
+ @Test
+ public void testPermanentDeleteOfUnknownVersionIsNotFound() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+
+ OMClientResponse response = deleteVersionAt(999L, false, 400L);
+ assertEquals(OzoneManagerProtocolProtos.Status.KEY_NOT_FOUND,
+ response.getOMResponse().getStatus());
+ }
+
+ @Test
+ public void testDeletingCurrentVersionPromotesTheNextNewest()
+ throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, false);
+ seedNoncurrentVersion(200L, false);
+
+ OMClientResponse response = deleteVersionAt(300L, false, 400L);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+
+ // the newest remaining version takes over, unchanged
+ OmKeyInfo current = currentVersion();
+ assertNotNull(current);
+ assertEquals(200L, current.getVersionId());
+ assertFalse(current.isDeleteMarker());
+ // and no longer counts as noncurrent
+ assertNull(noncurrentVersion(200L));
+ assertNotNull(noncurrentVersion(100L));
+ }
+
+ /**
+ * A version written by a transaction that the double buffer has not flushed
+ * yet lives only in the table cache. Promotion has to see it, otherwise an
+ * older version takes over and the newest one is orphaned.
+ */
+ @Test
+ public void testPromotionSeesVersionsStillInCache() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, false);
+ cacheOnlyNoncurrentVersion(200L);
+
+ deleteVersionAt(300L, false, 400L);
+
+ assertEquals(200L, currentVersion().getVersionId());
+ assertNotNull(noncurrentVersion(100L));
+ }
+
+ /**
+ * The mirror case: a version removed by an unflushed transaction is a
+ * tombstone in the cache while the DB still holds it. Promotion must not
+ * bring it back.
+ */
+ @Test
+ public void testPromotionSkipsVersionsTombstonedInCache() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, false);
+ seedNoncurrentVersion(200L, false);
+ // 200 is deleted but not flushed yet
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ new CacheKey<>(omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, 200L)),
+ CacheValue.get(350L));
+
+ deleteVersionAt(300L, false, 400L);
+
+ assertEquals(100L, currentVersion().getVersionId());
+ }
+
+ @Test
+ public void testDeletingTheOnlyVersionRemovesTheKey() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L);
+
+ OMClientResponse response = deleteVersionAt(300L, false, 400L);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+
+ assertNull(currentVersion());
+ }
+
+ /** Deleting a current delete marker is how S3 restores an object. */
+ @Test
+ public void testDeletingCurrentMarkerRestoresTheObject() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(300L, true);
+ seedNoncurrentVersion(100L, false);
+
+ OMClientResponse response = deleteVersionAt(300L, false, 400L);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+
+ OmKeyInfo current = currentVersion();
+ assertNotNull(current);
+ assertEquals(100L, current.getVersionId());
+ assertFalse(current.isDeleteMarker());
+ }
+
+ private OmKeyInfo currentVersion() throws Exception {
+ return omMetadataManager.getKeyTable(getBucketLayout()).get(
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName));
+ }
+
+ private OMRequest commitRequest(boolean isHsync, long dataSize,
+ long writerClientId) {
+ KeyArgs keyArgs = KeyArgs.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setKeyName(keyName)
+ .setModificationTime(Time.now())
+ .setDataSize(dataSize)
+ .build();
+ return OMRequest.newBuilder()
+ .setCommitKeyRequest(CommitKeyRequest.newBuilder()
+ .setKeyArgs(keyArgs)
+ .setClientID(writerClientId)
+ .setHsync(isHsync))
+ .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+
+ private OMClientResponse commitAt(long trxnLogIndex) throws Exception {
+ return commitAt(trxnLogIndex, false);
+ }
+
+ private OMClientResponse commitAt(long trxnLogIndex, boolean isHsync)
+ throws Exception {
+ OMClientResponse response =
+ commitAt(trxnLogIndex, isHsync, 0L, clientID);
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ return response;
+ }
+
+ /**
+ * Commits the key as the writer identified by {@code writerClientId}. A
+ * non-hsync commit tombstones its own open key, so a second write of the
+ * same key comes from a different client, as it would in practice.
+ */
+ private OMClientResponse commitAt(long trxnLogIndex, boolean isHsync,
+ long dataSize, long writerClientId) throws Exception {
+ OMRequestTestUtils.addKeyToTable(true, volumeName, bucketName, keyName,
+ writerClientId, replicationConfig, omMetadataManager);
+ return new OMKeyCommitRequest(
+ commitRequest(isHsync, dataSize, writerClientId),
+ getBucketLayout()).validateAndUpdateCache(ozoneManager, trxnLogIndex);
+ }
+
+ private OmKeyInfo noncurrentVersion(long versionId) throws Exception {
+ return omMetadataManager.getVersionedKeyTable().get(
+ omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionId));
+ }
+
+ @Test
+ public void testCommitOfNewKeyAssignsVersionIdAndHasNoNoncurrentVersion()
+ throws Exception {
+ setupVersionedBucket();
+
+ commitAt(500L);
+
+ OmKeyInfo current = omMetadataManager.getKeyTable(getBucketLayout()).get(
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName));
+ assertNotNull(current);
+ assertEquals(500L, current.getVersionId());
+ assertFalse(current.isDeleteMarker());
+ assertFalse(current.isNullVersion());
+ assertNull(noncurrentVersion(500L));
+ }
+
+ @Test
+ public void testOverwriteKeepsPreviousVersionAsNoncurrent()
+ throws Exception {
+ setupVersionedBucket();
+ String ozoneKey = seedCurrentVersion(100L);
+
+ commitAt(500L);
+
+ OmKeyInfo current =
+ omMetadataManager.getKeyTable(getBucketLayout()).get(ozoneKey);
+ assertEquals(500L, current.getVersionId());
+
+ OmKeyInfo noncurrent = noncurrentVersion(100L);
+ assertNotNull(noncurrent);
+ assertEquals(100L, noncurrent.getVersionId());
+ assertFalse(noncurrent.isNullVersion());
+ }
+
+ /**
+ * A record written before versioning was enabled carries no versionId, so on
+ * the first overwrite it becomes the key's single null version.
+ */
+ @Test
+ public void testPreVersioningRecordBecomesNullVersion() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(null);
+
+ commitAt(500L);
+
+ OmKeyInfo noncurrent =
+ noncurrentVersion(VersionIdGenerator.UNSET_VERSION_ID);
+ assertNotNull(noncurrent);
+ assertTrue(noncurrent.isNullVersion());
+ assertEquals(VersionIdGenerator.UNSET_VERSION_ID,
+ noncurrent.getVersionId());
+ }
+
+ /**
+ * The overwritten version keeps its blocks in the versionedKeyTable: they
+ * must not be queued for reclamation, and they must not be carried into the
+ * new current version's in-record block version list either.
+ */
+ @Test
+ public void testOverwriteDoesNotReclaimOrInheritPreviousBlocks()
+ throws Exception {
+ setupVersionedBucket();
+ String ozoneKey = seedCurrentVersion(100L);
+
+ commitAt(500L);
+
+ assertNull(omMetadataManager.getDeletedTable().get(ozoneKey));
+ OmKeyInfo current =
+ omMetadataManager.getKeyTable(getBucketLayout()).get(ozoneKey);
+ assertEquals(1, current.getKeyLocationVersions().size());
+ assertNotNull(noncurrentVersion(100L));
+ }
+
+ /**
+ * An hsync re-commit keeps updating the version it opened rather than
+ * creating a new one, so its versionId stays frozen and nothing moves to the
+ * versionedKeyTable.
+ */
+ @Test
+ public void testHsyncRecommitKeepsVersionIdFrozen() throws Exception {
+ setupVersionedBucket();
+
+ commitAt(500L, true);
+ OmKeyInfo firstCommit = omMetadataManager.getKeyTable(getBucketLayout())
+ .get(omMetadataManager.getOzoneKey(volumeName, bucketName, keyName));
+ assertEquals(500L, firstCommit.getVersionId());
+
+ commitAt(600L, true);
+ OmKeyInfo recommitted = omMetadataManager.getKeyTable(getBucketLayout())
+ .get(omMetadataManager.getOzoneKey(volumeName, bucketName, keyName));
+ assertEquals(500L, recommitted.getVersionId());
+ assertNull(noncurrentVersion(500L));
+ }
+
+ @Test
+ public void testDeleteInsertsMarkerAndKeepsPreviousVersion()
+ throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(100L);
+
+ deleteAt(200L);
+
+ OmKeyInfo current = currentVersion();
+ assertNotNull(current);
+ assertTrue(current.isDeleteMarker());
+ assertEquals(200L, current.getVersionId());
+ assertEquals(0L, current.getDataSize());
+ assertTrue(current.getLatestVersionLocations().getLocationList().isEmpty());
+
+ OmKeyInfo noncurrent = noncurrentVersion(100L);
+ assertNotNull(noncurrent);
+ assertFalse(noncurrent.isDeleteMarker());
+ }
+
+ /** S3 inserts a delete marker even for a key that does not exist. */
+ @Test
+ public void testDeleteOfMissingKeyStillInsertsMarker() throws Exception {
+ setupVersionedBucket();
+
+ deleteAt(200L);
+
+ OmKeyInfo current = currentVersion();
+ assertNotNull(current);
+ assertTrue(current.isDeleteMarker());
+ assertEquals(200L, current.getVersionId());
+ assertNull(noncurrentVersion(VersionIdGenerator.UNSET_VERSION_ID));
+ }
+
+ /** Deleting a key whose current version is already a marker stacks another. */
+ @Test
+ public void testDeleteStacksAnotherMarker() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(100L, true);
+
+ deleteAt(200L);
+
+ OmKeyInfo current = currentVersion();
+ assertTrue(current.isDeleteMarker());
+ assertEquals(200L, current.getVersionId());
+
+ OmKeyInfo stacked = noncurrentVersion(100L);
+ assertNotNull(stacked);
+ assertTrue(stacked.isDeleteMarker());
+ }
+
+ @Test
+ public void testDeleteOfPreVersioningRecordMovesItToNullVersion()
+ throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(null);
+
+ deleteAt(200L);
+
+ OmKeyInfo noncurrent =
+ noncurrentVersion(VersionIdGenerator.UNSET_VERSION_ID);
+ assertNotNull(noncurrent);
+ assertTrue(noncurrent.isNullVersion());
+ assertFalse(noncurrent.isDeleteMarker());
+ }
+
+ /**
+ * Every version counts against the bucket's space quota: an overwrite adds
+ * the new version's usage without releasing the version it supersedes.
+ */
+ @Test
+ public void testEachVersionCountsAgainstUsedBytes() throws Exception {
+ setupVersionedBucket();
+ String bucketKey =
+ omMetadataManager.getBucketKey(volumeName, bucketName);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ commitAt(500L, false, 100L, clientID).getOMResponse().getStatus());
+ long afterFirst = omMetadataManager.getBucketTable().get(bucketKey)
+ .getUsedBytes();
+ assertEquals(QuotaUtil.getReplicatedSize(100L, replicationConfig),
+ afterFirst);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ commitAt(600L, false, 300L, clientID + 1).getOMResponse().getStatus());
+ long afterSecond = omMetadataManager.getBucketTable().get(bucketKey)
+ .getUsedBytes();
+ assertEquals(afterFirst
+ + QuotaUtil.getReplicatedSize(300L, replicationConfig),
+ afterSecond);
+ assertEquals(2, omMetadataManager.getBucketTable().get(bucketKey)
+ .getUsedNamespace());
+ }
+
+ @Test
+ public void testVersionedWriteRejectedWhenSpaceQuotaExceeded()
+ throws Exception {
+ long quota = QuotaUtil.getReplicatedSize(150L, replicationConfig);
+ setupVersionedBucket(quota, OzoneConsts.QUOTA_RESET);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ commitAt(500L, false, 100L, clientID).getOMResponse().getStatus());
+ // the first version is not released, so the second one no longer fits
+ assertEquals(OzoneManagerProtocolProtos.Status.QUOTA_EXCEEDED,
+ commitAt(600L, false, 100L, clientID + 1).getOMResponse().getStatus());
+
+ OmKeyInfo current = currentVersion();
+ assertEquals(500L, current.getVersionId());
+ assertNull(noncurrentVersion(500L));
+ }
+
+ @Test
+ public void testVersionedWriteRejectedWhenNamespaceQuotaExceeded()
+ throws Exception {
+ setupVersionedBucket(OzoneConsts.QUOTA_RESET, 1L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ commitAt(500L, false, 0L, clientID).getOMResponse().getStatus());
+ // each version is a record of its own, so the second one needs namespace
+ assertEquals(OzoneManagerProtocolProtos.Status.QUOTA_EXCEEDED,
+ commitAt(600L, false, 0L, clientID + 1).getOMResponse().getStatus());
+ }
+
+ /** A delete marker is a record too, so it needs namespace quota. */
+ @Test
+ public void testDeleteMarkerRejectedWhenNamespaceQuotaExceeded()
+ throws Exception {
+ // the bucket already holds the one key its namespace quota allows
+ setupVersionedBucket(OzoneConsts.QUOTA_RESET, 1L, 1L);
+ seedCurrentVersion(100L);
+
+ OMClientResponse response = new OMKeyDeleteRequest(deleteRequest(),
+ getBucketLayout()).validateAndUpdateCache(ozoneManager, 200L);
+ assertEquals(OzoneManagerProtocolProtos.Status.QUOTA_EXCEEDED,
+ response.getOMResponse().getStatus());
+ assertFalse(currentVersion().isDeleteMarker());
+ // a rejected request must not leave the superseded version behind in the
+ // versionedKeyTable cache, and its response has to declare that table so
+ // that the double buffer cleans up whatever the request did touch
+ assertNull(noncurrentVersion(100L));
+ assertInstanceOf(OMKeyDeleteMarkerResponse.class, response);
+ }
+
+ /**
+ * A delete marker holds no blocks, so it consumes namespace but no space,
+ * and the superseded version keeps its own usage.
+ */
+ @Test
+ public void testDeleteMarkerConsumesNamespaceButNoSpace() throws Exception {
+ setupVersionedBucket();
+ seedCurrentVersion(100L);
+ String bucketKey =
+ omMetadataManager.getBucketKey(volumeName, bucketName);
+ OmBucketInfo before = omMetadataManager.getBucketTable().get(bucketKey);
+ long usedBytes = before.getUsedBytes();
+ long usedNamespace = before.getUsedNamespace();
+
+ deleteAt(200L);
+
+ OmBucketInfo after = omMetadataManager.getBucketTable().get(bucketKey);
+ assertEquals(usedBytes, after.getUsedBytes());
+ assertEquals(usedNamespace + 1, after.getUsedNamespace());
+ }
+
+ /**
+ * A write while versioning is suspended takes the key's null version slot
+ * instead of creating a version of its own.
+ */
+ @Test
+ public void testSuspendedWriteTakesTheNullVersionSlot() throws Exception {
+ setupSuspendedBucket();
+
+ commitAt(500L);
+
+ OmKeyInfo current = currentVersion();
+ assertNotNull(current);
+ assertTrue(current.isNullVersion());
+ assertEquals(500L, current.getVersionId());
+ }
+
+ /**
+ * Repeated suspended writes replace each other: versions do not accumulate,
+ * and the replaced record's blocks are queued for reclamation.
+ */
+ @Test
+ public void testSuspendedWriteReplacesTheCurrentNullVersion()
+ throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(100L, false, true, true);
+
+ OMKeyCommitResponse response =
+ (OMKeyCommitResponse) commitAt(500L);
+
+ assertTrue(currentVersion().isNullVersion());
+ assertEquals(500L, currentVersion().getVersionId());
+ // the replaced null version is not kept as a noncurrent version
+ assertNull(noncurrentVersion(100L));
+ // its blocks are queued for reclamation instead
+ assertReclaimed(response, 100L);
+ }
+
+ /** Asserts that the given version was queued for block reclamation. */
+ private void assertReclaimed(OMKeyCommitResponse response, long versionId) {
+ assertNotNull(response.getKeysToDelete());
+ assertTrue(response.getKeysToDelete().values().stream()
+ .flatMap(repeated -> repeated.getOmKeyInfoList().stream())
+ .anyMatch(info -> info.getVersionId() != null
+ && info.getVersionId() == versionId),
+ "version " + versionId + " was not queued for reclamation");
+ }
+
+ /**
+ * Versions created while versioning was enabled are not touched by a
+ * suspended write: the one it supersedes becomes noncurrent as usual.
+ */
+ @Test
+ public void testSuspendedWriteKeepsEnabledEraVersions() throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(100L, false);
+
+ commitAt(500L);
+
+ assertTrue(currentVersion().isNullVersion());
+ // the version it superseded is retained, as is the older one
+ assertNotNull(noncurrentVersion(300L));
+ assertFalse(noncurrentVersion(300L).isNullVersion());
+ assertNotNull(noncurrentVersion(100L));
+ }
+
+ /**
+ * The null version may be noncurrent, when versioning was enabled again
+ * after the write that created it. A suspended write still replaces it, and
+ * still becomes the current version.
+ */
+ @Test
+ public void testSuspendedWriteReplacesANoncurrentNullVersion()
+ throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(200L, true);
+ seedNoncurrentVersion(100L, false);
+
+ commitAt(500L);
+
+ OmKeyInfo current = currentVersion();
+ assertTrue(current.isNullVersion());
+ assertEquals(500L, current.getVersionId());
+ // the old null version is gone, everything else is retained
+ assertNull(noncurrentVersion(200L));
+ assertNotNull(noncurrentVersion(300L));
+ assertNotNull(noncurrentVersion(100L));
+ }
+
+ /**
+ * A delete while versioning is suspended writes a marker into the key's null
+ * version slot rather than creating a version.
+ */
+ @Test
+ public void testSuspendedDeleteWritesANullMarker() throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(300L);
+
+ deleteAt(500L);
+
+ OmKeyInfo current = currentVersion();
+ assertNotNull(current);
+ assertTrue(current.isDeleteMarker());
+ assertTrue(current.isNullVersion());
+ // the version it superseded is retained, as under an enabled bucket
+ assertNotNull(noncurrentVersion(300L));
+ }
+
+ /** The null marker replaces the null version that held the slot. */
+ @Test
+ public void testSuspendedDeleteReplacesTheCurrentNullVersion()
+ throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(100L, false, true, true);
+
+ OMKeyDeleteMarkerResponse response =
+ (OMKeyDeleteMarkerResponse) deleteAt(500L);
+
+ OmKeyInfo current = currentVersion();
+ assertTrue(current.isDeleteMarker());
+ assertTrue(current.isNullVersion());
+ // the replaced record is not kept as a noncurrent version
+ assertNull(noncurrentVersion(100L));
+ assertNotNull(response.getKeysToDelete());
+ }
+
+ /**
+ * Versions created while versioning was enabled stay readable and deletable
+ * by versionId after a suspended delete.
+ */
+ @Test
+ public void testSuspendedDeleteKeepsEnabledEraVersions() throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(300L);
+ seedNoncurrentVersion(200L, true);
+ seedNoncurrentVersion(100L, false);
+
+ deleteAt(500L);
+
+ assertTrue(currentVersion().isDeleteMarker());
+ // the superseded version and the older one are retained
+ assertNotNull(noncurrentVersion(300L));
+ assertNotNull(noncurrentVersion(100L));
+ // only the null version the marker replaced is gone
+ assertNull(noncurrentVersion(200L));
+
+ // and a retained version can still be deleted by versionId
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ deleteVersionAt(100L, false, 600L).getOMResponse().getStatus());
+ assertNull(noncurrentVersion(100L));
+ }
+
+ /**
+ * A key written before versioning was enabled carries no versionId and no
+ * null flag. It is the key's null version all the same, so version "null"
+ * addresses it - without the record ever having been rewritten.
+ */
+ @Test
+ public void testPreVersioningCurrentIsAddressableAsNullVersion()
+ throws Exception {
+ setupVersionedBucket();
+ String ozoneKey = seedCurrentVersion(null);
+ OmKeyInfo legacy = currentVersion();
+ assertNull(legacy.getVersionId());
+ assertFalse(legacy.isNullVersion());
+ assertTrue(legacy.isNullVersionRecord());
+
+ // deleting version "null" hits it even though it carries no flag
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ deleteVersionAt(null, true, 500L).getOMResponse().getStatus());
+ assertNull(omMetadataManager.getKeyTable(getBucketLayout()).get(ozoneKey));
+ }
+
+ /**
+ * A suspended write replaces a pre-versioning record, since that record is
+ * the key's null version.
+ */
+ @Test
+ public void testSuspendedWriteReplacesAPreVersioningRecord()
+ throws Exception {
+ setupSuspendedBucket();
+ seedCurrentVersion(null, false, false, true);
+
+ OMKeyCommitResponse response = (OMKeyCommitResponse) commitAt(500L);
+
+ OmKeyInfo current = currentVersion();
+ assertTrue(current.isNullVersion());
+ assertEquals(500L, current.getVersionId());
+ // it was replaced, not kept as a noncurrent version
+ assertNull(noncurrentVersion(VersionIdGenerator.UNSET_VERSION_ID));
+ assertNotNull(response.getKeysToDelete());
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMObjectVersionsReclaimRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMObjectVersionsReclaimRequest.java
new file mode 100644
index 000000000000..493fcedcdf17
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMObjectVersionsReclaimRequest.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.request.key;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.BucketVersioningStatus;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
+import org.apache.hadoop.ozone.om.response.key.OMObjectVersionsReclaimResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ObjectVersionsBucket;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ReclaimObjectVersionsRequest;
+import org.apache.hadoop.util.Time;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the reclamation of noncurrent object versions selected by
+ * VersionCleanupService: the versions leave the versionedKeyTable and their
+ * blocks are queued in the deletedTable, with the bucket quota released.
+ */
+public class TestOMObjectVersionsReclaimRequest extends OMKeyRequestTests {
+
+ private static final long BLOCK_LENGTH = 1000L;
+
+ @Override
+ public BucketLayout getBucketLayout() {
+ return BucketLayout.OBJECT_STORE;
+ }
+
+ @Test
+ public void testReclaimsSubmittedVersions() throws Exception {
+ setupVersionedBucket(3 * BLOCK_LENGTH, 3L);
+ seedNoncurrentVersion(100L);
+ seedNoncurrentVersion(200L);
+ seedNoncurrentVersion(300L);
+
+ OMObjectVersionsReclaimResponse response = reclaim(500L,
+ versionKeys(100L, 200L));
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+
+ // the reclaimed versions are tombstoned in the versionedKeyTable cache
+ assertReclaimedFromTable(100L);
+ assertReclaimedFromTable(200L);
+ // the version that was not submitted is untouched
+ assertNull(omMetadataManager.getVersionedKeyTable().getCacheValue(
+ new CacheKey<>(versionKey(300L))));
+
+ // both are queued for block reclamation
+ List queued = response.getKeysToDelete().values().stream()
+ .flatMap(repeated -> repeated.getOmKeyInfoList().stream())
+ .map(OmKeyInfo::getVersionId)
+ .collect(Collectors.toList());
+ assertEquals(Arrays.asList(100L, 200L), queued);
+
+ // and their space and namespace are released
+ OmBucketInfo bucketInfo = OMKeyRequest.getBucketInfo(omMetadataManager,
+ volumeName, bucketName);
+ assertEquals(BLOCK_LENGTH, bucketInfo.getUsedBytes());
+ assertEquals(1L, bucketInfo.getUsedNamespace());
+ }
+
+ /**
+ * Versions of one key share a deletedTable entry: it holds a
+ * RepeatedOmKeyInfo list that KeyDeletingService evaluates one record at a
+ * time.
+ */
+ @Test
+ public void testVersionsOfOneKeyShareADeletedTableEntry() throws Exception {
+ setupVersionedBucket(3 * BLOCK_LENGTH, 3L);
+ seedNoncurrentVersion(100L);
+ seedNoncurrentVersion(200L);
+
+ OMObjectVersionsReclaimResponse response = reclaim(500L,
+ versionKeys(100L, 200L));
+
+ assertEquals(1, response.getKeysToDelete().size());
+ assertEquals(2, response.getKeysToDelete().values().iterator().next()
+ .getOmKeyInfoList().size());
+ }
+
+ /**
+ * The service selects versions as of its own scan, so one may already be
+ * gone - permanently deleted, or promoted into the keyTable because the
+ * current version was deleted. Such a version is not this request's to
+ * remove.
+ */
+ @Test
+ public void testSkipsVersionsThatAreAlreadyGone() throws Exception {
+ setupVersionedBucket(BLOCK_LENGTH, 1L);
+ seedNoncurrentVersion(100L);
+
+ OMObjectVersionsReclaimResponse response = reclaim(500L,
+ versionKeys(100L, 200L));
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ assertReclaimedFromTable(100L);
+ // only the version that still existed was reclaimed and accounted for
+ assertEquals(1, response.getKeysToDelete().values().stream()
+ .mapToInt(repeated -> repeated.getOmKeyInfoList().size()).sum());
+ OmBucketInfo bucketInfo = OMKeyRequest.getBucketInfo(omMetadataManager,
+ volumeName, bucketName);
+ assertEquals(0L, bucketInfo.getUsedBytes());
+ assertEquals(0L, bucketInfo.getUsedNamespace());
+ }
+
+ @Test
+ public void testSkipsBucketThatNoLongerExists() throws Exception {
+ setupVersionedBucket(BLOCK_LENGTH, 1L);
+ seedNoncurrentVersion(100L);
+
+ OMRequest request = reclaimRequest(ObjectVersionsBucket.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName("deleted-bucket")
+ .addVersionKeys(versionKey(100L))
+ .build());
+ OMObjectVersionsReclaimResponse response =
+ (OMObjectVersionsReclaimResponse) new OMObjectVersionsReclaimRequest(
+ request).validateAndUpdateCache(ozoneManager, 500L);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ assertTrue(response.getKeysToDelete().isEmpty());
+ // the version of the bucket that does exist is left alone
+ assertNull(omMetadataManager.getVersionedKeyTable().getCacheValue(
+ new CacheKey<>(versionKey(100L))));
+ }
+
+ /** A delete marker holds no blocks: it releases namespace but no space. */
+ @Test
+ public void testDeleteMarkerReleasesNamespaceOnly() throws Exception {
+ setupVersionedBucket(BLOCK_LENGTH, 1L);
+ seedDeleteMarker(100L);
+
+ OMObjectVersionsReclaimResponse response = reclaim(500L,
+ versionKeys(100L));
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ assertReclaimedFromTable(100L);
+ // an empty record is not queued for block reclamation at all
+ assertTrue(response.getKeysToDelete().isEmpty());
+ OmBucketInfo bucketInfo = OMKeyRequest.getBucketInfo(omMetadataManager,
+ volumeName, bucketName);
+ // the marker is a record of its own, so it held a namespace slot but no
+ // space: reclaiming it gives the slot back and leaves usedBytes alone
+ assertEquals(BLOCK_LENGTH, bucketInfo.getUsedBytes());
+ assertEquals(0L, bucketInfo.getUsedNamespace());
+ }
+
+ /**
+ * A key whose only remaining version is a delete marker is invisible to
+ * reads and carries no versionId to address it by, so the whole key goes.
+ */
+ @Test
+ public void testReclaimsExpiredDeleteMarker() throws Exception {
+ setupVersionedBucket(BLOCK_LENGTH, 1L);
+ String markerKey = seedCurrentDeleteMarker();
+
+ OMObjectVersionsReclaimResponse response =
+ reclaimMarkers(500L, markerKey);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ CacheValue cached = omMetadataManager
+ .getKeyTable(getBucketLayout())
+ .getCacheValue(new CacheKey<>(markerKey));
+ assertNotNull(cached);
+ assertNull(cached.getCacheValue(), "the marker was not tombstoned");
+
+ // a marker holds no blocks, so nothing is queued for block reclamation
+ assertTrue(response.getKeysToDelete().isEmpty());
+ // it did hold a namespace slot of its own
+ OmBucketInfo bucketInfo = OMKeyRequest.getBucketInfo(omMetadataManager,
+ volumeName, bucketName);
+ assertEquals(BLOCK_LENGTH, bucketInfo.getUsedBytes());
+ assertEquals(0L, bucketInfo.getUsedNamespace());
+ }
+
+ /**
+ * Removing the marker while a noncurrent version survives would promote that
+ * version back to current, resurrecting an object the user deleted.
+ */
+ @Test
+ public void testKeepsMarkerWhileAVersionSurvives() throws Exception {
+ setupVersionedBucket(BLOCK_LENGTH, 2L);
+ String markerKey = seedCurrentDeleteMarker();
+ seedNoncurrentVersion(100L);
+
+ OMObjectVersionsReclaimResponse response =
+ reclaimMarkers(500L, markerKey);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ assertNull(omMetadataManager.getKeyTable(getBucketLayout())
+ .getCacheValue(new CacheKey<>(markerKey)));
+ OmBucketInfo bucketInfo = OMKeyRequest.getBucketInfo(omMetadataManager,
+ volumeName, bucketName);
+ assertEquals(2L, bucketInfo.getUsedNamespace());
+ }
+
+ /**
+ * A write since the scan makes the key's current version a real object
+ * again, so there is nothing expired to remove.
+ */
+ @Test
+ public void testSkipsMarkerSupersededSinceTheScan() throws Exception {
+ setupVersionedBucket(BLOCK_LENGTH, 1L);
+ String objectKey = omMetadataManager.getOzoneKey(volumeName, bucketName,
+ keyName);
+ OmKeyInfo live = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(200L)
+ .build();
+ omMetadataManager.getKeyTable(getBucketLayout()).put(objectKey, live);
+
+ OMObjectVersionsReclaimResponse response =
+ reclaimMarkers(500L, objectKey);
+
+ assertEquals(OzoneManagerProtocolProtos.Status.OK,
+ response.getOMResponse().getStatus());
+ assertNull(omMetadataManager.getKeyTable(getBucketLayout())
+ .getCacheValue(new CacheKey<>(objectKey)));
+ OmBucketInfo bucketInfo = OMKeyRequest.getBucketInfo(omMetadataManager,
+ volumeName, bucketName);
+ assertEquals(1L, bucketInfo.getUsedNamespace());
+ }
+
+ private String seedCurrentDeleteMarker() throws Exception {
+ OmKeyInfo marker = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(300L)
+ .setDeleteMarker(true)
+ .build();
+ String objectKey =
+ omMetadataManager.getOzoneKey(volumeName, bucketName, keyName);
+ omMetadataManager.getKeyTable(getBucketLayout()).put(objectKey, marker);
+ return objectKey;
+ }
+
+ private OMObjectVersionsReclaimResponse reclaimMarkers(long trxnLogIndex,
+ String... markerKeys) throws Exception {
+ OMRequest request = reclaimRequest(ObjectVersionsBucket.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .addAllMarkerKeys(Arrays.asList(markerKeys))
+ .build());
+ return (OMObjectVersionsReclaimResponse)
+ new OMObjectVersionsReclaimRequest(request)
+ .validateAndUpdateCache(ozoneManager, trxnLogIndex);
+ }
+
+ private void setupVersionedBucket(long usedBytes, long usedNamespace)
+ throws Exception {
+ OMRequestTestUtils.addVolumeToDB(volumeName, omMetadataManager);
+ OmBucketInfo bucketInfo = OmBucketInfo.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setBucketLayout(BucketLayout.OBJECT_STORE)
+ .setVersioningStatus(BucketVersioningStatus.ENABLED)
+ .setQuotaInBytes(OzoneConsts.QUOTA_RESET)
+ .setQuotaInNamespace(OzoneConsts.QUOTA_RESET)
+ .setUsedBytes(usedBytes)
+ .setUsedNamespace(usedNamespace)
+ .setCreationTime(Time.now())
+ .build();
+ omMetadataManager.getBucketTable().addCacheEntry(
+ new CacheKey<>(omMetadataManager.getBucketKey(volumeName, bucketName)),
+ CacheValue.get(1L, bucketInfo));
+ }
+
+ private void seedNoncurrentVersion(long versionId) throws Exception {
+ seedNoncurrentVersion(versionId, false);
+ }
+
+ private void seedDeleteMarker(long versionId) throws Exception {
+ seedNoncurrentVersion(versionId, true);
+ }
+
+ private void seedNoncurrentVersion(long versionId, boolean deleteMarker)
+ throws Exception {
+ OmKeyInfo version = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(versionId)
+ .setDeleteMarker(deleteMarker)
+ .build();
+ if (!deleteMarker) {
+ OMRequestTestUtils.addKeyLocationInfo(version, 0L, BLOCK_LENGTH);
+ }
+ omMetadataManager.getVersionedKeyTable()
+ .put(versionKey(versionId), version);
+ }
+
+ private String versionKey(long versionId) {
+ return omMetadataManager.getVersionedOzoneKey(volumeName, bucketName,
+ keyName, versionId);
+ }
+
+ private List versionKeys(long... versionIds) {
+ return Arrays.stream(versionIds).mapToObj(this::versionKey)
+ .collect(Collectors.toList());
+ }
+
+ private void assertReclaimedFromTable(long versionId) {
+ CacheValue cached = omMetadataManager.getVersionedKeyTable()
+ .getCacheValue(new CacheKey<>(versionKey(versionId)));
+ assertNotNull(cached, "version " + versionId + " was not reclaimed");
+ assertNull(cached.getCacheValue(),
+ "version " + versionId + " was not tombstoned");
+ }
+
+ private OMObjectVersionsReclaimResponse reclaim(long trxnLogIndex,
+ List keys) throws Exception {
+ OMRequest request = reclaimRequest(ObjectVersionsBucket.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .addAllVersionKeys(keys)
+ .build());
+ return (OMObjectVersionsReclaimResponse)
+ new OMObjectVersionsReclaimRequest(request)
+ .validateAndUpdateCache(ozoneManager, trxnLogIndex);
+ }
+
+ private OMRequest reclaimRequest(ObjectVersionsBucket versionsBucket) {
+ return OMRequest.newBuilder()
+ .setReclaimObjectVersionsRequest(
+ ReclaimObjectVersionsRequest.newBuilder()
+ .addVersionsPerBucket(versionsBucket))
+ .setCmdType(OzoneManagerProtocolProtos.Type.ReclaimObjectVersions)
+ .setClientId(UUID.randomUUID().toString()).build();
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartRequestTests.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartRequestTests.java
index e23b84f52939..e621c0c74d6a 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartRequestTests.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/S3MultipartRequestTests.java
@@ -45,6 +45,7 @@
import org.apache.hadoop.ozone.om.OmMetadataReader;
import org.apache.hadoop.ozone.om.OzoneManager;
import org.apache.hadoop.ozone.om.ResolvedBucket;
+import org.apache.hadoop.ozone.om.VersionIdAllocator;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.KeyValueUtil;
import org.apache.hadoop.ozone.om.request.OMClientRequest;
@@ -112,6 +113,8 @@ public void setup() throws Exception {
when(lvm.getMetadataLayoutVersion()).thenReturn(0);
when(ozoneManager.getVersionManager()).thenReturn(lvm);
when(ozoneManager.getConfiguration()).thenReturn(ozoneConfiguration);
+ when(ozoneManager.getVersionIdAllocator())
+ .thenReturn(new VersionIdAllocator(ozoneConfiguration));
when(ozoneManager.getConfig()).thenReturn(ozoneConfiguration.getObject(OmConfig.class));
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/TestS3MultipartUploadCompleteRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/TestS3MultipartUploadCompleteRequest.java
index 819f4bf448d2..1e1cf5d18fe4 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/TestS3MultipartUploadCompleteRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/multipart/TestS3MultipartUploadCompleteRequest.java
@@ -20,9 +20,11 @@
import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
import java.io.IOException;
import java.util.ArrayList;
@@ -35,6 +37,8 @@
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.BucketVersioningStatus;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
@@ -98,6 +102,51 @@ public void testValidateAndUpdateCacheSuccess() throws Exception {
checkDeleteTableCount(volumeName, bucketName, keyName, 1, uploadId);
}
+
+ /**
+ * Completing a multipart upload over an existing key on a versioned bucket
+ * creates a version like any other write: the version it supersedes has to
+ * be kept in the versionedKeyTable, not dropped from the keyTable and left
+ * out of the deletedTable.
+ */
+ @Test
+ public void testVersionedOverwriteKeepsPreviousVersion() throws Exception {
+ // versioning is only supported on OBJECT_STORE buckets
+ assumeFalse(getBucketLayout().isFileSystemOptimized());
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ String keyName = getKeyName();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, omMetadataManager,
+ OmBucketInfo.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ // versioning is only allowed on OBJECT_STORE buckets; it shares
+ // the keyTable with LEGACY, so the request helpers still apply
+ .setBucketLayout(BucketLayout.OBJECT_STORE)
+ .setVersioningStatus(BucketVersioningStatus.ENABLED));
+
+ checkValidateAndUpdateCacheSuccess(volumeName, bucketName, keyName,
+ new HashMap<>(), new HashMap<>(), 0L, 1L);
+ OmKeyInfo firstVersion = omMetadataManager.getKeyTable(getBucketLayout())
+ .get(getOzoneDBKey(volumeName, bucketName, keyName));
+ assertNotNull(firstVersion.getVersionId());
+
+ checkValidateAndUpdateCacheSuccess(volumeName, bucketName, keyName,
+ new HashMap<>(), new HashMap<>(), 10L, 2L);
+
+ OmKeyInfo current = omMetadataManager.getKeyTable(getBucketLayout())
+ .get(getOzoneDBKey(volumeName, bucketName, keyName));
+ assertNotEquals(firstVersion.getVersionId(), current.getVersionId());
+
+ // the superseded version survives as a noncurrent version
+ OmKeyInfo noncurrent = omMetadataManager.getVersionedKeyTable().get(
+ omMetadataManager.getVersionedOzoneKey(volumeName, bucketName, keyName,
+ firstVersion.getVersionId()));
+ assertNotNull(noncurrent,
+ "the version the upload superseded was neither kept nor reclaimed");
+ assertEquals(firstVersion.getVersionId(), noncurrent.getVersionId());
+ }
+
public void checkDeleteTableCount(String volumeName,
String bucketName, String keyName, int count, String uploadId)
throws Exception {
@@ -122,6 +171,21 @@ public void checkDeleteTableCount(String volumeName,
private String checkValidateAndUpdateCacheSuccess(String volumeName,
String bucketName, String keyName, Map metadata, Map tags) throws Exception {
+ return checkValidateAndUpdateCacheSuccess(volumeName, bucketName, keyName,
+ metadata, tags, 0L, getNamespaceCount());
+ }
+
+ /**
+ * @param trxnBase offset added to the transaction indexes, so that a test can
+ * run the flow more than once: versionIds have to increase within a key.
+ * @param expectedNamespace the bucket's used namespace once the upload
+ * completes; a versioned overwrite adds a record rather than replacing
+ * one, so the count grows.
+ */
+ private String checkValidateAndUpdateCacheSuccess(String volumeName,
+ String bucketName, String keyName, Map metadata,
+ Map tags, long trxnBase, long expectedNamespace)
+ throws Exception {
OMRequest initiateMPURequest = doPreExecuteInitiateMPU(volumeName,
bucketName, keyName, metadata, tags);
@@ -130,7 +194,7 @@ private String checkValidateAndUpdateCacheSuccess(String volumeName,
getS3InitiateMultipartUploadReq(initiateMPURequest);
OMClientResponse omClientResponse =
- s3InitiateMultipartUploadRequest.validateAndUpdateCache(ozoneManager, 1L);
+ s3InitiateMultipartUploadRequest.validateAndUpdateCache(ozoneManager, trxnBase + 1L);
long clientID = Time.now();
String multipartUploadID = omClientResponse.getOMResponse()
@@ -145,7 +209,7 @@ private String checkValidateAndUpdateCacheSuccess(String volumeName,
// Add key to open key table.
addKeyToTable(volumeName, bucketName, keyName, clientID);
- s3MultipartUploadCommitPartRequest.validateAndUpdateCache(ozoneManager, 2L);
+ s3MultipartUploadCommitPartRequest.validateAndUpdateCache(ozoneManager, trxnBase + 2L);
List partList = new ArrayList<>();
@@ -166,7 +230,7 @@ private String checkValidateAndUpdateCacheSuccess(String volumeName,
getS3MultipartUploadCompleteReq(completeMultipartRequest);
omClientResponse =
- s3MultipartUploadCompleteRequest.validateAndUpdateCache(ozoneManager, 3L);
+ s3MultipartUploadCompleteRequest.validateAndUpdateCache(ozoneManager, trxnBase + 3L);
BatchOperation batchOperation
= omMetadataManager.getStore().initBatchOperation();
@@ -201,8 +265,7 @@ private String checkValidateAndUpdateCacheSuccess(String volumeName,
.getCacheValue(new CacheKey<>(
omMetadataManager.getBucketKey(volumeName, bucketName)))
.getCacheValue();
- assertEquals(getNamespaceCount(),
- omBucketInfo.getUsedNamespace());
+ assertEquals(expectedNamespace, omBucketInfo.getUsedNamespace());
return multipartUploadID;
}