Skip to content

Commit e57ccc2

Browse files
committed
Use socket error codes (errnos) for connection rejections
1 parent 2da3c60 commit e57ccc2

File tree

6 files changed

+225
-52
lines changed

6 files changed

+225
-52
lines changed

src/Factory.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ public function createClient($uri)
5656

5757
$uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri);
5858
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) {
59-
return \React\Promise\reject(new \InvalidArgumentException('Invalid Redis URI given'));
59+
return \React\Promise\reject(new \InvalidArgumentException(
60+
'Invalid Redis URI given (EINVAL)',
61+
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
62+
));
6063
}
6164

6265
$args = array();
@@ -73,7 +76,10 @@ public function createClient($uri)
7376

7477
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
7578
// connection cancelled, start with rejecting attempt, then clean up
76-
$reject(new \RuntimeException('Connection to ' . $uri . ' cancelled'));
79+
$reject(new \RuntimeException(
80+
'Connection to ' . $uri . ' cancelled (ECONNABORTED)',
81+
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
82+
));
7783

7884
// either close successful connection or cancel pending connection attempt
7985
$connecting->then(function (ConnectionInterface $connection) {
@@ -88,7 +94,7 @@ public function createClient($uri)
8894
}, function (\Exception $e) use ($uri) {
8995
throw new \RuntimeException(
9096
'Connection to ' . $uri . ' failed: ' . $e->getMessage(),
91-
0,
97+
$e->getCode(),
9298
$e
9399
);
94100
});
@@ -106,8 +112,8 @@ function (\Exception $e) use ($client, $uri) {
106112
$client->close();
107113

108114
throw new \RuntimeException(
109-
'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage(),
110-
0,
115+
'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . ' (EACCES)',
116+
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13,
111117
$e
112118
);
113119
}
@@ -127,8 +133,8 @@ function (\Exception $e) use ($client, $uri) {
127133
$client->close();
128134

129135
throw new \RuntimeException(
130-
'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage(),
131-
0,
136+
'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . ' (ENOENT)',
137+
defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2,
132138
$e
133139
);
134140
}
@@ -147,7 +153,8 @@ function (\Exception $e) use ($client, $uri) {
147153
return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) {
148154
if ($e instanceof TimeoutException) {
149155
throw new \RuntimeException(
150-
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds'
156+
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)',
157+
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
151158
);
152159
}
153160
throw $e;

src/LazyClient.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ private function client()
115115
public function __call($name, $args)
116116
{
117117
if ($this->closed) {
118-
return \React\Promise\reject(new \RuntimeException('Connection closed'));
118+
return \React\Promise\reject(new \RuntimeException(
119+
'Connection closed (ENOTCONN)',
120+
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
121+
));
119122
}
120123

121124
$that = $this;

src/StreamingClient.php

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22

33
namespace Clue\React\Redis;
44

5-
use Evenement\EventEmitter;
6-
use Clue\Redis\Protocol\Parser\ParserInterface;
7-
use Clue\Redis\Protocol\Parser\ParserException;
8-
use Clue\Redis\Protocol\Serializer\SerializerInterface;
95
use Clue\Redis\Protocol\Factory as ProtocolFactory;
10-
use UnderflowException;
11-
use RuntimeException;
12-
use InvalidArgumentException;
13-
use React\Promise\Deferred;
146
use Clue\Redis\Protocol\Model\ErrorReply;
157
use Clue\Redis\Protocol\Model\ModelInterface;
168
use Clue\Redis\Protocol\Model\MultiBulkReply;
9+
use Clue\Redis\Protocol\Parser\ParserException;
10+
use Clue\Redis\Protocol\Parser\ParserInterface;
11+
use Clue\Redis\Protocol\Serializer\SerializerInterface;
12+
use Evenement\EventEmitter;
13+
use React\Promise\Deferred;
1714
use React\Stream\DuplexStreamInterface;
1815

1916
/**
@@ -47,18 +44,20 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars
4744
$stream->on('data', function($chunk) use ($parser, $that) {
4845
try {
4946
$models = $parser->pushIncoming($chunk);
50-
}
51-
catch (ParserException $error) {
52-
$that->emit('error', array($error));
47+
} catch (ParserException $error) {
48+
$that->emit('error', array(new \UnexpectedValueException(
49+
'Invalid data received: ' . $error->getMessage() . ' (EBADMSG)',
50+
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77,
51+
$error
52+
)));
5353
$that->close();
5454
return;
5555
}
5656

5757
foreach ($models as $data) {
5858
try {
5959
$that->handleMessage($data);
60-
}
61-
catch (UnderflowException $error) {
60+
} catch (\UnderflowException $error) {
6261
$that->emit('error', array($error));
6362
$that->close();
6463
return;
@@ -84,11 +83,20 @@ public function __call($name, $args)
8483
static $pubsubs = array('subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe');
8584

8685
if ($this->ending) {
87-
$request->reject(new RuntimeException('Connection closed'));
86+
$request->reject(new \RuntimeException(
87+
'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)',
88+
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
89+
));
8890
} elseif (count($args) !== 1 && in_array($name, $pubsubs)) {
89-
$request->reject(new InvalidArgumentException('PubSub commands limited to single argument'));
91+
$request->reject(new \InvalidArgumentException(
92+
'PubSub commands limited to single argument (EINVAL)',
93+
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
94+
));
9095
} elseif ($name === 'monitor') {
91-
$request->reject(new \BadMethodCallException('MONITOR command explicitly not supported'));
96+
$request->reject(new \BadMethodCallException(
97+
'MONITOR command explicitly not supported (ENOTSUP)',
98+
defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95)
99+
));
92100
} else {
93101
$this->stream->write($this->serializer->getRequestMessage($name, $args));
94102
$this->requests []= $request;
@@ -131,7 +139,10 @@ public function handleMessage(ModelInterface $message)
131139
}
132140

133141
if (!$this->requests) {
134-
throw new UnderflowException('Unexpected reply received, no matching request found');
142+
throw new \UnderflowException(
143+
'Unexpected reply received, no matching request found (ENOMSG)',
144+
defined('SOCKET_ENOMSG') ? SOCKET_ENOMSG : 42
145+
);
135146
}
136147

137148
$request = array_shift($this->requests);
@@ -173,8 +184,11 @@ public function close()
173184
// reject all remaining requests in the queue
174185
while($this->requests) {
175186
$request = array_shift($this->requests);
176-
/* @var $request Request */
177-
$request->reject(new RuntimeException('Connection closing'));
187+
/* @var $request Deferred */
188+
$request->reject(new \RuntimeException(
189+
'Connection closing (ENOTCONN)',
190+
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
191+
));
178192
}
179193
}
180194
}

