You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@ozone.apache.org by "peterxcli (via GitHub)" <gi...@apache.org> on 2026/08/28 09:09:43 UTC

[PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

peterxcli opened a new pull request, #11140:
URL: https://github.com/apache/ozone/pull/11140

   ## What changes were proposed in this pull request?
   
   S3 `CopyObject` in Ozone streams every byte of the source key from the datanodes
   through the S3 Gateway and writes it back as a new key, so copy latency is
   proportional to object size. There is no server side copy RPC in OM, which is
   why `ozone sh key cp` is a client side stream copy for the same reason.
   
   This adds an OM `CopyKey` request that creates the destination key from the
   source key's committed block list, so a copy moves no data at all.
   
   ### Why this is not just a new request
   
   Committed blocks are owned by exactly one key and there is no reference
   counting in OM or SCM. Deleting or overwriting either key would move the shared
   blocks to `deletedTable`, `KeyDeletingService` would hand them to SCM, and the
   surviving key would silently stop being readable, because a datanode asked to
   delete an already deleted block succeeds quietly. The one existing protection,
   `OMKeyRequest.filterOutBlocksStillInUse`, only compares against the new version
   of the *same* key.
   
   ### Approach
   
   * an optional `sharedBlockGroupId` on `KeyInfo` (field 23). Zero, the default
     for every existing key, means the key owns its blocks exclusively, so no
     database needs backfilling. A non-zero value is the `objectID` of the lineage
     root, and copies of copies join the group their source already belongs to.
   * a `sharedBlockGroupTable` column family holding **one sharer count per copy
     lineage, not one row per block**: copying a key with ten thousand blocks costs
     a single row.
   * `KeyDeletingService` consults that count at the single point where
     `deletedTable` entries become SCM block deletions. While a sharer outside the
     batch is alive, the blocks are withheld from the SCM call and the entry gets a
     synthetic success, exactly as empty keys already do, so the metadata is purged
     and the blocks are not. When the whole group dies together, one member
     releases the blocks and the rest are withheld so SCM is told once.
   * the count drop rides the same Ratis transaction as the purge, carried by the
     final `PurgeKeysRequest` batch on purpose: losing that batch after the rows
     were purged leaks blocks, which an audit can reclaim, whereas decrementing
     first and then failing to purge would let a retry decrement twice and release
     blocks a live key still uses.
   
   **No block release producer changes**, because the tag travels with `OmKeyInfo`
   into `deletedTable` through `OmUtils.prepareKeyForDelete`. Every failure
   direction degrades to a leak rather than to data loss, which is the main reason
   for preferring this over a per-block reference count that plain delete, bulk
   delete, commit overwrite, multipart complete, FSO directory purge and open key
   cleanup would each have to maintain correctly.
   
   A copy still falls back to reading and rewriting the data when the source is
   encrypted (block data is ciphertext under the source key's own DEK and IV), GDPR
   enforced (erasure destroys the key's own secret), or hsync active, when the
   replication config would change (the read path picks the EC or Ratis stream from
   the key's own config), and for `UploadPartCopy`, which cannot express a byte
   range as whole object sharing.
   
   Design doc: `hadoop-hdds/docs/content/design/server-side-copy-key.md`
   
   ### Scope, and what is deliberately missing
   
   This is a **proof of concept**, opened as a draft for design feedback rather
   than for merge. It covers `OBJECT_STORE` buckets, same bucket, no destination
   overwrite.
   
   Required before it could merge, and not done here:
   
   * **`OMLayoutFeature` gating.** This is the load-bearing guard: an older OM
     treats the tag as an unknown proto field and would reclaim shared blocks
     normally, which is silent data loss. Finalization is the only thing that can
     prevent a downgrade from doing that.
   * cross-bucket copy (needs ordered multi-bucket locking), destination overwrite,
     FSO buckets, `OzoneManagerVersion` negotiation, an `ozone repair om` audit that
     rebuilds counts by scanning for tags, Recon awareness, and S3 Gateway wiring
     with fallback.
   
   ### Relation to HDDS-569
   
   [HDDS-569](https://issues.apache.org/jira/browse/HDDS-569) proposed the same
   thing in 2018 and states the copy "in ozone world, this is just a metadata
   change", but its two attached patches define proto messages only, and the block
   reclaim problem above was never addressed. Its proto predates Ratis-replicated
   OM requests, FSO and snapshots, so nothing there is reusable today.
   
   ## What is the link to the Apache JIRA
   
   https://issues.apache.org/jira/browse/HDDS-16319
   
   ## How was this patch tested?
   
   * New integration test in the existing `TestKeyPurging` suite,
     `testCopiedKeyKeepsBlocksUntilLastSharerIsDeleted`: copies a key, asserts both
     keys name the same blocks with different `objectID`s and a shared group id
     with a sharer count of two, deletes the source, waits for the count row to
     disappear, and asserts the copy still reads back the original bytes before
     deleting it too. The existing test in that class still passes.
   * `BenchmarkCopyKey` compares both copy paths on a three-datanode
     `MiniOzoneCluster` and separately verifies that source and copy have identical
     MD5 digests. It is named `Benchmark` rather than `Test` so surefire does not
     pick it up in CI.
   
     | object size | read-and-rewrite copy | CopyKey | ratio |
     |-------------|----------------------|---------|-------|
     | 1 MiB       | 117 ms               | 7.5 ms  | 16x   |
     | 16 MiB      | 889 ms               | 8.2 ms  | 108x  |
     | 64 MiB      | 2 983 ms             | 6.7 ms  | 446x  |
     | 256 MiB     | 11 100 ms            | 9.2 ms  | 1204x |
   
     The shape is the result rather than the ratios: `CopyKey` shows no trend
     across a 256-fold size range because it is one OM Ratis transaction whatever
     the key holds. The ratios overstate a real cluster, where the byte copy beats
     the 18 to 23 MiB/s a single host manages; projecting from a realistic
     100 MiB/s gives roughly 320x at 256 MiB and 1280x at 1 GiB.
   * `TestOmMetadataManager`, `TestBucketLayoutAwareOMKeyFactory`,
     `TestOMKeyPurgeRequestAndResponse` and `TestSnapshotDiffValueParser` pass with
     the updated expectations.
   * `checkstyle.sh` clean; `rat.sh` reports nothing for the new files.
   
   Generated-by: Claude Code (claude-fable-5)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "peterxcli (via GitHub)" <gi...@apache.org>.
peterxcli commented on code in PR #11140:
URL: https://github.com/apache/ozone/pull/11140#discussion_r3879638991


##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyPurgeRequest.java:
##########
@@ -160,13 +171,49 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
         AUDIT.logWriteSuccess(ozoneManager.buildAuditMessageForSuccess(OMSystemAction.KEY_DELETION, auditParams));
       }
       return new OMKeyPurgeResponse(omResponse.build(), keysToBePurgedList, renamedKeysToBePurged, fromSnapshotInfo,
-          keysToUpdateList, bucketInfoList);
+          keysToUpdateList, bucketInfoList, updatedSharerCounts);
     } catch (IOException e) {
       AUDIT.logWriteFailure(ozoneManager.buildAuditMessageForFailure(OMSystemAction.KEY_DELETION, null, e));
       return new OMKeyPurgeResponse(createErrorOMResponse(omResponse, e));
     }
   }
 
+  /**
+   * Drops the sharer count of each block group whose sharers were reclaimed in
+   * this batch, and returns the new counts for the response to persist. A count
+   * that falls to one is removed instead: the single key left owns the blocks
+   * again, so its own deletion releases them through the ordinary path.
+   *
+   * @return new count per block group id, where a value of one or less means
+   * the row should be deleted.
+   */
+  private Map<Long, Long> applySharedBlockGroupDecrements(
+      List<SharedBlockGroupDecrement> decrements, OMMetadataManager omMetadataManager, long trxnLogIndex)
+      throws IOException {
+    if (decrements.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    Map<Long, Long> updatedCounts = new HashMap<>();
+    for (SharedBlockGroupDecrement decrement : decrements) {
+      long groupId = decrement.getSharedBlockGroupId();
+      Long currentCount = omMetadataManager.getSharedBlockGroupTable().get(groupId);
+      if (currentCount == null) {
+        // Already removed by an earlier batch; nothing left to count down.
+        continue;
+      }
+      long newCount = currentCount - decrement.getSharerCount();
+      updatedCounts.put(groupId, newCount);
+      if (newCount > 1) {
+        omMetadataManager.getSharedBlockGroupTable().addCacheEntry(
+            new CacheKey<>(groupId), CacheValue.get(trxnLogIndex, newCount));
+      } else {
+        omMetadataManager.getSharedBlockGroupTable().addCacheEntry(
+            new CacheKey<>(groupId), CacheValue.get(trxnLogIndex));
+      }

Review Comment:
   Good catch, fixed in b2cc9a40c0.
   
   This was a real hole in the invariant the rest of the design is built on. A negative `newCount` fell into the `<= 1` branch and deleted the row, which drops the protection for a group that may still have live sharers, so the next deletion would release blocks another key is using. That is data loss, and it is precisely the direction this design is supposed to make impossible.
   
   The guard now skips the decrement and logs an error when `newCount` would go below zero, keeping the row. A stale row costs a leak that an audit can reclaim, which is the failure direction we want.
   
   One clarification on the boundary: `newCount == 0` is legitimate and still deletes the row. When every sharer of a group is reclaimed in the same batch, `planSharedBlockGroups` withholds all but one member and emits a decrement equal to the number of members, so the count lands exactly on zero while that one member releases the blocks to SCM. Only a strictly negative result means the count and the tagged keys disagree.



##########
hadoop-hdds/docs/content/design/ratis-backed-streaming-readblock.md:
##########
@@ -0,0 +1,894 @@
+---
+title: Ratis-Backed Streaming ReadBlock
+summary: Design for group-aware and closed-replica ReadBlock over Ratis DataStream
+date: 2026-07-11
+status: draft
+author: Lixucheng
+---

Review Comment:
   You are right, and both files are removed in b2cc9a40c0.
   
   They were unrelated work in progress that lived untracked in the working tree and got swept into the first commit by an overly broad `git add`. Nothing in this PR referenced them. The only design doc the change set adds now is `server-side-copy-key.md`, which documents this feature.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "peterxcli (via GitHub)" <gi...@apache.org>.
peterxcli commented on code in PR #11140:
URL: https://github.com/apache/ozone/pull/11140#discussion_r3879637654


##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCopyRequest.java:
##########
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.request.key;
+
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_ALREADY_EXISTS;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_NOT_FOUND;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.BUCKET_LOCK;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.OMAction;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.KeyValueUtil;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfoGroup;
+import org.apache.hadoop.ozone.om.request.OMClientRequestUtils;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.key.OMKeyCopyResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CopyKeyRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CopyKeyResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
+import org.apache.hadoop.util.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles a server side key copy: the destination key is created as an
+ * independent key that reuses the source key's committed block locations, so no
+ * data is read or written. Both keys are tagged with a shared block group id
+ * and the group's sharer count is tracked in the sharedBlockGroupTable, which
+ * {@link org.apache.hadoop.ozone.om.service.KeyDeletingService} consults so the
+ * blocks are only released once the last sharer is reclaimed.
+ *
+ * <p>This is the proof-of-concept scope. A copy is rejected, and the caller is
+ * expected to fall back to reading and rewriting the data, when it would need
+ * to cross a bucket, overwrite an existing key, or touch encrypted, GDPR or
+ * hsync-active keys.
+ */
+public class OMKeyCopyRequest extends OMKeyRequest {
+
+  private static final Logger LOG =
+      LoggerFactory.getLogger(OMKeyCopyRequest.class);
+
+  public OMKeyCopyRequest(OMRequest omRequest, BucketLayout bucketLayout) {
+    super(omRequest, bucketLayout);
+  }
+
+  @Override
+  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
+    CopyKeyRequest copyKeyRequest =
+        super.preExecute(ozoneManager).getCopyKeyRequest();
+    Objects.requireNonNull(copyKeyRequest, "copyKeyRequest == null");
+
+    KeyArgs sourceKeyArgs = copyKeyRequest.getSourceKeyArgs();
+    KeyArgs destinationKeyArgs = copyKeyRequest.getDestinationKeyArgs();
+
+    if (!sourceKeyArgs.getVolumeName().equals(destinationKeyArgs.getVolumeName())
+        || !sourceKeyArgs.getBucketName().equals(destinationKeyArgs.getBucketName())) {
+      throw new OMException("Server side copy across buckets is not supported yet",
+          NOT_SUPPORTED_OPERATION);
+    }
+    if (sourceKeyArgs.getKeyName().equals(destinationKeyArgs.getKeyName())) {
+      throw new OMException("Server side copy onto the source key is not supported",
+          NOT_SUPPORTED_OPERATION);
+    }
+
+    KeyArgs.Builder normalizedSource = sourceKeyArgs.toBuilder()
+        .setKeyName(validateAndNormalizeKey(ozoneManager.getEnableFileSystemPaths(),
+            sourceKeyArgs.getKeyName(), getBucketLayout()));
+    KeyArgs.Builder normalizedDestination = destinationKeyArgs.toBuilder()
+        .setKeyName(validateAndNormalizeKey(ozoneManager.getEnableFileSystemPaths(),
+            destinationKeyArgs.getKeyName(), getBucketLayout()))
+        .setModificationTime(Time.now());
+
+    KeyArgs resolvedSource = resolveBucketAndCheckKeyAcls(normalizedSource.build(),
+        ozoneManager, ACLType.READ);
+    KeyArgs resolvedDestination = resolveBucketAndCheckKeyAcls(normalizedDestination.build(),
+        ozoneManager, ACLType.CREATE);
+
+    return getOmRequest().toBuilder()
+        .setUserInfo(getUserInfo())
+        .setCopyKeyRequest(copyKeyRequest.toBuilder()
+            .setSourceKeyArgs(resolvedSource)
+            .setDestinationKeyArgs(resolvedDestination))
+        .build();
+  }
+
+  @Override
+  @SuppressWarnings("checkstyle:methodlength")
+  public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, ExecutionContext context) {
+    final long trxnLogIndex = context.getIndex();

Review Comment:
   Agreed, and fixed in b2cc9a40c0.
   
   Added a `SERVER_SIDE_COPY` layout feature and gated `OMKeyCopyRequest.preExecute` with `@DisallowedUntilLayoutVersion(SERVER_SIDE_COPY)`. Since a copy is the only thing that can create a shared block, gating the copy means no shared block can exist until every OM in the cluster understands the tag, which closes the mixed-version window.
   
   On the second half of the suggestion, disabling shared-block behaviour in the deletion and purge paths until finalization: I would rather not. Those checks have to be unconditional. Pre-finalization they are inert anyway because no tagged key exists, and post-finalization they are exactly what keeps a shared block alive while another key still references it, so making them conditional would reintroduce the failure this change exists to prevent. Downgrade past a finalized layout version is already disallowed.
   
   Worth noting for anyone reading the diff: the client side still has no `OzoneManagerVersion` negotiation, so against an OM that has not finalized, a copy fails rather than transparently falling back to a read-and-rewrite copy. That is listed in the PR description as outstanding.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #11140:
URL: https://github.com/apache/ozone/pull/11140#issuecomment-5737671039

   This PR has been marked as stale due to 21 days of inactivity. Please comment or remove the stale label to keep it open. Otherwise, it will be automatically closed in 7 days.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #11140:
URL: https://github.com/apache/ozone/pull/11140#issuecomment-6181252187

   This PR has been marked as stale due to 21 days of inactivity. Please comment or remove the stale label to keep it open. Otherwise, it will be automatically closed in 7 days.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #11140:
URL: https://github.com/apache/ozone/pull/11140#discussion_r3879494238


##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCopyRequest.java:
##########
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.request.key;
+
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_ALREADY_EXISTS;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_NOT_FOUND;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.BUCKET_LOCK;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.OMAction;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.KeyValueUtil;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfoGroup;
+import org.apache.hadoop.ozone.om.request.OMClientRequestUtils;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.key.OMKeyCopyResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CopyKeyRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CopyKeyResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
+import org.apache.hadoop.util.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles a server side key copy: the destination key is created as an
+ * independent key that reuses the source key's committed block locations, so no
+ * data is read or written. Both keys are tagged with a shared block group id
+ * and the group's sharer count is tracked in the sharedBlockGroupTable, which
+ * {@link org.apache.hadoop.ozone.om.service.KeyDeletingService} consults so the
+ * blocks are only released once the last sharer is reclaimed.
+ *
+ * <p>This is the proof-of-concept scope. A copy is rejected, and the caller is
+ * expected to fall back to reading and rewriting the data, when it would need
+ * to cross a bucket, overwrite an existing key, or touch encrypted, GDPR or
+ * hsync-active keys.
+ */
+public class OMKeyCopyRequest extends OMKeyRequest {
+
+  private static final Logger LOG =
+      LoggerFactory.getLogger(OMKeyCopyRequest.class);
+
+  public OMKeyCopyRequest(OMRequest omRequest, BucketLayout bucketLayout) {
+    super(omRequest, bucketLayout);
+  }
+
+  @Override
+  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
+    CopyKeyRequest copyKeyRequest =
+        super.preExecute(ozoneManager).getCopyKeyRequest();
+    Objects.requireNonNull(copyKeyRequest, "copyKeyRequest == null");
+
+    KeyArgs sourceKeyArgs = copyKeyRequest.getSourceKeyArgs();
+    KeyArgs destinationKeyArgs = copyKeyRequest.getDestinationKeyArgs();
+
+    if (!sourceKeyArgs.getVolumeName().equals(destinationKeyArgs.getVolumeName())
+        || !sourceKeyArgs.getBucketName().equals(destinationKeyArgs.getBucketName())) {
+      throw new OMException("Server side copy across buckets is not supported yet",
+          NOT_SUPPORTED_OPERATION);
+    }
+    if (sourceKeyArgs.getKeyName().equals(destinationKeyArgs.getKeyName())) {
+      throw new OMException("Server side copy onto the source key is not supported",
+          NOT_SUPPORTED_OPERATION);
+    }
+
+    KeyArgs.Builder normalizedSource = sourceKeyArgs.toBuilder()
+        .setKeyName(validateAndNormalizeKey(ozoneManager.getEnableFileSystemPaths(),
+            sourceKeyArgs.getKeyName(), getBucketLayout()));
+    KeyArgs.Builder normalizedDestination = destinationKeyArgs.toBuilder()
+        .setKeyName(validateAndNormalizeKey(ozoneManager.getEnableFileSystemPaths(),
+            destinationKeyArgs.getKeyName(), getBucketLayout()))
+        .setModificationTime(Time.now());
+
+    KeyArgs resolvedSource = resolveBucketAndCheckKeyAcls(normalizedSource.build(),
+        ozoneManager, ACLType.READ);
+    KeyArgs resolvedDestination = resolveBucketAndCheckKeyAcls(normalizedDestination.build(),
+        ozoneManager, ACLType.CREATE);
+
+    return getOmRequest().toBuilder()
+        .setUserInfo(getUserInfo())
+        .setCopyKeyRequest(copyKeyRequest.toBuilder()
+            .setSourceKeyArgs(resolvedSource)
+            .setDestinationKeyArgs(resolvedDestination))
+        .build();
+  }
+
+  @Override
+  @SuppressWarnings("checkstyle:methodlength")
+  public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, ExecutionContext context) {
+    final long trxnLogIndex = context.getIndex();

Review Comment:
   This code introduces a new on-disk schema element (`sharedBlockGroupId` in `KeyInfo` + `sharedBlockGroupTable`) and a new request type (`CopyKey`), but the implementation shown here does not gate execution on an `OMLayoutFeature` / finalization check. That gap is dangerous in mixed-version or downgrade scenarios: an older OM will ignore the new proto field and can reclaim shared blocks as if they were exclusively owned, causing silent data loss. To make this safe for anything beyond a draft, add an explicit layout-feature gate at request execution time (and ideally also in key deletion/purge paths) that rejects `CopyKey` and/or disables shared-block behavior until the layout is finalized.



##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyPurgeRequest.java:
##########
@@ -160,13 +171,49 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
         AUDIT.logWriteSuccess(ozoneManager.buildAuditMessageForSuccess(OMSystemAction.KEY_DELETION, auditParams));
       }
       return new OMKeyPurgeResponse(omResponse.build(), keysToBePurgedList, renamedKeysToBePurged, fromSnapshotInfo,
-          keysToUpdateList, bucketInfoList);
+          keysToUpdateList, bucketInfoList, updatedSharerCounts);
     } catch (IOException e) {
       AUDIT.logWriteFailure(ozoneManager.buildAuditMessageForFailure(OMSystemAction.KEY_DELETION, null, e));
       return new OMKeyPurgeResponse(createErrorOMResponse(omResponse, e));
     }
   }
 
