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..4d9777893146 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
@@ -3060,6 +3060,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/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();