Skip to content

Commit 89a3b89

Browse files
committed
make failover a bit more configurable
1 parent b26dc4f commit 89a3b89

File tree

6 files changed

+206
-25
lines changed

6 files changed

+206
-25
lines changed

lib/ArangoDBClient/Connection.php

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -311,14 +311,15 @@ public function delete($url, array $customHeaders = [], $data = '')
311311
return $this->parseResponse($response);
312312
});
313313
}
314-
314+
315315
/**
316316
* Execute the specified callback, and try again if it fails because
317317
* the target server is not available. In this case, try again with failing
318318
* over to an alternative server (the new leader) until the maximum number
319319
* of failover attempts have been made
320320
*
321-
* @throws Exception
321+
* @throws Exception - either a ConnectException or a FailoverException or an
322+
* Exception produced by the callback function
322323
*
323324
* @param mixed $cb - the callback function to execute
324325
*
@@ -331,40 +332,74 @@ private function handleFailover($cb)
331332
return $cb();
332333
}
333334

334-
// now with failover
335-
$tried = [ ];
335+
// here we need to try it with failover
336+
$start = microtime(true);
337+
$tried = [];
338+
$notReachable = [];
336339
while (true) {
340+
$ep = $this->_options->getCurrentEndpoint();
341+
$normalized = Endpoint::normalizeHostname($ep);
342+
343+
if (isset($notReachable[$normalized])) {
344+
$this->notify('no more servers available to try connecting to');
345+
throw new ConnectException('no more servers available to try connecting to');
346+
}
347+
337348
try {
338-
// mark the endpoint as being used
339-
$tried[$this->_options->getCurrentEndpoint()] = true;
349+
// mark the endpoint as being tried
350+
$tried[$normalized] = true;
340351
return $cb();
341352
} catch (ConnectException $e) {
342353
// could not connect. now try again with a different server if possible
343-
if (count($tried) > $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
354+
if ($this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES] > 0 &&
355+
count($tried) > $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
356+
$this->notify('tried too many different servers in failover');
344357
throw $e;
345358
}
359+
// mark endpoint as unreachable
360+
$notReachable[$normalized] = true;
361+
362+
// move on to next endpoint
346363
$ep = $this->_options->nextEndpoint();
347-
if (isset($tried[$ep])) {
348-
// endpoint should have changed by failover procedure
349-
// if not, we can abort now
350-
throw $e;
364+
$normalized = Endpoint::normalizeHostname($ep);
365+
366+
if (isset($tried[$normalized])) {
367+
if (microtime(true) - $start >= $this->_options[ConnectionOptions::OPTION_FAILOVER_TIMEOUT]) {
368+
// timeout reached, we will abort now
369+
$this->notify('no servers reachable after failover timeout');
370+
throw $e;
371+
}
372+
// continue because we have not yet reached the timeout
373+
usleep(20 * 1000);
351374
}
352375
} catch (FailoverException $e) {
353376
// got a failover. now try again with a different server if possible
354-
if (count($tried) > $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
377+
// endpoint should have changed by failover procedure
378+
$ep = $this->_options->getCurrentEndpoint();
379+
$normalized = Endpoint::normalizeHostname($ep);
380+
381+
if ($this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES] > 0 &&
382+
count($tried) > $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
383+
$this->notify('tried too many different servers in failover');
355384
throw $e;
356385
}
357-
if (isset($tried[$this->_options->getCurrentEndpoint()])) {
358-
// endpoint should have changed by failover procedure
359-
// if not, we can abort now
360-
throw $e;
386+
if (isset($tried[$normalized])) {
387+
if (microtime(true) - $start >= $this->_options[ConnectionOptions::OPTION_FAILOVER_TIMEOUT]) {
388+
// timeout reached, we can abort now
389+
$this->notify('no servers reachable after failover timeout');
390+
throw $e;
391+
}
392+
// continue because we have not yet reached the timeout
393+
usleep(20 * 1000);
361394
}
395+
396+
// we need to try the recommended leader again
397+
unset($notReachable[$normalized]);
362398
}
363399
// let all other exception types escape from here
364400
}
365401
}
366402

367-
368403
/**
369404
* Recalculate the static HTTP header string used for all HTTP requests in this connection
370405
*/
@@ -616,7 +651,14 @@ public function parseResponse(HttpResponse $response)
616651

