Skip to content

Commit

Permalink
Merge #102
Browse files Browse the repository at this point in the history
102: fix: fixed for the issue (#101) r=myConsciousness a=myConsciousness

# 1. Description

<!-- Provide a description of what this PR is doing.
If you're modifying existing behavior, describe the existing behavior, how this PR is changing it,
and what motivated the change. If this is a breaking change, specify explicitly which APIs have been
changed. -->

## 1.1. Checklist

<!-- Before you create this PR confirm that it meets all requirements listed below by checking the
relevant checkboxes (`[x]`). This will ensure a smooth and quick review process. -->

- [x] The title of my PR starts with a [Conventional Commit] prefix (`fix:`, `feat:`, `docs:` etc).
- [x] I have read the [Contributor Guide] and followed the process outlined for submitting PRs.
- [x] I have updated/added tests for ALL new/updated/fixed functionality.
- [x] I have updated/added relevant documentation in `docs` and added dartdoc comments with `///`.
- [x] I have updated/added relevant examples in `examples`.

## 1.2. Breaking Change

<!-- Does your PR require users to manually update their apps to accommodate your change?

If the PR is a breaking change this should be indicated with suffix "!"  (for example, `feat!:`, `fix!:`). See [Conventional Commit] for details.
-->

- [x] Yes, this is a breaking change.
- [ ] No, this is _not_ a breaking change.

## 1.3. Related Issues

<!-- Provide a list of issues related to this PR from the [issue database].
Indicate which of these issues are resolved or fixed by this PR, i.e. Fixes #xxxx* !-->

<!-- Links -->

[issue database]: https://github.com/mastodon-dart/mastodon-api/issues
[contributor guide]: https://github.com/mastodon-dart/mastodon-api/blob/main/CONTRIBUTING.md
[style guide]: https://github.com/mastodon-dart/mastodon-api/blob/main/STYLEGUIDE.md
[conventional commit]: https://conventionalcommits.org


Co-authored-by: myConsciousness <contact@shinyakato.dev>
  • Loading branch information
bors[bot] and myConsciousness committed Dec 30, 2022
2 parents cbd49d3 + 5518213 commit 31e514e
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 433 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
- `POST /api/v1/tags/:id/unfollow`
- Supported `reports API methods`. ([#15](https://github.com/mastodon-dart/mastodon-api/issues/15))
- `POST /api/v1/reports`
- Supported retry algorithm have been changed to only `Exponential BackOff and Jitter`. All of the following factory constructors provided until now are deprecated and should be modified by `v0.5.0` to use the default constructor of `RetryConfig`. The following deprecated factory constructors will be removed in `v0.5.0`, and the `Exponential BackOff and Jitter` algorithm will always be used regardless of constructor. ([#101](https://github.com/mastodon-dart/mastodon-api/issues/101))
- `ofRegularIntervals`
- `ofExponentialBackOff`
- `ofExponentialBackOffAndJitter`

## v0.2.2

Expand Down
74 changes: 7 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,8 @@
- [1.4.3. OAuth 2.0 Authorization Code Flow](#143-oauth-20-authorization-code-flow)
- [1.4.4. Change the Timeout Duration](#144-change-the-timeout-duration)
- [1.4.5. Automatic Retry](#145-automatic-retry)
- [1.4.5.1. Regular Intervals](#1451-regular-intervals)
- [1.4.5.2. Exponential Backoff](#1452-exponential-backoff)
- [1.4.5.3. Exponential Backoff and Jitter](#1453-exponential-backoff-and-jitter)
- [1.4.5.4. Do Something on Retry](#1454-do-something-on-retry)
- [1.4.5.1. Exponential Backoff and Jitter](#1451-exponential-backoff-and-jitter)
- [1.4.5.2. Do Something on Retry](#1452-do-something-on-retry)
- [1.4.6. Thrown Exceptions](#146-thrown-exceptions)
- [1.5. Contribution 🏆](#15-contribution-)
- [1.6. Contributors ✨](#16-contributors-)
Expand Down Expand Up @@ -128,7 +126,7 @@ Future<void> main() async {
//! Automatic retry is available when server error or network error occurs
//! when communicating with the API.
retryConfig: RetryConfig.ofExponentialBackOffAndJitter(
retryConfig: RetryConfig(
maxAttempts: 5,
onExecute: (event) => print(
'Retry after ${event.intervalInSeconds} seconds... '
Expand Down Expand Up @@ -346,70 +344,13 @@ Due to the nature of this library's communication with external systems, timeout

When such timeouts occur, an effective countermeasure in many cases is to send the request again after a certain interval. And **mastodon_api** provides an **automatic retry** feature as a solution to this problem.

There are 3 retry methods provided by **mastodon_api**.

| Retry Strategy | Constructor | Description |
| ------------------------------ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Regular Intervals | RetryConfig.ofRegularIntervals | Retry at regular intervals. |
| Exponential Backoff | RetryConfig.ofExponentialBackOff | The retry interval is increased exponentially according to the number of retries. |
| Exponential Backoff and Jitter | RetryConfig.ofExponentialBackOffAndJitter | A random number called Jitter is added to increase the retry interval exponentially according to the number of retries. |

Also, errors subject to retry are as follows.

- When the status code of the response returned from Mastodon is `500` or `503`.
- When the network is temporarily lost and a `SocketException` is thrown.
- When communication times out temporarily and a `TimeoutException` is thrown

#### 1.4.5.1. Regular Intervals

It would be easy to imagine **retries at regular intervals**. For example, if a timeout occurs and the request is assumed to be retried 3 times, waiting for 5 seconds and then sending the request again, it can be defined as follows.

```dart
import 'package:mastodon_api/mastodon_api.dart';
Future<void> main() async {
final mastodon = MastodonApi(
instance: 'MASTODON_INSTANCE',
bearerToken: 'YOUR_TOKEN_HERE',
//! Add these lines.
retryConfig: RetryConfig.ofRegularIntervals(
maxAttempts: 3,
intervalInSeconds: 5,
),
);
}
```

#### 1.4.5.2. Exponential Backoff

Although retries can be effective by simply performing them at regular intervals as in the above example, sending a large number of requests at regular intervals when the server to which the request is being sent is experiencing a failure is something that should be avoided. Even if the network or server is already down, the retry process can further aggravate the situation by adding to the load.

The solution to these problems is to increase the interval exponentially for each retry. This is an algorithm called `Exponential Backoff` and **mastodon_api** supports a specification that allows easy use of this algorithm.

The **Exponential Backoff** algorithm can be applied on retries by defining **RetryConfig** as follows.

```dart
import 'package:mastodon_api/mastodon_api.dart';
Future<void> main() async {
final mastodon = MastodonApi(
instance: 'MASTODON_INSTANCE',
bearerToken: 'YOUR_TOKEN_HERE',
//! Add these lines.
retryConfig: RetryConfig.ofExponentialBackOff(
maxAttempts: 3,
),
);
}
```

In the above implementation, the interval increases exponentially for each retry count. It can be expressed by next formula.

> 2 ^ retryCount
#### 1.4.5.3. Exponential Backoff and Jitter
#### 1.4.5.1. Exponential Backoff and Jitter

Although the algorithm introduced earlier that exponentially increases the retry interval is already powerful, some may believe that it is not yet sufficient to distribute the sensation of retries. It's more distributed than equally spaced retries, but retries still occur at static intervals.

Expand All @@ -426,7 +367,7 @@ Future<void> main() async {
bearerToken: 'YOUR_TOKEN_HERE',
//! Add these lines.
retryConfig: RetryConfig.ofExponentialBackOffAndJitter(
retryConfig: RetryConfig(
maxAttempts: 3,
),
);
Expand All @@ -437,7 +378,7 @@ In the above implementation, the interval increases exponentially for each retry

> **(2 ^ retryCount) + jitter(Random Number between 0 ~ 3)**
#### 1.4.5.4. Do Something on Retry
#### 1.4.5.2. Do Something on Retry

It would be useful to output logging on retries and a popup notifying the user that a retry has been executed. So **mastodon_api** provides callbacks that can perform arbitrary processing when retries are executed.

Expand All @@ -450,9 +391,8 @@ Future<void> main() async {
final mastodon = MastodonApi(
instance: 'MASTODON_INSTANCE',
bearerToken: 'YOUR_TOKEN_HERE',
retryConfig: RetryConfig.ofRegularIntervals(
retryConfig: RetryConfig(
maxAttempts: 3,
intervalInSeconds: 5,
//! Add this line.
onExecute: (event) => print('Retrying... ${event.retryCount} times.'),
Expand Down
2 changes: 1 addition & 1 deletion example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Future<void> main() async {

//! Automatic retry is available when server error or network error occurs
//! when communicating with the API.
retryConfig: RetryConfig.ofExponentialBackOffAndJitter(
retryConfig: RetryConfig(
maxAttempts: 5,
onExecute: (event) => print(
'Retry after ${event.intervalInSeconds} seconds...'
Expand Down
19 changes: 3 additions & 16 deletions lib/src/core/client/retry_policy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'dart:math' as math;
// 🌎 Project imports:
import '../config/retry_config.dart';
import 'retry_event.dart';
import 'retry_strategy.dart';

abstract class RetryPolicy {
/// Returns the new instance of [RetryPolicy].
Expand Down Expand Up @@ -79,23 +78,11 @@ class _RetryPolicy implements RetryPolicy {

bool get _hasRetryConfig => _retryConfig != null;

int _computeWaitIntervals(final int retryCount) {
if (_retryConfig!.strategy == RetryStrategy.exponentialBackOff ||
_retryConfig!.strategy == RetryStrategy.exponentialBackOffAndJitter) {
return _computeExponentialBackOff(retryCount) + _jitter;
}

return _retryConfig!.intervalInSeconds;
}
int _computeWaitIntervals(final int retryCount) =>
_computeExponentialBackOff(retryCount) + _jitter;

int _computeExponentialBackOff(final int retryCount) =>
math.pow(2, retryCount).toInt();

int get _jitter {
if (_retryConfig!.strategy == RetryStrategy.exponentialBackOffAndJitter) {
return _random.nextInt(4);
}

return 0;
}
int get _jitter => _random.nextInt(4);
}
14 changes: 0 additions & 14 deletions lib/src/core/client/retry_strategy.dart

This file was deleted.

133 changes: 25 additions & 108 deletions lib/src/core/config/retry_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,79 +4,17 @@

// 🌎 Project imports:
import '../client/retry_event.dart';
import '../client/retry_strategy.dart';

/// This class represents an automatic retry configuration.
///
/// ## Retry at Regular Intervals
/// ## Exponential BackOff Algorithm and Jitter
///
/// The simplest way to specify automatic retries is to specify a fixed number
/// of times at fixed intervals. For example, to automatically retry up to
/// 5 times at 3 seconds intervals when a timeout occurs in the communication
/// process with the API, use [RetryConfig.ofRegularIntervals] like following.
///
/// ```dart
/// import 'package:mastodon_api/mastodon_api.dart';
///
/// void main() async {
/// final mastodon = MastodonApi(
/// bearerToken: 'YOUR_TOKEN_HERE',
/// retryConfig: RetryConfig.ofRegularIntervals(
/// maxAttempts: 5,
/// intervalInSeconds: 3,
/// ),
/// timeout: Duration(seconds: 20),
/// );
/// }
/// ```
///
/// ## Exponential Backoff Algorithm
///
/// But sometimes this retry method is too simplistic: if a timeout occurs when
/// communicating with the API, it is certainly useful to retry multiple times
/// at regular intervals, as in the previous example. However, it is undesirable
/// to have multiple clients sending large numbers of requests to the server at
/// short intervals simultaneously when the server is experiencing a failure.
///
/// To solve this problem somewhat, the library supports automatic retries with
/// the **Exponential BackOff algorithm**. This is an algorithm that
/// exponentially increases the retry interval based on the number of retries
/// performed. It can be enabled with the [RetryConfig.ofExponentialBackOff]
/// constructor as follows.
///
/// ```dart
/// import 'package:mastodon_api/mastodon_api.dart';
///
/// void main() async {
/// final mastodon = MastodonApi(
/// bearerToken: 'YOUR_TOKEN_HERE',
/// retryConfig: RetryConfig.ofExponentialBackOff(
/// maxAttempts: 5,
/// ),
/// timeout: Duration(seconds: 20),
/// );
/// }
/// ```
///
/// The **Exponential BackOff Algorithm** can be enabled by using the
/// [RetryConfig.ofExponentialBackOff] constructor, as in the example above.
/// And the interval, which increases with the number of retries, is then
/// calculated as follows.
///
/// > **2 ^ retryCount**
///
/// ## Exponential Backoff Algorithm and Jitter
///
/// However, sometimes simply increasing the wait time exponentially is still
/// Simply increasing the wait time exponentially is still
/// not sufficient to distribute retry timing. In addition to simply increasing
/// the interval exponentially, adding a random number called **Jitter** is
/// effective. This method allows for even greater flexibility in distributing
/// the timing of retries.
///
/// [RetryConfig.ofExponentialBackOff] constructor, as in the example above.
/// As always, you can use this algorithm by using
/// [RetryConfig.ofExponentialBackOffAndJitter] constructor as follows.
///
/// ```dart
/// import 'package:mastodon_api/mastodon_api.dart';
///
Expand All @@ -91,9 +29,7 @@ import '../client/retry_strategy.dart';
/// }
/// ```
///
/// The **Exponential BackOff and Jitter Algorithm** can be enabled by using the
/// [RetryConfig.ofExponentialBackOffAndJitter] constructor, as in the example
/// above. And the interval, which increases with the number of retries, is then
/// The interval, which increases with the number of retries, is then
/// calculated as follows.
///
/// > **(2 ^ retryCount) + jitter(Random Number between 0 ~ 3)**
Expand All @@ -103,79 +39,60 @@ import '../client/retry_strategy.dart';
/// Please note that [ArgumentError] is always raised if a negative number
/// is passed to the [maxAttempts] field of [RetryConfig].
class RetryConfig {
/// Returns the new instance of [RetryConfig].
RetryConfig({
required this.maxAttempts,
this.onExecute,
}) {
if (maxAttempts < 0) {
//! There is no use case where the number of retries is negative.
throw ArgumentError.value(
maxAttempts,
'maxAttempts',
'must be greater than or equal to 0',
);
}
}

/// Returns the new instance of [RetryConfig] of regular intervals.
@Deprecated('Use default constructor instead. Will be removed in v0.5.0')
factory RetryConfig.ofRegularIntervals({
required int maxAttempts,
// ignore: avoid_unused_constructor_parameters
int intervalInSeconds = 2,
Function(RetryEvent event)? onExecute,
}) =>
RetryConfig._(
strategy: RetryStrategy.regularIntervals,
RetryConfig(
maxAttempts: maxAttempts,
intervalInSeconds: intervalInSeconds,
onExecute: onExecute,
);

/// Returns the new instance of [RetryConfig] of Exponential Back Off.
@Deprecated('Use default constructor instead. Will be removed in v0.5.0')
factory RetryConfig.ofExponentialBackOff({
required int maxAttempts,
Function(RetryEvent event)? onExecute,
}) =>
RetryConfig._(
strategy: RetryStrategy.exponentialBackOff,
RetryConfig(
maxAttempts: maxAttempts,
intervalInSeconds: 0,
onExecute: onExecute,
);

/// Returns the new instance of [RetryConfig] of Exponential Back Off
/// and Jitter.
@Deprecated('Use default constructor instead. Will be removed in v0.5.0')
factory RetryConfig.ofExponentialBackOffAndJitter({
required int maxAttempts,
Function(RetryEvent event)? onExecute,
}) =>
RetryConfig._(
strategy: RetryStrategy.exponentialBackOffAndJitter,
RetryConfig(
maxAttempts: maxAttempts,
intervalInSeconds: 0,
onExecute: onExecute,
);

/// Returns the new instance of [RetryConfig].
RetryConfig._({
required this.strategy,
required this.maxAttempts,
required this.intervalInSeconds,
required this.onExecute,
}) {
if (maxAttempts < 0) {
//! There is no use case where the number of retries is negative.
throw ArgumentError.value(
maxAttempts,
'maxAttempts',
'must be greater than or equal to 0',
);
}

if (intervalInSeconds < 0) {
//! There is no use case where the number of intervals is negative.
throw ArgumentError.value(
intervalInSeconds,
'intervalInSeconds',
'must be greater than or equal to 0',
);
}
}

/// The strategy of retry.
final RetryStrategy strategy;

/// Maximum number of retry attempts.
final int maxAttempts;

/// Interval time in seconds unit.
final int intervalInSeconds;

/// A callback function to be called when the retry is executed.
final Function(RetryEvent event)? onExecute;
}

0 comments on commit 31e514e

Please sign in to comment.