tags;
private Long expectedDataGeneration = null;
+ private Long versionId = null;
+ private boolean isDeleteMarker;
+ private boolean isNullVersion;
public Builder() {
this.acls = AclListBuilder.empty();
@@ -533,6 +567,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 +741,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 +907,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 +971,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 +1017,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..1b293f37d136
--- /dev/null
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/VersionIdGenerator.java
@@ -0,0 +1,92 @@
+/*
+ * 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 #NULL_VERSION_ID} is reserved and is 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 {
+
+ /**
+ * Reserved id of the null version slot, rendered as the literal "null" by
+ * the S3 layer. Never returned by a generator.
+ */
+ long NULL_VERSION_ID = 0L;
+
+ /**
+ * Reserved id of the pinned first version of a key, a separate slot from
+ * {@link #NULL_VERSION_ID}. 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..cb24e5cbb571 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,32 @@ 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 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..a089227c636a 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,91 @@ public void protobufConversion() {
OmBucketInfo.getFromProtobuf(bucket.getProtobuf()));
}
+ @Test
+ public void versioningStatusDerivedFromLegacyFlag() {
+ // Records written before the versioningStatus field existed deserialize
+ // unchanged: the status is derived from the legacy isVersionEnabled flag.
+ OzoneManagerProtocolProtos.BucketInfo oldRecord =
+ OzoneManagerProtocolProtos.BucketInfo.newBuilder()
+ .setVolumeName("vol1")
+ .setBucketName("bucket")
+ .setIsVersionEnabled(false)
+ .setStorageType(HddsProtos.StorageTypeProto.DISK)
+ .build();
+ OmBucketInfo bucket = OmBucketInfo.getFromProtobuf(oldRecord);
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ bucket.getVersioningStatus());
+ assertFalse(bucket.getIsVersionEnabled());
+ assertEquals(bucket, OmBucketInfo.getFromProtobuf(bucket.getProtobuf()));
+
+ oldRecord = oldRecord.toBuilder().setIsVersionEnabled(true).build();
+ bucket = OmBucketInfo.getFromProtobuf(oldRecord);
+ assertEquals(BucketVersioningStatus.ENABLED,
+ bucket.getVersioningStatus());
+ assertTrue(bucket.getIsVersionEnabled());
+ 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");
+
+ // default is UNVERSIONED
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ builder.build().getVersioningStatus());
+
+ // legacy true -> ENABLED
+ builder.setIsVersionEnabled(true);
+ assertEquals(BucketVersioningStatus.ENABLED,
+ builder.build().getVersioningStatus());
+ assertTrue(builder.build().getIsVersionEnabled());
+
+ // explicit SUSPENDED forces the legacy flag to false
+ builder.setVersioningStatus(BucketVersioningStatus.SUSPENDED);
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ builder.build().getVersioningStatus());
+ assertFalse(builder.build().getIsVersionEnabled());
+
+ // legacy false does not clobber an explicitly SUSPENDED status
+ builder.setIsVersionEnabled(false);
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ builder.build().getVersioningStatus());
+
+ // a null status is a no-op (records without the new field)
+ builder.setVersioningStatus(null);
+ assertEquals(BucketVersioningStatus.SUSPENDED,
+ builder.build().getVersioningStatus());
+
+ // legacy false on a never-enabled bucket stays UNVERSIONED
+ OmBucketInfo unversioned = OmBucketInfo.newBuilder()
+ .setBucketName("bucket")
+ .setVolumeName("vol1")
+ .setIsVersionEnabled(false)
+ .build();
+ assertEquals(BucketVersioningStatus.UNVERSIONED,
+ unversioned.getVersioningStatus());
+ }
+
@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..534dbb5dd2af
--- /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.NULL_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(NULL_VERSION_ID, generator.generateVersionId(index, true));
+ assertNotEquals(NULL_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(NULL_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(NULL_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 pinnedFirstSentinelIsNotTheNullVersion() {
+ assertNotEquals(NULL_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..9e9a658c622a 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -808,6 +808,9 @@ 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;
}
enum BucketLayoutProto {
@@ -816,6 +819,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 +897,9 @@ 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;
}
message PrefixInfo {
@@ -1217,6 +1234,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
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..f21f94f5fc7e 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
@@ -172,6 +172,22 @@ 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. Since key names may themselves contain the
+ * separator, iterating consumers must check that the remainder after this
+ * prefix is exactly one fixed-width versionId suffix.
+ */
+ 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.
@@ -405,6 +421,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/om/OmMetadataManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
index 283bb4933580..bd1ff7c48aec 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
@@ -159,6 +159,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 +377,11 @@ public Table getKeyTable(BucketLayout bucketLayout) {
return keyTable;
}
+ @Override
+ public Table getVersionedKeyTable() {
+ return versionedKeyTable;
+ }
+
@Override
public Table getFileTable() {
return fileTable;
@@ -494,6 +500,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 +656,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_KEY_PREFIX;
+ }
+
@Override
public String getOzoneKeyFSO(String volumeName,
String bucketName,
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..524d7c35407d 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,7 @@ public OmBucketInfo getBucketInfo(String volume, String bucket)
.setDefaultReplicationConfig(
realBucket.getDefaultReplicationConfig())
.setIsVersionEnabled(realBucket.getIsVersionEnabled())
+ .setVersioningStatus(realBucket.getVersioningStatus())
.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..933c86d2f9f7 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/revVersionId :- KeyInfo |
* | deletedTable | /volume/bucket/key :- RepeatedKeyInfo |
* | openKeyTable | /volume/bucket/key/id :- KeyInfo |
* | multipartInfoTable | /volume/bucket/key/uploadId :- parts |
@@ -210,6 +211,19 @@ public final class OMDBDefinition extends DBDefinition.WithMap {
StringCodec.get(),
OmKeyInfo.getKeyTableCodec());
+ public static final String VERSIONED_KEY_TABLE = "versionedKeyTable";
+ /**
+ * versionedKeyTable: /volume/bucket/key/revVersionId :- 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.
+ */
+ 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 +367,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/request/bucket/OMBucketSetPropertyRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/OMBucketSetPropertyRequest.java
index a88e5fb73334..2a8f8da52071 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,7 @@
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.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 +175,29 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
//Check Versioning to update
Boolean versioning = omBucketArgs.getIsVersionEnabled();
- if (versioning != null) {
- bucketInfoBuilder.setIsVersionEnabled(versioning);
- LOG.debug("Updating bucket versioning for bucket: {} in volume: {}",
- bucketName, volumeName);
+ BucketVersioningStatus newVersioningStatus = omBucketArgs.getVersioningStatus();
+ if (newVersioningStatus == null && versioning != null) {
+ // Legacy flag from older clients: enabling always maps to ENABLED;
+ // disabling maps to SUSPENDED once versioning has ever been enabled
+ // (the S3 state machine forbids returning to UNVERSIONED).
+ if (versioning) {
+ newVersioningStatus = BucketVersioningStatus.ENABLED;
+ } else {
+ newVersioningStatus =
+ dbBucketInfo.getVersioningStatus() == BucketVersioningStatus.UNVERSIONED
+ ? BucketVersioningStatus.UNVERSIONED : BucketVersioningStatus.SUSPENDED;
+ }
+ }
+ if (newVersioningStatus != null) {
+ 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 quotaInBytes and quotaInNamespace to update
@@ -376,4 +396,27 @@ 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);
+ }
+ }
+ 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..faa683b6933f 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
@@ -52,6 +52,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 +304,23 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
}
validateAtomicRewrite(keyToDelete, omKeyInfo, auditMap);
+ final boolean s3Versioning = omBucketInfo.isS3VersioningEnabled();
// 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 (s3Versioning) {
+ // 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));
+ }
+ omKeyInfo = committedKeyBuilder.build();
// Update the block length for each block, return the allocated but
// uncommitted blocks
@@ -401,6 +411,24 @@ 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 (s3Versioning && keyToDelete != null && !isSameHsyncKey) {
+ versionedKeyInfo = keyToDelete.getVersionId() != null ? keyToDelete
+ : keyToDelete.toBuilder()
+ .setVersionId(VersionIdGenerator.NULL_VERSION_ID)
+ .setNullVersion(true)
+ .build();
+ dbVersionedKey = omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName, versionedKeyInfo.getVersionId());
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ dbVersionedKey, versionedKeyInfo, trxnLogIndex);
+ }
+
omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
dbOzoneKey, omKeyInfo, trxnLogIndex);
@@ -408,7 +436,8 @@ 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);
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..e98502370289 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
@@ -24,8 +24,13 @@
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.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,11 +47,14 @@
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.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.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeyRequest;
@@ -128,6 +136,12 @@ 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;
long startNanos = Time.monotonicNowNanos();
try {
String objectKey =
@@ -142,57 +156,73 @@ 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 (omBucketInfo.isS3VersioningEnabled()) {
+ // 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.
+ 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 +231,15 @@ 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);
+ omClientResponse = insertingDeleteMarker
+ ? new OMKeyDeleteMarkerResponse(errorResponse, getBucketLayout())
+ : new OMKeyDeleteResponse(errorResponse, getBucketLayout());
long endNanosDeleteKeyFailureLatencyNs = Time.monotonicNowNanos();
perfMetrics.setDeleteKeyFailureLatencyNs(endNanosDeleteKeyFailureLatencyNs - startNanos);
} finally {
@@ -222,7 +258,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 +277,93 @@ 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.
+ */
+ @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();
+
+ // 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(false)
+ .build();
+
+ String movedVersionedKeyName = null;
+ OmKeyInfo movedVersionedKeyInfo = null;
+ if (currentVersion != null) {
+ movedVersionedKeyInfo = currentVersion.getVersionId() != null
+ ? currentVersion
+ : currentVersion.toBuilder()
+ .setVersionId(VersionIdGenerator.NULL_VERSION_ID)
+ .setNullVersion(true)
+ .build();
+ movedVersionedKeyName = omMetadataManager.getVersionedOzoneKey(
+ volumeName, bucketName, keyName,
+ movedVersionedKeyInfo.getVersionId());
+ omMetadataManager.getVersionedKeyTable().addCacheEntry(
+ movedVersionedKeyName, movedVersionedKeyInfo, trxnLogIndex);
+ }
+
+ omBucketInfo.incrUsedNamespace(1L);
+
+ omMetadataManager.getKeyTable(getBucketLayout()).addCacheEntry(
+ objectKey, deleteMarker, trxnLogIndex);
+
+ return new OMKeyDeleteMarkerResponse(
+ omResponse.setDeleteKeyResponse(DeleteKeyResponse.newBuilder()).build(),
+ deleteMarker, objectKey, movedVersionedKeyName, movedVersionedKeyInfo,
+ omBucketInfo.copyObject());
+ }
+
/**
* 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..b0fef6cad794 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
@@ -959,11 +959,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.isS3VersioningEnabled();
+ 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/response/key/OMKeyCommitResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyCommitResponse.java
index 425c4f63ac5e..f36aff6dfe46 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,8 @@ public class OMKeyCommitResponse extends OmKeyResponse {
private OmKeyInfo newOpenKeyInfo;
private OmKeyInfo openKeyToUpdate;
private String openKeyNameToUpdate;
+ private String versionedKeyName;
+ private OmKeyInfo versionedKeyInfo;
@SuppressWarnings("checkstyle:ParameterNumber")
public OMKeyCommitResponse(
@@ -82,6 +85,17 @@ 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;
+ }
+
@Override
public void addToDBBatch(OMMetadataManager omMetadataManager,
BatchOperation batchOperation) throws IOException {
@@ -98,6 +112,11 @@ public void addToDBBatch(OMMetadataManager omMetadataManager,
omMetadataManager.getKeyTable(getBucketLayout())
.putWithBatch(batchOperation, ozoneKeyName, omKeyInfo);
+ if (versionedKeyInfo != null) {
+ omMetadataManager.getVersionedKeyTable()
+ .putWithBatch(batchOperation, versionedKeyName, versionedKeyInfo);
+ }
+
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..822596d57c9f
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/key/OMKeyDeleteMarkerResponse.java
@@ -0,0 +1,86 @@
+/*
+ * 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.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 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})
+public class OMKeyDeleteMarkerResponse extends OmKeyResponse {
+
+ private OmKeyInfo deleteMarker;
+ private String ozoneKeyName;
+ private String movedVersionedKeyName;
+ private OmKeyInfo movedVersionedKeyInfo;
+ private OmBucketInfo omBucketInfo;
+
+ 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();
+ }
+
+ @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);
+ }
+
+ 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/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/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..f506c5a41f3d 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;
@@ -120,6 +121,7 @@ public class TestOmMetadataManager {
VOLUME_TABLE,
BUCKET_TABLE,
KEY_TABLE,
+ VERSIONED_KEY_TABLE,
DELETED_TABLE,
OPEN_KEY_TABLE,
MULTIPART_INFO_TABLE,
@@ -172,6 +174,23 @@ public void testTransactionTable() throws Exception {
assertEquals(250, transactionInfo.getTransactionIndex());
}
+ @Test
+ public void testVersionedOzoneKeyOrdering() {
+ String prefix = omMetadataManager.getVersionedOzoneKeyPrefix("vol", "buck", "key");
+ assertEquals("/vol/buck/key/", 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 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..be5bf3375fab 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,153 @@ public void testValidateAndUpdateCacheWithQuotaNamespaceUsed()
"is less than used namespaceQuota");
}
+ @Test
+ public void testVersioningStatusTransitions() throws Exception {
+ String volumeName = UUID.randomUUID().toString();
+ String bucketName = UUID.randomUUID().toString();
+ OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+ omMetadataManager);
+ 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);
+ 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 -> ENABLED
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningFlagRequest(volumeName, bucketName, true))
+ .validateAndUpdateCache(ozoneManager, 2);
+ assertTrue(response.getOMResponse().getSuccess());
+ assertEquals(BucketVersioningStatus.ENABLED,
+ omMetadataManager.getBucketTable().get(bucketKey).getVersioningStatus());
+
+ // legacy false after enabling -> SUSPENDED, not UNVERSIONED
+ response = new OMBucketSetPropertyRequest(
+ createSetVersioningFlagRequest(volumeName, bucketName, false))
+ .validateAndUpdateCache(ozoneManager, 3);
+ assertTrue(response.getOMResponse().getSuccess());
+ OmBucketInfo 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));
+ }
+
+ 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..7c3e9cfa93a2
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyVersioningRequests.java
@@ -0,0 +1,440 @@
+/*
+ * 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.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 setupVersionedBucket(long quotaInBytes, long quotaInNamespace,
+ long usedNamespace) throws Exception {
+ OMRequestTestUtils.addVolumeToDB(volumeName, omMetadataManager);
+ OmBucketInfo bucketInfo = OmBucketInfo.newBuilder()
+ .setVolumeName(volumeName)
+ .setBucketName(bucketName)
+ .setBucketLayout(BucketLayout.OBJECT_STORE)
+ .setVersioningStatus(BucketVersioningStatus.ENABLED)
+ .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 {
+ OmKeyInfo keyInfo = OMRequestTestUtils.createOmKeyInfo(
+ volumeName, bucketName, keyName, replicationConfig)
+ .setVersionId(versionId)
+ .setDeleteMarker(deleteMarker)
+ .build();
+ 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 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.NULL_VERSION_ID);
+ assertNotNull(noncurrent);
+ assertTrue(noncurrent.isNullVersion());
+ assertEquals(VersionIdGenerator.NULL_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.NULL_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.NULL_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());
+ }
+}