memory leak
thk12 opened this issue · comments
i don't know where but there must be an memory leak when u try to get a lot of certificates in a loop.
code example test.php
to reproduce,
require 'ACMECert/ACMECert.php';
use skoerfgen\ACMECert\ACMECert;
for($i=0;$i<100;$i++) {
$ac=new ACMECert('https://acme-staging-v02.api.letsencrypt.org/directory');
$ac->loadAccountKey('file://'.'account_key.pem');
$hostname=substr(md5(uniqid().microtime()),0,5).'.hostname.tld';
$private_host_key=$ac->generateECKey('P-384');
file_put_contents("cert_private_key.$hostname.pem",$private_host_key);
$domain_config=array(
$hostname=>array('challenge'=>'http-01','docroot'=>'/var/www/vhosts/wildcard.hostname.tld/')
);
$handler=function($opts){
$fn=$opts['config']['docroot'].$opts['key'];
@mkdir(dirname($fn),0777,true);
file_put_contents($fn,$opts['value']);
return function($opts){
unlink($opts['config']['docroot'].$opts['key']);
};
};
$fullchain=$ac->getCertificateChain('file://'."cert_private_key.$hostname.pem",$domain_config,$handler);
file_put_contents("fullchain.$hostname.pem",$fullchain);
echo "$i: memory_get_usage=".(memory_get_usage()/1024)."kB\n";
}
example output of a cli php test.php | grep memory_get_usage
using php8.2
memory_get_usage=579.96875kB
memory_get_usage=597.1328125kB
memory_get_usage=614.28125kB
memory_get_usage=631.4296875kB
memory_get_usage=648.578125kB
memory_get_usage=665.7265625kB
memory_get_usage=682.875kB
memory_get_usage=700.0234375kB
memory_get_usage=717.171875kB
memory_get_usage=734.3203125kB
memory_get_usage=751.46875kB
memory_get_usage=768.6171875kB
memory_get_usage=785.765625kB
memory_get_usage=802.9140625kB
i know the PHP GC cleans it up, but the script itself should do it
Thank you for reporting this issue!
At first I did not find the reason for this behaviour, but then I realized that the destructor of each ACMECert instance only gets called at the end of the script. (Instead of on each iteration of the for loop)
Here is the code I used:
require 'ACMECert/ACMECert.php';
//use skoerfgen\ACMECert\ACMECert;
class ACMECert extends skoerfgen\ACMECert\ACMECert {
function __destruct(){
parent::__destruct();
echo '[DESTRUCT]',"\n";
}
}
for($i=0;$i<3;$i++) {
$ac=new ACMECert('https://acme-staging-v02.api.letsencrypt.org/directory');
$ac->loadAccountKey('file://'.'account_key.pem');
$hostname=substr(md5(uniqid().microtime()),0,5).'.hostname.tld';
$private_host_key=$ac->generateECKey('P-384');
file_put_contents("cert_private_key.$hostname.pem",$private_host_key);
$domain_config=array(
$hostname=>array('challenge'=>'http-01','docroot'=>'/var/www/vhosts/wildcard.hostname.tld/')
);
$handler=function($opts){
$fn=$opts['config']['docroot'].$opts['key'];
@mkdir(dirname($fn),0777,true);
file_put_contents($fn,$opts['value']);
return function($opts){
unlink($opts['config']['docroot'].$opts['key']);
};
};
$fullchain=$ac->getCertificateChain('file://'."cert_private_key.$hostname.pem",$domain_config,$handler);
file_put_contents("fullchain.$hostname.pem",$fullchain);
echo "$i: memory_get_usage=".(memory_get_usage()/1024)."kB\n";
}
relevant output:
0: memory_get_usage=593.90625kB
1: memory_get_usage=611.5859375kB
2: memory_get_usage=629.859375kB
[DESTRUCT]
[DESTRUCT]
[DESTRUCT]
However after some debugging I found the problem:
The function in ACMECert to get the HTTP headers holds a reference to the current ACMECert class, which prevents the garbage collector from reclaiming it.
From the PHP documentation:
When declared in the context of a class, the current class is automatically bound to it, making $this available inside of the function's scope. If this automatic binding of the current class is not wanted, then static anonymous functions may be used instead.
https://www.php.net/manual/en/functions.anonymous.php
So if I change the file src/ACMEv2.php
on line 303:
from:
CURLOPT_HEADERFUNCTION=>function($ch,$header)use(&$headers){
$headers[]=$header;
return strlen($header);
}
to:
CURLOPT_HEADERFUNCTION=>static function($ch,$header)use(&$headers){
$headers[]=$header;
return strlen($header);
}
I get the following output:
0: memory_get_usage=593.90625kB
[DESTRUCT]
1: memory_get_usage=594.0078125kB
[DESTRUCT]
2: memory_get_usage=594.078125kB
[DESTRUCT]
This should solve the problem. Can you confirm this?
can confirm! works like a charm with that fix! didn't know that either
ACMECert v3.2.2 has been released! It contains this fix.