This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new da848878439 [fix](fs) Restore legacy OSS bucket endpoint URI handling
(#69114)
da848878439 is described below
commit da8488784398c7176f0fb249847d9406c6e00825
Author: Gabriel <[email protected]>
AuthorDate: Fri Oct 16 09:34:05 2026 +0800
[fix](fs) Restore legacy OSS bucket endpoint URI handling (#69114)
### What problem does this PR solve?
Legacy OSS locations may encode the endpoint in the authority:
`scheme://<bucket>.<endpoint>/<path>`. FE-core normalizes these
locations for BE, but FE filesystem operations can receive the original
location retained in HMS.
Master now includes #68998, which handles the standard
`oss://<bucket>.oss-<region>.aliyuncs.com/<path>` form that triggered
the original `hive_on_hms_and_dlf` failure. The native OSS provider
still lacks the equivalent handling for its supported `s3`/`s3a` aliases
and custom endpoint authorities. For example, when the selected
filesystem provider is OSS:
```text
s3a://sample-bucket.oss-cn-hangzhou.aliyuncs.com/warehouse/part=value/file
oss://sample-bucket.custom-endpoint.example.com/warehouse/part=value/file
```
The remaining paths pass the entire authority as the bucket to the OSS
SDK, causing `The bucket name "..." is invalid`. They should instead use
bucket `sample-bucket`, preserve key `warehouse/part=value/file`, and
continue using the configured OSS endpoint. This PR restores the
provider-specific compatibility already implemented by FE-core's OSS
normalization.
Centralize the correction in the OSS provider and use it for all
URI-based object requests. Let the shared filesystem delegate URI
parsing to that provider as well: bulk/recursive deletion extracts
buckets before calling the SDK and must follow the same rule. Preserve
encoded keys and literal `+` characters; leave HTTP(S) parsing and other
providers unchanged, including valid dotted S3 bucket names.
### Release note
Fix Hive operations against legacy OSS locations containing a bucket and
endpoint in the URI authority.
### Check List (For Author)
- Test:
- [x] Unit Test: added seven focused tests. All seven tests pass on the
rebased branch, together with the upstream OSS regression tests.
- [x] `cd fe && mvn -pl fe-filesystem/fe-filesystem-oss -am test
-Dmaven.build.cache.enabled=false`: 369 tests passed after rebasing onto
master, including 51 OSS and 61 filesystem SPI tests. Maven Checkstyle
passed with zero violations.
- Coverage includes OSS/S3/S3A aliases under the OSS provider, both
addressing settings, encoded partition keys, custom endpoints and bucket
roots, write/copy/delete, bulk/recursive deletion, path-style HTTP,
unsupported schemes and dotted S3 buckets.
- `run-fe-ut.sh --run OssObjStorageTest` was attempted but stopped on a
local Thrift compiler mismatch (installed 0.16.0, required 0.24.0). The
focused modules above do not require Thrift generation. The full
cloud-backed Hive regression has not been rerun locally.
- Behavior changed:
- [x] Yes: native OSS requests accept legacy bucket.endpoint locations
consistently with existing FE path normalization.
- Does this need documentation?
- [x] No: restores existing URI compatibility without adding
configuration.
---
.../apache/doris/filesystem/oss/OssFileSystem.java | 6 ++
.../apache/doris/filesystem/oss/OssObjStorage.java | 39 +++++--
.../doris/filesystem/oss/OssObjStorageTest.java | 116 +++++++++++++++++++++
.../filesystem/spi/S3CompatibleFileSystem.java | 8 +-
.../doris/filesystem/spi/ObjectStorageUriTest.java | 8 ++
5 files changed, 164 insertions(+), 13 deletions(-)
diff --git
a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystem.java
b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystem.java
index dd29d9b729e..7758054dbfb 100644
---
a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystem.java
+++
b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystem.java
@@ -17,6 +17,7 @@
package org.apache.doris.filesystem.oss;
+import org.apache.doris.filesystem.spi.ObjectStorageUri;
import org.apache.doris.filesystem.spi.S3CompatibleFileSystem;
/**
@@ -27,4 +28,9 @@ public class OssFileSystem extends S3CompatibleFileSystem {
public OssFileSystem(OssObjStorage objStorage) {
super(objStorage, objStorage.isUsePathStyle(),
objStorage.getSupportedSchemes());
}
+
+ @Override
+ protected ObjectStorageUri parseUri(String uri) {
+ return ((OssObjStorage) objStorage).parseUri(uri);
+ }
}
diff --git
a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
index cfb1da68532..b17c21f114f 100644
---
a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
+++
b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
@@ -115,6 +115,23 @@ public class OssObjStorage implements ObjStorage<OSS> {
return properties.getSupportedSchemes();
}
+ ObjectStorageUri parseUri(String path) {
+ ObjectStorageUri uri = ObjectStorageUri.parse(path, isUsePathStyle(),
getSupportedSchemes());
+ if (path.regionMatches(true, 0, "http://", 0, 7)
+ || path.regionMatches(true, 0, "https://", 0, 8)) {
+ return uri;
+ }
+ int dot = uri.bucket().indexOf('.');
+ if (dot <= 0) {
+ return uri;
+ }
+ // HMS can retain legacy bucket.endpoint locations even when BE paths
are normalized.
+ // Rewrite only in the OSS provider: S3 bucket names may contain dots.
Preserve raw keys
+ // so encoded partition separators and literal '+' characters still
identify the same objects.
+ return ObjectStorageUri.parse("oss://" + uri.bucket().substring(0,
dot) + "/" + uri.key(),
+ isUsePathStyle(), getSupportedSchemes());
+ }
+
@Override
public OSS getClient() throws IOException {
return getClient(properties.getBucket());
@@ -199,7 +216,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public RemoteObjects listObjectsWithOptions(String remotePath,
ObjectListOptions options) throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
ListObjectsRequest request = new ListObjectsRequest(uri.bucket());
request.setPrefix(uri.key());
if (options != null) {
@@ -233,7 +250,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public RemoteObject headObject(String remotePath) throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
try {
ObjectMetadata metadata =
getClient(uri.bucket()).getObjectMetadata(uri.bucket(), uri.key());
return new RemoteObject(uri.key(), uri.key(), metadata.getETag(),
metadata.getContentLength(),
@@ -250,7 +267,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public void putObject(String remotePath, RequestBody requestBody) throws
IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(requestBody.contentLength());
try (InputStream content = requestBody.content()) {
@@ -264,7 +281,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public void deleteObject(String remotePath) throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
try {
getClient(uri.bucket()).deleteObject(uri.bucket(), uri.key());
} catch (OSSException e) {
@@ -279,8 +296,8 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public void copyObject(String srcPath, String dstPath) throws IOException {
- ObjectStorageUri src = ObjectStorageUri.parse(srcPath,
isUsePathStyle(), getSupportedSchemes());
- ObjectStorageUri dst = ObjectStorageUri.parse(dstPath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri src = parseUri(srcPath);
+ ObjectStorageUri dst = parseUri(dstPath);
try {
getClient(src.bucket()).copyObject(new CopyObjectRequest(
src.bucket(), src.key(), dst.bucket(), dst.key()));
@@ -292,7 +309,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public String initiateMultipartUpload(String remotePath) throws
IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
try {
InitiateMultipartUploadResult result =
getClient(uri.bucket()).initiateMultipartUpload(
new InitiateMultipartUploadRequest(uri.bucket(),
uri.key()));
@@ -306,7 +323,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public UploadPartResult uploadPart(String remotePath, String uploadId, int
partNum,
RequestBody body) throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
try (InputStream content = body.content()) {
com.aliyun.oss.model.UploadPartRequest request = new
com.aliyun.oss.model.UploadPartRequest();
request.setBucketName(uri.bucket());
@@ -326,7 +343,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public void completeMultipartUpload(String remotePath, String uploadId,
List<UploadPartResult> parts) throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
List<PartETag> partEtags = parts.stream()
.map(part -> new PartETag(part.partNumber(), part.etag()))
.collect(Collectors.toList());
@@ -341,7 +358,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public void abortMultipartUpload(String remotePath, String uploadId)
throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
try {
getClient(uri.bucket()).abortMultipartUpload(new
AbortMultipartUploadRequest(
uri.bucket(), uri.key(), uploadId));
@@ -353,7 +370,7 @@ public class OssObjStorage implements ObjStorage<OSS> {
@Override
public InputStream openInputStreamAt(String remotePath, long fromByte)
throws IOException {
- ObjectStorageUri uri = ObjectStorageUri.parse(remotePath,
isUsePathStyle(), getSupportedSchemes());
+ ObjectStorageUri uri = parseUri(remotePath);
try {
GetObjectRequest request = new GetObjectRequest(uri.bucket(),
uri.key());
if (fromByte > 0) {
diff --git
a/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssObjStorageTest.java
b/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssObjStorageTest.java
index e557875cbeb..60fa7a0972a 100644
---
a/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssObjStorageTest.java
+++
b/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssObjStorageTest.java
@@ -197,6 +197,122 @@ class OssObjStorageTest {
Assertions.assertFalse(request.getHeaders().containsKey(OSSHeaders.OSS_SECURITY_TOKEN));
}
+ @Test
+ void listObjects_acceptsLegacyBucketEndpointWithoutDecodingKeys() throws
Exception {
+ // HMS retains the original table location even when BE receives a
normalized write path.
+ String key = "warehouse/pt=a%2Fb%252Fc+value/";
+ for (String scheme : List.of("oss", "s3", "s3a")) {
+ for (boolean pathStyle : List.of(false, true)) {
+ OSS mockOss = Mockito.mock(OSS.class);
+
Mockito.when(mockOss.listObjects(Mockito.any(ListObjectsRequest.class)))
+ .thenReturn(new ObjectListing());
+ Map<String, String> props = buildBasicProps();
+ props.put("use_path_style", Boolean.toString(pathStyle));
+ OssObjStorage storage = new TestableOssObjStorage(props,
mockOss);
+
+ storage.listObjects(scheme +
"://my-bucket.oss-cn-hangzhou.aliyuncs.com/" + key, null);
+
+ ArgumentCaptor<ListObjectsRequest> captor =
ArgumentCaptor.forClass(ListObjectsRequest.class);
+ Mockito.verify(mockOss).listObjects(captor.capture());
+ Assertions.assertEquals("my-bucket",
captor.getValue().getBucketName());
+ Assertions.assertEquals(key, captor.getValue().getPrefix());
+ }
+ }
+ }
+
+ @Test
+ void headObject_acceptsBucketEndpointAndBucketRoot() throws Exception {
+ OSS mockOss = Mockito.mock(OSS.class);
+ Mockito.when(mockOss.getObjectMetadata(Mockito.anyString(),
Mockito.anyString()))
+ .thenReturn(new ObjectMetadata());
+ OssObjStorage storage = new TestableOssObjStorage(buildBasicProps(),
mockOss);
+
+
storage.headObject("oss://my-bucket.custom-endpoint.example.com/dir/a%2Fb+file");
+ storage.headObject("oss://my-bucket.oss-cn-hangzhou.aliyuncs.com");
+
+ Mockito.verify(mockOss).getObjectMetadata("my-bucket",
"dir/a%2Fb+file");
+ Mockito.verify(mockOss).getObjectMetadata("my-bucket", "");
+ }
+
+ @Test
+ void deleteFiles_usesSameBucketParsingAsObjectRequests() throws Exception {
+ // Batch deletion extracts buckets in the shared filesystem, bypassing
URI-based SDK methods.
+ OSS mockOss = Mockito.mock(OSS.class);
+
Mockito.when(mockOss.deleteObjects(Mockito.any(DeleteObjectsRequest.class)))
+ .thenReturn(new DeleteObjectsResult(Collections.emptyList()));
+ OssFileSystem filesystem = new OssFileSystem(new
TestableOssObjStorage(buildBasicProps(), mockOss));
+
+ filesystem.deleteFiles(List.of(
+
Location.of("oss://my-bucket.oss-cn-hangzhou.aliyuncs.com/dir/a%2Fb"),
+ Location.of("s3://my-bucket/dir/c+d")));
+
+ ArgumentCaptor<DeleteObjectsRequest> captor =
ArgumentCaptor.forClass(DeleteObjectsRequest.class);
+ Mockito.verify(mockOss).deleteObjects(captor.capture());
+ Assertions.assertEquals("my-bucket",
captor.getValue().getBucketName());
+ Assertions.assertEquals(List.of("dir/a%2Fb", "dir/c+d"),
captor.getValue().getKeys());
+ }
+
+ @Test
+ void writeCopyAndDelete_acceptLegacyAuthorities() throws Exception {
+ OSS mockOss = Mockito.mock(OSS.class);
+ OssObjStorage storage = new TestableOssObjStorage(buildBasicProps(),
mockOss);
+ String source =
"oss://my-bucket.oss-cn-hangzhou.aliyuncs.com/dir/a%2Fb";
+ String target =
"s3://other-bucket.oss-cn-hangzhou.aliyuncs.com/dir/a+b";
+
+ storage.putObject(source, RequestBody.of(new ByteArrayInputStream(new
byte[] {1}), 1));
+ storage.copyObject(source, target);
+ storage.deleteObject(source);
+
+ ArgumentCaptor<PutObjectRequest> put =
ArgumentCaptor.forClass(PutObjectRequest.class);
+ Mockito.verify(mockOss).putObject(put.capture());
+ Assertions.assertEquals("my-bucket", put.getValue().getBucketName());
+ Assertions.assertEquals("dir/a%2Fb", put.getValue().getKey());
+ ArgumentCaptor<CopyObjectRequest> copy =
ArgumentCaptor.forClass(CopyObjectRequest.class);
+ Mockito.verify(mockOss).copyObject(copy.capture());
+ Assertions.assertEquals("my-bucket",
copy.getValue().getSourceBucketName());
+ Assertions.assertEquals("dir/a%2Fb", copy.getValue().getSourceKey());
+ Assertions.assertEquals("other-bucket",
copy.getValue().getDestinationBucketName());
+ Assertions.assertEquals("dir/a+b",
copy.getValue().getDestinationKey());
+ Mockito.verify(mockOss).deleteObject("my-bucket", "dir/a%2Fb");
+ }
+
+ @Test
+ void recursiveDelete_usesNormalizedBucketForBatchRequest() throws
Exception {
+ OSS mockOss = Mockito.mock(OSS.class);
+ OSSObjectSummary summary = new OSSObjectSummary();
+ summary.setKey("dir/pt=a%2Fb/file");
+ ObjectListing listing = new ObjectListing();
+ listing.setObjectSummaries(List.of(summary));
+
Mockito.when(mockOss.listObjects(Mockito.any(ListObjectsRequest.class))).thenReturn(listing);
+
Mockito.when(mockOss.deleteObjects(Mockito.any(DeleteObjectsRequest.class)))
+ .thenReturn(new DeleteObjectsResult(Collections.emptyList()));
+ OssFileSystem filesystem = new OssFileSystem(new
TestableOssObjStorage(buildBasicProps(), mockOss));
+
+
filesystem.delete(Location.of("oss://my-bucket.oss-cn-hangzhou.aliyuncs.com/dir/"),
true);
+
+ ArgumentCaptor<DeleteObjectsRequest> captor =
ArgumentCaptor.forClass(DeleteObjectsRequest.class);
+ Mockito.verify(mockOss).deleteObjects(captor.capture());
+ Assertions.assertEquals("my-bucket",
captor.getValue().getBucketName());
+ Assertions.assertEquals(List.of("dir/pt=a%2Fb/file"),
captor.getValue().getKeys());
+ }
+
+ @Test
+ void headObject_keepsPathStyleHttpBucketAndRejectsUnsupportedSchemes()
throws Exception {
+ OSS mockOss = Mockito.mock(OSS.class);
+ Mockito.when(mockOss.getObjectMetadata(Mockito.anyString(),
Mockito.anyString()))
+ .thenReturn(new ObjectMetadata());
+ Map<String, String> props = buildBasicProps();
+ props.put("use_path_style", "true");
+ OssObjStorage storage = new TestableOssObjStorage(props, mockOss);
+
+
storage.headObject("https://oss-cn-hangzhou.aliyuncs.com/my-bucket/dir/a%2Fb");
+
+ Mockito.verify(mockOss).getObjectMetadata("my-bucket", "dir/a%2Fb");
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
storage.headObject("cos://my-bucket.endpoint.example.com/key"));
+ Mockito.verifyNoMoreInteractions(mockOss);
+ }
+
@Test
void listObjects_usesOssNativeClientAndMapsResult() throws Exception {
OSS mockOss = Mockito.mock(OSS.class);
diff --git
a/fe/fe-filesystem/fe-filesystem-spi/src/main/java/org/apache/doris/filesystem/spi/S3CompatibleFileSystem.java
b/fe/fe-filesystem/fe-filesystem-spi/src/main/java/org/apache/doris/filesystem/spi/S3CompatibleFileSystem.java
index 5942aa95a37..93fc8c0ba8d 100644
---
a/fe/fe-filesystem/fe-filesystem-spi/src/main/java/org/apache/doris/filesystem/spi/S3CompatibleFileSystem.java
+++
b/fe/fe-filesystem/fe-filesystem-spi/src/main/java/org/apache/doris/filesystem/spi/S3CompatibleFileSystem.java
@@ -210,8 +210,12 @@ public abstract class S3CompatibleFileSystem extends
ObjFileSystem {
} while (continuationToken != null);
}
- /** Parses {@code uri} respecting the underlying client's path-style
configuration. */
- private ObjectStorageUri parseUri(String uri) {
+ /**
+ * Parses {@code uri} respecting the underlying client's path-style
configuration.
+ * Providers must apply the same bucket parsing here and in object
requests, since batch
+ * deletion extracts bucket names here before calling the native client.
+ */
+ protected ObjectStorageUri parseUri(String uri) {
return ObjectStorageUri.parse(uri, usePathStyle, supportedSchemes);
}
diff --git
a/fe/fe-filesystem/fe-filesystem-spi/src/test/java/org/apache/doris/filesystem/spi/ObjectStorageUriTest.java
b/fe/fe-filesystem/fe-filesystem-spi/src/test/java/org/apache/doris/filesystem/spi/ObjectStorageUriTest.java
index 3c2557070da..21fe4a6842b 100644
---
a/fe/fe-filesystem/fe-filesystem-spi/src/test/java/org/apache/doris/filesystem/spi/ObjectStorageUriTest.java
+++
b/fe/fe-filesystem/fe-filesystem-spi/src/test/java/org/apache/doris/filesystem/spi/ObjectStorageUriTest.java
@@ -62,6 +62,14 @@ class ObjectStorageUriTest {
Assertions.assertEquals("path/to/file.csv", uri.key());
}
+ @Test
+ void parsePreservesDottedBucketsOutsideOssProvider() {
+ // OSS's legacy bucket.endpoint rewrite must not change legal dotted
S3 bucket names.
+ ObjectStorageUri uri =
ObjectStorageUri.parse("s3://logs.prod/dir/a%2Fb+file", false);
+ Assertions.assertEquals("logs.prod", uri.bucket());
+ Assertions.assertEquals("dir/a%2Fb+file", uri.key());
+ }
+
@Test
void parseS3aScheme() {
ObjectStorageUri uri = ObjectStorageUri.parse("s3a://my-bucket/key",
false);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]