Skip to content

Commit 5b74a0f

Browse files
authored
Merge pull request #128 from mermshaus/issue-125
Add test to illustrate #125 (stream_select/interrupt issue). Also a fix
2 parents aba52e4 + 1f4bcc9 commit 5b74a0f

10 files changed

+179
-70
lines changed

Diff for: composer.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,21 @@
2626
],
2727
"require": {
2828
"php": "^7.1 || ^8.0",
29+
"ext-pcntl": "*",
2930
"react/event-loop": "^1.0 || ^0.5 || ^0.4",
3031
"react/promise": "~2.2"
3132
},
3233
"require-dev": {
33-
"phpunit/phpunit": "^9.5 || ^7.5.20"
34+
"phpunit/phpunit": "^9.5 || ^7.5.20",
35+
"symfony/process": "^6.1 || ^4.4"
3436
},
3537
"autoload": {
3638
"psr-4": {
3739
"Bunny\\": "src/Bunny/"
3840
}
3941
},
4042
"autoload-dev": {
43+
"files": ["test/Library/functions.php"],
4144
"psr-4": {
4245
"Bunny\\": "test/Bunny/",
4346
"Bunny\\Test\\Library\\": "test/Library/"

Diff for: docker/bunny/Dockerfile

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ RUN apt-get update \
88
vim \
99
zip
1010

11+
RUN docker-php-ext-configure pcntl --enable-pcntl \
12+
&& docker-php-ext-install pcntl
13+
1114
RUN pecl install xdebug \
1215
&& docker-php-ext-enable xdebug
1316
RUN echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini

Diff for: src/Bunny/Client.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,11 @@ public function run($maxSeconds = null)
202202

203203
if (($n = @stream_select($r, $w, $e, $tvSec, $tvUsec)) === false) {
204204
$lastError = error_get_last();
205+
206+
// Note: The word "Unable" within the stream_select error message was spelled "unable" in PHP
207+
// versions < 8.
205208
if ($lastError !== null &&
206-
preg_match("/^stream_select\\(\\): unable to select \\[(\\d+)\\]:/", $lastError["message"], $m) &&
209+
preg_match("/^stream_select\\(\\): [Uu]nable to select \\[(\\d+)\\]:/", $lastError["message"], $m) &&
207210
intval($m[1]) === PCNTL_EINTR
208211
) {
209212
// got interrupted by signal, dispatch signals & continue

Diff for: test/Bunny/ClientTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
use Bunny\Exception\ClientException;
1111
use Bunny\Protocol\MethodBasicAckFrame;
1212
use Bunny\Protocol\MethodBasicReturnFrame;
13+
use Bunny\Test\Library\Environment;
14+
use Bunny\Test\Library\Paths;
1315
use Bunny\Test\Library\SynchronousClientHelper;
1416
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\Process\Exception\ProcessFailedException;
18+
use Symfony\Component\Process\Process;
19+
20+
use const SIGINT;
1521

1622
class ClientTest extends TestCase
1723
{
@@ -127,6 +133,35 @@ public function testDisconnectWithBufferedMessages()
127133
$channel->queueDelete("disconnect_test");
128134
}
129135

136+
/**
137+
* Spawns an external consumer process, and tries to stop it with SIGINT.
138+
*/
139+
public function testStopConsumerWithSigInt()
140+
{
141+
$queueName = 'stop-consumer-with-sigint';
142+
143+
$path = Paths::getTestsRootPath() . '/scripts/bunny-consumer.php';
144+
145+
$process = new Process([$path, Environment::getTestRabbitMqConnectionUri(), $queueName, '0']);
146+
147+
$process->start();
148+
149+
$signalSent = false;
150+
$starttime = microtime(true);
151+
152+
// Send SIGINT after 1.0 seconds
153+
while ($process->isRunning()) {
154+
if (!$signalSent && microtime(true) > $starttime + 1.0) {
155+
$process->signal(SIGINT);
156+
$signalSent = true;
157+
}
158+
159+
usleep(10000);
160+
}
161+
162+
self::assertTrue($process->isSuccessful(), $process->getOutput() . "\n" . $process->getErrorOutput());
163+
}
164+
130165
public function testGet()
131166
{
132167
$client = $this->helper->createClient();

Diff for: test/Library/AbstractClientHelper.php

-66
Original file line numberDiff line numberDiff line change
@@ -6,70 +6,4 @@
66

77
abstract class AbstractClientHelper
88
{
9-
/**
10-
* @param string $uri
11-
*
12-
* @return array
13-
*/
14-
protected function parseAmqpUri($uri): array
15-
{
16-
$uriComponents = parse_url($uri);
17-
18-
if (
19-
!isset($uriComponents['scheme'])
20-
|| !in_array($uriComponents['scheme'], ['amqp', 'amqps'])
21-
) {
22-
throw new \RuntimeException(
23-
sprintf(
24-
'URI scheme must be "amqp" or "amqps". URI given: "%s"',
25-
$uri
26-
)
27-
);
28-
}
29-
30-
$options = [];
31-
32-
if (isset($uriComponents['host'])) {
33-
$options['host'] = $uriComponents['host'];
34-
}
35-
36-
if (isset($uriComponents['port'])) {
37-
$options['port'] = $uriComponents['port'];
38-
}
39-
40-
if (isset($uriComponents['user'])) {
41-
$options['user'] = $uriComponents['user'];
42-
}
43-
44-
if (isset($uriComponents['pass'])) {
45-
$options['password'] = $uriComponents['pass'];
46-
}
47-
48-
if (isset($uriComponents['path'])) {
49-
$vhostCandidate = $uriComponents['path'];
50-
51-
if (strpos($vhostCandidate, '/') === 0) {
52-
$vhostCandidate = substr($vhostCandidate, 1);
53-
}
54-
55-
if (strpos($vhostCandidate, '/') !== false) {
56-
throw new \RuntimeException(
57-
sprintf(
58-
'An URI path component that is a valid vhost may not contain unescaped "/" characters. URI given: "%s"',
59-
$uri
60-
)
61-
);
62-
}
63-
64-
$vhostCandidate = rawurldecode($vhostCandidate);
65-
66-
$options['vhost'] = $vhostCandidate;
67-
}
68-
69-
if ($options['vhost'] === '') {
70-
$options['vhost'] = '/';
71-
}
72-
73-
return $options;
74-
}
759
}

Diff for: test/Library/AsynchronousClientHelper.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function getDefaultOptions(): array
2929
{
3030
$options = [];
3131

32-
$options = array_merge($options, $this->parseAmqpUri(Environment::getTestRabbitMqConnectionUri()));
32+
$options = array_merge($options, parseAmqpUri(Environment::getTestRabbitMqConnectionUri()));
3333

3434
return $options;
3535
}

Diff for: test/Library/Paths.php

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bunny\Test\Library;
6+
7+
final class Paths
8+
{
9+
public static function getTestsRootPath(): string
10+
{
11+
return dirname(__DIR__);
12+
}
13+
}

Diff for: test/Library/SynchronousClientHelper.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public function getDefaultOptions(): array
7373
{
7474
$options = [];
7575

76-
$options = array_merge($options, $this->parseAmqpUri(Environment::getTestRabbitMqConnectionUri()));
76+
$options = array_merge($options, parseAmqpUri(Environment::getTestRabbitMqConnectionUri()));
7777

7878
return $options;
7979
}

Diff for: test/Library/functions.php

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bunny\Test\Library;
6+
7+
function parseAmqpUri($uri): array
8+
{
9+
$uriComponents = parse_url($uri);
10+
11+
if (
12+
!isset($uriComponents['scheme'])
13+
|| !in_array($uriComponents['scheme'], ['amqp', 'amqps'])
14+
) {
15+
throw new \RuntimeException(
16+
sprintf(
17+
'URI scheme must be "amqp" or "amqps". URI given: "%s"',
18+
$uri
19+
)
20+
);
21+
}
22+
23+
$options = [];
24+
25+
if (isset($uriComponents['host'])) {
26+
$options['host'] = $uriComponents['host'];
27+
}
28+
29+
if (isset($uriComponents['port'])) {
30+
$options['port'] = $uriComponents['port'];
31+
}
32+
33+
if (isset($uriComponents['user'])) {
34+
$options['user'] = $uriComponents['user'];
35+
}
36+
37+
if (isset($uriComponents['pass'])) {
38+
$options['password'] = $uriComponents['pass'];
39+
}
40+
41+
if (isset($uriComponents['path'])) {
42+
$vhostCandidate = $uriComponents['path'];
43+
44+
if (strpos($vhostCandidate, '/') === 0) {
45+
$vhostCandidate = substr($vhostCandidate, 1);
46+
}
47+
48+
if (strpos($vhostCandidate, '/') !== false) {
49+
throw new \RuntimeException(
50+
sprintf(
51+
'An URI path component that is a valid vhost may not contain unescaped "/" characters. URI given: "%s"',
52+
$uri
53+
)
54+
);
55+
}
56+
57+
$vhostCandidate = rawurldecode($vhostCandidate);
58+
59+
$options['vhost'] = $vhostCandidate;
60+
}
61+
62+
if ($options['vhost'] === '') {
63+
$options['vhost'] = '/';
64+
}
65+
66+
return $options;
67+
}

Diff for: test/scripts/bunny-consumer.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
// Usage: bunny-consumer.php <amqp-uri> <queue-name> <max-seconds>
5+
6+
declare(strict_types=1);
7+
8+
namespace Bunny\Test\App;
9+
10+
use Bunny\Channel;
11+
use Bunny\Client;
12+
use Bunny\Message;
13+
14+
use function Bunny\Test\Library\parseAmqpUri;
15+
16+
require __DIR__ . '/../../vendor/autoload.php';
17+
18+
function app(array $args)
19+
{
20+
$connection = parseAmqpUri($args['amqpUri']);
21+
22+
$client = new Client($connection);
23+
24+
pcntl_signal(SIGINT, function () use ($client) {
25+
$client->disconnect()->done(function () use ($client) {
26+
$client->stop();
27+
});
28+
});
29+
30+
$client->connect();
31+
$channel = $client->channel();
32+
33+
$channel->qos(0, 1);
34+
$channel->queueDeclare($args['queueName']);
35+
$channel->consume(function (Message $message, Channel $channel) use ($client) {
36+
$channel->ack($message);
37+
});
38+
$client->run($args['maxSeconds'] > 0 ? $args['maxSeconds'] : null);
39+
}
40+
41+
$argv_copy = $argv;
42+
43+
array_shift($argv_copy);
44+
45+
$args = [
46+
'amqpUri' => array_shift($argv_copy),
47+
'queueName' => array_shift($argv_copy),
48+
'maxSeconds' => (int) array_shift($argv_copy),
49+
];
50+
51+
app($args);

0 commit comments

Comments
 (0)