github-actions[bot] commented on code in PR #65420:
URL: https://github.com/apache/doris/pull/65420#discussion_r3593637405


##########
be/src/io/fs/s3_obj_storage_client.cpp:
##########
@@ -173,7 +175,9 @@ ObjectStorageResponse S3ObjStorageClient::put_object(const 
ObjectStoragePathOpti
     MonotonicStopWatch watch;
     watch.start();
     auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
-            s3_put_rate_limit([&]() { return _client->PutObject(request); }),
+            s3_put_rate_limit(

Review Comment:
   [P1] Charge bytes at the retry-attempt boundary. This admission runs once 
around `PutObject`, but the factory installs 
`S3CustomRetryStrategy(max_s3_client_retry)` (default 10), so a retryable 1 GiB 
PUT can send the payload twice while consuming only 1 GiB of tokens. Azure's 
`UploadFrom`/`StageBlock` wrappers have the same per-operation placement around 
a client with `Retry.MaxRetries`. That lets failure-time wire throughput exceed 
the advertised bytes-per-second cap by the retry multiplier. Please acquire in 
each SDK retry attempt (Azure can use a per-retry policy), or explicitly 
define/rename this as a logical-request-byte limiter and test that contract.



##########
be/src/common/config.cpp:
##########
@@ -2197,6 +2240,7 @@ bool init(const char* conf_file, bool fill_conf_map, bool 
must_exist, bool set_t
         }                                                                      
                    \
         TYPE& ref_conf_value = *reinterpret_cast<TYPE*>((FIELD).storage);      
                    \
         TYPE old_value = ref_conf_value;                                       
                    \
+        ref_conf_value = new_value;                                            
                    \

Review Comment:
   [P1] Validate the candidate before publishing it. `DEFINE_VALIDATOR` 
installs a zero-argument closure that re-reads this global field, while numeric 
`set_config()` calls and runtime readers are unsynchronized. Two concurrent 
`/api/update_config` calls can therefore validate each other's values or let a 
failed call restore its stale `old_value` over a successful update. A rejected 
`variant_storage_parse_mode=3` is also briefly visible to the load path, whose 
default arm `CHECK`s and can terminate the BE. Please pass `new_value` directly 
to the validator and publish it only after validation succeeds (and serialize 
same-field publication/bookkeeping), with a barrier-based concurrency test.



##########
be/src/util/s3_util.cpp:
##########
@@ -156,67 +160,276 @@ constexpr char S3_NEED_OVERRIDE_ENDPOINT[] = 
"AWS_NEED_OVERRIDE_ENDPOINT";
 constexpr char S3_ROLE_ARN[] = "AWS_ROLE_ARN";
 constexpr char S3_EXTERNAL_ID[] = "AWS_EXTERNAL_ID";
 constexpr char S3_CREDENTIALS_PROVIDER_TYPE[] = 
"AWS_CREDENTIALS_PROVIDER_TYPE";
+
+#ifdef BE_TEST
+std::atomic<int64_t> s3_rate_limiter_cpu_cores_for_test {0};
+#endif
+
+int64_t get_s3_rate_limiter_cpu_cores() {
+#ifdef BE_TEST
+    const int64_t cpu_cores_for_test =
+            s3_rate_limiter_cpu_cores_for_test.load(std::memory_order_relaxed);
+    if (cpu_cores_for_test > 0) {
+        return cpu_cores_for_test;
+    }
+#endif
+    return CpuInfo::num_cores();
+}
+
+int64_t get_s3_qps_per_core(S3RateLimitType type) {
+    switch (type) {
+    case S3RateLimitType::GET:
+        return config::s3_get_qps_per_core;
+    case S3RateLimitType::PUT:
+        return config::s3_put_qps_per_core;
+    default:
+        CHECK(false) << "unknown S3 rate limiter type: " << to_string(type);
+        return -1;
+    }
+}
+
+int64_t get_s3_bytes_per_second_per_core(S3RateLimitType type) {
+    switch (type) {
+    case S3RateLimitType::GET:
+        return config::s3_get_bytes_per_second_per_core;
+    case S3RateLimitType::PUT:
+        return config::s3_put_bytes_per_second_per_core;
+    default:
+        CHECK(false) << "unknown S3 rate limiter type: " << to_string(type);
+        return -1;
+    }
+}
+
+int64_t compute_s3_rate_limit_from_core(int64_t per_core, int64_t cores, 
int64_t max_rate) {
+    max_rate = max_rate > 0 ? max_rate : std::numeric_limits<int64_t>::max();
+    if (per_core > max_rate / cores) {
+        return max_rate;
+    }
+    return per_core * cores;
+}
+
+S3RateLimiterConfig 
get_effective_s3_rate_limiter_config_with_cpu_cores(S3RateLimitType type,
+                                                                        
int64_t cpu_cores) {
+    S3RateLimiterConfig result;
+    int64_t per_core;
+    int64_t max_qps;
+
+    switch (type) {
+    case S3RateLimitType::GET:
+        per_core = config::s3_get_qps_per_core;
+        max_qps = config::s3_get_qps_max;
+        if (per_core < 0) {
+            result.token_limit = config::s3_get_token_limit;
+            result.token_per_second = config::s3_get_token_per_second;
+            result.bucket_tokens = config::s3_get_bucket_tokens;
+        }
+        break;
+    case S3RateLimitType::PUT:
+        per_core = config::s3_put_qps_per_core;
+        max_qps = config::s3_put_qps_max;
+        if (per_core < 0) {
+            result.token_limit = config::s3_put_token_limit;
+            result.token_per_second = config::s3_put_token_per_second;
+            result.bucket_tokens = config::s3_put_bucket_tokens;
+        }
+        break;
+    default:
+        CHECK(false) << "unknown S3 rate limiter type: " << to_string(type);
+        return result;
+    }
+
+    if (per_core >= 0) {
+        result.token_per_second = compute_s3_rate_limit_from_core(per_core, 
cpu_cores, max_qps);
+        result.bucket_tokens = result.token_per_second;
+    }
+    return result;
+}
+
+S3RateLimiterConfig 
get_effective_s3_bytes_rate_limiter_config_with_cpu_cores(S3RateLimitType type,
+                                                                              
int64_t cpu_cores) {
+    int64_t per_core = 0;
+    int64_t max_bytes_per_second = 0;
+
+    switch (type) {
+    case S3RateLimitType::GET:
+        per_core = config::s3_get_bytes_per_second_per_core;
+        max_bytes_per_second = config::s3_get_bytes_per_second_max;
+        break;
+    case S3RateLimitType::PUT:
+        per_core = config::s3_put_bytes_per_second_per_core;
+        max_bytes_per_second = config::s3_put_bytes_per_second_max;
+        break;
+    default:
+        CHECK(false) << "unknown S3 rate limiter type: " << to_string(type);
+    }
+
+    S3RateLimiterConfig result;
+    if (per_core > 0) {
+        result.token_per_second =
+                compute_s3_rate_limit_from_core(per_core, cpu_cores, 
max_bytes_per_second);
+        result.bucket_tokens = result.token_per_second;
+    }
+    return result;
+}
 } // namespace
 
-static std::atomic<int64_t> last_s3_get_token_bucket_tokens {0};
-static std::atomic<int64_t> last_s3_get_token_limit {0};
-static std::atomic<int64_t> last_s3_get_token_per_second {0};
-static std::atomic<int64_t> last_s3_put_token_per_second {0};
-static std::atomic<int64_t> last_s3_put_token_bucket_tokens {0};
-static std::atomic<int64_t> last_s3_put_token_limit {0};
+bvar::Adder<int64_t> get_bytes_rate_limit_ns("get_bytes_rate_limit_ns");
+bvar::Adder<int64_t> 
get_bytes_rate_limit_exceed_req_num("get_bytes_rate_limit_exceed_req_num");
+bvar::Adder<int64_t> put_bytes_rate_limit_ns("put_bytes_rate_limit_ns");
+bvar::Adder<int64_t> 
put_bytes_rate_limit_exceed_req_num("put_bytes_rate_limit_exceed_req_num");
 
-static std::atomic<bool> updating_get_limiter {false};
-static std::atomic<bool> updating_put_limiter {false};
+namespace {
+
+enum class S3RateLimiterKind {
+    QPS,
+    BYTES,
+};
+
+struct S3RateLimiterUpdateState {
+    S3RateLimiterConfig applied_config;
+    std::mutex update_lock;
+};
+
+std::array<std::array<S3RateLimiterUpdateState, 2>, 2> update_states;
+
+S3RateLimiterUpdateState& get_update_state(S3RateLimitType type, 
S3RateLimiterKind kind) {
+    DCHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT);
+    return update_states[static_cast<size_t>(kind)][static_cast<size_t>(type)];
+}
+
+void update_s3_rate_limiter(S3ClientFactory& factory, S3RateLimitType type, 
S3RateLimiterKind kind,
+                            int64_t cpu_cores) {
+    auto& state = get_update_state(type, kind);
+    std::lock_guard update_guard(state.update_lock);
+
+    const int64_t per_core = kind == S3RateLimiterKind::BYTES
+                                     ? get_s3_bytes_per_second_per_core(type)
+                                     : get_s3_qps_per_core(type);
+    const auto current =
+            kind == S3RateLimiterKind::BYTES
+                    ? 
get_effective_s3_bytes_rate_limiter_config_with_cpu_cores(type, cpu_cores)
+                    : 
get_effective_s3_rate_limiter_config_with_cpu_cores(type, cpu_cores);
+    const bool config_changed = state.applied_config.token_per_second != 
current.token_per_second ||
+                                state.applied_config.bucket_tokens != 
current.bucket_tokens ||
+                                state.applied_config.token_limit != 
current.token_limit;
+    if (!config_changed) {
+        return;
+    }
+
+    auto* limiter = kind == S3RateLimiterKind::BYTES ? 
factory.bytes_rate_limiter(type)
+                                                     : 
factory.rate_limiter(type);
+    const int ret =
+            limiter->reset(current.token_per_second, current.bucket_tokens, 
current.token_limit);
+    if (ret != 0) {
+        LOG(WARNING) << "Failed to reset S3 " << to_string(type)
+                     << (kind == S3RateLimiterKind::BYTES ? " bytes" : " QPS")
+                     << " rate limiter, error code: " << ret;
+        return;
+    }
+
+    const int64_t logged_cpu_cores = per_core > 0 ? cpu_cores : 1;
+    if (kind == S3RateLimiterKind::BYTES) {
+        LOG(INFO) << "Reset S3 " << to_string(type)
+                  << " bytes rate limiter, bytes_per_second=" << 
current.token_per_second
+                  << ", burst_bytes=" << current.bucket_tokens
+                  << ", cpu_cores=" << logged_cpu_cores;
+    } else if (per_core >= 0) {
+        LOG(INFO) << "Reset S3 " << to_string(type)
+                  << " QPS rate limiter, qps=" << current.token_per_second
+                  << ", burst=" << current.bucket_tokens << ", limit=" << 
current.token_limit
+                  << ", cpu_cores=" << logged_cpu_cores;
+    }
+
+    state.applied_config = current;
+}
+
+} // namespace
 
 S3RateLimiterHolder* S3ClientFactory::rate_limiter(S3RateLimitType type) {
     CHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << 
to_string(type);
     return _rate_limiters[static_cast<size_t>(type)].get();
 }
 