tests/FactoryStreamingClientTest.php

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR
200200
$this->logicalAnd(
201201
$this->isInstanceOf('RuntimeException'),
202202
$this->callback(function (\RuntimeException $e) {
203-
return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: ERR invalid password';
203+
return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: ERR invalid password (EACCES)';
204+
}),
205+
$this->callback(function (\RuntimeException $e) {
206+
return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
204207
}),
205208
$this->callback(function (\RuntimeException $e) {
206209
return $e->getPrevious()->getMessage() === 'ERR invalid password';
@@ -264,7 +267,10 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro
264267
$this->logicalAnd(
265268
$this->isInstanceOf('RuntimeException'),
266269
$this->callback(function (\RuntimeException $e) {
267-
return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: ERR DB index is out of range';
270+
return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: ERR DB index is out of range (ENOENT)';
271+
}),
272+
$this->callback(function (\RuntimeException $e) {
273+
return $e->getCode() === (defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2);
268274
}),
269275
$this->callback(function (\RuntimeException $e) {
270276
return $e->getPrevious()->getMessage() === 'ERR DB index is out of range';
@@ -284,6 +290,9 @@ public function testWillRejectIfConnectorRejects()
284290
$this->callback(function (\RuntimeException $e) {
285291
return $e->getMessage() === 'Connection to redis://127.0.0.1:2 failed: Foo';
286292
}),
293+
$this->callback(function (\RuntimeException $e) {
294+
return $e->getCode() === 42;
295+
}),
287296
$this->callback(function (\RuntimeException $e) {
288297
return $e->getPrevious()->getMessage() === 'Foo';
289298
})
@@ -299,7 +308,10 @@ public function testWillRejectIfTargetIsInvalid()
299308
$this->logicalAnd(
300309
$this->isInstanceOf('InvalidArgumentException'),
301310
$this->callback(function (\InvalidArgumentException $e) {
302-
return $e->getMessage() === 'Invalid Redis URI given';
311+
return $e->getMessage() === 'Invalid Redis URI given (EINVAL)';
312+
}),
313+
$this->callback(function (\InvalidArgumentException $e) {
314+
return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22);
303315
})
304316
)
305317
));
@@ -399,7 +411,10 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec
399411
$this->logicalAnd(
400412
$this->isInstanceOf('RuntimeException'),
401413
$this->callback(function (\RuntimeException $e) use ($safe) {
402-
return $e->getMessage() === 'Connection to ' . $safe . ' cancelled';
414+
return $e->getMessage() === 'Connection to ' . $safe . ' cancelled (ECONNABORTED)';
415+
}),
416+
$this->callback(function (\RuntimeException $e) {
417+
return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
403418
})
404419
)
405420
));
@@ -420,7 +435,10 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect()
420435
$this->logicalAnd(
421436
$this->isInstanceOf('RuntimeException'),
422437
$this->callback(function (\RuntimeException $e) {
423-
return $e->getMessage() === 'Connection to redis://127.0.0.1:2/123 cancelled';
438+
return $e->getMessage() === 'Connection to redis://127.0.0.1:2/123 cancelled (ECONNABORTED)';
439+
}),
440+
$this->callback(function (\RuntimeException $e) {
441+
return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
424442
})
425443
)
426444
));
@@ -446,7 +464,10 @@ public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExp
446464
$this->logicalAnd(
447465
$this->isInstanceOf('RuntimeException'),
448466
$this->callback(function (\Exception $e) {
449-
return $e->getMessage() === 'Connection to redis://127.0.0.1:2?timeout=0 timed out after 0 seconds';
467+
return $e->getMessage() === 'Connection to redis://127.0.0.1:2?timeout=0 timed out after 0 seconds (ETIMEDOUT)';
468+
}),
469+
$this->callback(function (\Exception $e) {
470+
return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
450471
})
451472
)
452473
));

tests/LazyClientTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,17 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection(
185185
$this->client->close();
186186
$promise = $this->client->ping();
187187

188-
$promise->then(null, $this->expectCallableOnce());
188+
$promise->then(null, $this->expectCallableOnceWith(
189+
$this->logicalAnd(
190+
$this->isInstanceOf('RuntimeException'),
191+
$this->callback(function (\RuntimeException $e) {
192+
return $e->getMessage() === 'Connection closed (ENOTCONN)';
193+
}),
194+
$this->callback(function (\RuntimeException $e) {
195+
return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107);
196+
})
197+
)
198+
));
189199
}
190200

191201
public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves()

0 commit comments

Comments
 (0)