Skip to content

Commit

Permalink
Merge pull request #621 from bugsnag/handle-ooms
Browse files Browse the repository at this point in the history
Allow Bugsnag\Handler to handle OOMs
  • Loading branch information
imjoehaines authored Feb 2, 2021
2 parents bc1d445 + 3e98f4d commit 3fad001
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 0 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

## TBD

### Enhancements

* Out of memory errors will now be reported by increasing the memory limit by 5 MiB. Use the new `memoryLimitIncrease` configuration option to change the amount of memory, or set it to `null` to disable the increase entirely.
[#621](https://github.com/bugsnag/bugsnag-php/pull/621)

## 3.25.0 (2020-11-25)

### Enhancements
Expand Down
24 changes: 24 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -935,4 +935,28 @@ public function getSessionClient()
{
return $this->config->getSessionClient();
}

/**
* Set the amount to increase the memory_limit when an OOM is triggered.
*
* This is an amount of bytes or 'null' to disable increasing the limit.
*
* @param int|null $value
*/
public function setMemoryLimitIncrease($value)
{
return $this->config->setMemoryLimitIncrease($value);
}

/**
* Get the amount to increase the memory_limit when an OOM is triggered.
*
* This will return 'null' if this feature is disabled.
*
* @return int|null
*/
public function getMemoryLimitIncrease()
{
return $this->config->getMemoryLimitIncrease();
}
}
35 changes: 35 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ class Configuration
*/
protected $buildEndpoint = self::BUILD_ENDPOINT;

/**
* The amount to increase the memory_limit to handle an OOM.
*
* The default is 5MiB and can be disabled by setting it to 'null'
*
* @var int|null
*/
protected $memoryLimitIncrease = 5242880;

/**
* Create a new config instance.
*
Expand Down Expand Up @@ -766,4 +775,30 @@ public function getSessionClient()

return $this->sessionClient;
}

/**
* Set the amount to increase the memory_limit when an OOM is triggered.
*
* This is an amount of bytes or 'null' to disable increasing the limit.
*
* @param int|null $value
*/
public function setMemoryLimitIncrease($value)
{
$this->memoryLimitIncrease = $value;

return $this;
}

/**
* Get the amount to increase the memory_limit when an OOM is triggered.
*
* This will return 'null' if this feature is disabled.
*
* @return int|null
*/
public function getMemoryLimitIncrease()
{
return $this->memoryLimitIncrease;
}
}
36 changes: 36 additions & 0 deletions src/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ class Handler
*/
protected $previousExceptionHandler;

/**
* A bit of reserved memory to ensure we are able to increase the memory
* limit on an OOM.
*
* We can't reserve all of the memory that we need to send OOM reports
* because this would have a big overhead on every request, instead of just
* on shutdown in requests with errors.
*
* @var string|null
*/
private $reservedMemory;

/**
* A regex that matches PHP OOM errors.
*
* @var string
*/
private $oomRegex = '/^Allowed memory size of (\d+) bytes exhausted \(tried to allocate \d+ bytes\)/';

/**
* Whether the shutdown handler will run.
*
Expand Down Expand Up @@ -136,6 +155,9 @@ public function registerExceptionHandler($callPrevious)
*/
public function registerShutdownHandler()
{
// Reserve some memory that we can free in the shutdown handler
$this->reservedMemory = str_repeat(' ', 1024 * 32);

register_shutdown_function([$this, 'shutdownHandler']);
}

