Magento’s memory leak bij product import

Voor een klant moest ik afgelopen week 26.000 producten importeren. Dit heb ik eerder gedaan voor MagicZeppo (toen ging het om 45.000 producten) en dat ging prima dus ik voorzag niet veel problemen, op het puntje na dat DataFlow erg traag is en het dus waarschijnlijk tussen de 10 en 20 uur ging duren. MagicZeppo draait op een server met 4gb intern geheugen en kon het allemaal prima handlen. De server waar de site waar ik nu aan werk op draait heeft maar 1gb aan boord en dat bleek een probleem; er zit namelijk een grove memory leak in Magento. Op de fora van Magento zelf wordt het veelvuldig genoemd (zelfs tot 2 jaar terug, zie bijv hier, hier, hier en hier) maar Varien schijnt er niet op te reageren; kunnen ze zelf de leak niet vinden?

Er is een developer die wel al een leak heeft gevonden, zie deze post. Hoewel de memory leakage met 90% wordt verminderd na het toepassen van deze fix, is het nog steeds aanwezig en duikt het bij 26.000 producten uiteindelijk toch wel een keer de kop op. Bovendien werkt het bestelformulier niet meer door die ‘fix’ dus een blijvende oplossing is het sowieso niet. Aangezien memory leaks opsporen in Magento een hel is (een stacktrace heeft al snel 20 calls, die elk op zich vaak ook weer 20 calls hebben) besloot ik om het probleem te ontwijken ipv het op te lossen.

Ik gebruik een PHP script voor imports ipv de Magento backend om zo imports te starten dmv een cronjob. Dit script is geschreven door de jongens bij Byte en kan je hier vinden. De CSV file die ik heb (met 26.312 regels) kan hij niet inladen vanwege de memory leak. Een 1000 regels tellende file wel dus ik heb mijn file in 26 stukken opgehakt; products-1.csv t/m products-26.csv. Daarna heb ik in het script van Byte de volgende regel toegevoegd tussen regel 64 en 65 (na de 2e try);

Mage::log("SKU::".$importData['sku']."::SKU imported",null,"products.log");

Zodoende logt hij elk individuele product dat wordt geimporteerd. Waarom ik dat doe wordt zo duidelijk. Vervolgens maak ik in de Magento database (rechtstreeks, via de backend duurt erg lang) 26 import profiles aan; profiel 10 t/m 36, en geef in elk profiel de bijbehorende CSV file aan (profile 10 heeft products-1.csv, profile 11 heeft products-2.csv, etc etc) . Daarna start ik de import met

php magento_product_import.php 10

Maar vanwege de memory leak stopt deze (bij mij) na ongeveer 150 – 200 producten, ipv de volle 1000 af te maken. Je kan hem daarna weer opnieuw opstarten maar dan begint hij weer van voor af aan; we dienen dus de producten die al geimporteerd zijn uit de CSV file te verwijderen zodat hij ze niet opnieuw inlaadt. Daarnaast wil je dat natuurlijk niet met de hand doen maar automatisch. Daarom heb ik een monitor script gemaakt die je dmv een cronjob elke minuut laat draaien. Deze checkt of de products.log file korter dan 10 seconden geleden is aangepast. Zo ja; dan is hij nog bezig. Zo nee; dan is het script gecrashed door de memory leak. Daarna kijkt hij in de log welke SKU’s al geweest zijn en verwijderd deze uit de desbetreffende CSV file. Dan start hij de import opnieuw. Door te checken of er CSV files leeg zijn weet hij waar hij is gebleven. Dit is het monitor script (snel bij elkaar gehackt en functies her en der geleend);

function delLineFromFile($fileName, $lineNum){
  if(!is_writable($fileName))
    {
    print "The file $fileName is not writable";
    exit;
    }
  else
      {
    $arr = file($fileName);
    }
  $lineToDelete = $lineNum-1;
 
	if($lineToDelete > sizeof($arr))
    {
    // "You have chosen a line number, <b>[$lineNum]</b>,  higher than the length of the file.";
    exit;
    }
  unset($arr["$lineToDelete"]);
  if (!$fp = fopen($fileName, 'w+'))
    {
        // "Cannot open file ($fileName)";
        exit;
	}
  if($fp)
    {
        foreach($arr as $line) { fwrite($fp,$line); }
        fclose($fp);
	}
}
 
function find($file,$text) {
	$lines = explode("\n",file_get_contents($file));
	$i=0;
	foreach($lines as $line) {
		$i++;
		if(stripos($line,$text)!==false) {
			return $i;
			exit;
		}
	}
}
 
function deleteFromCSV($sku,$csvnumber) {
	$linenumber = find("/var/www/domein.nl/htdocs/var/import/products-".$csvnumber.".csv",$sku);
	delLineFromFile("/var/www/domein.nl/htdocs/var/import/products-".$csvnumber.".csv",$linenumber);
}
 
$modified = filemtime("/var/www/domein.nl/htdocs/var/log/products.log");
 
// Opmerkingen mbt onderstaande code;
// 10 staat voor het aantal secondes verschil dat mag bestaan
// 27 staat voor het aantal csv files waarin alle producten staan (26+1)
// 1300 staat voor het aantal karakters dat de eerste header regel in een csv file lang is; dit verschilt dus hoogstwaarschijnlijk van de waarde die jij gebruikt
if((date("U")-$modified)>10) {
	for($i=1;$i<27;$i++) {
		if(strlen(file_get_contents("/var/www/domein.nl/htdocs/var/import/products-".$i.".csv"))>1300) {
			$csvnumber = $i;
			break;
		}
	}
	$profileId = $csvnumber+9;
	$lines = explode("\n",file_get_contents("/var/www/domein.nl/htdocs/var/log/products.log"));
	foreach($lines as $line) {
	        $end = stripos($line,"::SKU");
	        $start = (stripos($line,"SKU::")+strlen("SKU::"));
	        $length = $end-$start;
	        $sku = substr($line,(stripos($line,"SKU::")+strlen("SKU::")),$length);
        	deleteFromCSV($sku,$csvnumber);
	} 
 
	shell_exec("rm /var/www/domein.nl/htdocs/var/log/products.log");
 
	shell_exec("touch /var/www/domein.nl/htdocs/var/log/products.log");
	shell_exec("chmod 777 /var/www/domein.nl/htdocs/var/log/products.log");
	echo "\n##start import again\n";
shell_exec("php magento_product_import.php ".$profileId." &");
}

Plaats deze code in je Magento root en noem hem bijv ‘monitor.php’. Maak vervolgens een cronjob aan die elke minuut draait en nu hoort alles automatisch geimporteerd te worde