Skip to content

HDDS-15879. [Ozone Versioning] [T1] Metadata foundation - #10781

Open
symious wants to merge 6 commits into
apache:HDDS-15728from
symious:HDDS-15879
Open

HDDS-15879. [Ozone Versioning] [T1] Metadata foundation#10781
symious wants to merge 6 commits into
apache:HDDS-15728from
symious:HDDS-15879

Conversation

@symious

@symious symious commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This is the first basic task of Ozone versioning, including the following points

  • bucket proto three-state enum
  • set-property state machine
  • OmKeyInfo proto extension
  • versionedKeyTableColumnFamily
  • layout feature gate

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-15879

How was this patch tested?

unit test.

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clean foundation work! @symious I verified that the five commits map nicely to the design doc. Left a few small comments below.

}

/** No-op when status is null (e.g. records without the new field). */
public Builder setVersioningStatus(BucketVersioningStatus status) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work keeping the two fields in sync here! One spot that looks left behind:

OzoneManager#getBucketInfo copies only the legacy flag when resolving a link bucket:

return bucketInfo.toBuilder()
.setDefaultReplicationConfig(
realBucket.getDefaultReplicationConfig())
.setIsVersionEnabled(realBucket.getIsVersionEnabled())
.setStorageType(realBucket.getStorageType())
.setQuotaInBytes(realBucket.getQuotaInBytes())
.setQuotaInNamespace(realBucket.getQuotaInNamespace())
.setUsedBytes(realBucket.getUsedBytes())
.setSnapshotUsedBytes(realBucket.getSnapshotUsedBytes())
.setSnapshotUsedNamespace(realBucket.getSnapshotUsedNamespace())
.setUsedNamespace(realBucket.getUsedNamespace())
.addAllMetadata(realBucket.getMetadata())
.setBucketLayout(realBucket.getBucketLayout())
.build();

Since SUSPENDED maps to flag=false, a suspended bucket read through a link would come back as UNVERSIONED. Should we also add .setVersioningStatus(realBucket.getVersioningStatus()) there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, Updated, PTAL.

* Returns the requested versioning status, or null if not being changed.
* @return BucketVersioningStatus
*/
public BucketVersioningStatus getVersioningStatus() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny one: toAuditMap() still only records IS_VERSION_ENABLED, so a status change via the new field would show up as isVersionEnabled=null in the audit log. Worth adding a versioningStatus entry while we're here?