Expand Down Expand Up @@ -267,6 +289,9 @@ public function errorHandler($errno, $errstr, $errfile = '', $errline = 0)
*/
public function shutdownHandler()
{
// Free the reserved memory to give ourselves some room to work
$this->reservedMemory = null;

// If we're disabled, do nothing. This avoids reporting twice if the
// exception handler is forcing the native PHP handler to run
if (!self::$enableShutdownHandler) {
Expand All @@ -275,6 +300,17 @@ public function shutdownHandler()

$lastError = error_get_last();

// If this is an OOM and memory increase is enabled, bump the memory
// limit so we can report it
if ($lastError !== null
&& $this->client->getMemoryLimitIncrease() !== null
&& preg_match($this->oomRegex, $lastError['message'], $matches) === 1
) {
$currentMemoryLimit = (int) $matches[1];

ini_set('memory_limit', $currentMemoryLimit + $this->client->getMemoryLimitIncrease());
}

// Check if a fatal error caused this shutdown
if (!is_null($lastError) && ErrorTypes::isFatal($lastError['type']) && !$this->client->getConfig()->shouldIgnoreErrorCode($lastError['type'])) {
$report = Report::fromPHPError(
Expand Down
19 changes: 19 additions & 0 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,25 @@ public function testMakeGuzzleCreatesTimeoutCanBeSpecified()
$this->assertSame(2, $connectTimeout);
}

public function testMemoryLimitIncreaseDefault()
{
$this->assertSame(1024 * 1024 * 5, $this->client->getMemoryLimitIncrease());
}

public function testMemoryLimitIncreaseCanBeSet()
{
$this->client->setMemoryLimitIncrease(12345);

$this->assertSame(12345, $this->client->getMemoryLimitIncrease());
}

public function testMemoryLimitIncreaseCanBeSetToNull()
{
$this->client->setMemoryLimitIncrease(null);

$this->assertNull($this->client->getMemoryLimitIncrease());
}

private function getGuzzleOption($guzzle, $name)
{
if (GuzzleCompat::isUsingGuzzle5()) {
Expand Down
19 changes: 19 additions & 0 deletions tests/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,23 @@ public function testTheSessionEndpointCanBeSetIfNecessary()

$this->assertSame($expected, $this->config->getSessionEndpoint());
}

public function testMemoryLimitIncreaseDefault()
{
$this->assertSame(1024 * 1024 * 5, $this->config->getMemoryLimitIncrease());
}

public function testMemoryLimitIncreaseCanBeSet()
{
$this->config->setMemoryLimitIncrease(12345);

$this->assertSame(12345, $this->config->getMemoryLimitIncrease());
}

public function testMemoryLimitIncreaseCanBeSetToNull()
{
$this->config->setMemoryLimitIncrease(null);

$this->assertNull($this->config->getMemoryLimitIncrease());
}
}
2 changes: 2 additions & 0 deletions tests/phpt/_prelude.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use Bugsnag\Configuration;
use Bugsnag\Tests\phpt\Utilities\FakeGuzzle;

date_default_timezone_set('UTC');

$config = new Configuration('11111111111111111111111111111111');
$config->setNotifyEndpoint('http://localhost/notify');
$config->setSessionEndpoint('http://localhost/sessions');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--TEST--
Bugsnag\Handler should increase the memory limit by the configured amount when an OOM happens
--FILE--
<?php
$client = require __DIR__ . '/_prelude.php';
$client->setMemoryLimitIncrease(1024 * 1024 * 10);

ini_set('memory_limit', '2M');
var_dump(ini_get('memory_limit'));

$client->registerCallback(function () {
var_dump(ini_get('memory_limit'));
});

Bugsnag\Handler::register($client);

$a = str_repeat('a', 2147483647);

echo "No OOM!\n";
?>
--EXPECTF--
string(2) "2M"

Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 14
string(8) "12582912"
Guzzle request made (1 event)!
* Method: 'POST'
* URI: 'http://localhost/notify'
* Events:
- Allowed memory size of %d bytes exhausted (tried to allocate %d bytes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
Bugsnag\Handler should not increase the memory limit when memoryLimitIncrease is disabled
--FILE--
<?php
$client = require __DIR__ . '/_prelude.php';
$client->setMemoryLimitIncrease(null);

ini_set('memory_limit', '5M');
var_dump(ini_get('memory_limit'));

$client->registerCallback(function () {
// This should be the same as the first var_dump, because we should not have
// increase the memory limit
var_dump(ini_get('memory_limit'));
});

Bugsnag\Handler::register($client);

$a = str_repeat('a', 2147483647);

echo "No OOM!\n";
?>
--EXPECTF--
string(2) "5M"

Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 16
string(2) "5M"
Guzzle request made (1 event)!
* Method: 'POST'
* URI: 'http://localhost/notify'
* Events:
- Allowed memory size of %d bytes exhausted (tried to allocate %d bytes)
21 changes: 21 additions & 0 deletions tests/phpt/handler_should_report_oom_from_large_allocation.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Bugsnag\Handler should report OOMs triggered by single large allocations
--FILE--
<?php
$client = require __DIR__ . '/_prelude.php';

ini_set('memory_limit', '5M');

Bugsnag\Handler::register($client);

$a = str_repeat('a', 2147483647);

echo "No OOM!\n";
?>
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 8
Guzzle request made (1 event)!
* Method: 'POST'
* URI: 'http://localhost/notify'
* Events:
- Allowed memory size of %d bytes exhausted (tried to allocate %d bytes)
35 changes: 35 additions & 0 deletions tests/phpt/handler_should_report_oom_from_small_allocations.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Bugsnag\Handler should report OOMs triggered by many small allocations
--FILE--
<?php

$client = require __DIR__ . '/_prelude.php';

$handler = Bugsnag\Handler::register($client);

ini_set('memory_limit', '5M');

$i = 0;

gc_disable();

while ($i++ < 12345678) {
$a = new stdClass;
$a->b = $a;
}

echo "No OOM!\n";
?>
--SKIPIF--
<?php
if (PHP_MAJOR_VERSION < 7) {
echo "SKIP - PHP 5 does not run OOM in this test";
}
?>
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line %d
Guzzle request made (1 event)!
* Method: 'POST'
* URI: 'http://localhost/notify'
* Events:
- Allowed memory size of %d bytes exhausted (tried to allocate %d bytes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
Bugsnag\Handler should report OOMs triggered by many small allocations
--FILE--
<?php

$client = require __DIR__ . '/../_prelude.php';

$handler = Bugsnag\Handler::register($client);

ini_set('memory_limit', '5M');

$size = 1024 * 256;

$array = new SplFixedArray($size);

for ($i = 0; $i < $size; ++$i) {
$array[$i] = str_repeat('a', 32);
}

echo "No OOM!\n";
?>
--SKIPIF--
<?php
if (PHP_MAJOR_VERSION !== 5) {
echo 'SKIP — this test does not run on PHP 7 & 8';
}
?>
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 14
Guzzle request made (1 event)!
* Method: 'POST'
* URI: 'http://localhost/notify'
* Events:
- Allowed memory size of %d bytes exhausted (tried to allocate %d bytes)

0 comments on commit 3fad001

Please sign in to comment.