Repository: envoyproxy/envoy
Pull request #38359: Introduce based_value for minRTT and allow disabling dynamic sampling of minRTT
Author: login: wtzhang23
Created: 2025-02-08T20:43:08Z | Merged: 2025-03-24T02:28:58Z

Commit Message: introduce fixed_value in acc
Additional Description: This allows users to configure the adaptive concurrency filter to skip learning the minimum RTT and use a predetermined value. This is useful for upstreams that 
* Have predictable regressions under load 
* Are not sensitive to variable load.
* Have a clear latency target

Risk Level: High
Testing: Unit tests
Docs Changes: N/A
Release Notes: In PR
Platform Specific Features: N/A



Diff:
diff --git a/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto b/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
index 68f8294b8558c..6ba7477e91cba 100644
--- a/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
+++ b/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
@@ -47,18 +47,19 @@ message GradientControllerConfig {
   }
 
   // Parameters controlling the periodic minRTT recalculation.
-  // [#next-free-field: 6]
+  // [#next-free-field: 7]
   message MinimumRTTCalculationParams {
     option (udpa.annotations.versioning).previous_message_type =
         "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
         "MinimumRTTCalculationParams";
 
     // The time interval between recalculating the minimum request round-trip time. Has to be
-    // positive.
-    google.protobuf.Duration interval = 1 [(validate.rules).duration = {
-      required: true
-      gte {nanos: 1000000}
-    }];
+    // positive. If set to zero, dynamic sampling of the minRTT is disabled.
+    google.protobuf.Duration interval = 1 [(validate.rules).duration = {gte {nanos: 1000000}}];
+
+    // The fixed value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    // If dynamic sampling of the minRTT is disabled, this field must be set.
+    google.protobuf.Duration fixed_value = 6 [(validate.rules).duration = {gt {}}];
 
     // The number of requests to aggregate/sample during the minRTT recalculation window before
     // updating. Defaults to 50.
diff --git a/changelogs/current.yaml b/changelogs/current.yaml
index 3e66421e8bace..5b8fe3ee1b483 100644
--- a/changelogs/current.yaml
+++ b/changelogs/current.yaml
@@ -199,6 +199,9 @@ new_features:
   change: |
     Made the :ref:`credential injector filter <envoy_v3_api_msg_extensions.filters.http.credential_injector.v3.CredentialInjector>`
     work as an upstream filter.
+- area: adaptive concurrency
+  change: |
+    Allow fixing the minimum RTT by introducing ``fixed_value``.
 - area: lua
   change: |
     Added :ref:`clear_route_cache <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.clear_route_cache>` support to
diff --git a/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc b/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc
index cb5287bc08709..6afa69ab80790 100644
--- a/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc
+++ b/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc
@@ -41,8 +41,17 @@ GradientControllerConfig::GradientControllerConfig(
           PROTOBUF_PERCENT_TO_DOUBLE_OR_DEFAULT(proto_config, sample_aggregate_percentile, 50)),
       min_concurrency_(
           PROTOBUF_GET_WRAPPED_OR_DEFAULT(proto_config.min_rtt_calc_params(), min_concurrency, 3)),
+      fixed_value_(std::chrono::milliseconds(
+          DurationUtil::durationToMilliseconds(proto_config.min_rtt_calc_params().fixed_value()))),
       min_rtt_buffer_pct_(
-          PROTOBUF_PERCENT_TO_DOUBLE_OR_DEFAULT(proto_config.min_rtt_calc_params(), buffer, 25)) {}
+          PROTOBUF_PERCENT_TO_DOUBLE_OR_DEFAULT(proto_config.min_rtt_calc_params(), buffer, 25)) {
+
+  if (min_rtt_calc_interval_ < std::chrono::milliseconds(1) &&
+      fixed_value_ <= std::chrono::milliseconds::zero()) {
+    throw EnvoyException(
+        "adaptive_concurrency: neither `concurrency_update_interval` nor `fixed_value` set");
+  }
+}
 GradientController::GradientController(GradientControllerConfig config,
                                        Event::Dispatcher& dispatcher, Runtime::Loader&,
                                        const std::string& stats_prefix, Stats::Scope& scope,
@@ -70,7 +79,14 @@ GradientController::GradientController(GradientControllerConfig config,
     sample_reset_timer_->enableTimer(config_.sampleRTTCalcInterval());
   });
 
-  enterMinRTTSamplingWindow();
+  if (isMinRTTSamplingEnabled()) {
+    enterMinRTTSamplingWindow();
+  } else {
+    min_rtt_ = config_.fixedValue();
+    stats_.min_rtt_msecs_.set(
+        std::chrono::duration_cast<std::chrono::milliseconds>(min_rtt_).count());
+    updateConcurrencyLimit(config_.minConcurrency());
+  }
   sample_reset_timer_->enableTimer(config_.sampleRTTCalcInterval());
   stats_.concurrency_limit_.set(concurrency_limit_.load());
 }
