Skip to content

Commit 8523cbf

Browse files
malkuschwillemstuursma
authored andcommitted
Add PcntlTimeout utility class
PcntlTimeout::timeBoxed() can be used to put timeouts on system calls like flock(). This class works only in UNIX with enabled pcntl extension. It should not be used in applications which install SIGALRM signal handler or schedule alarms. See also: #6
1 parent a76b8f4 commit 8523cbf

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed

classes/util/PcntlTimeout.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace malkusch\lock\util;
4+
5+
use malkusch\lock\exception\TimeoutException;
6+
use malkusch\lock\exception\LockAcquireException;
7+
8+
/**
9+
* Timeout based on a scheduled alarm.
10+
*
11+
* This class requires the pcntl module.
12+
*
13+
* @author Markus Malkusch <[email protected]>
14+
* @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations
15+
* @license WTFPL
16+
* @internal
17+
*/
18+
final class PcntlTimeout
19+
{
20+
21+
/**
22+
* @var int Timeout in seconds
23+
*/
24+
private $timeout;
25+
26+
/**
27+
* Builds the timeout.
28+
*
29+
* @param int $timeout Timeout in seconds
30+
*/
31+
public function __construct($timeout)
32+
{
33+
if (!self::isSupported()) {
34+
throw new \RuntimeException("PCNTL module not enabled");
35+
}
36+
if ($timeout <= 0) {
37+
throw new \InvalidArgumentException("Timeout must be positiv and non zero");
38+
}
39+
$this->timeout = $timeout;
40+
}
41+
42+
/**
43+
* Runs the code and would eventually time out.
44+
*
45+
* This method has the side effect, that any signal handler
46+
* for SIGALRM will be reset to the default hanlder (SIG_DFL).
47+
* It also expects that there is no previously scheduled alarm.
48+
* If your application uses alarms ({@link pcntl_alarm()}) or
49+
* a signal handler for SIGALRM, don't use this method. It will
50+
* interfer with your application and lead to unexpected behaviour.
51+
*
52+
* @param callable $code Executed code block
53+
* @return mixed Return value of the executed block
54+
*
55+
* @throws TimeoutException Running the code timed out
56+
* @throws LockAcquireException Installing the timeout failed
57+
*/
58+
public function timeBoxed($code)
59+
{
60+
$signal = pcntl_signal(SIGALRM, function () {
61+
throw new TimeoutException("Timed out");
62+
});
63+
if (!$signal) {
64+
throw new LockAcquireException("Could not install signal");
65+
}
66+
$oldAlarm = pcntl_alarm($this->timeout);
67+
if ($oldAlarm != 0) {
68+
throw new LockAcquireException("Existing alarm was not expected");
69+
}
70+
try {
71+
return call_user_func($code);
72+
} finally {
73+
pcntl_alarm(0);
74+
pcntl_signal_dispatch();
75+
pcntl_signal(SIGALRM, SIG_DFL);
76+
}
77+
}
78+
79+
/**
80+
* Returns if this class is supported by the PHP runtime.
81+
*
82+
* This class requires the pcntl module. This method checks if
83+
* it is available.
84+
*
85+
* @return bool TRUE if this class is supported by the PHP runtime.
86+
*/
87+
public static function isSupported()
88+
{
89+
return extension_loaded("pcntl");
90+
}
91+
}

tests/util/PcntlTimeoutTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace malkusch\lock\util;
4+
5+
/**
6+
* Tests for PcntlTimeout
7+
*
8+
* @author Markus Malkusch <[email protected]>
9+
* @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations
10+
* @license WTFPL
11+
* @see PcntlTimeout
12+
* @requires pcntl
13+
*/
14+
class PcntlTimeoutTest extends \PHPUnit_Framework_TestCase
15+
{
16+
17+
/**
18+
* A long running system call should be interrupted
19+
*
20+
* @test
21+
* @expectedException malkusch\lock\exception\TimeoutException
22+
*/
23+
public function shouldTimeout()
24+
{
25+
$timeout = new PcntlTimeout(1);
26+
27+
$timeout->timeBoxed(function () {
28+
sleep(2);
29+
});
30+
}
31+
32+
/**
33+
* A short running system call should complete its execution
34+
*
35+
* @test
36+
*/
37+
public function shouldNotTimeout()
38+
{
39+
$timeout = new PcntlTimeout(1);
40+
41+
$result = $timeout->timeBoxed(function () {
42+
return 42;
43+
});
44+
45+
$this->assertEquals(42, $result);
46+
}
47+
48+
/**
49+
* When a previous scheduled alarm exists, it should fail
50+
*
51+
* @test
52+
* @expectedException malkusch\lock\exception\LockAcquireException
53+
*/
54+
public function shouldFailOnExistingAlarm()
55+
{
56+
try {
57+
pcntl_alarm(1);
58+
$timeout = new PcntlTimeout(1);
59+
60+
$timeout->timeBoxed(function () {
61+
sleep(1);
62+
});
63+
} finally {
64+
pcntl_alarm(0);
65+
}
66+
}
67+
68+
/**
69+
* After not timing out, there should be no alarm scheduled
70+
*
71+
* @test
72+
*/
73+
public function shouldResetAlarmWhenNotTimeout()
74+
{
75+
$timeout = new PcntlTimeout(3);
76+
77+
$timeout->timeBoxed(function () {
78+
});
79+
80+
$this->assertEquals(0, pcntl_alarm(0));
81+
}
82+
}

0 commit comments

Comments
 (0)