You are viewing a plain text version of this content. The canonical link for it is here.
Posted to jira@kafka.apache.org by GitBox <gi...@apache.org> on 2020/07/03 10:20:27 UTC
[GitHub] [kafka] dajac opened a new pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
dajac opened a new pull request #8977:
URL: https://github.com/apache/kafka/pull/8977
This PR does two things:
1. It introduces `QuotaEnforcementType` which allows to choose when the quota enforcement is done within the `Sensor`. The default `Permissive` mode works as previously. It enforces the quota after having accounted the new value. The new `Strict` mode enforces the quota before and thus does not account the value if the quota is violated.
2. It introduces a new `SampledStat` names `TokenBucket`. Our current quota enforcement mechanism does not cope well with bursty workloads, especially when the burst is significantly larger than the quota. The reason is that a large recorded burst remains in the samples until it gets pasted the window. During this time, the computed rate remains the same until it gets dropped. The `TokenBucket` sampled stat alleviate this issue by decreasing each sample by Quota * the time between itself and the previous sample. This mimics the token bucket algorithm which gives back credits at every second.
### Committer Checklist (excluded from commit message)
- [ ] Verify design and implementation
- [ ] Verify test coverage and CI build status
- [ ] Verify documentation (including upgrade notes)
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r452166182
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
Review comment:
1. It does not cause a problem here. As you said, `purgeObsoleteSamples` sets the value to zero and the lastWindowMs to now when it resets a sample. When the past `lastWindowMs` is higher that the current sample `lastWindowMs` or `now`, it has no effect: `Math.max(0, nowMs - lastWindowMs)`.
2. Right. I take current sample's lastWindowMs - previous sample's lastWindowMs to take this into account.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r454366138
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
Review comment:
We are on the same page regarding the token bucket algorithm.
My interpretation of backfilling was indeed wrong from a Token Bucket perspective. Thanks for clarifying this. I think that I understand the mechanics now but I am not sure to completely follow the last paragraph. This condition only applies if we are at the last sample already, isn't it? Also, you are basically saying that we must maintain the sum in oder to be able to take it into account, right? Instead, couldn't we carry on the amount that is above the quota of the previous sample into the new sample?
I am currently trying to implement this to compare with my current implementation. The implementation is quite complex if we compare to the current proposal. Let's see how it goes.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451871529
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/Sensor.java
##########
@@ -97,7 +97,25 @@ public static RecordingLevel forName(String name) {
public boolean shouldRecord(final int configId) {
return configId == DEBUG.id || configId == this.id;
}
+ }
+ public enum QuotaEnforcementType {
+ /**
+ * The quota is not enforced.
+ */
+ NONE,
+
+ /**
+ * The quota is enforced before updating the sensor. An update
+ * is rejected if the quota is already exhausted.
Review comment:
nit: somehow the first sentence is confusing because it seem to imply that the sensor gets updated in this case as well. Do you think maybe this sounds a bit clearer? "The check for quota violation is done prior to recording the value. If the quota is already exhausted, the value is not recorded into the sensor." Or perhaps some other Up to you.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451932855
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
Review comment:
I was not able to prove to myself that this algorithm is equivalent to a token bucket. It does seem to smooth out a large sample, but it is hard to understand its behavior and to reason about it.
There are couple of places where I think assumptions need to be checked:
1) `SampledStat.purgeObsoleteSamples` implementation sets current time (as of the method call) to every sample it resets. Would this cause a problem here? It seems like this code assumes that each next sample will have a timestamp that gets advanced correctly to (start time of the previous sample) + (time length of the sample).
2) Also, in situations where a gap between recorded values is longer than an interval of one sample, the `samples` list may have "holes" where a next sample in the list is two samples ahead if we consider continuous time. So because `samples` only contains samples with values, and skips the ones without recorded values, would the logic with tracking `lastWindowMs` work in all cases?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r454148540
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
Review comment:
Let me respond to the "backfilling" algorithm part first and I still need to think about the "combining samples" algorithm. The term backfilling is probably misleading for the algorithm I described, and it is different to the one you described.
The algorithm that you described would record a value into a sample corresponding to a current time, even if older samples have unused credits (quota - sample.value). I am assuming 1-second samples. This breaks token bucket properties because it enables the situation where we could expire credits that should have been used while we used "newer credits" (where credits being "newer" or "older" is an artifact of us using samples, and so we also need to be careful to use oldest credits first if we are implementing token bucket). Example (6 samples, quota = 5 → corresponds to max Burst = 30):
Record 2 at time T
Record 3 at time T + 3
Record 30 at time T + 6. At time T + 6, we would first expire the oldest sample, and here we are expiring 3 unused credits that could have been used when we recorded a value at time T + 3.
I wanted to take one step back to get on the same page, because we are implementing the token bucket somewhat backwards (because of existing code) and this makes it much more confusing relative to how simple the token bucket algorithm is.
A token bucket accumulates tokens with a constant rate R, one token per 1/R seconds, up to a maximum burst size B. Lets say, 1 token gives a permission to do 1 unit of work. Without the burst size, we would have to do a unit of work within 1/R seconds (until the next token gets created). The burst size essentially means that we keep the permission to do a unit of work for up to B / R seconds. When we reach a burst size and do not accumulate any more tokens, we can think about it as the oldest token expiring and the newly created token replacing the oldest one.
Adapting this to SampledSet, we are accumulating tokens in chunks of Q units by default (sample length = 1 second) instead one token every 1/R second and expiring in chunks of Q units too (when we expire the oldest sample). The current implementation records the value into a sample based on a timestamp: a sample is considered complete (Sample.isComplete) if the current time is outside the sample.
The algorithm I was proposing was to not "complete" the sample and keep it current, even if it is already in the past as long as it is not purged from the set, until we fill that sample up to sample.value = Q (used all credits). This ensures that we use the oldest credits first that have not yet expired. In other words, if we purge a sample and its value < Quota, there should be no newer samples with recorded values.
If we have to record a value even if this would violate the quota, we need to generalize the condition to when the sample is considered complete and make sure that we don't give credits that we do not have (based on rate and burst).
Suppose, we filled in all 6 samples up to quota each: [5, 5, 5, 5, 5, 5], and we have to record 3 at time corresponding to the last sample. So, we record 3 into the last sample: [5, 5, 5, 5, 5, 8]. Then, we record 3 at time that corresponds to a new sample (because we have no credits from the past), and expire the oldest one: 5 5 5 5 8 3. If the next value gets recorded at the time that corresponds to the next sample, we should not consider 3 < 5 (Q) as having credits. So, the condition for when a sample is considered complete:
Current time advanced passed the sample AND sum of values in all samples in SampleSet >= (Q * MetricConfig.samples), which is basically when the sum of values reached the burst size (the maximum number of credits).
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451932855
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
Review comment:
I was not able to prove to myself that this algorithm is equivalent to a token bucket. It does seem to smooth out a large sample, but it is hard to understand its behavior and to reason about it.
There are couple of places where I think assumptions need to be checked:
1) `SampledStat.purgeObsoleteSamples` implementation sets current time (of that method call) to every sample it resets. Would this cause a problem here? It seems like this code assumes that each next sample will have a timestamp that gets advanced correctly to <previous sample time> + <time length of the sample>.
2) Also, in situations where a gap between recorded values is longer than an interval of one sample, the `samples` list may have "holes" where a next sample in the list is two samples ahead if we consider continuous time. So because `samples` only contains samples with values, and skips the ones without recorded values, would the logic with tracking `lastWindowMs` work in all cases?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451932855
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
Review comment:
I was not able to prove to myself that this algorithm is equivalent to a token bucket. It does seem to smooth out the large sample, but it is hard to understand its behavior and to reason about it.
There are couple of places where I think assumptions need to be checked:
1) `SampledStat.purgeObsoleteSamples` implementation sets current time (of that method call) to every sample it resets. Would this cause a problem here? It seems like this code assumes that each next sample will have a timestamp that gets advanced correctly to <previous sample time> + <time length of the sample>.
2) Also, in situations where a gap between recorded values is longer than an interval of one sample, the `samples` list may have "holes" where a next sample in the list is two samples ahead if we consider continuous time. So because `samples` only contains samples with values, and skips the ones without recorded values, would the logic with tracking `lastWindowMs` work in all cases?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#issuecomment-663462152
I have opened a new proper PR: https://github.com/apache/kafka/pull/9072
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r452162969
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/Sensor.java
##########
@@ -97,7 +97,25 @@ public static RecordingLevel forName(String name) {
public boolean shouldRecord(final int configId) {
return configId == DEBUG.id || configId == this.id;
}
+ }
+ public enum QuotaEnforcementType {
+ /**
+ * The quota is not enforced.
+ */
+ NONE,
+
+ /**
+ * The quota is enforced before updating the sensor. An update
+ * is rejected if the quota is already exhausted.
Review comment:
That is indeed much better.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451932855
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
Review comment:
I was not able to prove to myself that this algorithm is equivalent to a token bucket. It does seem to smooth out a large sample, but it is hard to understand its behavior and to reason about it.
There are couple of places where I think assumptions need to be checked:
1) `SampledStat.purgeObsoleteSamples` implementation sets current time (as of the method call) to every sample it resets. Would this cause a problem here? It seems like this code assumes that each next sample will have a timestamp that gets advanced correctly to (previous sample time) + (time length of the sample).
2) Also, in situations where a gap between recorded values is longer than an interval of one sample, the `samples` list may have "holes" where a next sample in the list is two samples ahead if we consider continuous time. So because `samples` only contains samples with values, and skips the ones without recorded values, would the logic with tracking `lastWindowMs` work in all cases?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r454366138
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
Review comment:
We are on the same page regarding the token bucket algorithm.
My interpretation of backfilling was indeed wrong from a Token Bucket perspective. Thanks for clarifying this. I think that I understand the mechanics now but I am not sure to completely follow the last paragraph. This condition only applies if we are at the last sample already, isn't it? Also, you are basically saying that we must maintain the sum in oder to be able to take it into account, right? Instead, couldn't we carry on the amount that is above the quota of the previous sample into the new sample?
I am currently trying to implement this to compare with my current implementation. Let's see how it goes.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451932855
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
Review comment:
I was not able to prove to myself that this algorithm is equivalent to a token bucket. It does seem to smooth out a large sample, but it is hard to understand its behavior and to reason about it.
There are couple of places where I think assumptions need to be checked:
1) `SampledStat.purgeObsoleteSamples` implementation sets current time (as of the method call) to every sample it resets. Would this cause a problem here? It seems like this code assumes that each next sample will have a timestamp that gets advanced correctly to <previous sample time> + <time length of the sample>.
2) Also, in situations where a gap between recorded values is longer than an interval of one sample, the `samples` list may have "holes" where a next sample in the list is two samples ahead if we consider continuous time. So because `samples` only contains samples with values, and skips the ones without recorded values, would the logic with tracking `lastWindowMs` work in all cases?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] apovzner commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
apovzner commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r451937131
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
Review comment:
I think the following implementation of extension of a SampleStat would work close to TokenBucket. Instead of over-writing `combine()`, we could over-write `record()`. Instead of the current implementation of recording the value to a sample that corresponds to the current time, we backfill the oldest sample -- the one that still did not expire (within the whole window size) and whose value < quota * (sample length in seconds). Backfilling is equivalent to using "tokens" (unused quota for that sample) of samples that are still within the burst size (number of samples * sample length).
I think this would entail:
* overwriting Sample.isComplete to return true only if current condition AND value >= quota * sample length
* perhaps keep track of last incomplete sample;
* when backfilling last incomplete sample, only add the portion of the value until the quota for that sample is reached. Spill the remaining part of the value into the next sample.
This way, expiring samples will also "expire" the values in those samples that we backfilled because they had unused quota.
What do you think about this?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r455080029
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
+ int current = (startIndex + i) % this.samples.size();
+ Sample sample = this.samples.get(current);
+
+ // The total is decreased by (last window - current window) * quota. We basically
+ // pay back the amount of credits spent until we reach zero (dept fully paid).
+ total = adjustedTotal(total, quota, lastWindowMs, sample.lastWindowMs);
+
+ // The current sample is added to the total. If the sample is a negative number,
+ // we ensure that total remains >= 0. A negative number can be used to give back
+ // credits if there were not used. If the number of credits given back is higher
+ // than the current dept, we basically assume that the dept is paid off.
+ total = Math.max(0, total + sample.value);
+
+ // Memorize the current window so we can use for the next sample.
+ lastWindowMs = sample.lastWindowMs;
+ }
+
+ return adjustedTotal(total, quota, lastWindowMs, now);
+ }
+
+ private double adjustedTotal(double total, double quota, long lastWindowMs, long nowMs) {
+ return Math.max(0, total - delta(quota, lastWindowMs, nowMs));
+ }
+
+ private double delta(double quota, long lastWindowMs, long nowMs) {
+ return quota * convert(Math.max(0, nowMs - lastWindowMs));
+ }
+
+ private double convert(long timeMs) {
+ switch (unit) {
+ case NANOSECONDS:
+ return timeMs * 1000.0 * 1000.0;
+ case MICROSECONDS:
+ return timeMs * 1000.0;
+ case MILLISECONDS:
+ return timeMs;
+ case SECONDS:
+ return timeMs / 1000.0;
+ case MINUTES:
+ return timeMs / (60.0 * 1000.0);
+ case HOURS:
+ return timeMs / (60.0 * 60.0 * 1000.0);
+ case DAYS:
+ return timeMs / (24.0 * 60.0 * 60.0 * 1000.0);
+ default:
+ throw new IllegalStateException("Unknown unit: " + unit);
+ }
Review comment:
I just copy-pasted the code from another internal class and I have to admit that I was not aware of `TimeUnit.convert()`. Thanks for pointing that out.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r449633334
##########
File path: clients/src/test/java/org/apache/kafka/common/metrics/SensorTest.java
##########
@@ -209,4 +212,139 @@ public void shouldReturnPresenceOfMetrics() {
assertThat(sensor.hasMetrics(), is(true));
}
-}
\ No newline at end of file
+
+ @Test
+ public void testStrictQuotaEnforcement() {
+ final Time time = new MockTime(0, 0, 0);
+ final Metrics metrics = new Metrics(time);
+ final Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+ .quota(Quota.upperBound(10))
+ .timeWindow(1, TimeUnit.SECONDS)
+ .samples(11));
+ final MetricName metricName = metrics.metricName("rate", "test-group");
+ assertTrue(sensor.add(metricName, new Rate()));
+ final KafkaMetric rateMetric = metrics.metric(metricName);
+
+ // Recording a first value at T+0s to bring the avg rate to 9. Value is accepted
+ // because the quota is not exhausted yet.
+ sensor.record(90, time.milliseconds(), QuotaEnforcementType.STRICT);
+ assertEquals(9, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a second value at T+1s to bring the avg rate to 18. Value is accepted
+ // because the quota is not exhausted yet.
+ time.sleep(1000);
+ sensor.record(90, time.milliseconds(), QuotaEnforcementType.STRICT);
+ assertEquals(18, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a third value at T+2s is rejected immediately and rate is not updated
+ // because the quota is exhausted.
+ time.sleep(1000);
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(90, time.milliseconds(), QuotaEnforcementType.STRICT));
+ assertEquals(18, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ metrics.close();
+ }
+
+ @Test
+ public void testPermissiveQuotaEnforcement() {
+ final Time time = new MockTime(0, 0, 0);
+ final Metrics metrics = new Metrics(time);
+ final Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+ .quota(Quota.upperBound(10))
+ .timeWindow(1, TimeUnit.SECONDS)
+ .samples(11));
+ final MetricName metricName = metrics.metricName("rate", "test-group");
+ assertTrue(sensor.add(metricName, new Rate()));
+ final KafkaMetric rateMetric = metrics.metric(metricName);
+
+ // Recording a first value at T+0s to bring the avg rate to 9. Value is accepted
+ // because the quota is not exhausted yet.
+ sensor.record(90, time.milliseconds(), QuotaEnforcementType.PERMISSIVE);
+ assertEquals(9, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a second value at T+1s to bring the avg rate to 18. Value is accepted
+ // and rate is updated even though the quota is exhausted.
+ time.sleep(1000);
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(90, time.milliseconds(), QuotaEnforcementType.PERMISSIVE));
+ assertEquals(18, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a second value at T+1s to bring the avg rate to 27. Value is accepted
+ // and rate is updated even though the quota is exhausted.
+ time.sleep(1000);
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(90, time.milliseconds(), QuotaEnforcementType.PERMISSIVE));
+ assertEquals(27, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ metrics.close();
+ }
+
+ @Test
+ public void testStrictQuotaEnforcementWithDefaultRate() {
Review comment:
This test illustrate the problem that we are trying to resolve.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#issuecomment-658876239
@apovzner I just pushed an update that includes `BackfillTokenBucket` and an updated doc the other implementation. It also adds `TokenBucketComparaison` that compares the two implementations with the real (non sampled) algorithm.
Could you take a look at `BackfillTokenBucket` and tell me if it looks similar to the one you had in mind?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] splett2 commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
splett2 commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r453980384
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
+ public double combine(List<Sample> samples, MetricConfig config, long now) {
+ if (this.samples.isEmpty())
+ return 0;
+
+ final double quota = config.quota() != null ? config.quota().bound() : 0;
+ final int startIndex = (this.current + 1) % this.samples.size();
+
+ long lastWindowMs = Long.MAX_VALUE;
+ double total = 0.0;
+
+ for (int i = 0; i < this.samples.size(); i++) {
+ int current = (startIndex + i) % this.samples.size();
+ Sample sample = this.samples.get(current);
+
+ // The total is decreased by (last window - current window) * quota. We basically
+ // pay back the amount of credits spent until we reach zero (dept fully paid).
+ total = adjustedTotal(total, quota, lastWindowMs, sample.lastWindowMs);
+
+ // The current sample is added to the total. If the sample is a negative number,
+ // we ensure that total remains >= 0. A negative number can be used to give back
+ // credits if there were not used. If the number of credits given back is higher
+ // than the current dept, we basically assume that the dept is paid off.
+ total = Math.max(0, total + sample.value);
+
+ // Memorize the current window so we can use for the next sample.
+ lastWindowMs = sample.lastWindowMs;
+ }
+
+ return adjustedTotal(total, quota, lastWindowMs, now);
+ }
+
+ private double adjustedTotal(double total, double quota, long lastWindowMs, long nowMs) {
+ return Math.max(0, total - delta(quota, lastWindowMs, nowMs));
+ }
+
+ private double delta(double quota, long lastWindowMs, long nowMs) {
+ return quota * convert(Math.max(0, nowMs - lastWindowMs));
+ }
+
+ private double convert(long timeMs) {
+ switch (unit) {
+ case NANOSECONDS:
+ return timeMs * 1000.0 * 1000.0;
+ case MICROSECONDS:
+ return timeMs * 1000.0;
+ case MILLISECONDS:
+ return timeMs;
+ case SECONDS:
+ return timeMs / 1000.0;
+ case MINUTES:
+ return timeMs / (60.0 * 1000.0);
+ case HOURS:
+ return timeMs / (60.0 * 60.0 * 1000.0);
+ case DAYS:
+ return timeMs / (24.0 * 60.0 * 60.0 * 1000.0);
+ default:
+ throw new IllegalStateException("Unknown unit: " + unit);
+ }
Review comment:
why not just use `TimeUnit.convert()`?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r449633467
##########
File path: clients/src/test/java/org/apache/kafka/common/metrics/SensorTest.java
##########
@@ -209,4 +212,139 @@ public void shouldReturnPresenceOfMetrics() {
assertThat(sensor.hasMetrics(), is(true));
}
-}
\ No newline at end of file
+
+ @Test
+ public void testStrictQuotaEnforcement() {
+ final Time time = new MockTime(0, 0, 0);
+ final Metrics metrics = new Metrics(time);
+ final Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+ .quota(Quota.upperBound(10))
+ .timeWindow(1, TimeUnit.SECONDS)
+ .samples(11));
+ final MetricName metricName = metrics.metricName("rate", "test-group");
+ assertTrue(sensor.add(metricName, new Rate()));
+ final KafkaMetric rateMetric = metrics.metric(metricName);
+
+ // Recording a first value at T+0s to bring the avg rate to 9. Value is accepted
+ // because the quota is not exhausted yet.
+ sensor.record(90, time.milliseconds(), QuotaEnforcementType.STRICT);
+ assertEquals(9, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a second value at T+1s to bring the avg rate to 18. Value is accepted
+ // because the quota is not exhausted yet.
+ time.sleep(1000);
+ sensor.record(90, time.milliseconds(), QuotaEnforcementType.STRICT);
+ assertEquals(18, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a third value at T+2s is rejected immediately and rate is not updated
+ // because the quota is exhausted.
+ time.sleep(1000);
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(90, time.milliseconds(), QuotaEnforcementType.STRICT));
+ assertEquals(18, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ metrics.close();
+ }
+
+ @Test
+ public void testPermissiveQuotaEnforcement() {
+ final Time time = new MockTime(0, 0, 0);
+ final Metrics metrics = new Metrics(time);
+ final Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+ .quota(Quota.upperBound(10))
+ .timeWindow(1, TimeUnit.SECONDS)
+ .samples(11));
+ final MetricName metricName = metrics.metricName("rate", "test-group");
+ assertTrue(sensor.add(metricName, new Rate()));
+ final KafkaMetric rateMetric = metrics.metric(metricName);
+
+ // Recording a first value at T+0s to bring the avg rate to 9. Value is accepted
+ // because the quota is not exhausted yet.
+ sensor.record(90, time.milliseconds(), QuotaEnforcementType.PERMISSIVE);
+ assertEquals(9, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a second value at T+1s to bring the avg rate to 18. Value is accepted
+ // and rate is updated even though the quota is exhausted.
+ time.sleep(1000);
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(90, time.milliseconds(), QuotaEnforcementType.PERMISSIVE));
+ assertEquals(18, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Recording a second value at T+1s to bring the avg rate to 27. Value is accepted
+ // and rate is updated even though the quota is exhausted.
+ time.sleep(1000);
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(90, time.milliseconds(), QuotaEnforcementType.PERMISSIVE));
+ assertEquals(27, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ metrics.close();
+ }
+
+ @Test
+ public void testStrictQuotaEnforcementWithDefaultRate() {
+ final Time time = new MockTime(0, 0, 0);
+ final Metrics metrics = new Metrics(time);
+ final Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+ .quota(Quota.upperBound(2))
+ .timeWindow(1, TimeUnit.SECONDS)
+ .samples(11));
+ final MetricName metricName = metrics.metricName("rate", "test-group");
+ assertTrue(sensor.add(metricName, new Rate()));
+ final KafkaMetric rateMetric = metrics.metric(metricName);
+
+ // Recording a first value at T+0 to bring the avg rate to 3 which is already
+ // above the quota.
+ sensor.record(30, time.milliseconds(), QuotaEnforcementType.STRICT);
+ assertEquals(3, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ // Theoretically, we should wait 5s to bring back the avg rate to the define quota:
+ // ((30 / 10) - 2) / 2 * 10 = 5s
+ time.sleep(5000);
+
+ // But, recording a second value is rejected because the avr rate is still equal
+ // to 3 after 5s.
+ assertThrows(QuotaViolationException.class,
+ () -> sensor.record(30, time.milliseconds(), QuotaEnforcementType.STRICT));
+ assertEquals(3, rateMetric.measurableValue(time.milliseconds()), 0.1);
+
+ metrics.close();
+ }
+
+ @Test
+ public void testStrictQuotaEnforcementWithTokenBucketBasedRate() {
Review comment:
This test shows that the solution actually resolves the problem.
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [kafka] dajac commented on a change in pull request #8977: KAFKA-10162; Quota Enhancements (KIP-599)
Posted by GitBox <gi...@apache.org>.
dajac commented on a change in pull request #8977:
URL: https://github.com/apache/kafka/pull/8977#discussion_r452162828
##########
File path: clients/src/main/java/org/apache/kafka/common/metrics/stats/TokenBucket.java
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+package org.apache.kafka.common.metrics.stats;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.kafka.common.metrics.MetricConfig;
+
+import java.util.List;
+
+/**
+ * A {@link SampledStat} that mimics the behavior of a Token Bucket and is meant to
+ * be used in conjunction with a {@link Rate} and a {@link org.apache.kafka.common.metrics.Quota}.
+ *
+ * The {@link TokenBucket} considers each sample as the amount of credits spent during the sample's
+ * window while giving back credits based on the defined quota.
+ *
+ * At time T, it computes the total O as follow:
+ * - O(T) = max(0, O(T-1) - Q * (W(T) - W(T-1)) + S(T)
+ * Where:
+ * - Q is the defined Quota or 0 if undefined
+ * - W is the time of the sample or now if undefined
+ * - S is the value of the sample or 0 if undefined
+ *
+ * Example with 3 samples with a Quota = 2:
+ * - S1 at T+0s => 4
+ * - S2 at T+2s => 2
+ * - S3 at T+4s => 6
+ *
+ * The total at T+6s is computed as follow:
+ * - T0 => Total at T+0s => S1 = 4
+ * - T1 => Total at T+2s => max(0, T0 - Q * dT) + S2 = (4 - 2 * 2) + 2 = 2
+ * - T2 => Total at T+4s => max(0, T1 - Q * dT) + S3 = (2 - 2 * 2) + 6 = 6
+ * - T3 => Total at T+6s => max(0, T2 - Q * dT) = (6 - 2 * 2) = 2
+ */
+public class TokenBucket extends SampledStat {
+
+ private final TimeUnit unit;
+
+ public TokenBucket() {
+ this(TimeUnit.SECONDS);
+ }
+
+ public TokenBucket(TimeUnit unit) {
+ super(0);
+ this.unit = unit;
+ }
+
+ @Override
+ protected void update(Sample sample, MetricConfig config, double value, long now) {
+ sample.value += value;
+ }
+
+ @Override
Review comment:
My first implementation was doing what you described: backfilling past samples up to quota * sample length and putting the remaining in the last (or current) sample. You seems to use sample in singular quite often in your description so I wonder if you also meant to backfill all the past samples. Did you?
It turned out that backfilling past samples is not as strait-forward as it seems because, as you pointed out in your other comment, we may have holes or no past samples at all. Let me take few examples to illustrate.
Let's assume that we have the following settings:
- samples = 6
- window = 1s
- quota = 5
Example 1:
Let's assume that we record 2 at T and 30 at T+5. When 2 is recorded, a first sample at T is created with the value 2. Then, we record 30 at T+5. As it is above the quota of the current sample, we want to backfill T, T+1, T+2, T+3, T+4 and put the remaining in T+5. This mean, that we have to create samples for T+1, T+2, T+3 and T+4 because they don't exist in memory. We would end up with the following samples in memory:
T = 5, T+1 = 5, T+2 = 5, T+3 = 5, T+4 = 5, T+5 = 7.
Example 2:
Let's assume that we record 2 at T, 3 at T+3 and 30 at T+5. The mechanics is similar to the previous example in the difference that it requires to add new samples between existing past samples: T+1 and T+2; as well as adding sample T+4. We would end up with the following samples in memory:
T = 5, T+1 = 5, T+2 = 5, T+3 = 5, T+4 = 5, T+5 = 11.
With the current base implementation of the SampleStat, adding all the past samples, especially in between existing past samples is quite complex and possibly error-prone. The simplest would be to always allocate all the samples but this is something I would like to avoid.
Following this observation, I started to look at the problem from a different angle. Instead of spreading value at record time, I thought that we could adjust them while combining the samples. The idea is to run a token bucket algorithm in the combine method based on all the non-expired samples.
My implementation is basically a reversed Token Bucket, a token bucket which start at zero instead of stating at the maximum number of tokens.
Let's define the following:
* TK = The current number of tokens in the bucket
* B = The maximum burst (number of samples * sample length in our case)
* R = The rate at which we decrease tokens (quota * sample length in our case)
* T = The last time TK got updated
At time now, we update the number of tokens in the bucket with:
* TK = max(TK - (now - T) * R), 0)
A request with a burst worth of K is admitted if TK < B, and update TK = TK + K.
My implementation basically runs this, sample by sample in the chronological order, in the combine method modulo the admission verification that is done in the Rate.
It seems to me that this is close to our original token bucket based proposal and also pretty close to our backfilling old samples idea.
Does this help to clarify my proposal? WDYT?
----------------------------------------------------------------
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.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org