Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit aaf2384

Browse filesBrowse files
rhajekpeynman
authored andcommitted
feat: exponential random retry (influxdata#76)
* feat: exponential random retry
1 parent bc9ac9f commit aaf2384
Copy full SHA for aaf2384

File tree

Expand file treeCollapse file tree

9 files changed

+284
-92
lines changed
Filter options
Expand file treeCollapse file tree

9 files changed

+284
-92
lines changed

‎.circleci/config.yml

Copy file name to clipboardExpand all lines: .circleci/config.yml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
- checkout
118118
- run: |
119119
mkdir -p tools/php-cs-fixer
120-
composer require --working-dir=tools/php-cs-fixer friendsofphp/php-cs-fixer
120+
composer require --working-dir=tools/php-cs-fixer friendsofphp/php-cs-fixer:2.18.7
121121
tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=estimating --using-cache=no --diff
122122
123123
workflows:

‎CHANGELOG.md

Copy file name to clipboardExpand all lines: CHANGELOG.md
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 1.13.0 [unreleased]
22

3+
### Features
4+
1. [#76](https://github.com/influxdata/influxdb-client-php/pull/76): Exponential random backoff retry strategy
5+
36
## 1.12.0 [2021-04-01]
47

58
### Features

‎README.md

Copy file name to clipboardExpand all lines: README.md
+3-2Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ The writes are processed in batches which are configurable by `WriteOptions`:
166166
| **retryInterval** | the number of milliseconds to retry unsuccessful write. The retry interval is "exponentially" used when the InfluxDB server does not specify "Retry-After" header. | 5000 |
167167
| **jitterInterval** | the number of milliseconds before the data is written increased by a random amount | 0 |
168168
| **maxRetries** | the number of max retries when write fails | 5 |
169-
| **maxRetryDelay** | maximum delay when retrying write in milliseconds | 180000 |
170-
| **exponentialBase** | the base for the exponential retry delay, the next delay is computed as `retryInterval * exponentialBase^(attempts-1)` | 5 |
169+
| **maxRetryDelay** | maximum delay when retrying write in milliseconds | 125000 |
170+
| **maxRetryTime** | maximum total retry timeout in milliseconds | 180000 |
171+
| **exponentialBase** | the base for the exponential retry delay, the next delay is computed using random exponential backoff as a random value within the interval ``retryInterval * exponentialBase^(attempts-1)`` and ``retryInterval * exponentialBase^(attempts)``. Example for ``retryInterval=5000, exponentialBase=2, maxRetryDelay=125000, total=5`` Retry delays are random distributed values within the ranges of ``[5000-10000, 10000-20000, 20000-40000, 40000-80000, 80000-125000]`` | 2 |
171172
```php
172173
use InfluxDB2\Client;
173174
use InfluxDB2\WriteType as WriteType;

‎src/InfluxDB2/DefaultApi.php

Copy file name to clipboardExpand all lines: src/InfluxDB2/DefaultApi.php
-14Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,4 @@ function ($v, $k) {
130130
throw new InvalidArgumentException("The '${key}' should be defined as argument or default option: {$options}");
131131
}
132132
}
133-
134-
/**
135-
* Log message with specified severity to log file defined by: 'options['logFile']'.
136-
*
137-
* @param string $level log severity
138-
* @param string $message log message
139-
*/
140-
protected function log(string $level, string $message): void
141-
{
142-
$logFile = isset($this->options['logFile']) ? $this->options['logFile'] : "php://output";
143-
$logDate = date('H:i:s d-M-Y');
144-
145-
file_put_contents($logFile, "[{$logDate}]: [{$level}] - {$message}", FILE_APPEND);
146-
}
147133
}

‎src/InfluxDB2/WriteApi.php

Copy file name to clipboardExpand all lines: src/InfluxDB2/WriteApi.php
+13-51Lines changed: 13 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
namespace InfluxDB2;
44

5-
use GuzzleHttp\Exception\ConnectException;
6-
use InfluxDB2\Model\WritePrecision;
7-
85
/**
96
* Write time series data into InfluxDB.
107
* @package InfluxDB2
@@ -21,7 +18,7 @@ class WriteApi extends DefaultApi implements Writer
2118
/**
2219
* WriteApi constructor.
2320
* @param $options
24-
* @param array $writeOptions
21+
* @param array|null $writeOptions
2522
* @param array|null $pointSettings
2623
*/
2724
public function __construct($options, array $writeOptions = null, array $pointSettings = null)
@@ -134,55 +131,20 @@ public function writeRaw(string $data, string $precision = null, string $bucket
134131

135132
$queryParams = ["org" => $orgParam, "bucket" => $bucketParam, "precision" => $precisionParam];
136133

137-
$this->writeRawInternal($data, $queryParams, 1, $this->writeOptions->retryInterval);
138-
}
139-
140-
private function writeRawInternal(string $data, array $queryParams, int $attempts, int $retryInterval)
141-
{
142-
if ($this->writeOptions->jitterInterval > 0) {
143-
$jitterDelay = ($this->writeOptions->jitterInterval * 1000) * (rand(0, 1000) / 1000);
144-
usleep($jitterDelay);
145-
}
146-
147-
try {
134+
$retry = new WriteRetry(
135+
$this->writeOptions->maxRetries,
136+
$this->writeOptions->retryInterval,
137+
$this->writeOptions->maxRetryDelay,
138+
$this->writeOptions->exponentialBase,
139+
$this->writeOptions->maxRetryTime,
140+
$this->writeOptions->jitterInterval,
141+
$this->options['logFile'] ?? "php://output"
142+
);
143+
144+
$retry->retry(function () use ($data, $queryParams) {
148145
$this->post($data, "/api/v2/write", $queryParams);
149-
} catch (ApiException $e) {
150-
$code = $e->getCode();
151-
152-
if ($attempts > $this->writeOptions->maxRetries) {
153-
throw $e;
154-
}
155-
156-
if (($code == null || $code < 429) && !($e->getPrevious() instanceof ConnectException)) {
157-
throw $e;
158-
}
159-
160-
$headers = $e->getResponseHeaders();
161-
162-
if ($headers != null && array_key_exists('Retry-After', $headers)) {
163-
$timeout = (int)$headers['Retry-After'][0] * 1000000.0;
164-
} else {
165-
$timeout = min($retryInterval, $this->writeOptions->maxRetryDelay) * 1000.0;
166-
}
167-
168-
$timeoutInSec = $timeout / 1000000.0;
169-
$error = $e->getResponseBody();
170-
$error = isset($error) ? $error : $e->getMessage();
171-
172-
$message = "The retriable error occurred during writing of data. Reason: '{$error}'. Retry in: {$timeoutInSec}s.";
173-
$this->log("WARNING", $message);
174-
175-
usleep($timeout);
176-
177-
$this->writeRawInternal(
178-
$data,
179-
$queryParams,
180-
$attempts + 1,
181-
$retryInterval * $this->writeOptions->exponentialBase
182-
);
183-
}
146+
});
184147
}
185-
186148
public function close()
187149
{
188150
$this->closed = true;

‎src/InfluxDB2/WriteOptions.php

Copy file name to clipboardExpand all lines: src/InfluxDB2/WriteOptions.php
+15-6Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ class WriteOptions
77
const DEFAULT_BATCH_SIZE = 10;
88
const DEFAULT_RETRY_INTERVAL = 5000;
99
const DEFAULT_MAX_RETRIES = 5;
10-
const DEFAULT_MAX_RETRY_DELAY = 180000;
11-
const DEFAULT_EXPONENTIAL_BASE = 5;
10+
const DEFAULT_MAX_RETRY_DELAY = 125000;
11+
const DEFAULT_MAX_RETRY_TIME = 180000;
12+
const DEFAULT_EXPONENTIAL_BASE = 2;
1213
const DEFAULT_JITTER_INTERVAL = 0;
1314

1415
public $writeType;
@@ -18,6 +19,7 @@ class WriteOptions
1819
public $maxRetryDelay;
1920
public $exponentialBase;
2021
public $jitterInterval;
22+
public $maxRetryTime;
2123

2224
/**
2325
* WriteOptions constructor.
@@ -27,12 +29,18 @@ class WriteOptions
2729
* 'retryInterval' => number of milliseconds to retry unsuccessful write
2830
* 'maxRetries' => max number of retries when write fails
2931
* The retry interval is used when the InfluxDB server does not specify "Retry-After" header.
30-
* 'maxRetryDelay' => maximum delay when retrying write
31-
* 'exponentialBase' => the base for the exponential retry delay, the next delay is computed as
32-
* `retry_interval * exponentialBase^(attempts - 1)`
32+
* 'maxRetryDelay' => maximum delay when retrying write in milliseconds
33+
* 'maxRetryTime' => maximum total time when retrying write in milliseconds
34+
* 'exponentialBase' => the base for the exponential retry delay, the next delay is computed using
35+
* random exponential backoff as a random value within the interval
36+
* ``retryInterval * exponentialBase^(attempts-1)`` and
37+
* ``retryInterval * exponentialBase^(attempts)``.
38+
* Example for ``retryInterval=5000, exponentialBase=2, maxRetryDelay=125000, total=5``
39+
* Retry delays are random distributed values within the ranges of
40+
* ``[5000-10000, 10000-20000, 20000-40000, 40000-80000, 80000-125000]``
3341
* 'jitterInterval' => the number of milliseconds before the data is written increased by a random amount
3442
* ]
35-
* @param array $writeOptions Array containing the write parameters (See above)
43+
* @param array|null $writeOptions Array containing the write parameters (See above)
3644
*/
3745
public function __construct(array $writeOptions = null)
3846
{
@@ -42,6 +50,7 @@ public function __construct(array $writeOptions = null)
4250
$this->retryInterval = $writeOptions["retryInterval"] ?? self::DEFAULT_RETRY_INTERVAL;
4351
$this->maxRetries = $writeOptions["maxRetries"] ?? self::DEFAULT_MAX_RETRIES;
4452
$this->maxRetryDelay = $writeOptions["maxRetryDelay"] ?? self::DEFAULT_MAX_RETRY_DELAY;
53+
$this->maxRetryTime = $writeOptions["maxRetryTime"] ?? self::DEFAULT_MAX_RETRY_TIME;
4554
$this->exponentialBase = $writeOptions["exponentialBase"] ?? self::DEFAULT_EXPONENTIAL_BASE;
4655
$this->jitterInterval = $writeOptions["jitterInterval"] ?? self::DEFAULT_JITTER_INTERVAL;
4756
}

‎src/InfluxDB2/WriteRetry.php

Copy file name to clipboard
+143Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
namespace InfluxDB2;
4+
5+
use GuzzleHttp\Exception\ConnectException;
6+
7+
/**
8+
* Exponential random write retry.
9+
*/
10+
class WriteRetry
11+
{
12+
private $maxRetries;
13+
private $retryInterval;
14+
private $maxRetryDelay;
15+
private $exponentialBase;
16+
private $jitterInterval;
17+
private $maxRetryTime;
18+
private $retryTimout;
19+
/**
20+
* @var mixed|string
21+
*/
22+
private $logFile;
23+
24+
/**
25+
* WriteRetry constructor.
26+
*
27+
* @param int $maxRetries max number of retries when write fails
28+
* @param int $retryInterval number of milliseconds to retry unsuccessful write,
29+
* The retry interval is used when the InfluxDB server does not specify "Retry-After" header.
30+
* @param int $maxRetryDelay maximum delay when retrying write in milliseconds
31+
* @param int $exponentialBase the base for the exponential retry delay, the next delay is computed using
32+
* random exponential backoff as a random value within the interval
33+
* ``retryInterval * exponentialBase^(attempts-1)`` and
34+
* ``retryInterval * exponentialBase^(attempts)``.
35+
* Example for ``retryInterval=5000, exponentialBase=2, maxRetryDelay=125000, total=5``
36+
* Retry delays are random distributed values within the ranges of
37+
* ``[5000-10000, 10000-20000, 20000-40000, 40000-80000, 80000-125000]``
38+
*
39+
* @param int $maxRetryTime maximum total time when retrying write in milliseconds
40+
* @param int $jitterInterval the number of milliseconds before the data is written increased by a random amount
41+
* @param string $logFile logfile
42+
*/
43+
public function __construct(
44+
int $maxRetries = 5,
45+
int $retryInterval = 5000,
46+
int $maxRetryDelay = 125000,
47+
int $exponentialBase = 2,
48+
int $maxRetryTime = 180000,
49+
int $jitterInterval = 0,
50+
string $logFile = "php://output"
51+
) {
52+
$this->maxRetries = $maxRetries;
53+
$this->retryInterval = $retryInterval;
54+
$this->maxRetryDelay = $maxRetryDelay;
55+
$this->maxRetryTime = $maxRetryTime;
56+
$this->exponentialBase = $exponentialBase;
57+
$this->jitterInterval = $jitterInterval;
58+
$this->logFile = $logFile;
59+
60+
//retry timout
61+
$this->retryTimout = microtime(true) * 1000 + $maxRetryTime;
62+
}
63+
64+
/**
65+
* @throws ApiException
66+
*/
67+
public function retry($callable, $attempts = 0)
68+
{
69+
try {
70+
return call_user_func($callable);
71+
} catch (ApiException $e) {
72+
$error = $e->getResponseBody() ?? $e->getMessage();
73+
74+
if (!$this->isRetryable($e)) {
75+
throw $e;
76+
}
77+
$attempts++;
78+
if ($attempts > $this->maxRetries) {
79+
$this->log("ERROR", "Maximum retry attempts reached");
80+
throw $e;
81+
}
82+
83+
// throws exception when max retry time is exceeded
84+
if (microtime(true) * 1000 > $this->retryTimout) {
85+
$this->log("ERROR", "Maximum retry time $this->maxRetryTime ms exceeded");
86+
throw $e;
87+
}
88+
89+
$headers = $e->getResponseHeaders();
90+
if ($headers != null && array_key_exists('Retry-After', $headers)) {
91+
//jitter add in microseconds
92+
$jitterMicro = rand(0, $this->jitterInterval) * 1000;
93+
$timeout = (int)$headers['Retry-After'][0] * 1000000.0 + $jitterMicro;
94+
} else {
95+
$timeout = $this->getBackoffTime($attempts) * 1000;
96+
}
97+
98+
$timeoutInSec = $timeout / 1000000.0;
99+
100+
$message = "The retryable error occurred during writing of data. Reason: '$error'. Retry in: {$timeoutInSec}s.";
101+
$this->log("WARNING", $message);
102+
usleep($timeout);
103+
$this->retry($callable, $attempts);
104+
}
105+
}
106+
107+
public function isRetryable(ApiException $e): bool
108+
{
109+
$code = $e->getCode();
110+
if (($code == null || $code < 429) &&
111+
!($e->getPrevious() instanceof ConnectException)) {
112+
return false;
113+
}
114+
return true;
115+
}
116+
117+
public function getBackoffTime(int $attempt)
118+
{
119+
$range_start = $this->retryInterval;
120+
$range_stop = $this->retryInterval * $this->exponentialBase;
121+
122+
$i = 1;
123+
while ($i < $attempt) {
124+
$i += 1;
125+
$range_start = $range_stop;
126+
$range_stop = $range_stop * $this->exponentialBase;
127+
if ($range_stop > $this->maxRetryDelay) {
128+
break;
129+
}
130+
}
131+
132+
if ($range_stop > $this->maxRetryDelay) {
133+
$range_stop = $this->maxRetryDelay;
134+
}
135+
return $range_start + ($range_stop - $range_start) * (rand(0, 1000) / 1000);
136+
}
137+
138+
private function log(string $level, string $message): void
139+
{
140+
$logDate = date('H:i:s d-M-Y');
141+
file_put_contents($this->logFile, "[$logDate]: [$level] - $message".PHP_EOL, FILE_APPEND);
142+
}
143+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.