Skip to content

Commit f790c3e

Browse files
committed
more failover
1 parent 9c26b1c commit f790c3e

File tree

4 files changed

+198
-30
lines changed

4 files changed

+198
-30
lines changed

lib/ArangoDBClient/Connection.php

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class Connection
3939
* @var string
4040
*/
4141
private $_httpHeader = '';
42-
42+
4343
/**
4444
* Pre-assembled base URL for the current database
4545
* This is pre-calculated when connection options are set/changed, to avoid
@@ -312,16 +312,51 @@ public function delete($url, array $customHeaders = [], $data = '')
312312
});
313313
}
314314

315-
316-
private function handleFailover($cb) {
317-
$tries = 0;
315+
/**
316+
* Execute the specified callback, and try again if it fails because
317+
* the target server is not available. In this case, try again with failing
318+
* over to an alternative server (the new leader) until the maximum number
319+
* of failover attempts have been made
320+
*
321+
* @throws Exception
322+
*
323+
* @param mixed $cb - the callback function to execute
324+
*
325+
* @return HttpResponse
326+
*/
327+
private function handleFailover($cb)
328+
{
329+
if (!$this->_options->haveMultipleEndpoints()) {
330+
// the simple case: no failover
331+
return $cb();
332+
}
318333

334+
// now with failover
335+
$tried = [ ];
319336
while (true) {
320337
try {
338+
// mark the endpoint as being used
339+
$tried[$this->_options->getCurrentEndpoint()] = true;
321340
return $cb();
341+
} catch (ConnectException $e) {
342+
// could not connect. now try again with a different server if possible
343+
if (count($tried) > $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
344+
throw $e;
345+
}
346+
$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;
351+
}
322352
} catch (FailoverException $e) {
323-
// got a failover. now try again...
324-
if ($tries++ >= $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
353+
// got a failover. now try again with a different server if possible
354+
if (count($tried) > $this->_options[ConnectionOptions::OPTION_FAILOVER_TRIES]) {
355+
throw $e;
356+
}
357+
if (isset($tried[$this->_options->getCurrentEndpoint()])) {
358+
// endpoint should have changed by failover procedure
359+
// if not, we can abort now
325360
throw $e;
326361
}
327362
}
@@ -337,7 +372,7 @@ private function updateHttpHeader()
337372
{
338373
$this->_httpHeader = HttpHelper::EOL;
339374

340-
$endpoint = $this->_options[ConnectionOptions::OPTION_ENDPOINT];
375+
$endpoint = $this->_options->getCurrentEndpoint();
341376
if (Endpoint::getType($endpoint) !== Endpoint::TYPE_UNIX) {
342377
$this->_httpHeader .= sprintf('Host: %s%s', Endpoint::getHost($endpoint), HttpHelper::EOL);
343378
}
@@ -391,8 +426,7 @@ private function getHandle()
391426
}
392427

393428
// close handle
394-
@fclose($this->_handle);
395-
$this->_handle = 0;
429+
$this->closeHandle();
396430

397431
if (!$this->_options[ConnectionOptions::OPTION_RECONNECT]) {
398432
// if reconnect option not set, this is the end
@@ -409,6 +443,20 @@ private function getHandle()
409443

410444
return $handle;
411445
}
446+
447+
/**
448+
* Close an existing connection handle
449+
*
450+
* @return void
451+
*/
452+
453+
private function closeHandle()
454+
{
455+
if ($this->_handle && is_resource($this->_handle)) {
456+
@fclose($this->_handle);
457+
}
458+
$this->_handle = 0;
459+
}
412460

413461
/**
414462
* Execute an HTTP request and return the results
@@ -497,6 +545,24 @@ private function executeRequest($method, $url, $data, array $customHeaders = [])
497545

498546
$status = socket_get_status($handle);
499547
if ($status['timed_out']) {
548+
// can't connect to server because of timeout.
549+
// now check if we have additional servers to connect to
550+
if ($this->_options->haveMultipleEndpoints()) {
551+
// connect to next server in list
552+
$currentLeader = $this->_options->getCurrentEndpoint();
553+
$newLeader = $this->_options->nextEndpoint();
554+
555+
if ($newLeader && ($newLeader !== $currentLeader)) {
556+
// close existing connection
557+
$this->closeHandle();
558+
$this->updateHttpHeader();
559+
560+
$exception = new FailoverException("Got a timeout while waiting for the server's response", 408);
561+
$exception->setLeader($newLeader);
562+
throw $exception;
563+
}
564+
}
565+
500566
throw new ClientException('Got a timeout while waiting for the server\'s response', 408);
501567
}
502568

@@ -554,20 +620,22 @@ public function parseResponse(HttpResponse $response)
554620
// not a leader. now try to find new leader
555621
$leader = $response->getLeaderEndpointHeader();
556622
if ($leader) {
557-
// close existing connection
558-
if ($this->_handle && is_resource($this->_handle)) {
559-
@fclose($this->_handle);
560-
$this->_handle = 0;
561-
}
562-
563623
// have a different leader
564-
$this->_options[ConnectionOptions::OPTION_ENDPOINT] = $leader;
565-
$this->updateHttpHeader();
624+
$leader = Endpoint::normalize($leader);
566625

567-
$exception = new FailoverException($details['errorMessage'], $details['code']);
568-
$exception->setLeader($leader);
569-
throw $exception;
626+
$this->_options->addEndpoint($leader);
627+
628+
} else {
629+
$leader = $this->_options->nextEndpoint();
570630
}
631+
632+
// close existing connection
633+
$this->closeHandle();
634+
$this->updateHttpHeader();
635+
636+
$exception = new FailoverException(@$details['errorMessage'], @$details['code']);
637+
$exception->setLeader($leader);
638+
throw $exception;
571639
}
572640
}
573641

lib/ArangoDBClient/ConnectionOptions.php

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ class ConnectionOptions implements \ArrayAccess
3636
* @var Endpoint
3737
*/
3838
private $_endpoint;
39+
40+
/**
41+
* The index into the endpoints array that we will connect to (or are currently
42+
* connected to). This index will be increased in case the currently connected
43+
* server tells us there is a different leader. We will then simply connect
44+
* to the new leader, adjusting this index. If we don't know the new leader
45+
* we will try the next server from the list of endpoints until we find the leader
46+
* or have tried to often
47+
*
48+
* @var int
49+
*/
50+
private $_currentEndpointIndex = 0;
3951

4052
/**
4153
* Endpoint string index constant
@@ -292,11 +304,81 @@ public function getEndpoint()
292304
{
293305
if ($this->_endpoint === null) {
294306
// will also validate the endpoint
295-
$this->_endpoint = new Endpoint($this->_values[self::OPTION_ENDPOINT]);
307+
$this->_endpoint = new Endpoint($this->getCurrentEndpoint());
296308
}
297309

298310
return $this->_endpoint;
299311
}
312+
313+
/**
314+
* Get the current endpoint to use
315+
*
316+
* @return string - Endpoint string to connect to
317+
*/
318+
public function getCurrentEndpoint()
319+
{
320+
if (!is_array($this->_values[self::OPTION_ENDPOINT])) {
321+
return $this->_values[self::OPTION_ENDPOINT];
322+
}
323+
return $this->_values[self::OPTION_ENDPOINT][$this->_currentEndpointIndex];
324+
}
325+
326+
/**
327+
* Whether or not we have multiple endpoints to connect to
328+
*
329+
* @return bool - true if we have more than one endpoint to connect to
330+
*/
331+
public function haveMultipleEndpoints()
332+
{
333+
return is_array($this->_values[self::OPTION_ENDPOINT]) && (count($this->_values[self::OPTION_ENDPOINT]) > 1);
334+
}
335+
336+
/**
337+
* Add a new endpoint to the list of endpoints
338+
* if the endpoint is already in the list, it will not be added again
339+
* as a side-effect, this method will modify _currentEndpointIndex
340+
*
341+
* @param string $endpoint - the endpoint to add
342+
*
343+
* @return void
344+
*/
345+
public function addEndpoint($endpoint)
346+
{
347+
// can only add an endpoint here if the list of endpoints is already an array
348+
if (!is_array($this->_values[self::OPTION_ENDPOINT])) {
349+
// make it an array now
350+
$this->_values[self::OPTION_ENDPOINT] = [ $this->_values[self::OPTION_ENDPOINT] ];
351+
}
352+
$found = array_search($endpoint, $this->_values[self::OPTION_ENDPOINT]);
353+
if ($found === false) {
354+
// a new endpoint we have not seen before
355+
$this->_values[self::OPTION_ENDPOINT][] = $endpoint;
356+
$this->_currentEndpointIndex = count($this->_values[self::OPTION_ENDPOINT]) - 1;
357+
} else {
358+
// we have already got this endpoint
359+
$this->_currentEndpointIndex = $found;
360+
}
361+
}
362+
363+
/**
364+
* Return the next endpoint from the list of endpoints
365+
*
366+
* @return string - the next endpoint
367+
*/
368+
public function nextEndpoint()
369+
{
370+
if (!is_array($this->_values[self::OPTION_ENDPOINT])) {
371+
return $this->_values[self::OPTION_ENDPOINT];
372+
}
373+
374+
$numberOfEndpoints = count($this->_values[self::OPTION_ENDPOINT]);
375+
$this->_currentEndpointIndex++;
376+
if ($this->_currentEndpointIndex >= $numberOfEndpoints) {
377+
$this->_currentEndpointIndex = 0;
378+
}
379+
380+
return $this->_values[self::OPTION_ENDPOINT][$this->_currentEndpointIndex];
381+
}
300382

301383
/**
302384
* Get the default values for the options
@@ -381,27 +463,33 @@ private function validate()
381463

382464
if (isset($this->_values[self::OPTION_HOST]) && !isset($this->_values[self::OPTION_ENDPOINT])) {
383465
// upgrade host/port to an endpoint
384-
$this->_values[self::OPTION_ENDPOINT] = 'tcp://' . $this->_values[self::OPTION_HOST] . ':' . $this->_values[self::OPTION_PORT];
466+
$this->_values[self::OPTION_ENDPOINT] = [ 'tcp://' . $this->_values[self::OPTION_HOST] . ':' . $this->_values[self::OPTION_PORT] ];
385467
unset($this->_values[self::OPTION_HOST]);
386468
}
387469

388470
// set up a new endpoint, this will also validate it
389471
$this->getEndpoint();
390472

391-
$type = Endpoint::getType($this->_values[self::OPTION_ENDPOINT]);
473+
$ep = $this->getCurrentEndpoint();
474+
$type = Endpoint::getType($ep);
392475
if ($type === Endpoint::TYPE_UNIX) {
393476
// must set port to 0 for UNIX domain sockets
394477
$this->_values[self::OPTION_PORT] = 0;
395478
} elseif ($type === Endpoint::TYPE_SSL) {
396479
// must set port to 0 for SSL connections
397480
$this->_values[self::OPTION_PORT] = 0;
398481
} else {
399-
if (preg_match("/:(\d+)$/", $this->_values[self::OPTION_ENDPOINT], $match)) {
482+
if (preg_match("/:(\d+)$/", $ep, $match)) {
400483
// get port number from endpoint, to not confuse developers when dumping
401484
// connection details
402485
$this->_values[self::OPTION_PORT] = (int) $match[1];
403486
}
404487
}
488+
489+
if (is_array($this->_values[self::OPTION_ENDPOINT])) {
490+
// the number of endpoints we have determines the number of failover attempts we'll try
491+
$this->_values[self::OPTION_FAILOVER_TRIES] = count($this->_values[self::OPTION_ENDPOINT]);
492+
}
405493

406494
if (isset($this->_values[self::OPTION_AUTH_TYPE]) && !in_array(
407495
$this->_values[self::OPTION_AUTH_TYPE],

lib/ArangoDBClient/Endpoint.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,31 @@ public static function getHost($value)
167167
/**
168168
* check whether an endpoint specification is valid
169169
*
170-
* @param string $value - endpoint specification value
170+
* @param string $mixed - endpoint specification value (can be a string or an array of strings)
171171
*
172172
* @return bool - true if endpoint specification is valid, false otherwise
173173
*/
174174
public static function isValid($value)
175175
{
176-
if (!is_string($value)) {
176+
if (is_string($value)) {
177+
$value = [ $value ];
178+
}
179+
180+
if (!is_array($value) || count($value) === 0) {
177181
return false;
178182
}
179183

180-
$type = self::getType($value);
184+
foreach ($value as $ep) {
185+
if (!is_string($ep)) {
186+
return false;
187+
}
188+
$type = self::getType($ep);
181189

182-
return !($type === null);
190+
if ($type === null) {
191+
return false;
192+
}
193+
}
194+
return true;
183195
}
184196

185197

lib/ArangoDBClient/HttpHelper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class HttpHelper
8181
*/
8282
public static function createConnection(ConnectionOptions $options)
8383
{
84-
$endpoint = $options[ConnectionOptions::OPTION_ENDPOINT];
84+
$endpoint = $options->getCurrentEndpoint();
8585

8686
$context = stream_context_create();
8787

@@ -108,7 +108,7 @@ public static function createConnection(ConnectionOptions $options)
108108
if (!$fp) {
109109
throw new ConnectException(
110110
'cannot connect to endpoint \'' .
111-
$options[ConnectionOptions::OPTION_ENDPOINT] . '\': ' . $message, $errNo
111+
$endpoint . '\': ' . $message, $errNo
112112
);
113113
}
114114

0 commit comments

Comments
 (0)