Skip to content

Commit 5a3e2e2

Browse files
committed
add automatic failover to driver
this is not yet ideal, because the driver will still connect to the configured endpoint every time, only to find out that there is a new leader. it could be significantly improved by storing the current leader in some sort of cache
1 parent b2157e7 commit 5a3e2e2

File tree

7 files changed

+175
-20
lines changed

7 files changed

+175
-20
lines changed

lib/ArangoDBClient/Connection.php

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,11 @@ public function getOption($name)
206206
*/
207207
public function get($url, array $customHeaders = [])
208208
{
209-
$response = $this->executeRequest(HttpHelper::METHOD_GET, $url, '', $customHeaders);
209+
return $this->handleFailover(function() use (&$url, &$customHeaders) {
210+
$response = $this->executeRequest(HttpHelper::METHOD_GET, $url, '', $customHeaders);
210211

211-
return $this->parseResponse($response);
212+
return $this->parseResponse($response);
213+
});
212214
}
213215

214216
/**
@@ -224,9 +226,11 @@ public function get($url, array $customHeaders = [])
224226
*/
225227
public function post($url, $data, array $customHeaders = [])
226228
{
227-
$response = $this->executeRequest(HttpHelper::METHOD_POST, $url, $data, $customHeaders);
229+
return $this->handleFailover(function() use (&$url, &$data, &$customHeaders) {
230+
$response = $this->executeRequest(HttpHelper::METHOD_POST, $url, $data, $customHeaders);
228231

229-
return $this->parseResponse($response);
232+
return $this->parseResponse($response);
233+
});
230234
}
231235

232236
/**
@@ -242,9 +246,11 @@ public function post($url, $data, array $customHeaders = [])
242246
*/
243247
public function put($url, $data, array $customHeaders = [])
244248
{
245-
$response = $this->executeRequest(HttpHelper::METHOD_PUT, $url, $data, $customHeaders);
249+
return $this->handleFailover(function() use (&$url, &$data, &$customHeaders) {
250+
$response = $this->executeRequest(HttpHelper::METHOD_PUT, $url, $data, $customHeaders);
246251

247-
return $this->parseResponse($response);
252+
return $this->parseResponse($response);
253+
});
248254
}
249255

250256
/**
@@ -259,9 +265,11 @@ public function put($url, $data, array $customHeaders = [])
259265
*/
260266
public function head($url, array $customHeaders = [])
261267
{
262-
$response = $this->executeRequest(HttpHelper::METHOD_HEAD, $url, '', $customHeaders);
268+
return $this->handleFailover(function() use (&$url, &$data, &$customHeaders) {
269+
$response = $this->executeRequest(HttpHelper::METHOD_HEAD, $url, '', $customHeaders);
263270

264-
return $this->parseResponse($response);
271+
return $this->parseResponse($response);
272+
});
265273
}
266274

267275
/**
@@ -276,10 +284,12 @@ public function head($url, array $customHeaders = [])
276284
* @return HttpResponse
277285
*/
278286
public function patch($url, $data, array $customHeaders = [])
279-
{
280-
$response = $this->executeRequest(HttpHelper::METHOD_PATCH, $url, $data, $customHeaders);
287+
{
288+
return $this->handleFailover(function() use (&$url, &$data, &$customHeaders) {
289+
$response = $this->executeRequest(HttpHelper::METHOD_PATCH, $url, $data, $customHeaders);
281290

282-
return $this->parseResponse($response);
291+
return $this->parseResponse($response);
292+
});
283293
}
284294

285295
/**
@@ -295,9 +305,28 @@ public function patch($url, $data, array $customHeaders = [])
295305
*/
296306
public function delete($url, array $customHeaders = [], $data = '')
297307
{
298-
$response = $this->executeRequest(HttpHelper::METHOD_DELETE, $url, $data, $customHeaders);
308+
return $this->handleFailover(function() use (&$url, &$data) {
309+
$response = $this->executeRequest(HttpHelper::METHOD_DELETE, $url, $data, $customHeaders);
299310

300-
return $this->parseResponse($response);
311+
return $this->parseResponse($response);
312+
});
313+
}
314+
315+
316+
private function handleFailover($cb) {
317+
$tries = 0;
318+
319+
while (true) {
320+
try {
321+
return $cb();
322+
} catch (FailoverException $e) {
323+
// got a failover. now try again...
324+
if ($tries++ >= $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
325+
throw $e;
326+
}
327+
}
328+
// let all other exception types escape from here
329+
}
301330
}
302331

303332

@@ -477,7 +506,7 @@ private function executeRequest($method, $url, $data, array $customHeaders = [])
477506
}
478507