@@ -82,6 +98,8 @@ GradientControllerStats GradientController::generateStats(Stats::Scope& scope,
 }
 
 void GradientController::enterMinRTTSamplingWindow() {
+  ASSERT(isMinRTTSamplingEnabled());
+
   // There a potential race condition where setting the minimum concurrency multiple times in a row
   // resets the minRTT sampling timer and triggers the calculation immediately. This could occur
   // after the minRTT sampling window has already been entered, so we can simply return here knowing
@@ -108,6 +126,10 @@ void GradientController::enterMinRTTSamplingWindow() {
 }
 
 void GradientController::updateMinRTT() {
+  if (!isMinRTTSamplingEnabled()) {
+    return;
+  }
+
   // Only update minRTT when it is in minRTT sampling window and
   // number of samples is greater than or equal to the minRTTAggregateRequestCount.
   if (!inMinRTTSamplingWindow() ||
@@ -246,7 +268,7 @@ void GradientController::updateConcurrencyLimit(const uint32_t new_limit) {
   // cancel/re-enable the timer below and triggers overlapping minRTT windows. To protect against
   // this, there is an explicit check when entering the minRTT measurement that ensures there is
   // only a single minRTT measurement active at a time.
-  if (consecutive_min_concurrency_set_ >= 5) {
+  if (consecutive_min_concurrency_set_ >= 5 && isMinRTTSamplingEnabled()) {
     min_rtt_calc_timer_->enableTimer(std::chrono::milliseconds(0));
   }
 }
diff --git a/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.h b/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.h
index 1764f4aec6cb3..501b33319a159 100644
--- a/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.h
+++ b/source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.h
@@ -90,6 +90,8 @@ class GradientControllerConfig : public Logger::Loggable<Logger::Id::filter> {
     return runtime_.snapshot().getInteger(RuntimeKeys::get().MinConcurrencyKey, min_concurrency_);
   }
 
+  std::chrono::milliseconds fixedValue() const { return fixed_value_; }
+
   // The percentage is normalized to the range [0.0, 1.0].
   double minRTTBufferPercent() const {
     const double val = runtime_.snapshot().getDouble(RuntimeKeys::get().MinRTTBufferPercentKey,
@@ -142,6 +144,9 @@ class GradientControllerConfig : public Logger::Loggable<Logger::Id::filter> {
   // The concurrency limit set while measuring the minRTT.
   const uint32_t min_concurrency_;
 
+  // The fixed value of minRTT, if present.
+  const std::chrono::milliseconds fixed_value_;
+
   // The amount added to the measured minRTT as a hedge against natural variability in latency.
   const double min_rtt_buffer_pct_;
 };
@@ -221,6 +226,11 @@ class GradientController : public ConcurrencyController {
   // True if there is a minRTT sampling window active.
   bool inMinRTTSamplingWindow() const { return deferred_limit_value_.load() > 0; }
 
+  // True if minRTT is sampled.
+  bool isMinRTTSamplingEnabled() const {
+    return config_.fixedValue() <= std::chrono::milliseconds::zero();
+  }
+
   // ConcurrencyController.
   RequestForwardingAction forwardingDecision() override;
   void recordLatencySample(MonotonicTime rq_send_time) override;
diff --git a/test/extensions/filters/http/adaptive_concurrency/controller/gradient_controller_test.cc b/test/extensions/filters/http/adaptive_concurrency/controller/gradient_controller_test.cc
index 5f1b4ec436375..525efd7e5f6f4 100644
--- a/test/extensions/filters/http/adaptive_concurrency/controller/gradient_controller_test.cc
+++ b/test/extensions/filters/http/adaptive_concurrency/controller/gradient_controller_test.cc
@@ -129,6 +129,7 @@ TEST_F(GradientControllerConfigTest, BasicTest) {
   interval: 31s
   request_count: 52
   min_concurrency: 8
+  fixed_value: 42s
 )EOF";
 
   auto config = makeConfig(yaml, runtime_);
@@ -140,6 +141,26 @@ TEST_F(GradientControllerConfigTest, BasicTest) {
   EXPECT_EQ(config.sampleAggregatePercentile(), .425);
   EXPECT_EQ(config.jitterPercent(), .132);
   EXPECT_EQ(config.minConcurrency(), 8);
+  EXPECT_EQ(config.fixedValue(), std::chrono::seconds(42));
+}
+
+TEST_F(GradientControllerConfigTest, MissingMinRTTValues) {
+  const std::string yaml = R"EOF(
+  sample_aggregate_percentile:
+    value: 42.5
+  concurrency_limit_params:
+    max_concurrency_limit: 1337
+    concurrency_update_interval: 0.123s
+  min_rtt_calc_params:
+    jitter:
+      value: 13.2
+    request_count: 52
+    min_concurrency: 8
+  )EOF";
+
+  EXPECT_THROW_WITH_MESSAGE(
+      makeConfig(yaml, runtime_), EnvoyException,
+      "adaptive_concurrency: neither `concurrency_update_interval` nor `fixed_value` set");
 }
 
 TEST_F(GradientControllerConfigTest, Clamping) {
@@ -349,6 +370,97 @@ TEST_F(GradientControllerTest, MinRTTLogicTest) {
   verifyMinRTTValue(std::chrono::milliseconds(13));
 }
 
+TEST_F(GradientControllerTest, FixedMinRTT) {
+  const std::string yaml = R"EOF(
+sample_aggregate_percentile:
+  value: 50
+concurrency_limit_params:
+  max_concurrency_limit:
+  concurrency_update_interval: 0.1s
+min_rtt_calc_params:
+  fixed_value: 0.05s
+  min_concurrency: 7
+)EOF";
+
+  auto controller = makeController(yaml);
+  const auto min_rtt = std::chrono::milliseconds(50);
+
+  verifyMinRTTInactive();
+  EXPECT_EQ(controller->concurrencyLimit(), 7); // there is no sampled latency yet, so the
+                                                // concurrency limit defaults to the min concurrency
+  for (int i = 0; i < 7; ++i) {
+    tryForward(controller, true);
+  }
+  tryForward(controller, false);
+  tryForward(controller, false);
+  time_system_.advanceTimeAndRun(min_rtt, *dispatcher_, Event::Dispatcher::RunType::Block);
+  for (int i = 0; i < 7; ++i) {
+    EXPECT_EQ(controller->concurrencyLimit(), 7);
+    sampleLatency(controller, min_rtt);
+  }
+
+  // Verify the minRTT value hasn't changed
+  time_system_.advanceTimeAndRun(std::chrono::milliseconds(101) - min_rtt, *dispatcher_,
+                                 Event::Dispatcher::RunType::Block);
+  verifyMinRTTInactive();
+  verifyMinRTTValue(min_rtt);
+}
+
+TEST_F(GradientControllerTest, FixedMinRTTChangeConcurrency) {
+  const std::string yaml = R"EOF(
+sample_aggregate_percentile:
+  value: 50
+concurrency_limit_params:
+  max_concurrency_limit:
+  concurrency_update_interval: 0.1s
+min_rtt_calc_params:
+  fixed_value: 0.05s
+  min_concurrency: 7
+)EOF";
+
+  auto controller = makeController(yaml);
+  const auto min_rtt = std::chrono::milliseconds(50);
+  int current_concurrency = 7; // there is no sampled latency yet, so the
+                               // concurrency limit defaults to the min concurrency
+
+  // lower sampled latency to trigger increased concurrency
+  verifyMinRTTInactive();
+  for (int i = 0; i < current_concurrency; ++i) {
+    tryForward(controller, true);
+  }
+  time_system_.advanceTimeAndRun(min_rtt, *dispatcher_, Event::Dispatcher::RunType::Block);
+  for (int i = 0; i < current_concurrency; ++i) {
+    sampleLatency(controller, min_rtt - std::chrono::milliseconds(10));
+  }
+
+  time_system_.advanceTimeAndRun(std::chrono::milliseconds(101) - min_rtt, *dispatcher_,
+                                 Event::Dispatcher::RunType::Block);
+  EXPECT_GT(controller->concurrencyLimit(), current_concurrency);
+  current_concurrency = controller->concurrencyLimit();
+
+  // Ensure minRTT didn't change
+  verifyMinRTTInactive();
+  verifyMinRTTValue(min_rtt);
+
+  // increase sampled latency to trigger decreased concurrency
+  verifyMinRTTInactive();
+  for (int i = 0; i < current_concurrency; ++i) {
+    tryForward(controller, true);
+  }
+  time_system_.advanceTimeAndRun(min_rtt, *dispatcher_, Event::Dispatcher::RunType::Block);
+  for (int i = 0; i < current_concurrency; ++i) {
+    sampleLatency(controller, min_rtt + std::chrono::milliseconds(50));
+  }
+
+  time_system_.advanceTimeAndRun(std::chrono::milliseconds(101) - min_rtt, *dispatcher_,
+                                 Event::Dispatcher::RunType::Block);
+  EXPECT_LT(controller->concurrencyLimit(), current_concurrency);
+
+  // Ensure minRTT didn't change
+  verifyMinRTTInactive();
+  verifyMinRTTValue(min_rtt);
+}
+
 TEST_F(GradientControllerTest, CancelLatencySample) {
   const std::string yaml = R"EOF(
 sample_aggregate_percentile:


Reviews:
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T17:38:27Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T23:42:12Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T23:44:15Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T23:48:00Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T13:49:24Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T13:50:32Z
user: login: abeyad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T01:41:30Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T02:02:10Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T02:23:49Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T02:25:21Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T02:27:22Z
user: login: wbpcode
body: Thanks for the contribution. Please let's get agreement to the API first.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-27T03:05:43Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T03:48:25Z
user: login: wbpcode
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T06:21:42Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T11:44:30Z
user: login: abeyad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T15:14:52Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T16:46:34Z
user: login: abeyad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T19:27:28Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T19:44:04Z
user: login: abeyad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T19:45:53Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T00:41:58Z
user: login: tonya11en
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-02T23:14:36Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:28:28Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:56:10Z
user: login: wtzhang23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:56:19Z
user: login: abeyad
body: /lgtm api
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T00:08:10Z
user: login: tonya11en
body: This LGTM, modulo one minor change. Just throw the assert in there, remove the "precondition" comment, and we'll be good to go.

Thank you so much for your patience through all the back-and-forth.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-11T03:27:08Z
user: login: wbpcode
body: /lgtm api
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T03:37:19Z
user: login: tonya11en
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-11T16:58:56Z
user: login: abeyad
body: /lgtm api
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:08:44Z
user: login: wbpcode
body: LGTM. Thanks.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T02:28:50Z

Review comments:
diff_hunk: @@ -90,45 +98,46 @@ void GradientController::enterMinRTTSamplingWindow() {
     return;
   }
 
-  absl::MutexLock ml(&sample_mutation_mtx_);
+  absl::MutexLock ml(&sampleMutationMutex());
 
-  stats_.min_rtt_calculation_active_.set(1);
+  stats().min_rtt_calculation_active_.set(1);
 
   // Set the minRTT flag to indicate we're gathering samples to update the value. This will
   // prevent the sample window from resetting until enough requests are gathered to complete the
   // recalculation.
-  deferred_limit_value_.store(GradientController::concurrencyLimit());
+  deferred_limit_value_.store(DynamicGradientController::concurrencyLimit());
   updateConcurrencyLimit(config_.minConcurrency());
 
   // Throw away any latency samples from before the recalculation window as it may not represent
   // the minRTT.
-  hist_clear(latency_sample_hist_.get());
+  clearSamples();
 
-  min_rtt_epoch_ = time_source_.monotonicTime();
+  min_rtt_epoch_ = timeSource().monotonicTime();
path: source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc
user: login: wtzhang23
body: Pretty concerned about this block. I'd really like sampleMutationMutex to be abstracted within the base class to help prevent accidental double locking if this filter gets more complicated. Though a fix for this problem (e.g. passing in a closure to wrap a critical section, shrinking the critical section and moving the statements other than `clearSamples()` outside) may be worse than the solution.
created_at: 2025-02-23T17:38:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,13 +23,21 @@ Http::FilterFactoryCb AdaptiveConcurrencyFilterFactory::createFilterFactoryFromP
   using Proto = envoy::extensions::filters::http::adaptive_concurrency::v3::AdaptiveConcurrency;
   ASSERT(config.concurrency_controller_config_case() ==
          Proto::ConcurrencyControllerConfigCase::kGradientControllerConfig);
-  auto gradient_controller_config = Controller::GradientControllerConfig(
-      config.gradient_controller_config(), server_context.runtime());
-  controller = std::make_shared<Controller::GradientController>(
-      std::move(gradient_controller_config), server_context.mainThreadDispatcher(),
-      server_context.runtime(), acc_stats_prefix + "gradient_controller.", context.scope(),
-      server_context.api().randomGenerator(), server_context.timeSource());
-
+  if (config.has_gradient_controller_config()) {
+    auto gradient_controller_config = Controller::DynamicGradientControllerConfig(
+        config.gradient_controller_config(), server_context.runtime());
+    controller = std::make_shared<Controller::DynamicGradientController>(
+        std::move(gradient_controller_config), server_context.mainThreadDispatcher(),
+        acc_stats_prefix + "gradient_controller.", context.scope(),
+        server_context.api().randomGenerator(), server_context.timeSource());
+  } else {
+    ASSERT(config.has_pinned_gradient_controller_config());
+    auto gradient_controller_config = Controller::PinnedGradientControllerConfig(
+        config.pinned_gradient_controller_config(), server_context.runtime());
+    controller = std::make_shared<Controller::PinnedGradientController>(
+        std::move(gradient_controller_config), server_context.mainThreadDispatcher(),
+        acc_stats_prefix + "gradient_controller.", context.scope(), server_context.timeSource());
path: source/extensions/filters/http/adaptive_concurrency/config.cc
user: login: wtzhang23
body: Should I make the stats prefix unique?
created_at: 2025-02-23T23:42:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,33 +111,145 @@ class GradientControllerConfig : public Logger::Loggable<Logger::Id::filter> {
 
   using RuntimeKeys = ConstSingleton<RuntimeKeyValues>;
 
+private:
   Runtime::Loader& runtime_;
+  const std::chrono::milliseconds sample_rtt_calc_interval_;
+  const uint64_t max_concurrency_limit_;
+  const double sample_aggregate_percentile_;
+  const uint64_t min_concurrency_;
+  // The amount added to the measured minRTT as a hedge against natural variability in latency.
+  const double min_rtt_buffer_pct_;
+};
+
+class DynamicGradientControllerConfig : public Logger::Loggable<Logger::Id::filter>,
+                                        public GradientControllerConfig {
+public:
+  DynamicGradientControllerConfig(
+      const envoy::extensions::filters::http::adaptive_concurrency::v3::GradientControllerConfig&
+          proto_config,
+      Runtime::Loader& runtime);
+
+  std::chrono::milliseconds minRTTCalcInterval() const {
+    const auto ms = GradientControllerConfig::runtime().snapshot().getInteger(
+        GradientControllerConfig::RuntimeKeys::get().MinRTTCalcIntervalKey,
+        min_rtt_calc_interval_.count());
+    return std::chrono::milliseconds(ms);
+  }
+
+  uint32_t minRTTAggregateRequestCount() const {
+    return GradientControllerConfig::runtime().snapshot().getInteger(
+        GradientControllerConfig::RuntimeKeys::get().MinRTTAggregateRequestCountKey,
+        min_rtt_aggregate_request_count_);
+  }
+
+  // The percentage is normalized to the range [0.0, 1.0].
+  double jitterPercent() const {
+    const double val = GradientControllerConfig::runtime().snapshot().getDouble(
+        GradientControllerConfig::RuntimeKeys::get().JitterPercentKey, jitter_pct_);
+    return std::max(0.0, std::min(val, 100.0)) / 100.0;
+  }
 
+private:
   // The measured request round-trip time under ideal conditions.
   const std::chrono::milliseconds min_rtt_calc_interval_;
 
-  // The measured sample round-trip milliseconds from the previous time window.
-  const std::chrono::milliseconds sample_rtt_calc_interval_;
-
   // Randomized time delta added to the start of the minRTT calculation window.
   const double jitter_pct_;
 
-  // The maximum allowed concurrency value.
-  const uint32_t max_concurrency_limit_;
-
   // The number of requests to aggregate/sample during the minRTT recalculation.
   const uint32_t min_rtt_aggregate_request_count_;
+};
+using DynamicGradientControllerConfigSharedPtr = std::shared_ptr<DynamicGradientControllerConfig>;
 
-  // The percentile value considered when processing samples.
-  const double sample_aggregate_percentile_;
+class PinnedGradientControllerConfig : public Logger::Loggable<Logger::Id::filter>,
+                                       public GradientControllerConfig {
+public:
+  PinnedGradientControllerConfig(const envoy::extensions::filters::http::adaptive_concurrency::v3::
+                                     PinnedGradientControllerConfig& proto_config,
+                                 Runtime::Loader& runtime);
 
-  // The concurrency limit set while measuring the minRTT.
-  const uint32_t min_concurrency_;
+  std::chrono::nanoseconds minRTT() const { return min_rtt_; }
 
-  // The amount added to the measured minRTT as a hedge against natural variability in latency.
-  const double min_rtt_buffer_pct_;
+private:
+  // The fixed minRTT value.
+  const std::chrono::nanoseconds min_rtt_;
+};
+using PinnedGradientControllerConfigSharedPtr = std::shared_ptr<PinnedGradientControllerConfig>;
+
+class GradientController : public ConcurrencyController {
+public:
+  GradientController(GradientControllerConfig& config, Event::Dispatcher& dispatcher,
+                     const std::string& stats_prefix, Stats::Scope& scope, TimeSource& time_source);
+
+  // ConcurrencyController
+  RequestForwardingAction forwardingDecision() override;
+  void recordLatencySample(MonotonicTime rq_send_time) override;
+  void cancelLatencySample() override;
+  uint32_t concurrencyLimit() const override { return concurrency_limit_.load(); }
+
+  // Used in unit tests to validate worker thread interactions.
+  Thread::ThreadSynchronizer& synchronizer() { return synchronizer_; }
+
+protected:
+  virtual bool shouldSampleRTT() const { return true; }
+  virtual bool shouldRecordSample(MonotonicTime /*MonotonicTime*/) const { return true; }
+  virtual void recalculateAfterSample() ABSL_EXCLUSIVE_LOCKS_REQUIRED(sample_mutation_mtx_) {}
+  virtual std::chrono::nanoseconds minRTT() const = 0;
+  virtual void processConcurrencyLimitUpdate(int /*consecutive_min_concurrency_set*/) {}
+
+  Event::Dispatcher& dispatcher() { return dispatcher_; }
+  GradientControllerStats& stats() { return stats_; }
+  TimeSource& timeSource() { return time_source_; }
path: source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.h
user: login: wtzhang23
body: Maybe I should just make these fields protected instead of creating accessors.
created_at: 2025-02-23T23:44:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,21 +61,35 @@ GradientController::GradientController(GradientControllerConfig config,
       resetSampleWindow();
     }
 
-    sample_reset_timer_->enableTimer(config_.sampleRTTCalcInterval());
+    enableSamplingTimer();
   });
+  stats_.concurrency_limit_.set(config_.minConcurrency());
path: source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc
user: login: wtzhang23
body: Changed from `stats_.concurrency_limit_.set(concurrency_limit_.load())`. I think this should be equivalent?
created_at: 2025-02-23T23:48:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,8 +63,8 @@ if [[ "$ARCH" == "x86_64" ]]; then
   PLATFORM="linux-amd64"
path: bazel/external/aws_lc.genrule_cmd
user: login: wtzhang23
body: Affecting linter; should be in a separate PR
created_at: 2025-02-24T13:49:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,43 +22,37 @@ namespace HttpFilters {
 namespace AdaptiveConcurrency {
 namespace Controller {
 
-GradientControllerConfig::GradientControllerConfig(
+DynamicGradientControllerConfig::DynamicGradientControllerConfig(
path: source/extensions/filters/http/adaptive_concurrency/controller/gradient_controller.cc
user: login: wtzhang23
body: Will move the dynamic constructor/methods into a different file after initial review.
created_at: 2025-02-24T13:50:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: nit: s/are//
created_at: 2025-02-27T01:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: What is concurrency referring to?
created_at: 2025-02-27T01:39:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: Since there's a min_concurrency, should this just be renamed to max_concurrency?  Also, should we group them together at the bottom?
created_at: 2025-02-27T01:39:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,6 +134,10 @@ message AdaptiveConcurrency {
     // Gradient concurrency control will be used.
     GradientControllerConfig gradient_controller_config = 1
         [(validate.rules).message = {required: true}];
+
+    // Pinned gradient concurrency control will be used.
+    PinnedGradientControllerConfig pinned_gradient_controller_config = 4
+        [(validate.rules).message = {required: true}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: why is this a required field?
created_at: 2025-02-27T01:41:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: Concurrency here refers to a cap on the number of pending requests.
created_at: 2025-02-27T02:02:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: This is copied exactly taken from the existing controller. I don't have a huge preference, but I kept it the same for symmetry reasons. However it may be an opportunity to clean up the configuration.
created_at: 2025-02-27T02:23:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: I think this is meant to read as `The time period samples are taken...` but can be confusingly read as `the period of (time samples) are taken...`. This comment was also copied over from the existing controller.
created_at: 2025-02-27T02:25:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,6 +134,10 @@ message AdaptiveConcurrency {
     // Gradient concurrency control will be used.
     GradientControllerConfig gradient_controller_config = 1
         [(validate.rules).message = {required: true}];
+
+    // Pinned gradient concurrency control will be used.
+    PinnedGradientControllerConfig pinned_gradient_controller_config = 4
+        [(validate.rules).message = {required: true}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: I'm not sure how protovalidate works in this `oneof` case. Should both be not required? Does required mean that the variant must be configured? And if I remove the required, will it break backwards compatibility?
created_at: 2025-02-27T02:27:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wbpcode
body: I think this message copied the `ConcurrencyLimitCalculationParams` at `GradientControllerConfig`? Please don't do that.
created_at: 2025-02-27T03:00:59Z
author_association: MEMBER
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
+    google.protobuf.Duration concurrency_update_interval = 3 [(validate.rules).duration = {
+      required: true
+      gt {}
+    }];
+  }
+
+  // The percentile to use when summarizing aggregated samples. Defaults to p50.
+  type.v3.Percent sample_aggregate_percentile = 1;
+
+  ConcurrencyLimitCalculationParams concurrency_limit_params = 2
+      [(validate.rules).message = {required: true}];
+
+  google.protobuf.Duration min_rtt = 3 [(validate.rules).duration = {
+    required: true
+    gt {}
+  }];
+
+  google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wbpcode
body: The new configuration is almost same with the original `GradientControllerConfig`, execept the `min_rtt` is fixed value. It would be better to enhance the `GradientControllerConfig` or achieve your target.

It's pretty weird to see so similar to messages for same filter.
created_at: 2025-02-27T03:04:56Z
author_association: MEMBER
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
+    google.protobuf.Duration concurrency_update_interval = 3 [(validate.rules).duration = {
+      required: true
+      gt {}
+    }];
+  }
+
+  // The percentile to use when summarizing aggregated samples. Defaults to p50.
+  type.v3.Percent sample_aggregate_percentile = 1;
+
+  ConcurrencyLimitCalculationParams concurrency_limit_params = 2
+      [(validate.rules).message = {required: true}];
+
+  google.protobuf.Duration min_rtt = 3 [(validate.rules).duration = {
+    required: true
+    gt {}
+  }];
+
+  google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: We were concerned about backwards compatibility in my initial [commit](https://github.com/envoyproxy/envoy/pull/38359/commits/b7b7a654a162de809fd55c3e1e51f77d542a32ff) which just added a fixed-minrtt field, due to [needing to remove `required`](https://github.com/envoyproxy/envoy/pull/38359#issuecomment-2648895924).
created_at: 2025-02-27T03:48:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
+    google.protobuf.Duration concurrency_update_interval = 3 [(validate.rules).duration = {
+      required: true
+      gt {}
+    }];
+  }
+
+  // The percentile to use when summarizing aggregated samples. Defaults to p50.
+  type.v3.Percent sample_aggregate_percentile = 1;
+
+  ConcurrencyLimitCalculationParams concurrency_limit_params = 2
+      [(validate.rules).message = {required: true}];
+
+  google.protobuf.Duration min_rtt = 3 [(validate.rules).duration = {
+    required: true
+    gt {}
+  }];
+
+  google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wbpcode
body: More loose restriction is acceptable accoriding our API policy.
created_at: 2025-02-27T06:21:42Z
author_association: MEMBER
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
+    google.protobuf.Duration concurrency_update_interval = 3 [(validate.rules).duration = {
+      required: true
+      gt {}
+    }];
+  }
+
+  // The percentile to use when summarizing aggregated samples. Defaults to p50.
+  type.v3.Percent sample_aggregate_percentile = 1;
+
+  ConcurrencyLimitCalculationParams concurrency_limit_params = 2
+      [(validate.rules).message = {required: true}];
+
+  google.protobuf.Duration min_rtt = 3 [(validate.rules).duration = {
+    required: true
+    gt {}
+  }];
+
+  google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: If we can get api consensus for that commit, I'm happy to revert back to that change. Would make all our lives easier.
created_at: 2025-02-27T11:44:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
+    google.protobuf.Duration concurrency_update_interval = 3 [(validate.rules).duration = {
+      required: true
+      gt {}
+    }];
+  }
+
+  // The percentile to use when summarizing aggregated samples. Defaults to p50.
+  type.v3.Percent sample_aggregate_percentile = 1;
+
+  ConcurrencyLimitCalculationParams concurrency_limit_params = 2
+      [(validate.rules).message = {required: true}];
+
+  google.protobuf.Duration min_rtt = 3 [(validate.rules).duration = {
+    required: true
+    gt {}
+  }];
+
+  google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: Hmm, didn't know about `GradientControllerConfig`.  If just fixed RTT vs. computed RTT is the only difference, we can just add a new field in the existing message, so I agree with @wbpcode 's suggestion. I don't think removing `required` would present a backward compatibility issue, b/c all existing users would already have it set.
created_at: 2025-02-27T15:14:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +91,39 @@ message GradientControllerConfig {
   MinimumRTTCalculationParams min_rtt_calc_params = 3 [(validate.rules).message = {required: true}];
 }
 
+// Configuration parameters for the pinned gradient controller.
+message PinnedGradientControllerConfig {
+  // Parameters controlling the periodic recalculation of the concurrency limit from sampled request
+  // latencies.
+  message ConcurrencyLimitCalculationParams {
+    option (udpa.annotations.versioning).previous_message_type =
+        "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
+        "ConcurrencyLimitCalculationParams";
+
+    // The allowed upper-bound on the calculated concurrency limit. Defaults to 1000.
+    google.protobuf.UInt32Value max_concurrency_limit = 2 [(validate.rules).uint32 = {gt: 0}];
+
+    // The period of time samples are taken to recalculate the concurrency limit.
+    google.protobuf.Duration concurrency_update_interval = 3 [(validate.rules).duration = {
+      required: true
+      gt {}
+    }];
+  }
+
+  // The percentile to use when summarizing aggregated samples. Defaults to p50.
+  type.v3.Percent sample_aggregate_percentile = 1;
+
+  ConcurrencyLimitCalculationParams concurrency_limit_params = 2
+      [(validate.rules).message = {required: true}];
+
+  google.protobuf.Duration min_rtt = 3 [(validate.rules).duration = {
+    required: true
+    gt {}
+  }];
+
+  google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: Reverted back
created_at: 2025-02-27T16:46:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +71,9 @@ message GradientControllerConfig {
     // The concurrency limit set while measuring the minRTT. Defaults to 3.
     google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
 
+    // The base value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    google.protobuf.Duration base_value = 6 [(validate.rules).duration = {gt {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: question: what happens if `interval` < 1ms and `base_value` is not set?
created_at: 2025-02-27T19:27:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +71,9 @@ message GradientControllerConfig {
     // The concurrency limit set while measuring the minRTT. Defaults to 3.
     google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
 
+    // The base value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    google.protobuf.Duration base_value = 6 [(validate.rules).duration = {gt {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: Envoy rejects the config (it gets rounded down to 0ms)
created_at: 2025-02-27T19:44:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +71,9 @@ message GradientControllerConfig {
     // The concurrency limit set while measuring the minRTT. Defaults to 3.
     google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
 
+    // The base value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    google.protobuf.Duration base_value = 6 [(validate.rules).duration = {gt {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: abeyad
body: Might be good to note that in the comments, thanks!
created_at: 2025-02-27T19:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +71,9 @@ message GradientControllerConfig {
     // The concurrency limit set while measuring the minRTT. Defaults to 3.
     google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
 
+    // The base value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    google.protobuf.Duration base_value = 6 [(validate.rules).duration = {gt {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: Added clarifying statement.
created_at: 2025-02-28T00:41:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,18 +47,16 @@ message GradientControllerConfig {
   }
 
   // Parameters controlling the periodic minRTT recalculation.
-  // [#next-free-field: 6]
+  // [#next-free-field: 7]
   message MinimumRTTCalculationParams {
     option (udpa.annotations.versioning).previous_message_type =
         "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
         "MinimumRTTCalculationParams";
 
     // The time interval between recalculating the minimum request round-trip time. Has to be
-    // positive.
-    google.protobuf.Duration interval = 1 [(validate.rules).duration = {
-      required: true
-      gte {nanos: 1000000}
-    }];
+    // positive. If set to zero, dynamic sampling of the minRTT is disabled. This value must not
+    // be set to a value in the range (0, 1ms).
+    google.protobuf.Duration interval = 1 [(validate.rules).duration = {gte {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: tonya11en
body: I think it makes more sense to have `base_value` to take precedence over `interval` if set. Keep the `gte {nanos: 1000000}` and if the interval is not set the value read should be 0. This way there is less to remember and the value cannot be set to something invalid before reaching the filter config ingestion.

It might also make sense to change the name from `base_value` to `static_minrtt` or something along those lines. It makes it clearer IMO. 
created_at: 2025-03-02T23:10:03Z
author_association: MEMBER
diff_hunk: @@ -74,6 +72,10 @@ message GradientControllerConfig {
     // The concurrency limit set while measuring the minRTT. Defaults to 3.
     google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
 
+    // The base value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    // If dynamic sampling of the minRTT is disabled, this field must be set.
+    google.protobuf.Duration base_value = 6 [(validate.rules).duration = {gt {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: tonya11en
body: nit: Can you move this under `interval` since they are related?
created_at: 2025-03-02T23:14:03Z
author_association: MEMBER
diff_hunk: @@ -47,18 +47,16 @@ message GradientControllerConfig {
   }
 
   // Parameters controlling the periodic minRTT recalculation.
-  // [#next-free-field: 6]
+  // [#next-free-field: 7]
   message MinimumRTTCalculationParams {
     option (udpa.annotations.versioning).previous_message_type =
         "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
         "MinimumRTTCalculationParams";
 
     // The time interval between recalculating the minimum request round-trip time. Has to be
-    // positive.
-    google.protobuf.Duration interval = 1 [(validate.rules).duration = {
-      required: true
-      gte {nanos: 1000000}
-    }];
+    // positive. If set to zero, dynamic sampling of the minRTT is disabled. This value must not
+    // be set to a value in the range (0, 1ms).
+    google.protobuf.Duration interval = 1 [(validate.rules).duration = {gte {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: I renamed it to something generic just in case there was a future where it could be useful in addition to having a non-zero minRTT calculation interval (though don't intend to suggest much less implement it since internally we don't have a need for this). For example, for services that do not receive a consistent flow of traffic or there's a site-wide outage, this value could be used to have the gradient decay back to a normal value faster (though its probably better to just have a separate field like "idle interval" that resets the concurrency value which is what our internal non-envoy library implementation does). But maybe, I'm over-considering the future; let me know as I'm ok with having it renamed to `static_minrtt` or `fixed_value` like it was before. 
created_at: 2025-03-02T23:28:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,18 +47,16 @@ message GradientControllerConfig {
   }
 
   // Parameters controlling the periodic minRTT recalculation.
-  // [#next-free-field: 6]
+  // [#next-free-field: 7]
   message MinimumRTTCalculationParams {
     option (udpa.annotations.versioning).previous_message_type =
         "envoy.config.filter.http.adaptive_concurrency.v2alpha.GradientControllerConfig."
         "MinimumRTTCalculationParams";
 
     // The time interval between recalculating the minimum request round-trip time. Has to be
-    // positive.
-    google.protobuf.Duration interval = 1 [(validate.rules).duration = {
-      required: true
-      gte {nanos: 1000000}
-    }];
+    // positive. If set to zero, dynamic sampling of the minRTT is disabled. This value must not
+    // be set to a value in the range (0, 1ms).
+    google.protobuf.Duration interval = 1 [(validate.rules).duration = {gte {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: Reverted back to `fixed_value`.
created_at: 2025-03-02T23:56:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +72,10 @@ message GradientControllerConfig {
     // The concurrency limit set while measuring the minRTT. Defaults to 3.
     google.protobuf.UInt32Value min_concurrency = 4 [(validate.rules).uint32 = {gt: 0}];
 
+    // The base value for the minRTT. This value is used when minRTT is not sampled dynamically.
+    // If dynamic sampling of the minRTT is disabled, this field must be set.
+    google.protobuf.Duration base_value = 6 [(validate.rules).duration = {gt {}}];
path: api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto
user: login: wtzhang23
body: Moved up.
created_at: 2025-03-02T23:56:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: repokitteh-read-only[bot]
created_at: 2025-02-08T20:43:15Z
body: 
As a reminder, PRs marked as draft will not be automatically assigned reviewers,
or be handled by maintainer-oncall triage.

Please mark your PR as ready when you want it to be reviewed!



<details>
	<summary>:cat:</summary>

Caused by: https://github.com/envoyproxy/envoy/pull/38359 was opened by wtzhang23.

see: [more](https://github.com/envoyproxy/envoy/pull/38359), [trace](https://prod.repokitteh.app/traces/ui/envoyproxy/envoy/4ee62f70-e65d-11ef-91b3-77ab562b06b3).
</details>
author_association: NONE
user: login: repokitteh-read-only[bot]
created_at: 2025-02-08T20:43:19Z
body: CC @envoyproxy/api-shepherds: Your approval is needed for changes made to `(api/envoy/|docs/root/api-docs/)`.
envoyproxy/api-shepherds assignee is @abeyad
CC @envoyproxy/api-watchers: FYI only for changes made to `(api/envoy/|docs/root/api-docs/)`.


<details>
	<summary>:cat:</summary>

Caused by: https://github.com/envoyproxy/envoy/pull/38359 was opened by wtzhang23.

see: [more](https://github.com/envoyproxy/envoy/pull/38359), [trace](https://prod.repokitteh.app/traces/ui/envoyproxy/envoy/4ee62f70-e65d-11ef-91b3-77ab562b06b3).
</details>
author_association: NONE
user: login: wtzhang23
created_at: 2025-02-08T21:17:13Z
body: Some comments:
* I think for correctness, it may be useful to have this and the dynamic minrtt learner as a typed extension. That way, we don't have to worry about conditional critical sections, which I find risky. But for simplicity, I modified the existing controller.
* Another alternative is to instead of fixing minrtt, define min and max fields that act as clamps. This could be used by customers to signal they either don't care about tuning minrtt under a certain latency, or they don't care about how "truly" performant their service is and want a tight bound on latency. However, this still requires customers to tune the `min_concurrency` parameter and still requires "exploration phases" learning the minrtt.
author_association: CONTRIBUTOR
user: login: tonya11en
created_at: 2025-02-10T18:28:51Z
body: @wtzhang23 thanks for submitting the PR! We can't remove the `required` fields without causing incompatibilities, unfortunately. Can you implement this as a new concurrency controller? You should be able to just implement the interface. I suspect it won't be much more difficult, since you've already written the tests for this and the bulk of the complexity in the existing controller is the `min_rtt` calculation.

Let me know if this proves to be difficult and I can give it some more attention. Thanks!
author_association: MEMBER
user: login: wtzhang23
created_at: 2025-02-10T18:40:05Z
body: Sure. @tonya11en would you like this as a typed extension in case more controllers pop up?
author_association: CONTRIBUTOR
user: login: tonya11en
created_at: 2025-02-10T18:58:54Z
body: > Sure. @tonya11en would you like this as a typed extension in case more controllers pop up?

I don't think you need to go through all that. You should be able to just make a new config for the controller and add it to:
https://github.com/envoyproxy/envoy/blob/feaa04281d91a8977e79cb0c7f273412ebd835a6/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto#L99-L105

Then just instantiate the appropriate controller here, based on whichever controller gets configured:
https://github.com/envoyproxy/envoy/blob/main/source/extensions/filters/http/adaptive_concurrency/config.cc#L15
author_association: MEMBER
user: login: wtzhang23
created_at: 2025-02-17T01:02:58Z
body: Quickly sketched out the implementation. My C++ is a little rusty so feel free to critique it as harshly as you'd like :) (e.g. whether multiple inheritance is bad here and composition is preferred, or whether it's better to have the config class fields duplicated).

Another problem is this is using protobuf oneofs which from what I remember is advised against.
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-02-20T15:24:29Z
body: @tonya11en I was thinking about sharing the same logic between the two for sampling the sample RTT, but:
1) Didn't see much value in it since the shared logic is small
2) Think that spawning a timer in another location (e.g. a shared base class or another class) would be confusing.

Let me know your thoughts.
author_association: CONTRIBUTOR
user: login: tonya11en
created_at: 2025-02-20T19:10:51Z
body: I haven't taken another pass yet, but I'll let you decide on the cleanest approach. Let me know when you think it's ready and I'll review. Seems like there are some conflicts that need to be resolved at the moment.

/wait
author_association: MEMBER
user: login: wtzhang23
created_at: 2025-02-25T14:53:58Z
body: @tonya11en I think it is ready now. I ended up sharing a base class. PTAL when you have the time. There's no urgency getting a review right now though.
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-02-27T03:50:32Z
body: @wbpcode 
> I think this message copied the ConcurrencyLimitCalculationParams at GradientControllerConfig? Please don't do that.

Would you prefer I extract it out of the original config (not sure if this is backwards compatible; I think the bytes encoding should be the same but the reference would break) or reference the submessage explicitly in PinnedGradientControllerConfig via GradientControllerConfig.ConcurrencyLimitCalculationParams?
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-02-27T16:46:20Z
body: Reverted and renamed to `base_value` in case there is ever a use-case where we want to use that value but also want sampling to occur. 
author_association: CONTRIBUTOR
user: login: abeyad
created_at: 2025-02-27T19:45:55Z
body: /lgtm api


author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-03-09T22:44:56Z
body: Gentle bump :)
@abeyad can I have a rereview of the API? Only thing that changed were comments and field reorderings, and renamed `base_value` back to `fixed_value`.
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-03-11T04:34:01Z
body: @tonya11en thanks for you and all of y'all's assistance reviewing this too! Especially as I'm sure we all have busy schedules outside of Envoy
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-03-13T02:59:24Z
body: @abeyad sorry for tagging you again. Think you can /lgtm api again? For the future, I'll remember only to ask once the code changes are approved.

@wbpcode think the changes requested block can be dropped?
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-03-16T22:26:43Z
body: Thanks again for y'all's review. What's the next steps I need to do to get this merged?
author_association: CONTRIBUTOR
user: login: abeyad
created_at: 2025-03-17T14:57:28Z
body: > Thanks again for y'all's review. What's the next steps I need to do to get this merged?

Looks like @wbpcode requested some change that hasn't yet been incorporated, after that, @wbpcode can merge it as an Envoy maintainer.  Thanks!
author_association: CONTRIBUTOR
user: login: wtzhang23
created_at: 2025-03-17T15:05:02Z
body: @abeyad I think @wbpcode's comments were for the changes I made prior to the revert back to the simplified API. Though I may be not finding the exact change requested in which case I apologize in advance.
author_association: CONTRIBUTOR
user: login: abeyad
created_at: 2025-03-17T15:24:04Z
body: I think @wbpcode just needs to change his review from "Requested Changes" to "Approve" once he's good with the PR, then he can merge it.
author_association: CONTRIBUTOR