Magento: log the SQL that was buffering when fetchAll() hits memory_limit ========================================================================= Package: magento/zend-db (tested on 1.16.3) Author: Paxento, https://paxento.com/ Article: https://paxento.com/magento-checkout-memory-limit-stripe-invoice-scan/ License: provided as-is, without warranty. Test on staging before production. Diagnostic only, no behaviour change. When a request dies with a fatal error while Zend_Db_Statement_Pdo::fetchAll() is buffering rows, writes the SQL and the peak memory to the PHP error log. Apply while investigating, remove afterwards. Apply with cweagans/composer-patches or vaimo/composer-patches, e.g.: "patches": { "magento/zend-db": { "Log SQL on memory exhaustion in fetchAll": "patches/magento-log-sql-on-oom-in-fetchall.patch" } } --- a/library/Zend/Db/Statement/Pdo.php +++ b/library/Zend/Db/Statement/Pdo.php @@ -268,6 +268,18 @@ { return new IteratorIterator($this->_stmt); } + + /** + * SQL of the fetchAll() that is currently buffering rows, so a fatal shutdown can name it. + * + * @var string|null + */ + private static $_bufferingSql = null; + + /** + * @var bool + */ + private static $_fatalReporterRegistered = false; /** * Returns an array containing all of the result set rows. @@ -281,23 +293,60 @@ { if ($style === null) { $style = $this->_fetchMode; + } + if (!self::$_fatalReporterRegistered) { + self::$_fatalReporterRegistered = true; + register_shutdown_function([__CLASS__, 'reportFatalWhileBuffering']); } + self::$_bufferingSql = $this->_stmt->queryString; try { if ($style == PDO::FETCH_COLUMN) { if ($col === null) { $col = 0; } - return $this->_stmt->fetchAll($style, $col); + $result = $this->_stmt->fetchAll($style, $col); } else { - return $this->_stmt->fetchAll($style); + $result = $this->_stmt->fetchAll($style); } } catch (PDOException $e) { + self::$_bufferingSql = null; #require_once 'Zend/Db/Statement/Exception.php'; throw new Zend_Db_Statement_Exception($e->getMessage(), $e->getCode(), $e); } + self::$_bufferingSql = null; + return $result; } /** + * Names the query that was buffering rows when the request died, for memory-limit diagnostics. + * + * A memory exhaustion inside fetchAll() is reported by PHP against whichever code the last + * allocation happened in, and the session save handler may mask it entirely, so the query + * itself never reaches the log without this. + * + * @return void + */ + public static function reportFatalWhileBuffering() + { + if (self::$_bufferingSql === null) { + return; + } + + $error = error_get_last(); + if ($error === null || $error['type'] !== E_ERROR) { + return; + } + + ini_set('memory_limit', '-1'); + error_log(sprintf( + 'Fatal while buffering a result set (peak %d MB): %s -- SQL: %s', + memory_get_peak_usage(true) / 1048576, + $error['message'], + self::$_bufferingSql + )); + } + + /** * Returns a single column from the next row of a result set. * * @param int $col OPTIONAL Position of the column to fetch.