617652
// handle failover
618653
if (is_array($details) && isset($details['errorNum'])) {
654+
if ($details['errorNum'] === 1495) {
655+
// 1495 = leadership challenge is ongoing
656+
$exception = new FailoverException(@$details['errorMessage'], @$details['code']);
657+
throw $exception;
658+
}
659+
619660
if ($details['errorNum'] === 1496) {
661+
// 1496 = not a leader
620662
// not a leader. now try to find new leader
621663
$leader = $response->getLeaderEndpointHeader();
622664
if ($leader) {
@@ -866,8 +908,6 @@ public function setDatabase($database)
866908
}
867909

868910
/**
869-
* Get the database that is currently used with this connection
870-
*
871911
* Get the database to use with this connection, for example: 'my_database'
872912
*
873913
* @return string
@@ -876,6 +916,49 @@ public function getDatabase()
876916
{
877917
return $this->_database;
878918
}
919+
920+
/**
921+
* Test if a connection can be made using the specified connection options,
922+
* i.e. endpoint (host/port), username, password
923+
*
924+
* @return bool - true if the connection succeeds, false if not
925+
*/
926+
public function test()
927+
{
928+
try {
929+
$this->get(Urls::URL_ADMIN_VERSION);
930+
} catch (ConnectException $e) {
931+
return false;
932+
} catch (ServerException $e) {
933+
return false;
934+
} catch (ClientException $e) {
935+
return false;
936+
}
937+
return true;
938+
}
939+
940+
/**
941+
* Returns the current endpoint we are currently connected to
942+
* (or, if no connection has been established yet, the next endpoint
943+
* that we will connect to)
944+
*
945+
* @return string - the current endpoint that we are connected to
946+
*/
947+
public function getCurrentEndpoint()
948+
{
949+
return $this->_options->getCurrentEndpoint();
950+
}
951+
952+
/**
953+
* Calls the notification callback function to inform to make the
954+
* client application aware of some failure so it can do something
955+
* appropriate (e.g. logging)
956+
*
957+
* @return void
958+
*/
959+
private function notify($message) {
960+
$this->_options[ConnectionOptions::OPTION_NOTIFY_CALLBACK]($message);
961+
}
879962
}
880963

881964
class_alias(Connection::class, '\triagens\ArangoDb\Connection');

lib/ArangoDBClient/ConnectionOptions.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,15 @@ class ConnectionOptions implements \ArrayAccess
7070
const OPTION_TIMEOUT = 'timeout';
7171

7272
/**
73-
* Number of tries for failover constant
73+
* Number of servers tried in case of failover
74+
* if set to 0, then an unlimited amount of servers will be tried
7475
*/
7576
const OPTION_FAILOVER_TRIES = 'failoverTries';
77+
78+
/**
79+
* Max amount of time (in seconds) that is spent waiting on failover
80+
*/
81+
const OPTION_FAILOVER_TIMEOUT = 'failoverTimeout';
7682

7783
/**
7884
* Trace function index constant
@@ -83,6 +89,11 @@ class ConnectionOptions implements \ArrayAccess
8389
* "verify certificates" index constant
8490
*/
8591
const OPTION_VERIFY_CERT = 'verifyCert';
92+
93+
/**
94+
* "verify certificate host name" index constant
95+
*/
96+
const OPTION_VERIFY_CERT_NAME = 'verifyCertName';
8697

8798
/**
8899
* "allow self-signed" index constant
@@ -233,6 +244,11 @@ class ConnectionOptions implements \ArrayAccess
233244
* Entry for memcached cache ttl
234245
*/
235246
const OPTION_MEMCACHED_TTL = 'memcachedTtl';
247+
248+
/**
249+
* Entry for notification callback
250+
*/
251+
const OPTION_NOTIFY_CALLBACK = 'notifyCallback';
236252

237253
/**
238254
* Set defaults, use options provided by client and validate them
@@ -361,9 +377,19 @@ public function addEndpoint($endpoint)
361377
if (!is_string($endpoint) || !Endpoint::isValid($endpoint)) {
362378
throw new ClientException(sprintf("invalid endpoint specification '%s'", $endpoint));
363379
}
380+
$endpoint = Endpoint::normalize($endpoint);
364381

365382
assert(is_array($this->_values[self::OPTION_ENDPOINT]));
366383
$found = array_search($endpoint, $this->_values[self::OPTION_ENDPOINT]);
384+
if ($found === false) {
385+
$normalized = Endpoint::normalizeHostname($endpoint);
386+
$found = array_search($normalized, $this->_values[self::OPTION_ENDPOINT]);
387+
if ($found === false) {
388+
$normalized = Endpoint::denormalizeHostname($endpoint);
389+
$found = array_search($normalized, $this->_values[self::OPTION_ENDPOINT]);
390+
}
391+
}
392+
367393
if ($found === false) {
368394
// a new endpoint we have not seen before
369395
$this->_values[self::OPTION_ENDPOINT][] = $endpoint;
@@ -399,7 +425,7 @@ public function nextEndpoint()
399425
if ($numberOfEndpoints > 1) {
400426
$this->storeOptionsInCache();
401427
}
402-
428+
403429
return $endpoint;
404430
}
405431

@@ -415,6 +441,7 @@ private static function getDefaults()
415441
self::OPTION_HOST => null,
416442
self::OPTION_PORT => DefaultValues::DEFAULT_PORT,
417443
self::OPTION_FAILOVER_TRIES => DefaultValues::DEFAULT_FAILOVER_TRIES,
444+
self::OPTION_FAILOVER_TIMEOUT => DefaultValues::DEFAULT_FAILOVER_TIMEOUT,
418445
self::OPTION_TIMEOUT => DefaultValues::DEFAULT_TIMEOUT,
419446
self::OPTION_MEMCACHED_PERSISTENT_ID => 'arangodb-php-pool',
420447
self::OPTION_MEMCACHED_OPTIONS => [ ],
@@ -434,6 +461,7 @@ private static function getDefaults()
434461
self::OPTION_TRACE => null,
435462
self::OPTION_ENHANCED_TRACE => false,
436463
self::OPTION_VERIFY_CERT => DefaultValues::DEFAULT_VERIFY_CERT,
464+
self::OPTION_VERIFY_CERT_NAME => DefaultValues::DEFAULT_VERIFY_CERT_NAME,
437465
self::OPTION_ALLOW_SELF_SIGNED => DefaultValues::DEFAULT_ALLOW_SELF_SIGNED,
438466
self::OPTION_CIPHERS => DefaultValues::DEFAULT_CIPHERS,
439467
self::OPTION_AUTH_USER => null,
@@ -443,7 +471,8 @@ private static function getDefaults()
443471
self::OPTION_BATCH => false,
444472
self::OPTION_BATCHPART => false,
445473
self::OPTION_DATABASE => '_system',
446-
self::OPTION_CHECK_UTF8_CONFORM => DefaultValues::DEFAULT_CHECK_UTF8_CONFORM
474+
self::OPTION_CHECK_UTF8_CONFORM => DefaultValues::DEFAULT_CHECK_UTF8_CONFORM,
475+
self::OPTION_NOTIFY_CALLBACK => function ($message) {}
447476
];
448477
}
449478

@@ -500,6 +529,9 @@ private function validate()
500529
}
501530

502531
assert(is_array($this->_values[self::OPTION_ENDPOINT]));
532+
foreach ($this->_values[self::OPTION_ENDPOINT] as $key => $value) {
533+
$this->_values[self::OPTION_ENDPOINT][$key] = Endpoint::normalize($value);
534+
}
503535

504536
// validate endpoint
505537
$ep = $this->getCurrentEndpoint();

lib/ArangoDBClient/DefaultValues.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ abstract class DefaultValues
3131
const DEFAULT_TIMEOUT = 30;
3232

3333
/**
34-
* Default number of failover tries (used in case there is an automatic failover)
34+
* Default number of failover servers to try (used in case there is an automatic failover)
35+
* if set to 0, then an unlimited amount of servers will be tried
3536
*/
36-
const DEFAULT_FAILOVER_TRIES = 2;
37+
const DEFAULT_FAILOVER_TRIES = 3;
38+
39+
/**
40+
* Default max amount of time (in seconds) that is spent waiting on failover
41+
*/
42+
const DEFAULT_FAILOVER_TIMEOUT = 30;
3743

3844
/**
3945
* Default Authorization type (use HTTP basic authentication)
@@ -69,6 +75,11 @@ abstract class DefaultValues
6975
* Default value for SSL certificate verification
7076
*/
7177
const DEFAULT_VERIFY_CERT = false;
78+
79+
/**
80+
* Default value for SSL certificate host name verification
81+
*/
82+
const DEFAULT_VERIFY_CERT_NAME = false;
7283

7384
/**
7485
* Default value for accepting self-signed SSL certificates

lib/ArangoDBClient/Endpoint.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@ public static function listEndpoints(Connection $connection)
212212

213213
return $response->getJson();
214214
}
215+
216+
/**
217+
* Replaces "localhost" in hostname with "[::1]" in order to make these values the same
218+
* for later comparisons
219+
*
220+
* @param string $hostname - hostname
221+
*
222+
* @return string - normalized hostname
223+
*/
224+
public static function normalizeHostname($hostname) {
225+
// replace "localhost" with [::1] as arangod does
226+
return preg_replace("/^(tcp|ssl|https?):\/\/localhost:/", "\\1://[::1]:", $hostname);
227+
}
228+
229+
public static function denormalizeHostname($hostname) {
230+
// replace "localhost" with [::1] as arangod does
231+
return preg_replace("/^(tcp|ssl|https?):\/\/\[::1\]:/", "\\1://localhost:", $hostname);
232+
}
215233
}
216234

217235
class_alias(Endpoint::class, '\triagens\ArangoDb\Endpoint');

lib/ArangoDBClient/HttpHelper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public static function createConnection(ConnectionOptions $options)
8787
if (Endpoint::getType($endpoint) === Endpoint::TYPE_SSL) {
8888
// set further SSL options for the endpoint
8989
stream_context_set_option($context, 'ssl', 'verify_peer', $options[ConnectionOptions::OPTION_VERIFY_CERT]);
90+
@stream_context_set_option($context, 'ssl', 'verify_peer_name', $options[ConnectionOptions::OPTION_VERIFY_CERT_NAME]);
9091
stream_context_set_option($context, 'ssl', 'allow_self_signed', $options[ConnectionOptions::OPTION_ALLOW_SELF_SIGNED]);
9192

9293
if ($options[ConnectionOptions::OPTION_CIPHERS] !== null) {

0 commit comments

Comments
 (0)