bugsnag / bugsnag-laravel

BugSnag notifier for the Laravel PHP framework. Monitor and report Laravel errors.
https://docs.bugsnag.com/platforms/php/laravel/
MIT License
876 stars 129 forks source link

OomBootstrapper has no effect in assisting logging out of memory errors #439

Closed ragingdave closed 3 years ago

ragingdave commented 3 years ago

When attempting to integrate the OomBootstrapper into a laravel application, it seems that it has no effect and errors were reported to bugsnag without the bootstrapper in place. Perhaps there's something I'm missing or this code was meant for an earlier version of laravel.

Steps to reproduce

Add a route like:

Route::get('test', function () {
    $arr = [];
    while (true) {
            $arr[] = str_repeat(' ', 32);
    }
});

or add a job with similar content that you can queue.

Trigger either with bugsnag configured properly, and see that it logs to bugsnag without the bootstrapper.

Environment

imjoehaines commented 3 years ago

Hey @ragingdave, this is down to how PHP's memory management and array allocation works

In short, when PHP attempts to allocate a single large amount of memory that would exceed the memory limit, it refuses to allocate it and crashes instead. This leaves a lot of memory unused because the allocation never happened, so Bugsnag can work successfully even without the OomBootstrapper

This is what's happening in your example because, when the array hits certain size thresholds, the entire array will be reallocated in one go. You can verify this by looking at the amount of bytes that were being allocated in the OOM error message — it will be much larger than the size of a 32 character string. This is an optimisation added in PHP 7 — PHP 5 will work as you expected and fail on the string allocation instead (see https://3v4l.org/jZEsW)

The OomBootstrapper exists for when PHP runs OOM from many small allocations. When this happens, there may not be enough memory for Bugsnag to report the error — in fact, Laravel itself can crash before handing the exception to Bugsnag when this happens. Forcing PHP to run OOM from small allocations in a repeatable way takes some trickery, but you can look at our test suite to see an example of how to do this:

Route::get('/oom/small', function () {
    // reduce the memory limit so we run OOM quicker
    ini_set('memory_limit', memory_get_usage() + (1024 * 1024 * 5));
    ini_set('display_errors', true);

    $i = 0;

    // turn off the circular reference collector, this stop PHP from freeing the memory we allocate in the loop
    gc_disable();

    // loop an arbitrary number of times; this avoids an infinite loop on CI if the OOM never happens
    while ($i++ < 12345678) {
        // allocate a new stdClass object
        $a = new stdClass;
        // create a circular reference to $a to prevent PHP's garbage collection from cleaning it up
        $a->b = $a;
    }

    return 'should never get here';
});

This is a very synthetic example as it needs to run reliably in our test suite across all the PHP versions we support, but you should be able to see the OomBootstrapper working with the route above 🙂

ragingdave commented 3 years ago

Awesome thanks for the snippet! That worked exactly as I would have expected, so it seems this attempts to catch the edge cases that can't be caught normally just by how php handles garbage collection rather than a specific catch-all.