mirror of
https://github.com/Seldaek/monolog.git
synced 2025-08-05 04:37:38 +02:00
Merge pull request #1578 from juan-morales/fix-oom-streamhandler
Fix - OutOfMemory error in StreamHandler
This commit is contained in:
@@ -25,8 +25,10 @@ use Monolog\Utils;
|
|||||||
*/
|
*/
|
||||||
class StreamHandler extends AbstractProcessingHandler
|
class StreamHandler extends AbstractProcessingHandler
|
||||||
{
|
{
|
||||||
protected const MAX_CHUNK_SIZE = 2147483647;
|
/** @const int */
|
||||||
|
protected const MAX_CHUNK_SIZE = 100 * 1024 * 1024;
|
||||||
|
/** @var int */
|
||||||
|
protected $streamChunkSize = self::MAX_CHUNK_SIZE;
|
||||||
/** @var resource|null */
|
/** @var resource|null */
|
||||||
protected $stream;
|
protected $stream;
|
||||||
/** @var ?string */
|
/** @var ?string */
|
||||||
@@ -50,9 +52,22 @@ class StreamHandler extends AbstractProcessingHandler
|
|||||||
public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
|
public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
|
||||||
{
|
{
|
||||||
parent::__construct($level, $bubble);
|
parent::__construct($level, $bubble);
|
||||||
|
|
||||||
|
if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
|
||||||
|
if ($phpMemoryLimit > 0) {
|
||||||
|
// use max 10% of allowed memory for the chunk size
|
||||||
|
$this->streamChunkSize = max((int) ($phpMemoryLimit / 10), 10*1024);
|
||||||
|
}
|
||||||
|
// else memory is unlimited, keep the buffer to the default 100MB
|
||||||
|
} else {
|
||||||
|
// no memory limit information, use a conservative 10MB
|
||||||
|
$this->streamChunkSize = 10*10*1024;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_resource($stream)) {
|
if (is_resource($stream)) {
|
||||||
$this->stream = $stream;
|
$this->stream = $stream;
|
||||||
stream_set_chunk_size($this->stream, self::MAX_CHUNK_SIZE);
|
|
||||||
|
stream_set_chunk_size($this->stream, $this->streamChunkSize);
|
||||||
} elseif (is_string($stream)) {
|
} elseif (is_string($stream)) {
|
||||||
$this->url = Utils::canonicalizePath($stream);
|
$this->url = Utils::canonicalizePath($stream);
|
||||||
} else {
|
} else {
|
||||||
@@ -95,6 +110,14 @@ class StreamHandler extends AbstractProcessingHandler
|
|||||||
return $this->url;
|
return $this->url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getStreamChunkSize(): int
|
||||||
|
{
|
||||||
|
return $this->streamChunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +141,7 @@ class StreamHandler extends AbstractProcessingHandler
|
|||||||
|
|
||||||
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url));
|
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url));
|
||||||
}
|
}
|
||||||
stream_set_chunk_size($stream, self::MAX_CHUNK_SIZE);
|
stream_set_chunk_size($stream, $this->streamChunkSize);
|
||||||
$this->stream = $stream;
|
$this->stream = $stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -226,4 +226,38 @@ final class Utils
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string with a valid 'memory_limit' format, to bytes.
|
||||||
|
*
|
||||||
|
* @param string|false $val
|
||||||
|
* @return int|false Returns an integer representing bytes. Returns FALSE in case of error.
|
||||||
|
*/
|
||||||
|
public static function expandIniShorthandBytes($val)
|
||||||
|
{
|
||||||
|
if (!is_string($val)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// support -1
|
||||||
|
if ((int) $val < 0) {
|
||||||
|
return (int) $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^\s*(?<val>\d+)(?:\.\d+)?\s*(?<unit>[gmk]?)\s*$/i', $val, $match)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$val = (int) $match['val'];
|
||||||
|
switch (strtolower($match['unit'] ?? '')) {
|
||||||
|
case 'g':
|
||||||
|
$val *= 1024;
|
||||||
|
case 'm':
|
||||||
|
$val *= 1024;
|
||||||
|
case 'k':
|
||||||
|
$val *= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Monolog\Handler;
|
namespace Monolog\Handler;
|
||||||
|
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
use Monolog\Test\TestCase;
|
use Monolog\Test\TestCase;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
|
|
||||||
@@ -220,4 +221,65 @@ class StreamHandlerTest extends TestCase
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function provideMemoryValues()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['1M', (int) (1024*1024/10)],
|
||||||
|
['10M', (int) (1024*1024)],
|
||||||
|
['1024M', (int) (1024*1024*1024/10)],
|
||||||
|
['1G', (int) (1024*1024*1024/10)],
|
||||||
|
['2000M', (int) (2000*1024*1024/10)],
|
||||||
|
['2050M', (int) (2050*1024*1024/10)],
|
||||||
|
['2048M', (int) (2048*1024*1024/10)],
|
||||||
|
['3G', (int) (3*1024*1024*1024/10)],
|
||||||
|
['2560M', (int) (2560*1024*1024/10)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideMemoryValues
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testPreventOOMError($phpMemory, $expectedChunkSize)
|
||||||
|
{
|
||||||
|
$previousValue = ini_set('memory_limit', $phpMemory);
|
||||||
|
|
||||||
|
if ($previousValue === false) {
|
||||||
|
$this->markTestSkipped('We could not set a memory limit that would trigger the error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = tmpfile();
|
||||||
|
|
||||||
|
if ($stream === false) {
|
||||||
|
$this->markTestSkipped('We could not create a temp file to be use as a stream.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$exceptionRaised = false;
|
||||||
|
|
||||||
|
$handler = new StreamHandler($stream);
|
||||||
|
stream_get_contents($stream, 1024);
|
||||||
|
|
||||||
|
ini_set('memory_limit', $previousValue);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSimpleOOMPrevention()
|
||||||
|
{
|
||||||
|
$previousValue = ini_set('memory_limit', '2048M');
|
||||||
|
|
||||||
|
if ($previousValue === false) {
|
||||||
|
$this->markTestSkipped('We could not set a memory limit that would trigger the error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = tmpfile();
|
||||||
|
new StreamHandler($stream);
|
||||||
|
stream_get_contents($stream);
|
||||||
|
ini_set('memory_limit', $previousValue);
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -141,4 +141,49 @@ class UtilsTest extends \PHPUnit_Framework_TestCase
|
|||||||
[-1, 'UNDEFINED_ERROR'],
|
[-1, 'UNDEFINED_ERROR'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function provideIniValuesToConvertToBytes()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['1', 1],
|
||||||
|
['2', 2],
|
||||||
|
['2.5', 2],
|
||||||
|
['2.9', 2],
|
||||||
|
['1B', false],
|
||||||
|
['1X', false],
|
||||||
|
['1K', 1024],
|
||||||
|
['1 K', 1024],
|
||||||
|
[' 5 M ', 5*1024*1024],
|
||||||
|
['1G', 1073741824],
|
||||||
|
['', false],
|
||||||
|
[null, false],
|
||||||
|
['A', false],
|
||||||
|
['AA', false],
|
||||||
|
['B', false],
|
||||||
|
['BB', false],
|
||||||
|
['G', false],
|
||||||
|
['GG', false],
|
||||||
|
['-1', -1],
|
||||||
|
['-123', -123],
|
||||||
|
['-1A', -1],
|
||||||
|
['-1B', -1],
|
||||||
|
['-123G', -123],
|
||||||
|
['-B', false],
|
||||||
|
['-A', false],
|
||||||
|
['-', false],
|
||||||
|
[true, false],
|
||||||
|
[false, false],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideIniValuesToConvertToBytes
|
||||||
|
* @param mixed $input
|
||||||
|
* @param int|false $expected
|
||||||
|
*/
|
||||||
|
public function testExpandIniShorthandBytes($input, $expected)
|
||||||
|
{
|
||||||
|
$result = Utils::expandIniShorthandBytes($input);
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user