You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by we...@apache.org on 2026/10/19 22:24:37 UTC
(ozone) branch master updated: HDDS-16948. Remove the per-call InfoBucket from rooted-ofs open (#11613)
This is an automated email from the ASF dual-hosted git repository.
jojochuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new bb296c9daa4 HDDS-16948. Remove the per-call InfoBucket from rooted-ofs open (#11613)
bb296c9daa4 is described below
commit bb296c9daa47f627c51d9899c8e29acfc7272412
Author: Andrey Yarovoy <11...@users.noreply.github.com>
AuthorDate: Mon Oct 19 18:24:30 2026 -0400
HDDS-16948. Remove the per-call InfoBucket from rooted-ofs open (#11613)
---
.../apache/hadoop/ozone/OzoneManagerVersion.java | 5 +
.../ozone/client/protocol/ClientProtocol.java | 11 +
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 26 +-
.../apache/hadoop/ozone/om/helpers/OmKeyArgs.java | 4 +-
...OzoneManagerProtocolClientSideTranslatorPB.java | 3 +
.../ozone/AbstractRootedOzoneFileSystemTest.java | 39 +++
.../hadoop/fs/ozone/TestOfsOpenRpcBenchmark.java | 313 +++++++++++++++++++++
.../src/main/proto/OmClientProtocol.proto | 3 +
.../apache/hadoop/ozone/om/OmMetadataReader.java | 8 +-
.../protocolPB/OzoneManagerRequestHandler.java | 1 +
.../hadoop/ozone/om/TestOMMetadataReader.java | 74 +++++
.../protocolPB/TestOzoneManagerRequestHandler.java | 31 ++
.../ozone/BasicRootedOzoneClientAdapterImpl.java | 17 ++
.../TestBasicRootedOzoneClientAdapterReadFile.java | 164 +++++++++++
.../hadoop/ozone/client/ClientProtocolStub.java | 6 +
15 files changed, 699 insertions(+), 6 deletions(-)
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
index 7c74864d027..5507a237f9a 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
@@ -80,6 +80,11 @@ public enum OzoneManagerVersion implements ComponentVersion {
+ "server-side when the request sets requireFileSystemLayout, so file "
+ "system clients no longer need the client-side InfoBucket layout "
+ "check for listStatus"),
+ GET_KEY_INFO_REJECTS_OBS(102,
+ "OzoneManager version that rejects getKeyInfo on OBJECT_STORE buckets "
+ + "server-side when the request sets requireFileSystemLayout, so file "
+ + "system clients no longer need the client-side InfoBucket layout "
+ + "check for open"),
// Only operations included in the first release with version 102 may share it.
// S3 volume context support added later needs a new version.
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index f2b89b4d74f..58fa263e35c 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -1163,6 +1163,17 @@ void createDirectory(String volumeName, String bucketName, String keyName)
OzoneInputStream readFile(String volumeName, String bucketName,
String keyName) throws IOException;
+ /**
+ * Creates an input stream for reading file contents, like
+ * {@link #readFile(String, String, String)}.
+ *
+ * @param requireFileSystemLayout if true, OM rejects the read with
+ * NOT_SUPPORTED_OPERATION if the resolved bucket layout is OBJECT_STORE.
+ * An OM older than OzoneManagerVersion.GET_KEY_INFO_REJECTS_OBS ignores it.
+ */
+ OzoneInputStream readFile(String volumeName, String bucketName,
+ String keyName, boolean requireFileSystemLayout) throws IOException;
+
/**
* Creates an output stream for writing to a file.
*
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 71650110650..22b74226072 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -2051,6 +2051,11 @@ private OmKeyInfo getS3PartKeyInfo(
public OmKeyInfo getKeyInfo(
String volumeName, String bucketName, String keyName,
boolean forceUpdateContainerCache) throws IOException {
+ return getKeyInfo(volumeName, bucketName, keyName, forceUpdateContainerCache, false);
+ }
+
+ private OmKeyInfo getKeyInfo(String volumeName, String bucketName, String keyName,
+ boolean forceUpdateContainerCache, boolean requireFileSystemLayout) throws IOException {
Objects.requireNonNull(volumeName, "volumeName == null");
Objects.requireNonNull(bucketName, "bucketName == null");
Objects.requireNonNull(keyName, "keyName == null");
@@ -2061,6 +2066,7 @@ public OmKeyInfo getKeyInfo(
.setSortDatanodesInPipeline(topologyAwareReadEnabled)
.setLatestVersionLocation(getLatestVersionLocation)
.setForceUpdateContainerCacheFromSCM(forceUpdateContainerCache)
+ .setRequireFileSystemLayout(requireFileSystemLayout)
.build();
return getKeyInfo(keyArgs);
}
@@ -2464,12 +2470,19 @@ public void createDirectory(String volumeName, String bucketName,
@Override
public OzoneInputStream readFile(String volumeName, String bucketName,
String keyName) throws IOException {
+ return readFile(volumeName, bucketName, keyName, false);
+ }
+
+ @Override
+ public OzoneInputStream readFile(String volumeName, String bucketName,
+ String keyName, boolean requireFileSystemLayout) throws IOException {
OmKeyArgs keyArgs = new OmKeyArgs.Builder()
.setVolumeName(volumeName)
.setBucketName(bucketName)
.setKeyName(keyName)
.setSortDatanodesInPipeline(topologyAwareReadEnabled)
.setLatestVersionLocation(getLatestVersionLocation)
+ .setRequireFileSystemLayout(requireFileSystemLayout)
.build();
final OmKeyInfo keyInfo;
if (OzoneManagerVersion.OPTIMIZED_GET_KEY_INFO.isSupportedBy(omVersion)) {
@@ -2482,7 +2495,7 @@ public OzoneInputStream readFile(String volumeName, String bucketName,
} else {
keyInfo = ozoneManagerClient.lookupFile(keyArgs);
}
- return getInputStreamWithRetryFunction(keyInfo);
+ return getInputStreamWithRetryFunction(keyInfo, requireFileSystemLayout);
}
@Override
@@ -2505,10 +2518,19 @@ public OzoneOutputStream createFile(String volumeName, String bucketName,
*/
private OzoneInputStream getInputStreamWithRetryFunction(
OmKeyInfo keyInfo) throws IOException {
+ return getInputStreamWithRetryFunction(keyInfo, false);
+ }
+
+ /**
+ * Like {@link #getInputStreamWithRetryFunction(OmKeyInfo)}; the retry lookup
+ * also asks OM to reject an OBJECT_STORE bucket if requireFileSystemLayout.
+ */
+ private OzoneInputStream getInputStreamWithRetryFunction(
+ OmKeyInfo keyInfo, boolean requireFileSystemLayout) throws IOException {
return createInputStream(keyInfo, omKeyInfo -> {
try {
return getKeyInfo(omKeyInfo.getVolumeName(), omKeyInfo.getBucketName(),
- omKeyInfo.getKeyName(), true);
+ omKeyInfo.getKeyName(), true, requireFileSystemLayout);
} catch (IOException e) {
LOG.error("Unable to lookup key {} on retry.", keyInfo.getKeyName(), e);
return null;
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
index 2280845cd3b..9a6ade36350 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
@@ -64,8 +64,8 @@ public final class OmKeyArgs extends WithMetadata implements Auditable {
// Original S3/list prefix when keyName is empty (root listing). Used for STS
// auth to check LIST on this prefix instead of "*".
private final String listPrefix;
- // Set by file system clients so that OM rejects a listing on an
- // OBJECT_STORE bucket.
+ // Set by file system clients so that OM rejects a listing or a key info
+ // read on an OBJECT_STORE bucket.
private final boolean requireFileSystemLayout;
private final String expectedETag;
private final boolean derivedKeyPiggyBacking;
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index e53d2dad73b..5d5e20d0a34 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -944,6 +944,9 @@ public KeyInfoWithVolumeContext getKeyInfo(OmKeyArgs args,
GetKeyInfoRequest.Builder req = GetKeyInfoRequest.newBuilder();
req.setKeyArgs(args.toProtobuf());
req.setAssumeS3Context(assumeS3Context);
+ if (args.isRequireFileSystemLayout()) {
+ req.setRequireFileSystemLayout(true);
+ }
OMRequest omRequest = createOMRequest(Type.GetKeyInfo)
.setGetKeyInfoRequest(req)
diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
index 99234920214..d1edd78db6a 100644
--- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
+++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
@@ -1990,6 +1990,45 @@ void testListStatusOrphanLinkBucketIsEmpty() throws Exception {
assertEquals(BUCKET_NOT_FOUND, exception.getResult());
}
+ @Test
+ void testOpenDoesNotTriggerInfoBucket() throws Exception {
+ Path file = new Path(bucketPath, "open-rpc-" + RandomStringUtils.secure().nextAlphanumeric(5));
+ ContractTestUtils.writeTextFile(fs, file, "data", true);
+
+ OMMetrics metrics = getOMMetrics();
+ long bucketInfosBefore = metrics.getNumBucketInfos();
+
+ try (FSDataInputStream stream = fs.open(file)) {
+ assertEquals('d', stream.read());
+ }
+
+ assertEquals(bucketInfosBefore, metrics.getNumBucketInfos(), "open must not trigger InfoBucket");
+ }
+
+ @Test
+ void testOpenRejectsObsBucket() throws Exception {
+ OzoneBucket obsBucket = DataTestUtil.createVolumeAndBucket(client, BucketLayout.OBJECT_STORE);
+ DataTestUtil.createKey(obsBucket, "obs-key", "data".getBytes(StandardCharsets.UTF_8));
+ Path obsKeyPath = new Path(new Path(new Path(OZONE_URI_DELIMITER, obsBucket.getVolumeName()),
+ obsBucket.getName()), "obs-key");
+
+ OMMetrics metrics = getOMMetrics();
+ long bucketInfosBefore = metrics.getNumBucketInfos();
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> fs.open(obsKeyPath));
+ assertThat(exception.getMessage()).contains(obsBucket.getName());
+ assertThat(exception.getMessage()).contains("OBJECT_STORE");
+ assertEquals(bucketInfosBefore, metrics.getNumBucketInfos(), "open must not trigger InfoBucket");
+ }
+
+ @Test
+ void testOpenInMissingBucketFails() throws Exception {
+ // The failure now comes from GetKeyInfo instead of InfoBucket, with the same result code.
+ Path missingFile = new Path(new Path(new Path(OZONE_URI_DELIMITER, volumeName), "missing-bucket"), "file");
+ OMException exception = assertThrows(OMException.class, () -> fs.open(missingFile));
+ assertEquals(BUCKET_NOT_FOUND, exception.getResult());
+ }
+
@Test
void testGetFileStatus() throws Exception {
String volumeNameLocal = getRandomNonExistVolumeName();
diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOfsOpenRpcBenchmark.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOfsOpenRpcBenchmark.java
new file mode 100644
index 00000000000..81fcb9a3176
--- /dev/null
+++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOfsOpenRpcBenchmark.java
@@ -0,0 +1,313 @@
+/*
+ * 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.fs.ozone;
+
+import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+import static org.apache.ozone.test.MetricsAsserts.getLongCounter;
+import static org.apache.ozone.test.MetricsAsserts.getMetrics;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.contract.ContractTestUtils;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.client.BucketArgs;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.apache.hadoop.ozone.om.OMMetrics;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Benchmark for the OFS open OM RPC count (HDDS-16948).
+ *
+ * <p>The same workload runs unchanged on the baseline ({@code master}: InfoBucket + GetKeyInfo per open) and on the
+ * fix (a single GetKeyInfo that asks OM to reject an OBJECT_STORE bucket). It follows
+ * {@link TestOfsListStatusRpcBenchmark}: a file in each of {@link #NUM_BUCKETS} buckets is opened
+ * {@link #ACCESSES_PER_BUCKET} times from a shuffled access sequence, single-threaded and with
+ * {@link #CONCURRENT_THREADS} client threads. Each call opens and closes the file without reading it, so datanode
+ * reads do not mask the OM saving.
+ */
+@Tag("benchmark")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestOfsOpenRpcBenchmark {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TestOfsOpenRpcBenchmark.class);
+
+ /** Distinct buckets in the working set. */
+ private static final int NUM_BUCKETS = 200;
+ /** open calls per bucket. */
+ private static final int ACCESSES_PER_BUCKET = 10;
+ private static final int TOTAL_CALLS = NUM_BUCKETS * ACCESSES_PER_BUCKET;
+ private static final long SHUFFLE_SEED = 20250831L;
+ /** Client threads issuing open in the concurrent comparison. */
+ private static final int CONCURRENT_THREADS = 10;
+ /** Size of each opened file. */
+ private static final int FILE_SIZE = 1024;
+
+ private MiniOzoneCluster cluster;
+ private OzoneClient client;
+ private OzoneConfiguration conf;
+ private String rootPath;
+
+ private final List<Path> accessSequence = new ArrayList<>(TOTAL_CALLS);
+
+ @BeforeAll
+ void init() throws IOException, InterruptedException, TimeoutException {
+ conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_DEFAULT_BUCKET_LAYOUT, BucketLayout.FILE_SYSTEM_OPTIMIZED.name());
+ cluster = MiniOzoneCluster.newBuilder(conf)
+ .setNumDatanodes(3)
+ .build();
+ cluster.waitForClusterToBeReady();
+ client = cluster.newClient();
+ rootPath = String.format("%s://%s/", OzoneConsts.OZONE_OFS_URI_SCHEME, conf.get(OZONE_OM_ADDRESS_KEY));
+
+ ObjectStore objectStore = client.getObjectStore();
+ String volName = "benchvol";
+ objectStore.createVolume(volName);
+ OzoneVolume volume = objectStore.getVolume(volName);
+ BucketArgs fsoArgs = BucketArgs.newBuilder().setBucketLayout(BucketLayout.FILE_SYSTEM_OPTIMIZED).build();
+ byte[] data = ContractTestUtils.dataset(FILE_SIZE, 'a', 26);
+ try (FileSystem setupFs = FileSystem.newInstance(URI.create(rootPath), conf)) {
+ for (int i = 0; i < NUM_BUCKETS; i++) {
+ String buckName = "benchbucket-" + i;
+ volume.createBucket(buckName, fsoArgs);
+ Path file = new Path("/" + volName + "/" + buckName + "/file");
+ ContractTestUtils.createFile(setupFs, file, true, data);
+ for (int a = 0; a < ACCESSES_PER_BUCKET; a++) {
+ accessSequence.add(file);
+ }
+ }
+ assertThat(setupFs.getFileStatus(accessSequence.get(0)).getLen()).isEqualTo(FILE_SIZE);
+ }
+ Collections.shuffle(accessSequence, new Random(SHUFFLE_SEED));
+ }
+
+ @AfterAll
+ void shutdown() {
+ IOUtils.closeQuietly(client);
+ if (cluster != null) {
+ cluster.shutdown();
+ }
+ }
+
+ @Test
+ void benchmarkOpen() throws Exception {
+ // Prime OM-side state and the JVM so the measured run pays no cold-start.
+ runWorkload(1);
+
+ Result single = runWorkload(1);
+ Result concurrent = runWorkload(CONCURRENT_THREADS);
+
+ assertWorkloadInvariants(single);
+ assertWorkloadInvariants(concurrent);
+
+ logResult("single-threaded", single);
+ logResult(CONCURRENT_THREADS + " concurrent threads", concurrent);
+ }
+
+ /**
+ * Invariants of the workload: every open issues one GetKeyInfo, and the bucket and volume RPCs stay within one per
+ * open.
+ */
+ private void assertWorkloadInvariants(Result r) {
+ assertThat(r.getKeyInfoRpcs).isEqualTo(TOTAL_CALLS);
+ assertThat(r.bucketInfoRpcs).isBetween(0L, (long) TOTAL_CALLS);
+ assertThat(r.volumeInfoRpcs).isBetween(0L, (long) TOTAL_CALLS);
+ }
+
+ private void logResult(String label, Result r) {
+ long omRpcs = r.volumeInfoRpcs + r.bucketInfoRpcs + r.getKeyInfoRpcs;
+ LOG.info(String.format("%n"
+ + "BENCHRESULT OFS open OM RPC benchmark (HDDS-16948) - %s%n"
+ + " buckets=%d, accesses/bucket=%d, total open=%d, threads=%d, file size=%d%n"
+ + " ------------------------------------------------------------%n"
+ + " InfoVolume RPCs : %d%n"
+ + " InfoBucket RPCs : %d%n"
+ + " GetKeyInfo RPCs : %d%n"
+ + " total OM RPCs : %d%n"
+ + " OM RPCs per call : %.2f%n"
+ + " ------------------------------------------------------------%n"
+ + " latency mean : %.3f ms%n"
+ + " latency p50 : %.3f ms%n"
+ + " latency p90 : %.3f ms%n"
+ + " latency p99 : %.3f ms%n"
+ + " latency max : %.3f ms%n"
+ + " throughput : %.1f open/s%n",
+ label, NUM_BUCKETS, ACCESSES_PER_BUCKET, TOTAL_CALLS, r.threads, FILE_SIZE,
+ r.volumeInfoRpcs, r.bucketInfoRpcs, r.getKeyInfoRpcs,
+ omRpcs, omRpcs / (double) TOTAL_CALLS,
+ millis(r.mean()), millis(r.percentile(0.50)),
+ millis(r.percentile(0.90)), millis(r.percentile(0.99)),
+ millis(r.max()), r.opsPerSecond()));
+ }
+
+ private static double millis(double nanos) {
+ return nanos / (double) TimeUnit.MILLISECONDS.toNanos(1);
+ }
+
+ /** OMMetrics has no getter for this counter, so read it from the metrics source, as on the baseline. */
+ private static long getKeyInfoCount() {
+ return getLongCounter("NumGetKeyInfo", getMetrics("OMMetrics"));
+ }
+
+ private Result runWorkload(int threads) throws IOException, InterruptedException {
+ OzoneConfiguration runConf = new OzoneConfiguration(conf);
+ runConf.set(FS_DEFAULT_NAME_KEY, rootPath);
+
+ OMMetrics metrics = cluster.getOzoneManager().getMetrics();
+ long[] latencyNanos = new long[TOTAL_CALLS];
+ // A new FileSystem instance per run, so each run starts with no client-side state carried over.
+ try (FileSystem fs = FileSystem.newInstance(URI.create(rootPath), runConf)) {
+ long volumeInfosBefore = metrics.getNumVolumeInfos();
+ long bucketInfosBefore = metrics.getNumBucketInfos();
+ long getKeyInfoBefore = getKeyInfoCount();
+ long elapsedNanos = threads == 1
+ ? runSequential(fs, latencyNanos)
+ : runConcurrent(fs, threads, latencyNanos);
+ return new Result(threads,
+ metrics.getNumVolumeInfos() - volumeInfosBefore,
+ metrics.getNumBucketInfos() - bucketInfosBefore,
+ getKeyInfoCount() - getKeyInfoBefore,
+ elapsedNanos, latencyNanos);
+ }
+ }
+
+ private static void openAndClose(FileSystem fs, Path path) throws IOException {
+ fs.open(path).close();
+ }
+
+ private long runSequential(FileSystem fs, long[] latencyNanos) throws IOException {
+ long startNanos = System.nanoTime();
+ int i = 0;
+ for (Path path : accessSequence) {
+ long callStart = System.nanoTime();
+ openAndClose(fs, path);
+ latencyNanos[i++] = System.nanoTime() - callStart;
+ }
+ return System.nanoTime() - startNanos;
+ }
+
+ private long runConcurrent(FileSystem fs, int threads, long[] latencyNanos)
+ throws IOException, InterruptedException {
+ ExecutorService pool = Executors.newFixedThreadPool(threads);
+ CountDownLatch startGate = new CountDownLatch(1);
+ CountDownLatch doneGate = new CountDownLatch(threads);
+ AtomicReference<IOException> failure = new AtomicReference<>();
+ // Disjoint, contiguous slices of the shuffled sequence; each thread writes only its own latency indices, so no
+ // synchronization is needed per call.
+ int chunk = (TOTAL_CALLS + threads - 1) / threads;
+ for (int t = 0; t < threads; t++) {
+ final int from = t * chunk;
+ final int to = Math.min(TOTAL_CALLS, from + chunk);
+ pool.submit(() -> {
+ try {
+ startGate.await();
+ for (int i = from; i < to; i++) {
+ long callStart = System.nanoTime();
+ openAndClose(fs, accessSequence.get(i));
+ latencyNanos[i] = System.nanoTime() - callStart;
+ }
+ } catch (IOException e) {
+ failure.compareAndSet(null, e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ doneGate.countDown();
+ }
+ });
+ }
+ long startNanos = System.nanoTime();
+ startGate.countDown();
+ doneGate.await();
+ long elapsedNanos = System.nanoTime() - startNanos;
+ pool.shutdownNow();
+ if (failure.get() != null) {
+ throw failure.get();
+ }
+ return elapsedNanos;
+ }
+
+ private static final class Result {
+ private final int threads;
+ private final long volumeInfoRpcs;
+ private final long bucketInfoRpcs;
+ private final long getKeyInfoRpcs;
+ private final long elapsedNanos;
+ private final long[] sortedLatencyNanos;
+
+ Result(int threads, long volumeInfoRpcs, long bucketInfoRpcs, long getKeyInfoRpcs, long elapsedNanos,
+ long[] latencyNanos) {
+ this.threads = threads;
+ this.volumeInfoRpcs = volumeInfoRpcs;
+ this.bucketInfoRpcs = bucketInfoRpcs;
+ this.getKeyInfoRpcs = getKeyInfoRpcs;
+ this.elapsedNanos = elapsedNanos;
+ this.sortedLatencyNanos = latencyNanos.clone();
+ Arrays.sort(this.sortedLatencyNanos);
+ }
+
+ double opsPerSecond() {
+ return TOTAL_CALLS / (elapsedNanos / (double) TimeUnit.SECONDS.toNanos(1));
+ }
+
+ double mean() {
+ long sum = 0;
+ for (long l : sortedLatencyNanos) {
+ sum += l;
+ }
+ return sum / (double) sortedLatencyNanos.length;
+ }
+
+ double percentile(double q) {
+ int idx = (int) Math.ceil(q * sortedLatencyNanos.length) - 1;
+ idx = Math.max(0, Math.min(sortedLatencyNanos.length - 1, idx));
+ return sortedLatencyNanos[idx];
+ }
+
+ double max() {
+ return sortedLatencyNanos[sortedLatencyNanos.length - 1];
+ }
+ }
+}
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index bff28097b4b..efdae1cbc4e 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -1430,6 +1430,9 @@ message LookupKeyResponse {
message GetKeyInfoRequest {
required KeyArgs keyArgs = 1;
optional bool assumeS3Context = 2;
+ // Set by file system clients: OM rejects the request with
+ // NOT_SUPPORTED_OPERATION if the resolved bucket layout is OBJECT_STORE.
+ optional bool requireFileSystemLayout = 3;
}
message GetKeyInfoResponse {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index 240591cd3b1..9c7f913bda0 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -197,6 +197,9 @@ public KeyInfoWithVolumeContext getKeyInfo(final OmKeyArgs args,
normalizeKeyArgs(bucket.update(resolvedVolumeArgs), bucket);
try {
+ if (resolvedArgs.isRequireFileSystemLayout()) {
+ validateFileSystemLayout(bucket);
+ }
if (isAclEnabled) {
captureLatencyNs(perfMetrics.getGetKeyInfoAclCheckLatencyNs(), () ->
checkAcls(ResourceType.KEY,
@@ -320,8 +323,9 @@ startKey, maxListingPageSize, getClientAddress(),
/**
* Rejects file system reads on a bucket whose link-resolved layout is
* OBJECT_STORE, so file system clients need no InfoBucket to check it.
- * listStatus and listStatusLight call it only when the client asked for it,
- * because they also serve object store callers (S3 shallow key listing).
+ * listStatus, listStatusLight and getKeyInfo call it only when the client
+ * asked for it, because they also serve object store callers (S3 shallow key
+ * listing, S3 GetObject and HeadObject, readKey).
*/
private static void validateFileSystemLayout(ResolvedBucket bucket) throws OMException {
if (bucket.bucketLayout() != null) {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 495e5dd9aed..5d6be583ab2 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -727,6 +727,7 @@ private GetKeyInfoResponse getKeyInfo(GetKeyInfoRequest request,
.setForceUpdateContainerCacheFromSCM(
keyArgs.getForceUpdateContainerCacheFromSCM())
.setMultipartUploadPartNumber(keyArgs.getMultipartNumber())
+ .setRequireFileSystemLayout(request.getRequireFileSystemLayout())
.build();
KeyInfoWithVolumeContext keyInfo = impl.getKeyInfo(omKeyArgs,
request.getAssumeS3Context());
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
index 4343480376f..37f8c79a910 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
@@ -24,6 +24,7 @@
import static org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.VOLUME;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -56,7 +57,9 @@
import org.apache.hadoop.ozone.om.helpers.ListKeysResult;
import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
+import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
@@ -68,6 +71,9 @@
import org.apache.hadoop.security.UserGroupInformation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.slf4j.Logger;
@@ -971,4 +977,72 @@ public void listStatusServesObjectStoreLayoutWhenNotRequested() throws Exception
verify(keyManager).listStatus(any(OmKeyArgs.class), eq(false), eq(""), anyLong(), any(), eq(false));
verify(keyManager).listStatus(any(OmKeyArgs.class), eq(false), eq(""), anyLong(), any(), eq(false), eq(false));
}
+
+ private static OmMetadataReader newGetKeyInfoReader(KeyManager keyManager, String bucketName,
+ BucketLayout layout) throws IOException {
+ OzoneManager ozoneManager = mock(OzoneManager.class);
+ when(ozoneManager.getAclsEnabled()).thenReturn(false);
+ when(ozoneManager.getConfiguration()).thenReturn(new OzoneConfiguration());
+ when(ozoneManager.getBucketManager()).thenReturn(mock(BucketManager.class));
+ when(ozoneManager.getVolumeManager()).thenReturn(mock(VolumeManager.class));
+ OMPerformanceMetrics perfMetrics = mock(OMPerformanceMetrics.class);
+ when(perfMetrics.getGetKeyInfoResolveBucketLatencyNs()).thenReturn(mock(ConcurrentMutableRate.class));
+ when(ozoneManager.getPerfMetrics()).thenReturn(perfMetrics);
+ when(ozoneManager.resolveBucketLink(any(OmKeyArgs.class)))
+ .thenReturn(new ResolvedBucket("vol", bucketName, "vol", bucketName, "owner", layout));
+ when(ozoneManager.getS3VolumeContext(true)).thenReturn(new S3VolumeContext(
+ OmVolumeArgs.newBuilder().setVolume("vol").setAdminName("admin").setOwnerName("owner").build(), "user"));
+ when(keyManager.getKeyInfo(any(OmKeyArgs.class), any(ResolvedBucket.class), any()))
+ .thenReturn(mock(OmKeyInfo.class));
+ return new OmMetadataReader(keyManager, mock(PrefixManager.class), ozoneManager, mock(org.slf4j.Logger.class),
+ mock(AuditLogger.class), mock(OmMetadataReaderMetrics.class), null);
+ }
+
+ private static OmKeyArgs getKeyInfoArgs(String bucketName, boolean requireFileSystemLayout) {
+ return new OmKeyArgs.Builder()
+ .setVolumeName("vol")
+ .setBucketName(bucketName)
+ .setKeyName("file")
+ .setRequireFileSystemLayout(requireFileSystemLayout)
+ .build();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void getKeyInfoRejectsObjectStoreLayout(boolean assumeS3Context) throws Exception {
+ KeyManager keyManager = mock(KeyManager.class);
+ OmMetadataReader reader = newGetKeyInfoReader(keyManager, "obs-bucket", BucketLayout.OBJECT_STORE);
+
+ OMException exception = assertThrows(OMException.class,
+ () -> reader.getKeyInfo(getKeyInfoArgs("obs-bucket", true), assumeS3Context));
+ assertEquals(ResultCodes.NOT_SUPPORTED_OPERATION, exception.getResult());
+ assertTrue(exception.getMessage().contains("obs-bucket"));
+ assertTrue(exception.getMessage().contains("OBJECT_STORE"));
+
+ verify(keyManager, never()).getKeyInfo(any(), any(), any());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = BucketLayout.class, names = {"FILE_SYSTEM_OPTIMIZED", "LEGACY"})
+ public void getKeyInfoAllowsFileSystemLayouts(BucketLayout layout) throws Exception {
+ KeyManager keyManager = mock(KeyManager.class);
+ OmMetadataReader reader = newGetKeyInfoReader(keyManager, "fs-bucket", layout);
+
+ assertNotNull(reader.getKeyInfo(getKeyInfoArgs("fs-bucket", true), false).getKeyInfo());
+
+ verify(keyManager).getKeyInfo(any(OmKeyArgs.class), any(ResolvedBucket.class), any());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void getKeyInfoServesObjectStoreLayoutWhenNotRequested(boolean assumeS3Context) throws Exception {
+ // Object store callers, such as S3 GetObject / HeadObject and readKey, read
+ // OBJECT_STORE buckets without asking for a file system layout.
+ KeyManager keyManager = mock(KeyManager.class);
+ OmMetadataReader reader = newGetKeyInfoReader(keyManager, "obs-bucket", BucketLayout.OBJECT_STORE);
+
+ assertNotNull(reader.getKeyInfo(getKeyInfoArgs("obs-bucket", false), assumeS3Context).getKeyInfo());
+
+ verify(keyManager).getKeyInfo(any(OmKeyArgs.class), any(ResolvedBucket.class), any());
+ }
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
index db49e81906c..e061296d5b0 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
@@ -42,6 +42,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.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.KeyInfoWithVolumeContext;
import org.apache.hadoop.ozone.om.helpers.ListKeysLightResult;
import org.apache.hadoop.ozone.om.helpers.ListKeysResult;
import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
@@ -404,6 +405,36 @@ public void testListStatusForwardsRequireFileSystemLayout(boolean require) throw
Assertions.assertEquals(require, listStatusLightArgs.getValue().isRequireFileSystemLayout());
}
+ /**
+ * GetKeyInfo must forward requireFileSystemLayout from the request into the
+ * OmKeyArgs handed to the OM.
+ */
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testGetKeyInfoForwardsRequireFileSystemLayout(boolean require) throws IOException {
+ OzoneManagerRequestHandler requestHandler = getRequestHandler(10);
+ OzoneManager ozoneManager = requestHandler.getOzoneManager();
+ KeyInfoWithVolumeContext keyInfo = KeyInfoWithVolumeContext.newBuilder().setKeyInfo(getMockedOmKeyInfo()).build();
+ Mockito.when(ozoneManager.getKeyInfo(any(OmKeyArgs.class), Mockito.anyBoolean())).thenReturn(keyInfo);
+ OzoneManagerProtocolProtos.GetKeyInfoRequest getKeyInfoRequest =
+ OzoneManagerProtocolProtos.GetKeyInfoRequest.newBuilder()
+ .setKeyArgs(OzoneManagerProtocolProtos.KeyArgs.newBuilder()
+ .setVolumeName("volume").setBucketName("bucket").setKeyName("key"))
+ .setRequireFileSystemLayout(require)
+ .build();
+ OzoneManagerProtocolProtos.OMRequest request = Mockito.mock(OzoneManagerProtocolProtos.OMRequest.class);
+ Mockito.when(request.getTraceID()).thenReturn("traceId");
+ Mockito.when(request.getCmdType()).thenReturn(OzoneManagerProtocolProtos.Type.GetKeyInfo);
+ Mockito.when(request.getGetKeyInfoRequest()).thenReturn(getKeyInfoRequest);
+
+ OzoneManagerProtocolProtos.OMResponse response = requestHandler.handleReadRequest(request);
+ Assertions.assertEquals(OzoneManagerProtocolProtos.Status.OK, response.getStatus());
+
+ ArgumentCaptor<OmKeyArgs> keyArgs = ArgumentCaptor.forClass(OmKeyArgs.class);
+ Mockito.verify(ozoneManager).getKeyInfo(keyArgs.capture(), Mockito.eq(false));
+ Assertions.assertEquals(require, keyArgs.getValue().isRequireFileSystemLayout());
+ }
+
/**
* Test to verify BasicOmKeyInfo encryption field works in listKeysLight.
*/
diff --git a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
index cb867af7299..7edd69c3c8e 100644
--- a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
+++ b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
@@ -390,6 +390,23 @@ public InputStream readFile(String pathStr) throws IOException {
config);
String key = ofsPath.getKeyName();
try {
+ // An empty bucket name takes the getBucket path, which reports it as FileNotFoundException.
+ if (!ofsPath.getBucketName().isEmpty()
+ && proxy.getOmVersion().compareTo(OzoneManagerVersion.GET_KEY_INFO_REJECTS_OBS) >= 0) {
+ // Ask OM to reject an OBJECT_STORE bucket (HDDS-16948), so read without fetching the bucket first.
+ try {
+ return proxy.readFile(ofsPath.getVolumeName(), ofsPath.getBucketName(), key, true).getInputStream();
+ } catch (OMException e) {
+ if (e.getResult() == OMException.ResultCodes.NOT_SUPPORTED_OPERATION) {
+ // Surface it as IllegalArgumentException, matching the client-side
+ // layout check of the fallback path below.
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ throw e;
+ }
+ }
+ // Older OM has no server-side OBJECT_STORE check; validate the bucket
+ // layout on the client, matching pre-HDDS-16948 behavior.
OzoneBucket bucket = getBucket(ofsPath, false);
return bucket.readFile(key).getInputStream();
} catch (OMException ex) {
diff --git a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterReadFile.java b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterReadFile.java
new file mode 100644
index 00000000000..7d0706466b4
--- /dev/null
+++ b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterReadFile.java
@@ -0,0 +1,164 @@
+/*
+ * 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.fs.ozone;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.OzoneManagerVersion;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.io.OzoneInputStream;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+/**
+ * Unit tests for {@link BasicRootedOzoneClientAdapterImpl#readFile}, covering the read without InfoBucket added by
+ * HDDS-16948 and the fallback used when the OM predates {@link OzoneManagerVersion#GET_KEY_INFO_REJECTS_OBS}.
+ * Uses a partial mock so no OM connection is required.
+ */
+public class TestBasicRootedOzoneClientAdapterReadFile {
+
+ private static final String FILE_PATH = "/vol/bucket/dir/file";
+
+ private BasicRootedOzoneClientAdapterImpl adapter;
+ private ClientProtocol proxy;
+ private OzoneBucket bucket;
+ private InputStream stream;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ adapter = mock(BasicRootedOzoneClientAdapterImpl.class, CALLS_REAL_METHODS);
+ proxy = mock(ClientProtocol.class);
+ bucket = mock(OzoneBucket.class);
+ stream = mock(InputStream.class);
+ OzoneInputStream ozoneStream = new OzoneInputStream(stream);
+
+ when(proxy.readFile("vol", "bucket", "dir/file", true)).thenReturn(ozoneStream);
+ when(proxy.getBucketDetails("vol", "bucket")).thenReturn(bucket);
+ when(bucket.getName()).thenReturn("bucket");
+ when(bucket.getBucketLayout()).thenReturn(BucketLayout.FILE_SYSTEM_OPTIMIZED);
+ when(bucket.readFile("dir/file")).thenReturn(ozoneStream);
+
+ setField("proxy", proxy);
+ setField("objectStore", mock(ObjectStore.class));
+ setField("config", new OzoneConfiguration());
+
+ // Default to an OM that performs the server-side OBJECT_STORE rejection.
+ when(proxy.getOmVersion()).thenReturn(OzoneManagerVersion.GET_KEY_INFO_REJECTS_OBS);
+ }
+
+ private void setField(String name, Object value) throws Exception {
+ Field field = BasicRootedOzoneClientAdapterImpl.class.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(adapter, value);
+ }
+
+ private void failRead(OMException e) throws IOException {
+ when(proxy.readFile("vol", "bucket", "dir/file", true)).thenThrow(e);
+ }
+
+ @Test
+ public void readIsTheOnlyOmCall() throws IOException {
+ assertSame(stream, adapter.readFile(FILE_PATH));
+
+ verify(proxy).readFile("vol", "bucket", "dir/file", true);
+ verify(proxy, never()).getBucketDetails(anyString(), anyString());
+ }
+
+ @Test
+ public void objectStoreRejectionSurfacesAsIllegalArgumentException() throws IOException {
+ String message = "Bucket: bucket has layout: OBJECT_STORE, which does not support file system semantics.";
+ failRead(new OMException(message, ResultCodes.NOT_SUPPORTED_OPERATION));
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> adapter.readFile(FILE_PATH));
+ assertEquals(message, e.getMessage());
+ verify(proxy, never()).getBucketDetails(anyString(), anyString());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = ResultCodes.class, names = {"KEY_NOT_FOUND", "FILE_NOT_FOUND", "NOT_A_FILE"})
+ public void missingFileSurfacesAsFileNotFoundException(ResultCodes code) throws IOException {
+ failRead(new OMException("missing", code));
+
+ FileNotFoundException e = assertThrows(FileNotFoundException.class, () -> adapter.readFile(FILE_PATH));
+ assertThat(e.getMessage()).startsWith(code.name());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = ResultCodes.class, names = {"BUCKET_NOT_FOUND", "VOLUME_NOT_FOUND", "PERMISSION_DENIED"})
+ public void otherErrorsAreRethrownWithoutFetchingTheBucket(ResultCodes code) throws IOException {
+ // A missing bucket or volume fails as before, with the same result code that InfoBucket reported.
+ failRead(new OMException("failed", code));
+
+ OMException e = assertThrows(OMException.class, () -> adapter.readFile(FILE_PATH));
+ assertEquals(code, e.getResult());
+ verify(proxy, never()).getBucketDetails(anyString(), anyString());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = OzoneManagerVersion.class, names = {"LOOKUP_FILE_REJECTS_OBS", "ZDU", "LIST_STATUS_REJECTS_OBS"})
+ public void olderOmFallsBackToClientSideBucketCheck(OzoneManagerVersion version) throws IOException {
+ // An OM finalized for ZDU reports ZDU, or a later version, even without this change, so only an OM at or above
+ // GET_KEY_INFO_REJECTS_OBS is trusted to reject OBJECT_STORE.
+ when(proxy.getOmVersion()).thenReturn(version);
+
+ assertSame(stream, adapter.readFile(FILE_PATH));
+
+ verify(proxy).getBucketDetails("vol", "bucket");
+ verify(bucket).readFile("dir/file");
+ verify(proxy, never()).readFile(anyString(), anyString(), anyString(), anyBoolean());
+ }
+
+ @Test
+ public void olderOmRejectsObsBucketClientSide() throws IOException {
+ when(proxy.getOmVersion()).thenReturn(OzoneManagerVersion.LIST_STATUS_REJECTS_OBS);
+ when(bucket.getBucketLayout()).thenReturn(BucketLayout.OBJECT_STORE);
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> adapter.readFile(FILE_PATH));
+ assertThat(e.getMessage()).contains("Bucket: bucket has layout: OBJECT_STORE");
+ verify(bucket, never()).readFile(anyString());
+ }
+
+ @Test
+ public void pathWithoutBucketIsFileNotFound() throws IOException {
+ assertThrows(FileNotFoundException.class, () -> adapter.readFile("/vol"));
+
+ verify(proxy, never()).readFile(anyString(), anyString(), anyString(), anyBoolean());
+ }
+}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index 7d2ce8efa6a..6f5956d5a56 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -673,6 +673,12 @@ public OzoneInputStream readFile(String volumeName, String bucketName,
return null;
}
+ @Override
+ public OzoneInputStream readFile(String volumeName, String bucketName, String keyName,
+ boolean requireFileSystemLayout) throws IOException {
+ return null;
+ }
+
@Override
public OzoneOutputStream createFile(String volumeName, String bucketName,
String keyName, long size,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org