public Map<String, String> toAuditMap() {
Map<String, String> auditMap = new LinkedHashMap<>();
auditMap.put(OzoneConsts.VOLUME, this.volumeName);
auditMap.put(OzoneConsts.BUCKET, this.bucketName);
auditMap.put(OzoneConsts.GDPR_FLAG,
getMetadata().get(OzoneConsts.GDPR_FLAG));
auditMap.put(OzoneConsts.IS_VERSION_ENABLED,
String.valueOf(this.isVersionEnabled));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, PTAL.

// absent on records that predate versioning support (treated as the null
// version). A delete marker has isDeleteMarker set and no data blocks.
// isNullVersion marks the single overwritable "null version" slot per key.
private final Long versionId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also add these three fields to isKeyInfoSame() (and toString())? Right now two records differing only in versionId, or a delete marker vs. a regular key, compare as equal. Looks like the follow-up tasks will rely on OmKeyInfo equality in tests; having this in the foundation would prevent false-positive assertions later. OmBucketInfo already includes the new status in both, so this would keep the two helpers consistent.

public boolean isKeyInfoSame(OmKeyInfo omKeyInfo, boolean checkPath,
boolean checkKeyLocationVersions,
boolean checkModificationTime,
boolean checkUpdateID,
boolean checkOwnerName) {
boolean isEqual = dataSize == omKeyInfo.dataSize &&
creationTime == omKeyInfo.creationTime &&
volumeName.equals(omKeyInfo.volumeName) &&
bucketName.equals(omKeyInfo.bucketName) &&
replicationConfig.equals(omKeyInfo.replicationConfig) &&
Objects.equals(getMetadata(), omKeyInfo.getMetadata()) &&
Objects.equals(acls, omKeyInfo.acls) &&
Objects.equals(getTags(), omKeyInfo.getTags()) &&
getObjectID() == omKeyInfo.getObjectID();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, Updated, PTAL.

@jojochuang

Copy link
Copy Markdown
Contributor

@yandrey321

@chungen0126

Copy link
Copy Markdown
Contributor

Thanks @symious for working on this! However, based on a recent community discussion, we’ve decided to deprecate Google Docs in favor of maintaining Markdown files via PRs (see #8284 ). Could you please convert the Google Doc into a Markdown file and submit it as a PR instead? Thanks!

@symious

symious commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@chungen0126 OEP file here: #10822

@symious
symious force-pushed the HDDS-15879 branch 2 times, most recently from 09d92b3 to e89e8be Compare July 31, 2026 02:28
@symious symious changed the title HDDS-15879. Ozone Versioning Metadata foundation HDDS-15879. [Ozone Versioning] [T1] Metadata foundation Jul 31, 2026
processingPhase = RequestProcessingPhase.PRE_PROCESS,
requestType = Type.SetBucketProperty
)
public static OMRequest disallowSetBucketPropertyWithVersioningStatus(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gate covers SetBucketProperty, but CreateBucket carries the same BucketInfo.versioningStatus and persists it (OmBucketInfo.getFromProtobuf) with no matching OBJECT_VERSIONING validator — so a pre-finalized cluster would accept a CreateBucket that sets it, bypassing this contract (and the create path runs no canTransitionTo). Not triggerable yet since no client sends the field. Worth mirroring this validator on Type.CreateBucket, or a TODO + follow-up Jira.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Following S3 here: CreateBucket has no versioning parameter, versioning is only set on an existing bucket via PutBucketVersioning. So instead of the layout gate, T1.2 now rejects any CreateBucket carrying versioningStatus outright with INVALID_REQUEST. That covers post-finalization too, so I dropped the mirrored validator from T1.5.

@symious
symious force-pushed the HDDS-15879 branch 3 times, most recently from 1128ce3 to e79471c Compare August 24, 2026 07:35
symious and others added 6 commits August 28, 2026 11:07
S3 gives a bucket three versioning states, while Ozone has a single
isVersionEnabled boolean. This adds the three-state status alongside the
flag rather than in place of it, so existing buckets and older clients go
on working unchanged.

BucketVersioningStatus holds the three states and the state machine that
governs them: UNVERSIONED may move anywhere, but once versioning has been
enabled or suspended a bucket can never return to UNVERSIONED. The proto
gains a matching enum and an optional versioningStatus on both BucketInfo
and BucketArgs.

OmBucketInfo keeps the two representations in sync in both directions: a
status derives the flag (ENABLED -> true), and a record carrying only the
flag derives a status, so a bucket written before this change still
answers getVersioningStatus(). Disabling the flag is the asymmetric case -
it leaves an explicitly SUSPENDED status alone, since the state machine
has no way back to UNVERSIONED.

Nothing enforces the state machine yet; this commit only defines it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A bucket's versioning status may only change along the state machine the
previous commit defined. OMBucketSetPropertyRequest now checks the
requested status against the one the bucket already holds and rejects
with INVALID_REQUEST what the state machine forbids - the return to
UNVERSIONED once versioning has been enabled or suspended.

A request carrying only the legacy flag is mapped onto the same machine
before that check: enabling always means ENABLED, while disabling means
SUSPENDED, except on a bucket that is still UNVERSIONED, where it stays
UNVERSIONED.

The status is refused outright at bucket creation. S3 has no way to create
a bucket already in a versioning state: CreateBucket carries no such
parameter, and the state is set afterwards through PutBucketVersioning.
versioningStatus sits on BucketInfo because that message is the bucket's
on-disk record and the shape InfoBucket and ListBuckets return, not
because CreateBucket needs it; honouring it there would let a caller land
on any status in one step, with none of the above applied. Nothing
populates the field on a create today, so the request is rejected rather
than quietly ignored - ignoring it would leave a future caller believing
it had created a versioned bucket.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each section below was a separate commit; they are folded
here so the task's review fixes land as one change.

* Address comments

* Use a 0x00 separator in versionedKeyTable dbKeys

  Key names in OBJECT_STORE buckets contain '/' verbatim, so a '/' separator
  interleaves a key's versions with those of keys nested under it, breaking
  the single-seek promotion and the merged ListObjectVersions order.

* Do not derive a versioning status from the legacy flag

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants