Coverage Report

Created: 2026-07-31 16:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/s3_rate_limiter_manager.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <array>
21
#include <cstdint>
22
#include <memory>
23
#include <mutex>
24
25
#include "cpp/token_bucket_rate_limiter.h"
26
27
namespace doris {
28
29
// The final rate limit values applied to the token buckets, resolved from all the
30
// s3_{get,put}_* configs plus the current CPU core count. All precedence rules
31
// (per-core vs legacy, caps, overflow guard) live in resolve_s3_rate_limit() only.
32
struct S3EffectiveRateLimit {
33
    int64_t qps = 0;              // QPS bucket rate; 0 = unlimited
34
    int64_t burst = 0;            // QPS bucket capacity
35
    int64_t count_limit = 0;      // cumulative request cap (legacy path only); 0 = unlimited
36
    int64_t bytes_per_second = 0; // bytes bucket rate; 0 = unlimited
37
38
    bool operator==(const S3EffectiveRateLimit&) const = default;
39
};
40
41
// Pure function of configs and `cores`; no side effects.
42
// s3_{get,put}_requests_per_second_per_core < 0 falls back to the legacy absolute token
43
// configs and `cores` does not participate; otherwise qps = min(per_core * cores, qps_max).
44
S3EffectiveRateLimit resolve_s3_rate_limit(S3RateLimitType type, int64_t cores);
45
46
// Cores used to derive effective limits: config::s3_rate_limiter_cpu_cores_override
47
// (> 0) wins; otherwise re-read the cgroup cpu quota (serverless BEs can be resized
48
// in place), falling back to physical cores. Always >= 1.
49
int64_t s3_rate_limiter_cpu_cores();
50
51
// Directly reset the GET/PUT QPS bucket. Note that the daemon refresh thread will
52
// override a manual reset as soon as the config-resolved parameters differ.
53
int reset_s3_rate_limiter(S3RateLimitType type, size_t max_speed, size_t max_burst, size_t limit);
54
55
enum class S3RateLimitRejectReason {
56
    NONE,
57
    QPS,
58
    BYTES,
59
};
60
61
// Owns the 4 process-wide token buckets (GET/PUT x QPS/bytes). Independent of
62
// S3ClientFactory so that instantiating it never initializes the AWS SDK.
63
class S3RateLimiterManager {
64
public:
65
    static S3RateLimiterManager& instance();
66
67
    // Idempotent: re-resolve effective limits from the current configs and core count,
68
    // and reset only the buckets whose parameters actually changed. The comparison
69
    // baseline is each bucket's own parameters -- there is no shadow state to drift.
70
    // Called from the daemon refresh thread; safe to call from anywhere.
71
    void refresh();
72
73
    S3RateLimiterHolder* qps_limiter(S3RateLimitType type);
74
    S3RateLimiterHolder* bytes_limiter(S3RateLimitType type);
75
76
    S3RateLimiterManager(const S3RateLimiterManager&) = delete;
77
    S3RateLimiterManager& operator=(const S3RateLimiterManager&) = delete;
78
79
private:
80
    S3RateLimiterManager();
81
82
    std::mutex _refresh_lock;
83
    std::array<std::unique_ptr<S3RateLimiterHolder>, 2> _qps_limiters;
84
    std::array<std::unique_ptr<S3RateLimiterHolder>, 2> _bytes_limiters;
85
};
86
87
// RAII admission for one logical object storage request.
88
//
89
// The constructor charges the QPS bucket (may sleep when throttled; rejected only by
90
// the legacy token_limit cumulative cap) and then reserves `estimated_bytes` from the
91
// bytes bucket, clamped to at most 1 second worth of bandwidth so a single huge IO
92
// cannot create unbounded upfront debt (may sleep; normally does not reject because its
93
// count limit is 0).
94
//
95
// settle(actual) refunds the difference when the actual transferred bytes are smaller
96
// than the reservation (e.g. a short read at EOF). An unsettled guard keeps the full
97
// reservation charged, which is the conservative choice for failed requests.
98
//
99
// The guard pins the bucket generation it charged: if refresh() resets the bytes
100
// bucket while the request is in flight, settle() refunds on the old generation
101
// (kept alive by the guard's shared_ptr) instead of polluting the fresh bucket.
102
class S3RateLimitGuard {
103
public:
104
    S3RateLimitGuard(S3RateLimitType type, size_t estimated_bytes);
105
426k
    ~S3RateLimitGuard() = default;
106
107
    S3RateLimitGuard(const S3RateLimitGuard&) = delete;
108
    S3RateLimitGuard& operator=(const S3RateLimitGuard&) = delete;
109
110
426k
    bool ok() const { return _ok; }
111
13
    S3RateLimitRejectReason reject_reason() const { return _reject_reason; }
112
    void settle(size_t actual_bytes);
113
114
private:
115
    size_t _reserved = 0;
116
    std::shared_ptr<TokenBucketRateLimiter> _charged_bucket;
117
    bool _ok = true;
118
    bool _settled = false;
119
    S3RateLimitRejectReason _reject_reason = S3RateLimitRejectReason::NONE;
120
};
121
122
} // namespace doris