+  /**
+   * Drops the sharer count of each block group whose sharers were reclaimed in
+   * this batch, and returns the new counts for the response to persist. A count
+   * that falls to one is removed instead: the single key left owns the blocks
+   * again, so its own deletion releases them through the ordinary path.
+   *
+   * @return new count per block group id, where a value of one or less means
+   * the row should be deleted.
+   */
+  private Map<Long, Long> applySharedBlockGroupDecrements(
+      List<SharedBlockGroupDecrement> decrements, OMMetadataManager omMetadataManager, long trxnLogIndex)
+      throws IOException {
+    if (decrements.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    Map<Long, Long> updatedCounts = new HashMap<>();
+    for (SharedBlockGroupDecrement decrement : decrements) {
+      long groupId = decrement.getSharedBlockGroupId();
+      Long currentCount = omMetadataManager.getSharedBlockGroupTable().get(groupId);
+      if (currentCount == null) {
+        // Already removed by an earlier batch; nothing left to count down.
+        continue;
+      }
+      long newCount = currentCount - decrement.getSharerCount();
+      updatedCounts.put(groupId, newCount);
+      if (newCount > 1) {
+        omMetadataManager.getSharedBlockGroupTable().addCacheEntry(
+            new CacheKey<>(groupId), CacheValue.get(trxnLogIndex, newCount));
+      } else {
+        omMetadataManager.getSharedBlockGroupTable().addCacheEntry(
+            new CacheKey<>(groupId), CacheValue.get(trxnLogIndex));
+      }

Review Comment:
   If `decrement.getSharerCount()` exceeds `currentCount`, `newCount` becomes negative. The subsequent logic treats `newCount <= 1` as a signal to delete the row, which can incorrectly remove the protection for a still-shared group (risking premature block release later). To keep failure modes biased toward leaks rather than data loss, add an explicit guard: if `decrement.getSharerCount() > currentCount` (or `newCount < 0`), log a warning/error and skip applying that decrement (or clamp in a way that preserves the row).



##########
hadoop-hdds/docs/content/design/ratis-backed-streaming-readblock.md:
##########
@@ -0,0 +1,894 @@
+---
+title: Ratis-Backed Streaming ReadBlock
+summary: Design for group-aware and closed-replica ReadBlock over Ratis DataStream
+date: 2026-07-11
+status: draft
+author: Lixucheng
+---

Review Comment:
   The PR title/description focus on server-side `CopyKey` with shared block groups, but this PR also adds additional (unrelated) design docs (e.g., Ratis-backed `ReadBlock`, deterministic IO fault injection POC). This makes the change set harder to review and reason about. Consider moving unrelated design-doc additions into separate PR(s) so the `CopyKey` implementation and its design doc can be reviewed independently.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #11140:
URL: https://github.com/apache/ozone/pull/11140#issuecomment-6354108827

   Thank you for your contribution. This PR is being closed due to inactivity. Please contact a maintainer if you would like to reopen it.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org


Re: [PR] HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data [ozone]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] closed pull request #11140: HDDS-16319. Support server side key copy that shares blocks instead of rewriting the data
URL: https://github.com/apache/ozone/pull/11140


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org