Skip to content

Commit

Permalink
Do 2 alarms
Browse files Browse the repository at this point in the history
  • Loading branch information
Nyholm committed Mar 30, 2021
1 parent 0b6d7b2 commit 9356e4a
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 9 deletions.
30 changes: 26 additions & 4 deletions src/Timeout/Timeout.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ final class Timeout
/** @var bool */
private static $initialized = false;

/** @var string|null */
private static $stackTrace = null;

/**
* Automatically setup a timeout (based on the AWS Lambda timeout).
*
Expand Down Expand Up @@ -38,9 +41,9 @@ public static function enable(int $remainingTimeInMillis): void

$remainingTimeInSeconds = (int) floor($remainingTimeInMillis / 1000);

// The script will timeout 1 second before the remaining time
// The script will timeout 2 seconds before the remaining time
// to allow some time for Bref/our app to recover and cleanup
$margin = 1;
$margin = 2;

$timeoutDelayInSeconds = max(1, $remainingTimeInSeconds - $margin);

Expand All @@ -53,6 +56,8 @@ public static function enable(int $remainingTimeInMillis): void
*/
private static function init(): void
{
Timeout::$stackTrace = null;

if (self::$initialized) {
return;
}
Expand All @@ -66,21 +71,38 @@ private static function init(): void
// 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 {
throw new LambdaTimeout('Maximum AWS Lambda execution time reached');
if (Timeout::$stackTrace !== null) {
// we already thrown an exception, do a harder exit.
error_log('Lambda timed out');
error_log((new LambdaTimeout)->getTraceAsString());
error_log('Original stack trace');
error_log(Timeout::$stackTrace);

exit(1);
}

$exception = new LambdaTimeout('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;
}

/**
* Reset timeout.
* Cancel all current timeouts.
*
* @internal
*/
public static function reset(): void
{
if (self::$initialized) {
pcntl_alarm(0);
Timeout::$stackTrace = null;
}
}
}
2 changes: 2 additions & 0 deletions tests/Runtime/LambdaRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Bref\Runtime\LambdaRuntime;
use Bref\Test\Server;
use Bref\Timeout\LambdaTimeout;
use Bref\Timeout\Timeout;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
Expand Down Expand Up @@ -67,6 +68,7 @@ public function test Lambda timeouts can be anticipated()
$elapsedTime = microtime(true) - $start;
// The Lambda timeout was 2 seconds, we expect the Bref timeout to trigger 1 second before that: 1 second
$this->assertEqualsWithDelta(1, $elapsedTime, 0.2);
Timeout::reset();

$this->assertInvocationErrorResult(LambdaTimeout::class, 'Maximum AWS Lambda execution time reached');
$this->assertErrorInLogs(LambdaTimeout::class, 'Maximum AWS Lambda execution time reached');
Expand Down
11 changes: 6 additions & 5 deletions tests/Timeout/TimeoutTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public function test enable()
{
Timeout::enable(3000);
$timeout = pcntl_alarm(0);
// 2 seconds (1 second shorter than the 3s remaining time)
$this->assertSame(2, $timeout);
// 1 second (2 seconds shorter than the 3s remaining time)
$this->assertSame(1, $timeout);
}

public function test enable in FPM()
Expand All @@ -41,7 +41,7 @@ public function test enable in FPM()

Timeout::enableInFpm();
$timeout = pcntl_alarm(0);
$this->assertEqualsWithDelta(29, $timeout, 1);
$this->assertEqualsWithDelta(28, $timeout, 1);
}

public function test enable in FPM requires the context()
Expand All @@ -53,13 +53,14 @@ public function test enable in FPM requires the context()
public function test timeouts are interrupted in time()
{
$start = microtime(true);
Timeout::enable(2000);
Timeout::enable(3000);
try {
sleep(4);
$this->fail('We expect a LambdaTimeout before we reach this line');
} catch (LambdaTimeout $e) {
$time = 1000 * (microtime(true) - $start);
$this->assertEqualsWithDelta(1000, $time, 200, 'We must wait about 1 second');
$this->assertEqualsWithDelta(2000, $time, 200, 'We must wait about 1 second');
Timeout::reset();
} catch (\Throwable $e) {
$this->fail('It must throw a LambdaTimeout.');
}
Expand Down

0 comments on commit 9356e4a

Please sign in to comment.