blob: bc283cc8bef0aaa16598997b4850a9b4d6a31207 [file]
/*
* 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.
*/
#include "paimon/fs/oss/oss_file_system.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <utility>
#include "alibabacloud/oss2/OSSClient.h"
#include "alibabacloud/oss2/Operation.h"
#include "alibabacloud/oss2/Types.h"
#include "alibabacloud/oss2/io/ByteWriter.h"
#include "alibabacloud/oss2/models/BucketBasic.h"
#include "alibabacloud/oss2/models/ObjectBasic.h"
#include "fmt/format.h"
#include "paimon/executor.h"
namespace paimon::oss {
namespace {
namespace oss2 = alibabacloud::oss2;
constexpr int32_t kMaxKeysPerRequest = 1000;
bool IsNotFoundError(const oss2::OperationError& error) {
return error.getStatusCode() == 404 || error.getCode() == "NoSuchKey" ||
error.getCode() == "NoSuchBucket" || error.getCode() == "NotFound";
}
Status ToPaimonStatus(const oss2::OperationError& error, const std::string& operation,
const ObjectStorePath& path) {
std::string message = fmt::format("OSS {} 'oss://{}/{}' failed: code={}, status={}, message={}",
operation, path.bucket, path.key, error.getCode(),
error.getStatusCode(), error.getMessage());
if (!error.getRequestId().empty()) {
message += fmt::format(", request_id={}", error.getRequestId());
}
if (IsNotFoundError(error)) {
return Status::NotExist(message);
}
if (error.getCode() == "RequestCanceled") {
return Status::Cancelled(message);
}
return Status::IOError(message);
}
class OssObjectStoreClient : public ObjectStoreClient,
public std::enable_shared_from_this<OssObjectStoreClient> {
public:
OssObjectStoreClient(std::string bucket, std::shared_ptr<oss2::OSSClient> client,
std::unique_ptr<Executor> executor)
: bucket_(std::move(bucket)), client_(std::move(client)), executor_(std::move(executor)) {}
Result<ObjectMetadata> HeadObject(const ObjectStorePath& path) const override {
PAIMON_RETURN_NOT_OK(ValidateBucket(path));
oss2::models::HeadObjectRequest request;
request.setBucket(path.bucket).setKey(path.key);
oss2::HeadObjectOutcome outcome = client_->headObject(request);
if (!outcome.has_value()) {
return ToPaimonStatus(outcome.error(), "HeadObject", path);
}
const oss2::models::HeadObjectResult& result = outcome.value();
if (result.getContentLength() < 0) {
return Status::IOError("OSS HeadObject response is missing Content-Length");
}
return ObjectMetadata{
path.key, result.getContentLength(),
ObjectStoreFileSystemUtils::ParseModificationTime(result.getLastModified())};
}
Result<ListObjectsResult> ListObjects(const ObjectStorePath& path,
const std::string& continuation_token,
int32_t max_keys) const override {
PAIMON_RETURN_NOT_OK(ValidateBucket(path));
oss2::models::ListObjectsV2Request request;
request.setBucket(path.bucket).setPrefix(path.key).setDelimiter("/");
if (!continuation_token.empty()) {
request.setContinuationToken(continuation_token);
}
if (max_keys > 0) {
// OSS limits ListObjectsV2 requests to 1000 keys.
request.setMaxKeys(std::min(max_keys, kMaxKeysPerRequest));
}
oss2::ListObjectsV2Outcome outcome = client_->listObjectsV2(request);
if (!outcome.has_value()) {
return ToPaimonStatus(outcome.error(), "ListObjectsV2", path);
}
const oss2::models::ListObjectsV2Result& value = outcome.value();
ListObjectsResult result;
result.objects.reserve(value.getContents().size());
for (const oss2::models::ObjectSummary& object : value.getContents()) {
result.objects.push_back(ObjectMetadata{
object.key, object.size,
ObjectStoreFileSystemUtils::ParseModificationTime(object.lastModified)});
}
result.common_prefixes.reserve(value.getCommonPrefixes().size());
for (const oss2::models::CommonPrefix& prefix : value.getCommonPrefixes()) {
result.common_prefixes.push_back(prefix.prefix);
}
result.is_truncated = value.getIsTruncated();
result.continuation_token = value.getNextContinuationToken();
return result;
}
Result<int64_t> GetObjectRange(const ObjectStorePath& path, int64_t offset, int64_t size,
char* buffer) const override {
PAIMON_RETURN_NOT_OK(ValidateBucket(path));
if (size == 0) {
return 0;
}
auto writer = std::make_shared<std::shared_ptr<oss2::MemoryWriter>>();
oss2::SinkFactory sink;
sink.isOneShot = false;
sink.supplier = [buffer, size, writer](int64_t, const oss2::HeaderCollection&) {
auto memory_writer = std::make_shared<oss2::MemoryWriter>(
reinterpret_cast<uint8_t*>(buffer), static_cast<size_t>(size));
*writer = memory_writer;
return memory_writer;
};
oss2::models::GetObjectRequest request;
request.setBucket(path.bucket)
.setKey(path.key)
.setRange(fmt::format("bytes={}-{}", offset, offset + size - 1))
.setRangeBehavior("standard")
.setSinkFactory(std::move(sink));
oss2::GetObjectOutcome outcome = client_->getObject(request);
if (!outcome.has_value()) {
return ToPaimonStatus(outcome.error(), "GetObject", path);
}
int64_t written =
*writer ? static_cast<int64_t>((*writer)->written()) : static_cast<int64_t>(0);
if (written != size) {
return Status::IOError(
fmt::format("OSS GetObject read {} bytes for oss://{}/{}, expected {}", written,
path.bucket, path.key, size));
}
return written;
}
void GetObjectRangeAsync(const ObjectStorePath& path, int64_t offset, int64_t size,
char* buffer, std::function<void(Status)>&& callback) const override {
std::shared_ptr<const OssObjectStoreClient> self = shared_from_this();
executor_->Add([self = std::move(self), path, offset, size, buffer,
callback = std::move(callback)]() mutable {
Result<int64_t> result = self->GetObjectRange(path, offset, size, buffer);
callback(result.ok() ? Status::OK() : result.status());
});
}
private:
Status ValidateBucket(const ObjectStorePath& path) const {
if (path.bucket != bucket_) {
return Status::Invalid(
fmt::format("OSS file system for bucket '{}' cannot access "
"'oss://{}/{}'",
bucket_, path.bucket, path.key));
}
return Status::OK();
}
std::string bucket_;
std::shared_ptr<oss2::OSSClient> client_;
std::unique_ptr<Executor> executor_;
};
} // namespace
OssFileSystem::OssFileSystem(std::string bucket, std::shared_ptr<oss2::OSSClient> client,
std::unique_ptr<Executor> executor)
: ObjectStoreFileSystem("oss", std::make_shared<OssObjectStoreClient>(
std::move(bucket), std::move(client), std::move(executor))) {
}
} // namespace paimon::oss