Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
326471f
T1.1. Add bucket versioning three-state enum to proto and bucket helpers
symious Jul 16, 2026
b61c47f
T1.2. Enforce the bucket versioning state machine
symious Jul 16, 2026
ebc2e92
T1.3. Add versionId, isDeleteMarker and isNullVersion to OmKeyInfo
symious Jul 16, 2026
2b6bfa0
T1.4. Add versionedKeyTable column family for noncurrent object versions
symious Jul 16, 2026
640ddc2
T1.5. Gate object versioning behind OMLayoutFeature.OBJECT_VERSIONING
symious Jul 16, 2026
b723516
T1. Address comments
symious Aug 28, 2026
8f6b1ba
T1. Address second review round
symious Sep 10, 2026
fa2921a
T2.1. Propose a versionId from a pluggable generator
symious Aug 20, 2026
c66142c
T2.2. Settle a proposed versionId against a per-key floor
symious Aug 20, 2026
839d5b3
T2.3. Pin the first version of a key to a known id
symious Aug 20, 2026
52d5866
T3.1. Keep the overwritten current version in the versionedKeyTable
symious Jul 29, 2026
9f7925f
T3.2. Insert a delete marker on DELETE without a versionId
symious Jul 29, 2026
944cbf4
T3.3. Count every version against the bucket quota
symious Jul 29, 2026
148a117
T3.4. Insert a delete marker on a batch DELETE too
symious Aug 20, 2026
75cbb4f
T3. Address second review round
symious Sep 1, 2026
0fb87ea
T3. Address third review round
symious Sep 10, 2026
d81a4fd
T4.1. Read a specific object version by versionId
symious Jul 31, 2026
f4aa284
T4.2. Permanently delete a noncurrent version by versionId
symious Jul 31, 2026
2f05070
T4.3. Promote the next-newest version when the current one is deleted
symious Jul 31, 2026
9120f6c
T4. Address comments
symious Aug 28, 2026
bc7b200
T4. Address second review round
symious Sep 1, 2026
16122e6
T4. Address third review round
symious Sep 10, 2026
26b7b8c
T4. Address fourth review round
symious Sep 11, 2026
a4faf3d
T4. Address fifth review round
symious Sep 15, 2026
e9dd462
T4. Address sixth review round
symious Sep 15, 2026
5ac9baa
T4. Address seventh review round
symious Sep 15, 2026
c0bd7c6
T4. Address eighth review round
symious Sep 15, 2026
c20733d
T5.1. Take the null version slot on a write while versioning is suspe…
symious Aug 4, 2026
d7d8f85
T5.2. Write a null delete marker while versioning is suspended
symious Aug 4, 2026
0de20fe
T5.3. Address pre-versioning records as the key's null version
symious Aug 4, 2026
08899e0
T5.4. Keep the superseded version when a multipart upload completes
symious Aug 4, 2026
342fdb8
T5. Address comments
symious Aug 28, 2026
3527821
T5. Address second review round
symious Sep 1, 2026
0c27ba4
T5. Address third review round
symious Sep 10, 2026
d2b6f0b
T6.1. Add the versioning actions to the lifecycle rule model
symious Aug 20, 2026
07ae1ea
T6.2. Reclaim noncurrent versions in the lifecycle scan
symious Aug 20, 2026
0d2160c
T6.3. Expire an object on a versioned bucket into a delete marker
symious Aug 21, 2026
b89f3fc
T6.4. Remove a delete marker once nothing is left under it
symious Aug 21, 2026
85b3b32
T6.5. Run the lifecycle service by default, and require it for versio…
symious Aug 21, 2026
e638fff
T6. Address comments
symious Aug 28, 2026
d0a057a
T6. Address second review round
symious Sep 1, 2026
eecd072
T7.1. Keep snapshots and object versioning off the same bucket
symious Aug 7, 2026
dfe4ea7
T7.2. Add an unsafe opt-in that lifts the snapshot exclusion for testing
symious Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ public final class OzoneConsts {

public static final String OM_KEY_PREFIX = "/";
public static final String DOUBLE_SLASH_OM_KEY_PREFIX = "//";
/**
* Separates a key name from the versionId suffix in versionedKeyTable DB keys.
* OM_KEY_PREFIX cannot be used: OBJECT_STORE key names contain '/' verbatim, so
* a key's versions would interleave with the versions of keys nested under it.
* 0x00 is the minimum byte value, so {keyName} + this separator can never be a
* prefix of {keyName} + "/". Written as an octal escape because a literal
* unicode escape for NUL is expanded by the Java lexer before parsing.
*/
public static final String OM_VERSIONED_KEY_SEPARATOR = "\0";
public static final String OM_USER_PREFIX = "$";
public static final String OM_S3_PREFIX = "S3:";
public static final String OM_S3_CALLER_CONTEXT_PREFIX = "S3Auth:S3G|";
Expand Down Expand Up @@ -299,6 +308,14 @@ public final class OzoneConsts {
public static final String STORAGE_TYPE = "storageType";
public static final String RESOURCE_TYPE = "resourceType";
public static final String IS_VERSION_ENABLED = "isVersionEnabled";
public static final String VERSIONING_STATUS = "versioningStatus";
// Audit keys for the object versions a write creates, demotes or removes.
public static final String VERSION_ID = "versionId";
public static final String REQUESTED_VERSION_ID = "requestedVersionId";
public static final String SUPERSEDED_VERSION_ID = "supersededVersionId";
public static final String DELETED_VERSION_ID = "deletedVersionId";
public static final String PROMOTED_VERSION_ID = "promotedVersionId";
public static final String NULL_VERSION = "nullVersion";
public static final String CREATION_TIME = "creationTime";
public static final String MODIFICATION_TIME = "modificationTime";
public static final String DATA_SIZE = "dataSize";
Expand Down
54 changes: 52 additions & 2 deletions hadoop-hdds/common/src/main/resources/ozone-default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5216,9 +5216,11 @@

<property>
<name>ozone.lifecycle.service.enabled</name>
<value>false</value>
<value>true</value>
<tag>OZONE</tag>
<description>It specifies whether to enable lifecycle management service.</description>
<description>It specifies whether to enable lifecycle management service.
S3 object versioning depends on it to reclaim versions, so OM refuses to
enable versioning on a bucket while this is off.</description>
</property>
<property>
<name>ozone.lifecycle.service.move.to.trash.enabled</name>
Expand Down Expand Up @@ -5286,4 +5288,52 @@
ozone.lifecycle.service.state.save.keys.processed is satisfied.
</description>
</property>
<property>
<name>ozone.om.versioning.version-id-generator</name>
<value>org.apache.hadoop.ozone.om.helpers.UniqueIdVersionIdGenerator</value>
<tag>OZONE, OM</tag>
<description>
Implementation of org.apache.hadoop.ozone.om.helpers.VersionIdGenerator used to propose the
versionId of an object version. The default numbers a version with the time it was written,
through the same scheme Ozone uses for block local IDs;
org.apache.hadoop.ozone.om.helpers.PinnedFirstVersionIdGenerator additionally pins the first
version of every key to a reserved sentinel, so that it can be referenced without listing the
key's versions first.
The setting is cluster-wide and read when an OM starts, so a change takes effect on every
bucket once the OMs have been restarted; it is not recorded in bucket metadata. Keys written
under a previous generator keep the ids they were given: a proposal is taken as a floor when
the version is applied, so a generator proposing ids below those a previous one handed out
delays the new numbering on the affected keys rather than disordering them.
</description>
</property>
<property>
<name>ozone.om.versioning.lookup.warning.threshold</name>
<value>5s</value>
<tag>OZONE, OM, PERFORMANCE</tag>
<description>
How long a lookup of a key's noncurrent versions may take on the write path before it is
logged at WARN. The lookup walks the versionedKeyTable cache and opens an iterator on the DB,
both inline on the write, and creating a RocksDB iterator can be slow under load. The latency
is also reported as an OM performance metric regardless of this threshold.
</description>
</property>
<property>
<name>ozone.om.snapshot.versioning.coexistence.enabled</name>
<value>false</value>
<tag>OZONE, OM</tag>
<description>
UNSAFE, FOR TESTING ONLY. Do not enable on a production cluster.

When false, the default, OM refuses to create a snapshot of a bucket that has S3 object
versioning enabled or suspended, and refuses to enable or resume versioning on a bucket that
still has snapshots. The two features cannot be combined safely yet: a snapshot shares blocks
with the active object store and keeps them only because key reclamation skips what a previous
snapshot references, and that lookup is not version-aware, so reclaiming a noncurrent version
can delete blocks a snapshot still points at.

Setting this to true lifts those checks so that the integration tests for snapshot-aware
version reclamation can be written against a real mixed-state bucket. On such a bucket, data
loss is possible.
</description>
</property>
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,21 @@ public OzoneLifecycleConfiguration(String volume, String bucket,
public static class OzoneLCExpiration {
private final Integer days;
private final String date;
private final boolean expiredObjectDeleteMarker;

public OzoneLCExpiration(Integer days, String date) {
this(days, date, false);
}

public OzoneLCExpiration(Integer days, String date,
boolean expiredObjectDeleteMarker) {
this.days = days;
this.date = date;
this.expiredObjectDeleteMarker = expiredObjectDeleteMarker;
}

public boolean isExpiredObjectDeleteMarker() {
return expiredObjectDeleteMarker;
}

public String getDate() {
Expand All @@ -62,6 +73,28 @@ public Integer getDays() {
}
}

/**
* A class that encapsulates OzoneLCNoncurrentVersionExpiration.
*/
public static class OzoneLCNoncurrentVersionExpiration {
private final Integer noncurrentDays;
private final Integer newerNoncurrentVersions;

public OzoneLCNoncurrentVersionExpiration(Integer noncurrentDays,
Integer newerNoncurrentVersions) {
this.noncurrentDays = noncurrentDays;
this.newerNoncurrentVersions = newerNoncurrentVersions;
}

public Integer getNoncurrentDays() {
return noncurrentDays;
}

public Integer getNewerNoncurrentVersions() {
return newerNoncurrentVersions;
}
}

/**
* A class that encapsulates OzoneLCAbortIncompleteMultipartUpload.
*/
Expand Down Expand Up @@ -136,19 +169,34 @@ public static class OzoneLCRule {
private final String status;
private final OzoneLCExpiration expiration;
private final OzoneLCAbortIncompleteMultipartUpload abortIncompleteMultipartUpload;
private final OzoneLCNoncurrentVersionExpiration noncurrentVersionExpiration;
private final OzoneLCFilter filter;

public OzoneLCRule(String id, String prefix, String status,
OzoneLCExpiration expiration, OzoneLCAbortIncompleteMultipartUpload abortIncompleteMultipartUpload,
OzoneLCFilter filter) {
this(id, prefix, status, expiration, abortIncompleteMultipartUpload, null,
filter);
}

@SuppressWarnings("checkstyle:ParameterNumber")
public OzoneLCRule(String id, String prefix, String status,
OzoneLCExpiration expiration, OzoneLCAbortIncompleteMultipartUpload abortIncompleteMultipartUpload,
OzoneLCNoncurrentVersionExpiration noncurrentVersionExpiration,
OzoneLCFilter filter) {
this.id = id;
this.prefix = prefix;
this.status = status;
this.expiration = expiration;
this.abortIncompleteMultipartUpload = abortIncompleteMultipartUpload;
this.noncurrentVersionExpiration = noncurrentVersionExpiration;
this.filter = filter;
}

public OzoneLCNoncurrentVersionExpiration getNoncurrentVersionExpiration() {
return noncurrentVersionExpiration;
}

public String getId() {
return id;
}
Expand Down Expand Up @@ -200,7 +248,15 @@ public static OzoneLifecycleConfiguration fromOmLifecycleConfiguration(
OzoneLifecycleConfiguration.OzoneLCExpiration e = null;
if (r.getExpiration() != null) {
e = new OzoneLifecycleConfiguration.OzoneLCExpiration(
r.getExpiration().getDays(), r.getExpiration().getDate());
r.getExpiration().getDays(), r.getExpiration().getDate(),
r.getExpiration().isExpiredObjectDeleteMarker());
}

OzoneLifecycleConfiguration.OzoneLCNoncurrentVersionExpiration n = null;
if (r.getNoncurrentVersionExpiration() != null) {
n = new OzoneLifecycleConfiguration.OzoneLCNoncurrentVersionExpiration(
r.getNoncurrentVersionExpiration().getNoncurrentDays(),
r.getNoncurrentVersionExpiration().getNewerNoncurrentVersions());
}

OzoneLifecycleConfiguration.OzoneLCAbortIncompleteMultipartUpload a = null;
Expand All @@ -221,7 +277,7 @@ public static OzoneLifecycleConfiguration fromOmLifecycleConfiguration(
}

rules.add(new OzoneLifecycleConfiguration.OzoneLCRule(r.getId(),
r.getPrefix(), (r.isEnabled() ? "Enabled" : "Disabled"), e, a, f));
r.getPrefix(), (r.isEnabled() ? "Enabled" : "Disabled"), e, a, n, f));
}

return new OzoneLifecycleConfiguration(lifecycleConfiguration.getVolume(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
case DeleteLifecycleConfiguration:
case SetLifecycleServiceStatus:
case SaveLifecycleScanState:
case ReclaimObjectVersions:
return false;
case UnknownCommand:
return false;
Expand Down Expand Up @@ -474,6 +475,9 @@ public static boolean shouldSendToFollower(OMRequest omRequest) {
case DeleteLifecycleConfiguration:
case SetLifecycleServiceStatus:
case SaveLifecycleScanState:
// Submitted by the leader's own lifecycle scan, like the scan state it
// travels with.
case ReclaimObjectVersions:
case UnknownCommand:
return false;
case EchoRPC:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.apache.hadoop.hdds.client.ReplicationFactor;
import org.apache.hadoop.hdds.client.ReplicationType;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.UniqueIdVersionIdGenerator;
import org.apache.hadoop.ozone.om.helpers.VersionIdGenerator;
import org.apache.ratis.util.TimeDuration;
import org.rocksdb.CompactRangeOptions.BottommostLevelCompaction;

Expand Down Expand Up @@ -188,7 +190,9 @@ public final class OMConfigKeys {
= 5;
public static final String OZONE_KEY_LIFECYCLE_SERVICE_ENABLED =
"ozone.lifecycle.service.enabled";
public static final boolean OZONE_KEY_LIFECYCLE_SERVICE_ENABLED_DEFAULT = false;
// On by default: S3 object versioning relies on the lifecycle scan to
// reclaim versions, and a rule that never runs is worse than no rule.
public static final boolean OZONE_KEY_LIFECYCLE_SERVICE_ENABLED_DEFAULT = true;
public static final String OZONE_KEY_LIFECYCLE_SERVICE_DELETE_BATCH_SIZE =
"ozone.lifecycle.service.delete.batch-size";
public static final int OZONE_KEY_LIFECYCLE_SERVICE_DELETE_BATCH_SIZE_DEFAULT = 1000;
Expand Down Expand Up @@ -768,6 +772,33 @@ public final class OMConfigKeys {
"ozone.om.ratis.events.max.limit";
public static final int OZONE_OM_RATIS_EVENTS_MAX_LIMIT_DEFAULT = 100;

public static final String OZONE_OM_VERSIONING_VERSION_ID_GENERATOR =
"ozone.om.versioning.version-id-generator";
public static final Class<? extends VersionIdGenerator>
OZONE_OM_VERSIONING_VERSION_ID_GENERATOR_DEFAULT = UniqueIdVersionIdGenerator.class;

/**
* How long a lookup of a key's noncurrent versions may take on the write
* path before it is logged. The lookup walks the versionedKeyTable cache
* and opens a DB iterator, which can be slow to create under load.
*/
public static final String
OZONE_OM_VERSIONING_LOOKUP_WARNING_THRESHOLD =
"ozone.om.versioning.lookup.warning.threshold";
public static final String
OZONE_OM_VERSIONING_LOOKUP_WARNING_THRESHOLD_DEFAULT = "5s";

/**
* Lifts the mutual exclusion between snapshots and object versioning on a
* bucket. Unsafe: a version reclaimed from the active object store can take
* blocks a snapshot still references. For tests only.
*/
public static final String
OZONE_OM_SNAPSHOT_VERSIONING_COEXISTENCE_ENABLED =
"ozone.om.snapshot.versioning.coexistence.enabled";
public static final boolean
OZONE_OM_SNAPSHOT_VERSIONING_COEXISTENCE_ENABLED_DEFAULT = false;

/**
* Never constructed.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ public enum ResultCodes {
ATOMIC_WRITE_CONFLICT,

LIFECYCLE_CONFIGURATION_NOT_FOUND,
UPDATE_ID_NOT_MATCH
UPDATE_ID_NOT_MATCH,

/**
* The addressed version exists but is a delete marker. Kept distinct from
* KEY_NOT_FOUND so that the S3 Gateway can answer such a read with 405
* MethodNotAllowed rather than 404; the gateway mapping itself is not in
* place yet, and until it is the condition surfaces as an internal error.
*/
KEY_IS_DELETE_MARKER
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.ozone.protocol.proto.OzoneManagerProtocolProtos.BucketVersioningStatusProto;

/**
* S3-compatible bucket versioning state machine:
* UNVERSIONED -&gt; ENABLED &lt;-&gt; SUSPENDED.
* Once versioning has been enabled, a bucket can never return to UNVERSIONED.
*/
public enum BucketVersioningStatus {
UNVERSIONED,
ENABLED,
SUSPENDED;

public static BucketVersioningStatus fromProto(BucketVersioningStatusProto proto) {
switch (proto) {
case VERSIONING_ENABLED:
return ENABLED;
case VERSIONING_SUSPENDED:
return SUSPENDED;
case UNVERSIONED:
default:
return UNVERSIONED;
}
}

public BucketVersioningStatusProto toProto() {
switch (this) {
case ENABLED:
return BucketVersioningStatusProto.VERSIONING_ENABLED;
case SUSPENDED:
return BucketVersioningStatusProto.VERSIONING_SUSPENDED;
default:
return BucketVersioningStatusProto.UNVERSIONED;
}
}

/** The legacy isVersionEnabled flag value kept in sync with this status. */
public boolean toVersionEnabledFlag() {
return this == ENABLED;
}

/**
* Whether a bucket may transition from this status to {@code target}.
* The only forbidden transition is back to UNVERSIONED after versioning
* has been enabled or suspended (matches the S3 state machine).
*/
public boolean canTransitionTo(BucketVersioningStatus target) {
return this == UNVERSIONED || target != UNVERSIONED;
}
}
Loading
Loading