blob: b01c6e76d5a45d66fb7f763d7380027788bea8a1 [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.
*/
#pragma once
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include "paimon/logging.h"
#include "paimon/result.h"
#include "paimon/status.h"
namespace paimon {
/// HTTP status codes used by the REST catalog.
struct HttpStatus {
static constexpr int64_t kOk = 200;
static constexpr int64_t kAccepted = 202;
static constexpr int64_t kNoContent = 204;
static constexpr int64_t kBadRequest = 400;
static constexpr int64_t kUnauthorized = 401;
static constexpr int64_t kForbidden = 403;
static constexpr int64_t kNotFound = 404;
static constexpr int64_t kConflict = 409;
static constexpr int64_t kTooManyRequests = 429;
static constexpr int64_t kInternalServerError = 500;
static constexpr int64_t kNotImplemented = 501;
static constexpr int64_t kServiceUnavailable = 503;
};
/// A blocking HTTP client for the REST catalog based on libcurl. HTTP 429/503
/// responses are retried for all methods, transient transport errors only for
/// idempotent ones, with exponential backoff honoring a positive `Retry-After`
/// response header (delta-seconds or HTTP-date form). A backoff sleep is bounded by
/// `retry_max_delay_ms` and the whole request by `retry_timeout_ms`; a `Retry-After`
/// beyond the remaining budget stops retrying rather than shortening the sleep.
/// Redirects to http(s) targets are followed by default, keeping the method and body
/// of POST/DELETE requests; callers can disable them for request-bound signatures.
class RestHttpClient {
public:
struct Config {
int64_t connect_timeout_ms = 180 * 1000;
int64_t request_timeout_ms = 180 * 1000;
int32_t max_retries = 5;
int64_t retry_base_delay_ms = 1000;
/// Upper bound for a single backoff sleep; a longer `Retry-After` is still
/// honored, bounded only by `retry_timeout_ms`.
int64_t retry_max_delay_ms = 64 * 1000;
/// Overall budget for one `Execute` call, covering all attempts and retry
/// sleeps; a retry whose delay does not fit the remaining budget is not
/// attempted.
int64_t retry_timeout_ms = 5 * 60 * 1000;
};
struct Response {
int64_t code = 0;
std::string body;
/// Response headers with lower-cased names.
std::map<std::string, std::string> headers;
bool IsSuccessful() const {
return code == HttpStatus::kOk || code == HttpStatus::kAccepted ||
code == HttpStatus::kNoContent;
}
};
/// Lets one thread abandon the `Execute` calls another thread runs with it. A retry
/// backoff ends at once. A transfer stops within about a second: libcurl checks for the
/// cancellation in its progress callback, which it calls about once a second while no
/// data moves. A request already sent may still reach the server.
class Cancellation {
public:
void Cancel();
bool IsCancelled() const;
/// Waits up to `delay_ms`, returning early once cancelled.
void WaitFor(int64_t delay_ms) const;
private:
mutable std::mutex mutex_;
mutable std::condition_variable cancelled_cv_;
std::atomic<bool> cancelled_{false};
};
/// Creates a client against `base_uri`. The uri is normalized: surrounding
/// whitespace and all trailing '/' are stripped and "http://" is prepended when no
/// scheme is present.
static Result<std::unique_ptr<RestHttpClient>> Create(const std::string& base_uri);
static Result<std::unique_ptr<RestHttpClient>> Create(const std::string& base_uri,
const Config& config);
~RestHttpClient();
/// Executes `method` ("GET", "POST" or "DELETE") on `path` (already url-encoded,
/// starting with '/'); query parameter keys and values are url-encoded internally.
/// Header names must be valid HTTP tokens and values must not contain CR, LF or
/// NUL; a violating header fails the request before anything is sent. Returns the
/// final response, which may carry a non-2xx code, or an error status when the
/// request could not be transported at all. Only transient transport errors (an
/// established connection breaking mid-request or a truncated response body) are
/// retried; every other transport failure fails immediately. `follow_redirects`
/// must be false when authentication headers are bound to the original request.
///
/// `retry_safe=false` disables retries and redirects, overriding `follow_redirects`,
/// to prevent replay when the original request's outcome is unknown.
///
/// Once `cancellation` is cancelled, the call returns a `Cancelled` status instead of
/// waiting for a response, starting another attempt or finishing a retry backoff; null
/// leaves the call bounded only by its timeouts.
Result<Response> Execute(const std::string& method, const std::string& path,
const std::map<std::string, std::string>& query_params,
const std::map<std::string, std::string>& headers,
const std::string& body, bool follow_redirects = true,
bool retry_safe = true,
const Cancellation* cancellation = nullptr) const;
const std::string& GetBaseUri() const {
return base_uri_;
}
static std::string NormalizeUri(const std::string& uri);
/// Builds "k1=v1&k2=v2" with url-encoded keys and values.
static std::string BuildQueryString(const std::map<std::string, std::string>& query_params);
/// Computes the delay before the retry following the `execution_count`-th attempt
/// (counted from 1), in ms, or `nullopt` when retrying should stop. A positive
/// `Retry-After` header of `response` (delta-seconds, or HTTP-date resolved against
/// `now_epoch_seconds`) wins and is never shortened: beyond `remaining_budget_ms`
/// it yields `nullopt`. Otherwise exponential backoff with up to 10% jitter
/// applies, clamped to `retry_max_delay_ms`, yielding `nullopt` when even the
/// clamped delay exceeds `remaining_budget_ms`. `response` is null when the attempt
/// failed with a transport error.
static std::optional<int64_t> ComputeRetryDelayMs(const Config& config, int32_t execution_count,
const Response* response,
int64_t now_epoch_seconds,
int64_t remaining_budget_ms);
private:
RestHttpClient(const std::string& base_uri, const Config& config);
/// Pool of the libcurl easy handles used by `ExecuteOnce`, also keeping libcurl's
/// global state alive for the lifetime of the client.
class HandlePool;
/// On a transport failure, `transport_retriable` reports whether the failure kind
/// may be retried; see `Execute` for which kinds are not.
Result<Response> ExecuteOnce(const std::string& method, const std::string& url,
const std::map<std::string, std::string>& headers,
const std::string& body, bool follow_redirects,
const Cancellation* cancellation, bool* transport_retriable) const;
std::optional<int64_t> GetRetryDelayMs(int32_t execution_count, const Response* response,
int64_t remaining_budget_ms) const;
std::unique_ptr<HandlePool> handle_pool_;
std::string base_uri_;
Config config_;
std::shared_ptr<Logger> logger_;
};
} // namespace paimon