479508
$response = new HttpResponse($result, $url, $method, $wasAsync);
480-
509+
481510
if ($traceFunc) {
482511
// call tracer func
483512
if ($this->_options[ConnectionOptions::OPTION_ENHANCED_TRACE]) {
@@ -518,6 +547,30 @@ public function parseResponse(HttpResponse $response)
518547
if ($body !== '') {
519548
// check if we can find details in the response body
520549
$details = json_decode($body, true);
550+
551+
// handle failover
552+
if (is_array($details) && isset($details['errorNum'])) {
553+
if ($details['errorNum'] === 1496) {
554+
// not a leader. now try to find new leader
555+
$leader = $response->getLeaderEndpointHeader();
556+
if ($leader) {
557+
// close existing connection
558+
if ($this->_handle && is_resource($this->_handle)) {
559+
@fclose($this->_handle);
560+
$this->_handle = 0;
561+
}
562+
563+
// have a different leader
564+
$this->_options[ConnectionOptions::OPTION_ENDPOINT] = $leader;
565+
$this->updateHttpHeader();
566+
567+
$exception = new FailoverException($details['errorMessage'], $details['code']);
568+
$exception->setLeader($leader);
569+
throw $exception;
570+
}
571+
}
572+
}
573+
521574
if (is_array($details) && isset($details['errorMessage'])) {
522575
// yes, we got details
523576
$exception = new ServerException($details['errorMessage'], $details['code']);

lib/ArangoDBClient/ConnectionOptions.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ class ConnectionOptions implements \ArrayAccess
5656
* Timeout value index constant
5757
*/
5858
const OPTION_TIMEOUT = 'timeout';
59+
60+
/**
61+
* Number of tries for failover constant
62+
*/
63+
const OPTION_FAILOVER_TRIES = 'failoverTries';
5964

6065
/**
6166
* Trace function index constant
@@ -304,6 +309,7 @@ private static function getDefaults()
304309
self::OPTION_ENDPOINT => null,
305310
self::OPTION_HOST => null,
306311
self::OPTION_PORT => DefaultValues::DEFAULT_PORT,
312+
self::OPTION_FAILOVER_TRIES => DefaultValues::DEFAULT_FAILOVER_TRIES,
307313
self::OPTION_TIMEOUT => DefaultValues::DEFAULT_TIMEOUT,
308314
self::OPTION_CREATE => DefaultValues::DEFAULT_CREATE,
309315
self::OPTION_UPDATE_POLICY => DefaultValues::DEFAULT_UPDATE_POLICY,

lib/ArangoDBClient/DefaultValues.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ abstract class DefaultValues
3030
*/
3131
const DEFAULT_TIMEOUT = 30;
3232

33+
/**
34+
* Default number of failover tries (used in case there is an automatic failover)
35+
*/
36+
const DEFAULT_FAILOVER_TRIES = 2;
37+
3338
/**
3439
* Default Authorization type (use HTTP basic authentication)
3540
*/

lib/ArangoDBClient/Endpoint.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ class Endpoint
5555
/**
5656
* Regexp for TCP endpoints
5757
*/
58-
const REGEXP_TCP = '/^tcp:\/\/(.+?):(\d+)\/?$/';
58+
const REGEXP_TCP = '/^(tcp|http):\/\/(.+?):(\d+)\/?$/';
5959

6060
/**
6161
* Regexp for SSL endpoints
6262
*/
63-
const REGEXP_SSL = '/^ssl:\/\/(.+?):(\d+)\/?$/';
63+
const REGEXP_SSL = '/^(ssl|https):\/\/(.+?):(\d+)\/?$/';
6464

6565
/**
6666
* Regexp for UNIX socket endpoints
@@ -130,6 +130,19 @@ public static function getType($value)
130130

131131
return null;
132132
}
133+
134+
135+
/**
136+
* Return normalize an endpoint string - will convert http: into tcp:, and https: into ssl:
137+
*
138+
* @param string $value - endpoint string
139+
*
140+
* @return string - normalized endpoint string
141+
*/
142+
public static function normalize($value)
143+
{
144+
return preg_replace([ "/http:/", "/https:/" ], [ "tcp:", "ssl:" ], $value);
145+
}
133146

134147
/**
135148
* Return the host name of an endpoint
@@ -141,11 +154,11 @@ public static function getType($value)
141154
public static function getHost($value)
142155
{
143156
if (preg_match(self::REGEXP_TCP, $value, $matches)) {
144-
return $matches[1];
157+
return preg_replace("/^http:/", "tcp:", $matches[2]);
145158
}
146159

147160
if (preg_match(self::REGEXP_SSL, $value, $matches)) {
148-
return $matches[1];
161+
return preg_replace("/^https:/", "ssl:", $matches[2]);
149162
}
150163

151164
return null;
@@ -177,7 +190,7 @@ public static function isValid($value)
177190
*
178191
* @param Connection $connection - the connection to be used
179192
*
180-
* @link https://docs.arangodb.com/HTTP/Endpoints/index.html
193+
* @link https://docs.arangodb.com/HTTP/Endpoints/index.html
181194
* @return array $responseArray - The response array.
182195
* @throws \ArangoDBClient\Exception
183196
*/
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/**
4+
* ArangoDB PHP client: failover exception
5+
*
6+
* @package ArangoDBClient
7+
* @author Jan Steemann
8+
* @copyright Copyright 2018, ArangoDB GmbH, Cologne, Germany
9+
*/
10+
11+
namespace ArangoDBClient;
12+
13+
/**
14+
* Failover-Exception
15+
*
16+
* This exception type will be thrown internally when a failover happens
17+
*
18+
* @package ArangoDBClient
19+
* @since 3.3
20+
*/
21+
class FailoverException extends Exception
22+
{
23+
/**
24+
* New leader endpoint
25+
*
26+
* @param string
27+
*/
28+
private $_leader;
29+
30+
/**
31+
* Return a string representation of the exception
32+
*
33+
* @return string - string representation
34+
*/
35+
public function __toString()
36+
{
37+
return __CLASS__ . ': ' . $this->getLeader();
38+
}
39+
40+
/**
41+
* Set the new leader endpoint
42+
*
43+
* @param string - the new leader endpoint
44+
*
45+
* @return void
46+
*/
47+
public function setLeader($leader)
48+
{
49+
$this->_leader = $leader;
50+
}
51+
52+
/**
53+
* Return the new leader endpoint
54+
*
55+
* @return string - new leader endpoint
56+
*/
57+
public function getLeader()
58+
{
59+
return $this->_leader;
60+
}
61+
}
62+
63+
class_alias(FailoverException::class, '\triagens\ArangoDb\Failover');

lib/ArangoDBClient/HttpHelper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public static function createConnection(ConnectionOptions $options)
9797
}
9898

9999
$fp = @stream_socket_client(
100-
$endpoint,
100+
Endpoint::normalize($endpoint),
101101
$errNo,
102102
$message,
103103
$options[ConnectionOptions::OPTION_TIMEOUT],

lib/ArangoDBClient/HttpResponse.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ class HttpResponse
7373
* HTTP location header
7474
*/
7575
const HEADER_LOCATION = 'location';
76+
77+
/**
78+
* HTTP leader endpoint header, used in failover
79+
*/
80+
const HEADER_LEADER_ENDPOINT = 'x-arango-endpoint';
7681

7782
/**
7883
* Set up the response
@@ -164,6 +169,16 @@ public function getLocationHeader()
164169
{
165170
return $this->getHeader(self::HEADER_LOCATION);
166171
}
172+
173+
/**
174+
* Return the leader location HTTP header of the response
175+
*
176+
* @return string - header value, NULL is header wasn't set in response
177+
*/
178+
public function getLeaderEndpointHeader()
179+
{
180+
return $this->getHeader(self::HEADER_LEADER_ENDPOINT);
181+
}
167182

168183
/**
169184
* Return the body of the response

0 commit comments

Comments
 (0)