Schreibmaschine mit memory_limit

Normalerweise sitze ich nicht um 1:20 Uhr noch am Rechner, um PHP-Probleme zu debuggen. Insbesondere nicht, wenn’s keine bezahlte Auftragsarbeit ist, sondern nacktes „Ich! Will! Das! Verstehen!“. Aber genau sowas hatte ich heute. Nachdem ich vor einigen Tagen schon mal ergebnislos an dem folgenden Problem herumgedoktert hatte, habe ich mich heute nochmal darin verbissen.

Ein User betreibt einen Magento-Shop auf unserer Hosting-Plattform Uberspace.de. Dazu gehören auch einige Hintergrundjobs, und zwar unter anderem dieser hier, der ganz offenkundig am memory_limit scheitert:

[demosite@krypton ~]$ php -c ~/cli /home/demosite/html/www.demosite.tld/shell/indexer.php status 
PHP Fatal error:  Allowed memory size of 262144 bytes exhausted (tried to allocate 232 bytes) in /var/www/virtual/demosite/www.demosite.tld/app/code/core/Mage/Index/Model/Mysql4/Process.php on line 110

Moment mal – ein memory_limit von gerade mal 256K? Das kann ja eigentlich nicht sein. In der php.ini steht nämlich 512M, und das wertet PHP an sich auch korrekt aus:

[demosite@krypton ~]$ php -c ~/cli -i | grep memory_limit
memory_limit => 512M => 512M

Nun gibt’s aber ja auch die Möglichkeit, in PHP-Scripts zur Laufzeit mit ini_set() einzelne Einstellungen dynamisch zu ändern, also auch das memory_limit. Schauen wir also mal, wo das möglicherweise noch so passiert:

[demosite@krypton ~]$ grep -r memory_limit /var/www/virtual/demosite/www.demosite.tld | grep -v cache
/var/www/virtual/demosite/www.demosite.tld/lib/Varien/.svn/text-base/Pear.php.svn-base:        @ini_set('memory_limit', '256M');
/var/www/virtual/demosite/www.demosite.tld/lib/Varien/Pear.php:        @ini_set('memory_limit', '256M');
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/.svn/text-base/Manager.php.svn-base:     * Default value is 2/3 of memory_limit php.ini variable
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/.svn/text-base/Manager.php.svn-base:        $memoryLimitStr = trim(ini_get('memory_limit'));
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/Manager.php:     * Default value is 2/3 of memory_limit php.ini variable
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/Manager.php:        $memoryLimitStr = trim(ini_get('memory_limit'));
/var/www/virtual/demosite/www.demosite.tld/magiczoomplus/module/magictoolbox/core/.svn/text-base/magictoolbox.params.class.php.svn-base:@ini_set('memory_limit', '512M');
/var/www/virtual/demosite/www.demosite.tld/magiczoomplus/module/magictoolbox/core/magictoolbox.params.class.php:@ini_set('memory_limit', '512M');
/var/www/virtual/demosite/www.demosite.tld/.svn/text-base/payment_check.php.svn-base:ini_set('memory_limit', '1024M');
/var/www/virtual/demosite/www.demosite.tld/.svn/text-base/.htaccess.svn-base:  php_value memory_limit 128M
/var/www/virtual/demosite/www.demosite.tld/.htaccess:  php_value memory_limit 128M

Okay, kommt einige Male vor, durchaus auch mit wechselnden Werten, aber nirgendwo etwas mit 256K. Was also ist hier los?

XDebug muss her. Flugs kompiliert und die xdebug.so via zend_extension in die php.ini eingebunden und xdebug.auto_trace=1 gesetzt. Dann nochmal ausgeführt und geschaut, und siehe da – dreimal wird ini_set() aufgerufen:

[demosite@krypton ~]$ grep ini_set traces/trace.3557638816.xt 
    0.1443    8707248         -> ini_set() /var/www/virtual/demosite/www.demosite.tld/shell/abstract.php:117
    0.1443    8707384         -> ini_set() /var/www/virtual/demosite/www.demosite.tld/shell/abstract.php:117
    0.1444    8706736         -> ini_set() /var/www/virtual/demosite/www.demosite.tld/shell/abstract.php:123

Also mal geschaut, was an dieser Stelle so für Code steht – und das hat mich wirklich überrascht:

    /**
     * Parse .htaccess file and apply php settings to shell script
     *
     */
    protected function _applyPhpVariables()
    {
        $htaccess = $this->_getRootPath() . '.htaccess';
        if (file_exists($htaccess)) {
            // parse htaccess file
            $data = file_get_contents($htaccess);
            $matches = array();
            preg_match_all('#^\s+?php_value\s+([a-z_]+)\s+(.+)$#siUm', $data, $matches, PREG_SET_ORDER);
            if ($matches) {
                foreach ($matches as $match) {
                    @ini_set($match[1], $match[2]);
               	}
            }
            preg_match_all('#^\s+?php_flag\s+([a-z_]+)\s+(.+)$#siUm', $data, $matches, PREG_SET_ORDER);
            if ($matches) {
                foreach ($matches as $match) {
                    @ini_set($match[1], $match[2]);
                }
            }
        }
    }

Es kommt hier also Code zum Tragen, der bei der Ausführung eines PHP-Scripts auf der Kommandozeile eine .htaccess-Datei auswertet, die in diesem Kontext nun eigentlich überhaupt keine Rolle spielt – handelt es sich dabei doch um eine webserver-spezifische Konfigurationsdatei. Also flugs dort nochmal reingeschaut:

[demosite@krypton ~]$ grep memory_limit /var/www/virtual/demosite/www.demosite.tld/.htaccess
  php_value memory_limit 128M

Hm. Da steht aber auch nichts von 256K. Wo zum Kuckuck kommt das her? Also auf die Schnelle – es ist spät – schnell ein wenig Debugging-Code an die entsprechende Stelle gesetzt, um einfach mal auszugeben, was genau dort geschrieben wird (jaja, das ist nicht elegant, aber man muss auch mal pragmatisch sein dürfen):

               	foreach ($matches as $match) {
                    echo "setting '" . $match[1] . "' to '" . $match[2] . "'\n";
                    @ini_set($match[1], $match[2]);
                }

Die Ausgabe hat dann doch reichlich überrascht:

[demosite@krypton ~]$ php -c ~/cli /home/demosite/html/www.demosite.tld/shell/indexer.php status 
'etting 'memory_limit' to '128M
'etting 'max_execution_time' to '18000
PHP Fatal error:  Allowed memory size of 262144 bytes exhausted (tried to allocate 92 bytes) in /var/www/virtual/demosite/www.demosite.tld/app/code/core/Mage/Index/Model/Mysql4/Process.php on line 110

Auch dem ungeübten Betrachter wird auffallen, dass die Ausgabe nicht mit setting, sondern mit 'etting beginnt, und dafür nicht mit '128M' endet, sondern mit '128M. Was zum..? Es wird doch nicht..?

Ein kleiner Exkurs: Unix-Systeme benutzen andere Zeilenumbrüche als Windows-Systeme. Während unter Unix Zeilenumbrüche mit einem einzelnen line feed ("\n") separiert werden, werden unter Windows Zeilenumbrüche mit einem carriage return, line feed ("\r\n") separiert: Carriage return, der gute alte Wagenrücklauf wie bei der Schreibmaschine – am Hebel ziehen, um den Druckkopf wieder am Zeilenanfang zu positionieren; Druckwalze eine Zeile vorrücken lassen.

Und genau so sah das hier aus: Das zweite ' von '128M' ist nach vorne gerutscht; ein carriage return hat stattgefunden. Und das wiederum heißt, dass … Moment, kurz nachgucken …

[demosite@krypton ~]$ file /var/www/virtual/demosite/www.demosite.tld/.htaccess
/var/www/virtual/demosite/www.demosite.tld/.htaccess: ASCII English text, with CRLF line terminators

… richtig: Die Inhalte der .htaccess-Datei sind mit Windows-Zeilenumbrüchen gespeichert. Werden jene nun auf einem Unix-System eingelesen, was die Zeilen am „\n“ auftrennt, dann hat memory_limit hier faktisch nicht den Wert „128M„, sondern den Wert „128M\r“ – Wagenrücklauf inklusive. Und genau das ist es, was PHP nun so gar nicht schmeckt. Verdammte Axt!

Also los:

[demosite@krypton ~]$ dos2unix /var/www/virtual/demosite/www.demosite.tld/.htaccess
dos2unix: converting file /var/www/virtual/demosite/www.demosite.tld/.htaccess to UNIX format ...

Getestet:

[demosite@krypton ~]$ php -c ~/cli /home/demosite/html/www.demosite.tld/shell/indexer.php status 
setting 'memory_limit' to '128M'
setting 'max_execution_time' to '18000'
Product Attributes:            Pending
Product Prices:                Pending
Catalog Url Rewrites:          Pending
Product Flat Data:             Pending
Category Flat Data:            Pending
Category Products:             Pending
Catalog Search Index:          Running
Stock status:                  Pending

Problem gelöst. Script läuft. Nur noch die Debug-Zeile wieder rausnehmen. So, erledigt.

Und jetzt ist wirklich Zeit für’s Bett.

Tags:

9 Antworten zu “Schreibmaschine mit memory_limit”

  1. Arne sagt:

    Heftig. Solche Fehler sind echt ne interessante Sache. Das Problem mit dem „ich muss es jetzt“ finden kenne ich nur zu gut :-) Respekt!

  2. Mario sagt:

    Sehr interessant! Der Beitrag ist gleich auf der Magento-gut-zu-wissen-Liste gelandet. Darf man noch erfahren, um welche Magento-Version es sich handelt?

  3. Jonas Pasche sagt:

    Das weiß ich leider nicht (keine Ahnung, in welcher Datei sich diese Info finden ließe; ich kenne Magento nicht wirklich).

  4. Tim sagt:

    Genialer Titel! ;)

  5. masc sagt:

    Wäre da nicht mindestens ein trim($match[n]) im PHP-Code empfehlenswert, bevor man die Werte übernimmt?

  6. Jonas Pasche sagt:

    Schaden würde es bestimmt nicht.

  7. name sagt:

    So etwas gehört doch verboten !

    Ne .htaccess in ein CLI Script einbinden und dann auch noch so stümperhaft die Datei einlesen…..

    argh…

  8. Mathias sagt:

    Hatte den Fehler auch mal bei einer Magento-Installation (glaub war eine 1.4.x Version), hatte damals aber nicht die Zeit/Lust/… das Ganze so ausführlich zu debuggen und mir dann mit einem Workaround beholfen. Nächstes Mal weiß ich, was zu tun ist. Thx!

  9. DaDaDaniel sagt:

    Oh mann, meine Magneto! :(
    Kenne das auch zu gut!

    Danke für den Beitrag, da bekomm ich wieder Motivation, daran zu arbeiten!


Impressum