-
-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Bref] Added support for handle timeouts
- Loading branch information
Showing
7 changed files
with
245 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace Runtime\Bref\Timeout; | ||
|
||
/** | ||
* The application took too long to produce a response. This exception is thrown | ||
* to give the application a chance to flush logs and shut itself down before | ||
* the power to AWS Lambda is disconnected. | ||
* | ||
* @author Tobias Nyholm <[email protected]> | ||
*/ | ||
class LambdaTimeoutException extends \RuntimeException | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
<?php | ||
|
||
namespace Runtime\Bref\Timeout; | ||
|
||
/** | ||
* Helper class to trigger an exception just before the Lambda times out. This | ||
* will give the application a chance to shut down. | ||
* | ||
* @author Tobias Nyholm <[email protected]> | ||
*/ | ||
final class Timeout | ||
{ | ||
/** @var bool */ | ||
private static $initialized = false; | ||
|
||
/** @var string|null */ | ||
private static $stackTrace = null; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public static function enable(int $remainingTimeInMillis): void | ||
{ | ||
self::init(); | ||
|
||
if (!self::$initialized) { | ||
return; | ||
} | ||
|
||
$remainingTimeInSeconds = (int) floor($remainingTimeInMillis / 1000); | ||
|
||
// The script will timeout 2 seconds before the remaining time | ||
// to allow some time for Bref/our app to recover and cleanup | ||
$margin = 2; | ||
|
||
$timeoutDelayInSeconds = max(1, $remainingTimeInSeconds - $margin); | ||
|
||
// Trigger SIGALRM in X seconds | ||
pcntl_alarm($timeoutDelayInSeconds); | ||
} | ||
|
||
/** | ||
* Setup custom handler for SIGALRM. | ||
*/ | ||
private static function init(): void | ||
{ | ||
self::$stackTrace = null; | ||
|
||
if (self::$initialized) { | ||
return; | ||
} | ||
|
||
if (!function_exists('pcntl_async_signals')) { | ||
trigger_error('Could not enable timeout exceptions because pcntl extension is not enabled.'); | ||
|
||
return; | ||
} | ||
|
||
pcntl_async_signals(true); | ||
// Setup a handler for SIGALRM that throws an exception | ||
// This will interrupt any running PHP code, including `sleep()` or code stuck waiting for I/O. | ||
pcntl_signal(SIGALRM, function (): void { | ||
if (null !== Timeout::$stackTrace) { | ||
// we have already thrown an exception, do a harder exit. | ||
error_log('Lambda timed out'); | ||
error_log((new LambdaTimeoutException())->getTraceAsString()); | ||
error_log('Original stack trace'); | ||
error_log(Timeout::$stackTrace); | ||
|
||
exit(1); | ||
} | ||
|
||
$exception = new LambdaTimeoutException('Maximum AWS Lambda execution time reached'); | ||
Timeout::$stackTrace = $exception->getTraceAsString(); | ||
|
||
// Trigger another alarm after 1 second to do a hard exit. | ||
pcntl_alarm(1); | ||
|
||
throw $exception; | ||
}); | ||
|
||
self::$initialized = true; | ||
} | ||
|
||
/** | ||
* Cancel all current timeouts. | ||
* | ||
* @internal | ||
*/ | ||
public static function reset(): void | ||
{ | ||
if (self::$initialized) { | ||
pcntl_alarm(0); | ||
self::$stackTrace = null; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
namespace Runtime\Bref\Tests; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Runtime\Bref\Timeout\LambdaTimeoutException; | ||
use Runtime\Bref\Timeout\Timeout; | ||
|
||
class TimeoutTest extends TestCase | ||
{ | ||
public static function setUpBeforeClass(): void | ||
{ | ||
if (!function_exists('pcntl_async_signals')) { | ||
self::markTestSkipped('PCNTL extension is not enabled.'); | ||
} | ||
} | ||
|
||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
unset($_SERVER['LAMBDA_INVOCATION_CONTEXT']); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
Timeout::reset(); | ||
parent::tearDown(); | ||
} | ||
|
||
public function testEnable() | ||
{ | ||
Timeout::enable(3000); | ||
$timeout = pcntl_alarm(0); | ||
// 1 second (2 seconds shorter than the 3s remaining time) | ||
$this->assertSame(1, $timeout); | ||
} | ||
|
||
public function testTimeoutsAreInterruptedInTime() | ||
{ | ||
$start = microtime(true); | ||
Timeout::enable(3000); | ||
try { | ||
sleep(4); | ||
$this->fail('We expect a LambdaTimeout before we reach this line'); | ||
} catch (LambdaTimeoutException $e) { | ||
$time = 1000 * (microtime(true) - $start); | ||
$this->assertEqualsWithDelta(1000, $time, 200, 'We must wait about 1 second'); | ||
Timeout::reset(); | ||
} catch (\Throwable $e) { | ||
$this->fail('It must throw a LambdaTimeout.'); | ||
} | ||
} | ||
} |