-template <S3RateLimitType LimiterType>
-void update_rate_limiter_if_changed(int64_t current_tps, int64_t 
current_bucket,
-                                    int64_t current_limit, 
std::atomic<int64_t>& last_tps,
-                                    std::atomic<int64_t>& last_bucket,
-                                    std::atomic<int64_t>& last_limit,
-                                    std::atomic<bool>& updating_flag, const 
char* limiter_name) {
-    if (last_tps.load(std::memory_order_relaxed) != current_tps ||
-        last_bucket.load(std::memory_order_relaxed) != current_bucket ||
-        last_limit.load(std::memory_order_relaxed) != current_limit) {
-        bool expected = false;
-        if (!updating_flag.compare_exchange_strong(expected, true, 
std::memory_order_acq_rel)) {
-            return;
-        }
-        if (last_tps.load(std::memory_order_acquire) != current_tps ||
-            last_bucket.load(std::memory_order_acquire) != current_bucket ||
-            last_limit.load(std::memory_order_acquire) != current_limit) {
-            int ret =
-                    reset_s3_rate_limiter(LimiterType, current_tps, 
current_bucket, current_limit);
-
-            if (ret == 0) {
-                last_tps.store(current_tps, std::memory_order_release);
-                last_bucket.store(current_bucket, std::memory_order_release);
-                last_limit.store(current_limit, std::memory_order_release);
-            } else {
-                LOG(WARNING) << "Failed to reset S3 " << limiter_name
-                             << " rate limiter, error code: " << ret;
-            }
-        }
+S3RateLimiterHolder* S3ClientFactory::bytes_rate_limiter(S3RateLimitType type) 
{
+    CHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << 
to_string(type);
+    return _bytes_rate_limiters[static_cast<size_t>(type)].get();
+}
 
-        updating_flag.store(false, std::memory_order_release);
+bool acquire_s3_rate_limit(S3RateLimitType type, size_t bytes) {
+    if (!config::enable_s3_rate_limiter) {
+        return true;
+    }
+

Review Comment:
   [P2] Skip a disabled QPS holder too. `s3_*_qps_per_core=0` intentionally 
publishes an all-zero holder, but this call still enters `add_with_config(1)`: 
it takes the holder shared mutex, bumps the `shared_ptr` references, takes the 
limiter spinlock, increments `_count`, and runs the callback although it cannot 
throttle or reject. In a supported bytes-only setup this serializes every 
S3/Azure operation, including zero-byte metadata requests, on a no-op QPS 
limiter. The existing `is_enabled()` guard fixes only the symmetric 
disabled-bytes case. Please gate this call on the active QPS holder's 
`is_enabled()` state and add a bytes-only fast-path test.



##########
be/test/common/config_test.cpp:
##########
@@ -32,8 +32,10 @@ namespace doris {
 using namespace config;
 
 class ConfigTest : public testing::Test {
-    void SetUp() override { config::Register::_s_field_map->clear(); }
-    void TearDown() override { config::Register::_s_field_map->clear(); }
+    void SetUp() override { 
_saved_fields.swap(*config::Register::_s_field_map); }
+    void TearDown() override { 
_saved_fields.swap(*config::Register::_s_field_map); }

Review Comment:
   [P2] Restore the matching config snapshot too. `DumpAllConfigs` fills the 
process-global `full_conf_map` with these fixture-only `cfg_*` keys, but this 
teardown swaps back only `Register::_s_field_map`. Afterward 
`get_config_info()` joins the restored production registry against the stale 
test map and returns no production entries, making later tests/order-dependent 
helpers observe an inconsistent config state. Please save/restore 
`full_conf_map` (including its initially-null state) together with the 
registry, or rebuild it after teardown.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to