diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java index 4f2aebcb..b32fa44c 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/bots/DefaultBotOptions.java @@ -2,6 +2,7 @@ package org.telegram.telegrambots.bots; import org.apache.http.client.config.RequestConfig; import org.telegram.telegrambots.generics.BotOptions; +import org.telegram.telegrambots.updatesreceivers.ExponentialBackOff; import java.util.List; @@ -14,6 +15,7 @@ import java.util.List; public class DefaultBotOptions implements BotOptions { private int maxThreads; ///< Max number of threads used for async methods executions (default 1) private RequestConfig requestConfig; + private ExponentialBackOff exponentialBackOff; private Integer maxWebhookConnections; private List allowedUpdates; @@ -56,4 +58,16 @@ public class DefaultBotOptions implements BotOptions { public void setRequestConfig(RequestConfig requestConfig) { this.requestConfig = requestConfig; } + + public ExponentialBackOff getExponentialBackOff() { + return exponentialBackOff; + } + + /** + * @implSpec Default implementation assumes starting at 500ms and max time of 60 minutes + * @param exponentialBackOff ExponentialBackOff to be used when long polling fails + */ + public void setExponentialBackOff(ExponentialBackOff exponentialBackOff) { + this.exponentialBackOff = exponentialBackOff; + } } diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java index 499d6ecc..6c3186b5 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java @@ -133,6 +133,7 @@ public class DefaultBotSession implements BotSession { private class ReaderThread extends Thread implements UpdatesReader { private CloseableHttpClient httpclient; + private ExponentialBackOff exponentialBackOff; private RequestConfig requestConfig; @Override @@ -143,6 +144,11 @@ public class DefaultBotSession implements BotSession { .setMaxConnTotal(100) .build(); requestConfig = options.getRequestConfig(); + exponentialBackOff = options.getExponentialBackOff(); + + if (exponentialBackOff == null) { + exponentialBackOff = new ExponentialBackOff(); + } if (requestConfig == null) { requestConfig = RequestConfig.copy(RequestConfig.custom().build()) @@ -191,28 +197,37 @@ public class DefaultBotSession implements BotSession { HttpEntity ht = response.getEntity(); BufferedHttpEntity buf = new BufferedHttpEntity(ht); String responseContent = EntityUtils.toString(buf, StandardCharsets.UTF_8); - try { - List updates = request.deserializeResponse(responseContent); - if (updates.isEmpty()) { - synchronized (this) { - this.wait(500); - } - } else { - updates.removeIf(x -> x.getUpdateId() < lastReceivedUpdate); - lastReceivedUpdate = updates.parallelStream() - .map( - Update::getUpdateId) - .max(Integer::compareTo) - .orElse(0); - receivedUpdates.addAll(updates); - - synchronized (receivedUpdates) { - receivedUpdates.notifyAll(); - } + if (response.getStatusLine().getStatusCode() >= 500) { + BotLogger.warn(LOGTAG, responseContent); + synchronized (this) { + this.wait(500); + } + } else { + try { + List updates = request.deserializeResponse(responseContent); + exponentialBackOff.reset(); + + if (updates.isEmpty()) { + synchronized (this) { + this.wait(500); + } + } else { + updates.removeIf(x -> x.getUpdateId() < lastReceivedUpdate); + lastReceivedUpdate = updates.parallelStream() + .map( + Update::getUpdateId) + .max(Integer::compareTo) + .orElse(0); + receivedUpdates.addAll(updates); + + synchronized (receivedUpdates) { + receivedUpdates.notifyAll(); + } + } + } catch (JSONException e) { + BotLogger.severe(responseContent, LOGTAG, e); } - }catch (JSONException e) { - BotLogger.severe(responseContent, LOGTAG, e); } } catch (InvalidObjectException | TelegramApiRequestException e) { BotLogger.severe(LOGTAG, e); @@ -226,7 +241,7 @@ public class DefaultBotSession implements BotSession { BotLogger.severe(LOGTAG, global); try { synchronized (this) { - this.wait(500); + this.wait(exponentialBackOff.nextBackOffMillis()); } } catch (InterruptedException e) { if (!running) { diff --git a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/ExponentialBackOff.java b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/ExponentialBackOff.java new file mode 100644 index 00000000..b748d58a --- /dev/null +++ b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/ExponentialBackOff.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2011 Google Inc. + * + * Licensed 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.telegram.telegrambots.updatesreceivers; + +import com.google.common.base.Preconditions; + +/** + * Implementation of BackOff that increases the back off period for each retry attempt using + * a randomization function that grows exponentially. + * + *

+ * {@link #nextBackOffMillis()} is calculated using the following formula: + *

+ * + *
+ randomized_interval =
+ retry_interval * (random value in range [1 - randomization_factor, 1 + randomization_factor])
+ * 
+ * + *

+ * In other words {@link #nextBackOffMillis()} will range between the randomization factor + * percentage below and above the retry interval. For example, using 2 seconds as the base retry + * interval and 0.5 as the randomization factor, the actual back off period used in the next retry + * attempt will be between 1 and 3 seconds. + *

+ * + *

+ * Note: max_interval caps the retry_interval and not the randomized_interval. + *

+ * + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * {@link ExponentialBackOff#STOP}. The elapsed time can be reset by calling {@link #reset()}. + *

+ * + *

+ * Example: The default retry_interval is .5 seconds, default randomization_factor is 0.5, default + * multiplier is 1.5 and the default max_interval is 1 minute. For 10 tries the sequence will be + * (values in seconds) and assuming we go over the max_elapsed_time on the 10th try: + *

+ * + *
+ request#     retry_interval     randomized_interval
+
+ 1             0.5                [0.25,   0.75]
+ 2             0.75               [0.375,  1.125]
+ 3             1.125              [0.562,  1.687]
+ 4             1.687              [0.8435, 2.53]
+ 5             2.53               [1.265,  3.795]
+ 6             3.795              [1.897,  5.692]
+ 7             5.692              [2.846,  8.538]
+ 8             8.538              [4.269, 12.807]
+ 9            12.807              [6.403, 19.210]
+ 10           19.210              {@link ExponentialBackOff#maxElapsedTimeMillis}
+ * 
+ * + *

+ * Implementation is not thread-safe. + *

+ * + * @since 1.15 + * @author Ravi Mistry + */ +public class ExponentialBackOff { + /** The default initial interval value in milliseconds (0.5 seconds). */ + public static final int DEFAULT_INITIAL_INTERVAL_MILLIS = 500; + + /** + * The default randomization factor (0.5 which results in a random period ranging between 50% + * below and 50% above the retry interval). + */ + public static final double DEFAULT_RANDOMIZATION_FACTOR = 0.5; + + /** The default multiplier value (1.5 which is 50% increase per back off). */ + public static final double DEFAULT_MULTIPLIER = 1.5; + + /** The default maximum back off time in milliseconds (15 minutes). */ + public static final int DEFAULT_MAX_INTERVAL_MILLIS = 30000; + + /** The default maximum elapsed time in milliseconds (60 minutes). */ + public static final int DEFAULT_MAX_ELAPSED_TIME_MILLIS = 3600000; + + /** The current retry interval in milliseconds. */ + private int currentIntervalMillis; + + /** The initial retry interval in milliseconds. */ + private final int initialIntervalMillis; + + /** + * The randomization factor to use for creating a range around the retry interval. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ */ + private final double randomizationFactor; + + /** The value to multiply the current interval with for each retry attempt. */ + private final double multiplier; + + /** + * The maximum value of the back off period in milliseconds. Once the retry interval reaches this + * value it stops increasing. + */ + private final int maxIntervalMillis; + + /** + * The system time in nanoseconds. It is calculated when an ExponentialBackOffPolicy instance is + * created and is reset when {@link #reset()} is called. + */ + long startTimeNanos; + + /** + * The maximum elapsed time after instantiating {@link ExponentialBackOff} or calling + * {@link #reset()} after which {@link #nextBackOffMillis()} returns this value. + */ + private final int maxElapsedTimeMillis; + + /** + * Creates an instance of ExponentialBackOffPolicy using default values. + * + *

+ * To override the defaults use {@link Builder}. + *

+ * + *
    + *
  • {@code initialIntervalMillis} defaults to {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}
  • + *
  • {@code randomizationFactor} defaults to {@link #DEFAULT_RANDOMIZATION_FACTOR}
  • + *
  • {@code multiplier} defaults to {@link #DEFAULT_MULTIPLIER}
  • + *
  • {@code maxIntervalMillis} defaults to {@link #DEFAULT_MAX_INTERVAL_MILLIS}
  • + *
  • {@code maxElapsedTimeMillis} defaults in {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}
  • + *
+ */ + public ExponentialBackOff() { + this(new Builder()); + } + + /** + * @param builder builder + */ + protected ExponentialBackOff(Builder builder) { + initialIntervalMillis = builder.initialIntervalMillis; + randomizationFactor = builder.randomizationFactor; + multiplier = builder.multiplier; + maxIntervalMillis = builder.maxIntervalMillis; + maxElapsedTimeMillis = builder.maxElapsedTimeMillis; + Preconditions.checkArgument(initialIntervalMillis > 0); + Preconditions.checkArgument(0 <= randomizationFactor && randomizationFactor < 1); + Preconditions.checkArgument(multiplier >= 1); + Preconditions.checkArgument(maxIntervalMillis >= initialIntervalMillis); + Preconditions.checkArgument(maxElapsedTimeMillis > 0); + reset(); + } + + /** Sets the interval back to the initial retry interval and restarts the timer. */ + public final void reset() { + currentIntervalMillis = initialIntervalMillis; + startTimeNanos = nanoTime(); + } + + /** + * {@inheritDoc} + * + *

+ * This method calculates the next back off interval using the formula: randomized_interval = + * retry_interval +/- (randomization_factor * retry_interval) + *

+ * + *

+ * Subclasses may override if a different algorithm is required. + *

+ */ + public long nextBackOffMillis() { + // Make sure we have not gone over the maximum elapsed time. + if (getElapsedTimeMillis() > maxElapsedTimeMillis) { + return maxElapsedTimeMillis; + } + int randomizedInterval = + getRandomValueFromInterval(randomizationFactor, Math.random(), currentIntervalMillis); + incrementCurrentInterval(); + return randomizedInterval; + } + + /** + * Returns a random value from the interval [randomizationFactor * currentInterval, + * randomizationFactor * currentInterval]. + */ + static int getRandomValueFromInterval( + double randomizationFactor, double random, int currentIntervalMillis) { + double delta = randomizationFactor * currentIntervalMillis; + double minInterval = currentIntervalMillis - delta; + double maxInterval = currentIntervalMillis + delta; + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + int randomValue = (int) (minInterval + (random * (maxInterval - minInterval + 1))); + return randomValue; + } + + /** Returns the initial retry interval in milliseconds. */ + public final int getInitialIntervalMillis() { + return initialIntervalMillis; + } + + /** + * Returns the randomization factor to use for creating a range around the retry interval. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ */ + public final double getRandomizationFactor() { + return randomizationFactor; + } + + /** + * Returns the current retry interval in milliseconds. + */ + public final int getCurrentIntervalMillis() { + return currentIntervalMillis; + } + + /** + * Returns the value to multiply the current interval with for each retry attempt. + */ + public final double getMultiplier() { + return multiplier; + } + + /** + * Returns the maximum value of the back off period in milliseconds. Once the current interval + * reaches this value it stops increasing. + */ + public final int getMaxIntervalMillis() { + return maxIntervalMillis; + } + + /** + * Returns the maximum elapsed time in milliseconds. + * + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * this value. The elapsed time can be reset by calling {@link #reset()}. + *

+ */ + public final int getMaxElapsedTimeMillis() { + return maxElapsedTimeMillis; + } + + /** + * Returns the elapsed time in milliseconds since an {@link ExponentialBackOff} instance is + * created and is reset when {@link #reset()} is called. + * + *

+ * The elapsed time is computed using {@link System#nanoTime()}. + *

+ */ + public final long getElapsedTimeMillis() { + return (nanoTime() - startTimeNanos) / 1000000; + } + + /** + * Increments the current interval by multiplying it with the multiplier. + */ + private void incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if (currentIntervalMillis >= maxIntervalMillis / multiplier) { + currentIntervalMillis = maxIntervalMillis; + } else { + currentIntervalMillis *= multiplier; + } + } + + /** + * Nano time using {@link System#nanoTime()} + * @return time in nanoseconds + */ + private long nanoTime() { + return System.nanoTime(); + } + + /** + * Builder for {@link ExponentialBackOff}. + * + *

+ * Implementation is not thread-safe. + *

+ */ + public static class Builder { + + /** The initial retry interval in milliseconds. */ + int initialIntervalMillis = DEFAULT_INITIAL_INTERVAL_MILLIS; + + /** + * The randomization factor to use for creating a range around the retry interval. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ */ + double randomizationFactor = DEFAULT_RANDOMIZATION_FACTOR; + + /** The value to multiply the current interval with for each retry attempt. */ + double multiplier = DEFAULT_MULTIPLIER; + + /** + * The maximum value of the back off period in milliseconds. Once the retry interval reaches + * this value it stops increasing. + */ + int maxIntervalMillis = DEFAULT_MAX_INTERVAL_MILLIS; + + /** + * The maximum elapsed time in milliseconds after instantiating {@link ExponentialBackOff} or + * calling {@link #reset()} after which {@link #nextBackOffMillis()} returns + * this value. + */ + int maxElapsedTimeMillis = DEFAULT_MAX_ELAPSED_TIME_MILLIS; + + public Builder() { + } + + /** Builds a new instance of {@link ExponentialBackOff}. */ + public ExponentialBackOff build() { + return new ExponentialBackOff(this); + } + + /** + * Returns the initial retry interval in milliseconds. The default value is + * {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. + */ + public final int getInitialIntervalMillis() { + return initialIntervalMillis; + } + + /** + * Sets the initial retry interval in milliseconds. The default value is + * {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. Must be {@code > 0}. + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setInitialIntervalMillis(int initialIntervalMillis) { + this.initialIntervalMillis = initialIntervalMillis; + return this; + } + + /** + * Returns the randomization factor to use for creating a range around the retry interval. The + * default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public final double getRandomizationFactor() { + return randomizationFactor; + } + + /** + * Sets the randomization factor to use for creating a range around the retry interval. The + * default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}. Must fall in the range + * {@code 0 <= randomizationFactor < 1}. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setRandomizationFactor(double randomizationFactor) { + this.randomizationFactor = randomizationFactor; + return this; + } + + /** + * Returns the value to multiply the current interval with for each retry attempt. The default + * value is {@link #DEFAULT_MULTIPLIER}. + */ + public final double getMultiplier() { + return multiplier; + } + + /** + * Sets the value to multiply the current interval with for each retry attempt. The default + * value is {@link #DEFAULT_MULTIPLIER}. Must be {@code >= 1}. + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setMultiplier(double multiplier) { + this.multiplier = multiplier; + return this; + } + + /** + * Returns the maximum value of the back off period in milliseconds. Once the current interval + * reaches this value it stops increasing. The default value is + * {@link #DEFAULT_MAX_INTERVAL_MILLIS}. Must be {@code >= initialInterval}. + */ + public final int getMaxIntervalMillis() { + return maxIntervalMillis; + } + + /** + * Sets the maximum value of the back off period in milliseconds. Once the current interval + * reaches this value it stops increasing. The default value is + * {@link #DEFAULT_MAX_INTERVAL_MILLIS}. + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setMaxIntervalMillis(int maxIntervalMillis) { + this.maxIntervalMillis = maxIntervalMillis; + return this; + } + + /** + * Returns the maximum elapsed time in milliseconds. The default value is + * {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. + * + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * this value. The elapsed time can be reset by calling {@link #reset()}. + *

+ */ + public final int getMaxElapsedTimeMillis() { + return maxElapsedTimeMillis; + } + + /** + * Sets the maximum elapsed time in milliseconds. The default value is + * {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. Must be {@code > 0}. + * + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * this value. The elapsed time can be reset by calling {@link #reset()}. + *

+ * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setMaxElapsedTimeMillis(int maxElapsedTimeMillis) { + this.maxElapsedTimeMillis = maxElapsedTimeMillis; + return this; + } + } +} \ No newline at end of file