Утечка памяти в программе, которая использует LWP::UserAgent для загрузки файла
Я пытаюсь восстановить Perl-скрипт, который использовал давно. Это для загрузки файлов из облачного хранилища на мой локальный клиент. Я уверен, что тогда все работало нормально, но теперь у меня проблема LWP::UserAgent
загружает файл полностью в память перед записью на диск. Ожидаемое и прежнее поведение будет заключаться в том, что во время загрузки он должен записывать куски полученного файла в целевое устройство.
Я пытаюсь в настоящее время на OSX с Perl 5.16.3 и 5.18, а также пробовал на Windows, но я больше не знаю версию Perl. Я довольно уверен, что это связано с версией Perl, но я не знаю, какой я использовал тогда, и я хочу знать, что изменилось.
sub downloadFile {
my $url = shift;
my $filename = shift;
my $temp_filename = shift;
my $expected_size = shift;
( $download_size, $received_size, $avg_speed, $avg_speed_s, $avg_speed_q,
$speed_count, $speed, $byte_offset, $http_status
) = ( 0, 0, 0, 0, 0, 0, 0, 0, 0 );
if ( -e $temp_filename and !$options{'no-resume'} ) {
my @stat = stat($temp_filename);
if ( $expected_size > $stat[7] ) {
$byte_offset = $stat[7];
$received_size = $stat[7];
}
}
open DOWNLOAD, ( $byte_offset > 0 ) ? ">>" : ">", $temp_filename
or die "Unable to create download file: $!";
binmode DOWNLOAD;
$last_tick = time();
my $host = "myhost";
if ( $url =~ m/http:\/\/(.*?)\//gi ) {
$host = $1;
}
$agent->credentials(
$host . ":80",
"Login Required",
$config->{"account_name"},
$config->{"account_password"} );
my $response = $agent->get(
$url,
':content_cb' => \&didReceiveData,
':read_size_hint' => ( 2**14 ) );
close DOWNLOAD;
my @stat = stat($temp_filename);
my $actual_size = $stat[7];
if ( ! $response->is_success() ) {
printfvc( 0,
"\rDownload failed: %s",
'red',
$response->status_line() );
return 0;
}
elsif ( $actual_size != $expected_size ) {
printfvc( 0,
"\rDownloaded file does not have expected size (%s vs. %s)",
'red',
$actual_size, $expected_size );
return 0;
}
else {
rename $temp_filename, $filename;
printfvc( 0,
"\rDownload succeeded ",
'green' );
return 1;
}
}
sub didReceiveData {
my ( $data, $cb_response, $protocol ) = @_;
#my($response, $ua, $h, $data) = @_;
my $data_size = scalar( length($data) );
$received_size += $data_size;
$speed_count += $data_size;
my $now = time();
if ( $last_tick < $now ) {
$speed = $speed_count;
$speed_count = 0;
$last_tick = $now;
$avg_speed_q++;
$avg_speed_s += $speed;
$avg_speed = $avg_speed_s / $avg_speed_q;
}
if ( $download_size > 0 and $http_status eq "200" or $http_status eq "206" ) {
print DOWNLOAD $data;
printf("-> %.1f %% (%s of %s, %s/s) %s ",
( $received_size / $download_size ) * 100,
fsize($received_size),
fsize($download_size),
fsize($speed),
$avg_speed_q > 3
? fduration( ( $download_size - $received_size ) / $avg_speed ) . " remaining"
: ""
) if ( $verbosity >= 0 );
}
else {
printf("-> Initiating transfer...") if ( $verbosity >= 0 );
}
return 1;
}
выход:
mun-m-sele:PutIO-Perl-folder-sync sele$ perl putiosync.pl
Syncing folder 'Test' to '/Users/sele/Downloads/Test'...
1 files queued to download
5MB.zip
Fetching '5MB.zip' [1 of 1]
-> 0.3 % (16.0 kiB of 5.0 MiB, 16.0 kiB/s)
-> 0.6 % (32.0 kiB of 5.0 MiB, 16.0 kiB/s)
-> 0.9 % (48.0 kiB of 5.0 MiB, 16.0 kiB/s)
.
.
.
-> 99.1 % (5.0 MiB of 5.0 MiB, 16.0 kiB/s)
-> 99.4 % (5.0 MiB of 5.0 MiB, 16.0 kiB/s)
-> 99.7 % (5.0 MiB of 5.0 MiB, 16.0 kiB/s)
Download succeeded
Таким образом, выходные данные ожидаются, НО все же эти выходные данные появляются только после загрузки файла в память.
content_cb
не вызывается во время загрузки (проверяется простым print("cb")
к вершине didReceiveData
Обновить
Я обнаружил, что он работает, как и ожидалось, на Windows Strawberry Perl 5.16.2. Я могу предоставить вам версии пакета, если вы скажете мне, что и как;)
3 ответа
Ваш собственный код содержит много несоответствий, таких как поддержка возобновления, поддержка нескольких серверов, ведение журнала прогресса, учетные данные сайта, временные файлы загрузки, обработка ошибок и вычисления средней скорости. Ни один из них не имеет отношения к основной проблеме, которую вы описали, и именно поэтому я попросил вас создать минимальный, полный и проверяемый пример. Я не понимаю ваш отказ или почему вы, кажется, цепляетесь за идею, что ошибка в Perl, а не в вашем собственном коде
Без этого все, что я могу сделать, это продемонстрировать, что техника работает хорошо. Вот то, что вы должны были сгенерировать как демонстрацию проблемы. Он очень мало отличается от вашего собственного кода и работает нормально. Он загружает официальный ISO-образ дистрибутива Ubuntu для настольных ПК, который содержит около 1,4 ГБ информации. Процесс использует 17 МБ памяти и завершается за 14 минут. Размер результирующего файла точно соответствует Content-Length
указано в заголовке HTTP
Помимо этого никто не может помочь вам в дальнейшем. Я призываю вас принять помощь экспертов, когда вы попросили об этом. Также стоит отметить, что проблема часто обнаруживается в процессе создания MCVE из вашей неисправной программы: вы, скорее всего, удалите несущественную часть кода и обнаружите, что проблема исчезла.
use strict;
use warnings 'all';
use LWP;
use constant ISO_URL => 'http://releases.ubuntu.com/16.04/ubuntu-16.04-desktop-amd64.iso';
STDOUT->autoflush;
my $ua = LWP::UserAgent->new;
my $expected;
{
my $res = $ua->head(ISO_URL);
$expected = $res->header('Content-Length');
printf "Expected file size is %.3fMB\n", $expected / 1024**2;
}
my ($iso_file) = ISO_URL =~ m{([^/]+)\z};
open my $iso_fh, '>:raw', $iso_file or die $!;
my $total;
my $pc = 0;
{
my $res = $ua->get(
ISO_URL,
':content_cb' => \&content_cb,
':read_size_hint' => 16 * 1024,
);
close $iso_fh or die $!;
print $res->status_line, "\n";
printf "Final file size is %.3fMB\n", (-s $iso_file) / 1024**2;
}
sub content_cb {
my ( $data, $res ) = @_;
die $res->status_line unless $res->is_success;
print $iso_fh $data;
$total += length $data;
while ( $pc < 100 * $total / $expected ) {
printf "%3d%%\n", $pc++;
}
}
выход
Expected file size is 1417.047MB
0%
1%
2%
3%
4%
5%
:
:
95%
96%
97%
98%
99%
200 OK
Final file size is 1417.047MB
Может быть, проблема в файловом вводе / выводе, а не в LWP? Я предполагаю, что данные не сбрасываются в файл, пока вы не закроете файл.
Ниже приведен пример кода о том, как заставить файл обрабатывать сброс данных на жесткий диск:
{ my $ofh = select LOG;
$| = 1;
select $ofh;
}
Проверять, выписываться perldoc -q flush
и эта интересная статья о буферизации " Страдание от буферизации?".
Основная проблема с вашим кодом заключается в том, что $http_status
никогда не назначается. Это может быть установлено только обратным вызовом didReceiveData
или после завершения всей загрузки, когда get
выход из вызова
Но ваш обратный вызов проверяет, $http_status eq "200"
(который должен быть $cb_response->is_success
) перед печатью на дескриптор файла DOWNLOAD, чтобы ничего не могло быть записано
Я могу поверить, что ваш код увеличивает объем памяти, потому что он бесконечно печатает -> Initiating transfer...
в STDOUT, но ничего не будет записано во временный файл из-за непроверенного статуса HTTP. Я уверен, что вы наблюдали за тем, как ваш процесс запускается и умирает с ошибкой "Недостаточно памяти", и сразу обвиняли Perl, даже не пытаясь загрузить файл размером 1 КБ. Ваш код никогда не работал, и ваш вопрос и поддержка тех, кто вам помог бы, возмутителен
"Я почти уверен, что в то время это работало нормально" - не очень хорошее начало, но когда вы тогда отклоняете на этом основании все приложения решения или запроса на информацию, вы становитесь смешным