diff --git a/Bots.ipr b/Bots.ipr index 422dfb6d..6cbcc842 100644 --- a/Bots.ipr +++ b/Bots.ipr @@ -317,70 +317,70 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -504,37 +504,37 @@ - + - + - + - + - + - + - + - + - + - + - + - + @@ -581,59 +581,59 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -647,147 +647,147 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/README.md b/README.md index 5a5071d1..2301f786 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,12 @@ Just import add the library to your project with one of these options: org.telegram telegrambots - 2.4.4 + 2.4.4.5 ``` - 2. Using Jitpack from [here](https://jitpack.io/#rubenlagus/TelegramBots/2.4.4.4) - 3. Download the jar(including all dependencies) from [here](https://github.com/rubenlagus/TelegramBots/releases/tag/2.4.4.4) + 2. Using Jitpack from [here](https://jitpack.io/#rubenlagus/TelegramBots/2.4.4.5) + 3. Download the jar(including all dependencies) from [here](https://github.com/rubenlagus/TelegramBots/releases/tag/2.4.4.5) In order to use Long Polling mode, just create your own bot extending `org.telegram.telegrambots.bots.TelegramLongPollingBot`. diff --git a/TelegramBots.wiki/Changelog.md b/TelegramBots.wiki/Changelog.md index 36e8b8f3..b6c0accb 100644 --- a/TelegramBots.wiki/Changelog.md +++ b/TelegramBots.wiki/Changelog.md @@ -34,4 +34,12 @@ ### 2.4.4.4 ### 1. EditMessageText, EditMessageCaption and EditMessageReplyMarkup now return a `Serializable` object that can be `Boolean` or `Message` -**[[How to update to version 2.4.4.4|How-To-Update#2.4.4.4]]** \ No newline at end of file +**[[How to update to version 2.4.4.4|How-To-Update#2.4.4.4]]** + +### 2.4.4.5 ### +1. New validations for AnswerInlineQuery according to Telegram Bots API changes. +2. Added Maven-enforcer-plugin to Maven pom. +3. Added new How to send photos by file_id to FAQ. +4. Added reference to new gitbook about this library. +5. Added custom ExponentialBackOff waiting time when having network problems in long-polling mode. (Custom implementation is allowed via BotOptions) +3. Bug fixing: #184, #183 \ No newline at end of file diff --git a/TelegramBots.wiki/FAQ.md b/TelegramBots.wiki/FAQ.md index 86c5db18..e1c1d13f 100644 --- a/TelegramBots.wiki/FAQ.md +++ b/TelegramBots.wiki/FAQ.md @@ -1,5 +1,6 @@ * [How to get picture?](#how_to_get_picture) -* [How to send photos?](#how_to_send_photos) +* [How to send photos?](#how_to_send_photos) +* [How do I send photos by file_id?](#how_to_send_photos_file_id) * [How to use custom keyboards?](#how_to_use_custom_keyboards) * [How can I run my bot?](#how_to_host) * [How can I compile my project?](#how_to_compile) @@ -122,6 +123,31 @@ There are several method to send a photo to an user using `sendPhoto` method: Wi } ``` +## How to send photo by its file_id? ## + +In this example we will check if user sends to bot a photo, if it is, get Photo's file_id and send this photo by file_id to user. +```java +// If it is a photo +if (update.hasMessage() && update.getMessage().hasPhoto()) { + // Array with photos + List photos = update.getMessage().getPhoto(); + // Get largest photo's file_id + String f_id = photos.stream() + .sorted(Comparator.comparing(PhotoSize::getFileSize).reversed()) + .findFirst() + .orElse(null).getFileId(); + // Send photo by file_id we got before + SendPhoto msg = new SendPhoto() + .setChatId(update.getMessage().getChatId()) + .setPhoto(f_id) + .setCaption("Photo"); + try { + sendPhoto(msg); // Call method to send the photo + } catch (TelegramApiException e) { + e.printStackTrace(); + } + } +``` ## How to use custom keyboards? ## @@ -177,4 +203,4 @@ You don't need to spend a lot of money into hosting your own telegram bot. Basic ## How can I compile my project? ## This is just one way, how you can compile it (here with maven). The example below below is compiling the TelegramBotsExample repo. - [![asciicast](https://asciinema.org/a/4np9i2u9onuitkg287ism23kj.png)](https://asciinema.org/a/4np9i2u9onuitkg287ism23kj) \ No newline at end of file + [![asciicast](https://asciinema.org/a/4np9i2u9onuitkg287ism23kj.png)](https://asciinema.org/a/4np9i2u9onuitkg287ism23kj) diff --git a/TelegramBots.wiki/Getting-Started.md b/TelegramBots.wiki/Getting-Started.md index cb9d6649..1b264f32 100644 --- a/TelegramBots.wiki/Getting-Started.md +++ b/TelegramBots.wiki/Getting-Started.md @@ -11,13 +11,13 @@ First you need ot get the library and add it to your project. There are few poss org.telegram telegrambots - 2.4.4.4 + 2.4.4.5 ``` * With **Gradle**: ```groovy - compile group: 'org.telegram', name: 'telegrambots', version: '2.4.4.4' + compile group: 'org.telegram', name: 'telegrambots', version: '2.4.4.5' ``` 2. Don't like **Maven Central Repository**? It can also be taken from [Jitpack](https://jitpack.io/#rubenlagus/TelegramBots). diff --git a/TelegramBots.wiki/Home.md b/TelegramBots.wiki/Home.md index 869d59d9..3d2ab1d9 100644 --- a/TelegramBots.wiki/Home.md +++ b/TelegramBots.wiki/Home.md @@ -1 +1,3 @@ -Welcome to the TelegramBots wiki. Use the sidebar on the right. If you're not sure what to look at, why not take a look at the [[Getting Started|Getting-Started]] guide? \ No newline at end of file +Welcome to the TelegramBots wiki. Use the sidebar on the right. If you're not sure what to look at, why not take a look at the [[Getting Started|Getting-Started]] guide? + +If you want more detailed explanations, you can also visit this [gitbook by MonsterDeveloper's](https://www.gitbook.com/button/status/book/monsterdeveloper/writing-telegram-bots-on-java) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 04c7dc69..f464327f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.telegram Bots pom - 2.4.4.4 + 2.4.4.5 telegrambots @@ -24,6 +24,6 @@ true - 2.4.4.4 + 2.4.4.5 \ No newline at end of file diff --git a/telegrambots-meta/pom.xml b/telegrambots-meta/pom.xml index a3031f57..1c630ae2 100644 --- a/telegrambots-meta/pom.xml +++ b/telegrambots-meta/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambots-meta - 2.4.4.4 + 2.4.4.5 jar Telegram Bots Meta @@ -60,7 +60,8 @@ UTF-8 UTF-8 4.1.0 - 2.8.5 + 2.8.7 + 2.8.0 20160810 4.12 @@ -75,11 +76,17 @@ com.fasterxml.jackson.core jackson-databind ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + + com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + ${jacksonanotation.version} org.json @@ -203,6 +210,24 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + enforce + + enforce + + + + + + + + + diff --git a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/methods/AnswerInlineQuery.java b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/methods/AnswerInlineQuery.java index 5f4e3572..97ec161e 100644 --- a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/methods/AnswerInlineQuery.java +++ b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/methods/AnswerInlineQuery.java @@ -12,6 +12,7 @@ import org.telegram.telegrambots.exceptions.TelegramApiValidationException; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.regex.Pattern; /** * @author Ruben Bermudez @@ -119,12 +120,26 @@ public class AnswerInlineQuery extends BotApiMethod { @Override public void validate() throws TelegramApiValidationException { - if (inlineQueryId == null) { + if (inlineQueryId == null || inlineQueryId.isEmpty()) { throw new TelegramApiValidationException("InlineQueryId can't be empty", this); } if (results == null) { throw new TelegramApiValidationException("Results array can't be null", this); } + if (switchPmText != null) { + if (switchPmText.isEmpty()) { + throw new TelegramApiValidationException("SwitchPmText can't be empty", this); + } + if (switchPmParameter == null || switchPmParameter.isEmpty()) { + throw new TelegramApiValidationException("SwitchPmParameter can't be empty if switchPmText is present", this); + } + if (switchPmParameter.length() > 64) { + throw new TelegramApiValidationException("SwitchPmParameter can't be longer than 64 chars", this); + } + if (!Pattern.matches("[A-Za-z0-9_]+", switchPmParameter.trim() )) { + throw new TelegramApiValidationException("SwitchPmParameter only allows A-Z, a-z, 0-9 and _ characters", this); + } + } for (InlineQueryResult result : results) { result.validate(); } diff --git a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/Location.java b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/Location.java index 026faf24..e3044933 100644 --- a/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/Location.java +++ b/telegrambots-meta/src/main/java/org/telegram/telegrambots/api/objects/Location.java @@ -16,19 +16,19 @@ public class Location implements BotApiObject { private static final String LATITUDE_FIELD = "latitude"; @JsonProperty(LONGITUDE_FIELD) - private Double longitude; ///< Longitude as defined by sender + private Float longitude; ///< Longitude as defined by sender @JsonProperty(LATITUDE_FIELD) - private Double latitude; ///< Latitude as defined by sender + private Float latitude; ///< Latitude as defined by sender public Location() { super(); } - public Double getLongitude() { + public Float getLongitude() { return longitude; } - public Double getLatitude() { + public Float getLatitude() { return latitude; } diff --git a/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/TestDeserialization.java b/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/TestDeserialization.java index 6ad6504a..77b125c2 100644 --- a/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/TestDeserialization.java +++ b/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/TestDeserialization.java @@ -172,8 +172,8 @@ public class TestDeserialization { Assert.assertEquals("offset", inlineQuery.getOffset()); assertFromUser(inlineQuery.getFrom()); Assert.assertNotNull(inlineQuery.getLocation()); - Assert.assertEquals(Double.valueOf("0.234242534"), inlineQuery.getLocation().getLatitude()); - Assert.assertEquals(Double.valueOf("0.234242534"), inlineQuery.getLocation().getLongitude()); + Assert.assertEquals(Float.valueOf("0.234242534"), inlineQuery.getLocation().getLatitude()); + Assert.assertEquals(Float.valueOf("0.234242534"), inlineQuery.getLocation().getLongitude()); } private void assertCallbackQuery(CallbackQuery callbackQuery) { diff --git a/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/apimethods/TestAnswerInlineQuery.java b/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/apimethods/TestAnswerInlineQuery.java new file mode 100644 index 00000000..49467789 --- /dev/null +++ b/telegrambots-meta/src/test/java/org/telegram/telegrambots/test/apimethods/TestAnswerInlineQuery.java @@ -0,0 +1,119 @@ +package org.telegram.telegrambots.test.apimethods; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.telegram.telegrambots.api.methods.AnswerInlineQuery; +import org.telegram.telegrambots.exceptions.TelegramApiValidationException; + +import java.util.ArrayList; + +/** + * @author Ruben Bermudez + * @version 1.0 + */ +public class TestAnswerInlineQuery { + private AnswerInlineQuery answerInlineQuery; + + @Before + public void setUp() throws Exception { + answerInlineQuery = new AnswerInlineQuery(); + } + + @Test + public void TestInlineQueryIdMustBePresent() throws Exception { + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("InlineQueryId can't be empty", e.getMessage()); + } + } + + @Test + public void TestInlineQueryIdCanNotBeEmpty() throws Exception { + answerInlineQuery.setInlineQueryId(""); + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("InlineQueryId can't be empty", e.getMessage()); + } + } + + @Test + public void TestResultsMustBePresent() throws Exception { + answerInlineQuery.setInlineQueryId("RANDOMEID"); + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("Results array can't be null", e.getMessage()); + } + } + + @Test + public void TestSwitchPmTextCanNotBeEmpty() throws Exception { + answerInlineQuery.setInlineQueryId("RANDOMEID"); + answerInlineQuery.setResults(new ArrayList<>()); + answerInlineQuery.setSwitchPmText(""); + + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("SwitchPmText can't be empty", e.getMessage()); + } + } + + @Test + public void TestSwitchPmParameterIsMandatoryIfSwitchPmTextIsPresent() throws Exception { + answerInlineQuery.setInlineQueryId("RANDOMEID"); + answerInlineQuery.setResults(new ArrayList<>()); + answerInlineQuery.setSwitchPmText("Test Text"); + + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("SwitchPmParameter can't be empty if switchPmText is present", e.getMessage()); + } + } + + @Test + public void TestSwitchPmParameterCanNotBeEmptyIfSwitchPmTextIsPresent() throws Exception { + answerInlineQuery.setInlineQueryId("RANDOMEID"); + answerInlineQuery.setResults(new ArrayList<>()); + answerInlineQuery.setSwitchPmText("Test Text"); + answerInlineQuery.setSwitchPmParameter(""); + + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("SwitchPmParameter can't be empty if switchPmText is present", e.getMessage()); + } + } + + @Test + public void TestSwitchPmParameterContainsUpTo64Chars() throws Exception { + answerInlineQuery.setInlineQueryId("RANDOMEID"); + answerInlineQuery.setResults(new ArrayList<>()); + answerInlineQuery.setSwitchPmText("Test Text"); + answerInlineQuery.setSwitchPmParameter("2AAQlw4BwzXwFNXMk5rReQC3YbhbgNqq4BGqyozjRTtrsok4shsB8u4NXeslfpOsL"); + + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("SwitchPmParameter can't be longer than 64 chars", e.getMessage()); + } + } + + @Test + public void TestSwitchPmParameterOnlyContainsAcceptedCharacters() throws Exception { + answerInlineQuery.setInlineQueryId("RANDOMEID"); + answerInlineQuery.setResults(new ArrayList<>()); + answerInlineQuery.setSwitchPmText("Test Text"); + answerInlineQuery.setSwitchPmParameter("*"); + + try { + answerInlineQuery.validate(); + } catch (TelegramApiValidationException e) { + Assert.assertEquals("SwitchPmParameter only allows A-Z, a-z, 0-9 and _ characters", e.getMessage()); + } + } +} diff --git a/telegrambots/pom.xml b/telegrambots/pom.xml index df9d3462..e16d2933 100644 --- a/telegrambots/pom.xml +++ b/telegrambots/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.telegram telegrambots - 2.4.4.4 + 2.4.4.5 jar Telegram Bots @@ -59,13 +59,14 @@ UTF-8 UTF-8 - 2.25 + 2.25.1 1.19.3 - 4.5.2 + 4.5.3 20160810 - 2.8.5 + 2.8.7 + 2.8.0 2.5 - 2.4.4.4 + 2.4.4.5 @@ -89,17 +90,42 @@ com.fasterxml.jackson.core jackson-annotations + ${jacksonanotation.version} + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider ${jackson.version} com.fasterxml.jackson.core jackson-databind ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + + org.glassfish.jersey.media jersey-media-json-jackson ${glassfish.version} + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-base + + org.glassfish.jersey.containers @@ -261,6 +287,24 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + enforce-versions + + enforce + + + + + + + + + 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 02d00f04..6c3186b5 100644 --- a/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java +++ b/telegrambots/src/main/java/org/telegram/telegrambots/updatesreceivers/DefaultBotSession.java @@ -36,6 +36,8 @@ import java.util.List; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.TimeUnit; +import static org.telegram.telegrambots.Constants.SOCKET_TIMEOUT; + /** * @author Ruben Bermudez * @version 1.0 @@ -131,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 @@ -141,6 +144,18 @@ 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()) + .setSocketTimeout(SOCKET_TIMEOUT) + .setConnectTimeout(SOCKET_TIMEOUT) + .setConnectionRequestTimeout(SOCKET_TIMEOUT).build(); + } super.start(); } @@ -182,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); @@ -217,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