fix(IMAP): оптимизация обработки email для предотвращения memory exhaustion
Корневая проблема: библиотека webklex/php-imap загружала все тела писем по умолчанию, что приводило к исчерпанию 256MB памяти при 50+ письмах. Решение (3 уровня): Уровень 1 - Конфигурация: - config/imap.php: добавлены fetch_body=false, fetch_flags=false, fetch_limit=20 - php.ini: увеличен memory_limit с 256M до 1G, добавлены OPcache настройки Уровень 2 - Оптимизация кода: - FetchUnreadEmailsTask: setFetchBody(false), лимиты, MemoryAwareTrait - FetchEmailNewsAction: batch-обработка (10 писем), gc_collect_cycles() - DownloadAttachmentsTask: lazy load вложений, extension whitelist (security) - ConnectToImapTask: MemoryAwareTrait для мониторинга Уровень 3 - Инфраструктура: - ProcessEmailNewsJob: queue job для асинхронной обработки (3 попытки, 300s timeout) - FetchEmailNewsCommand: поддержка --async режима - MemoryAwareTrait: reusable трейт для мониторинга памяти Security fixes: - Extension whitelist для предотвращения path traversal - Убраны реальные email из config/imap.php - Добавлена проверка на null message объект Docs: - README_EMAIL_OPTIMIZATION.md: полная документация по оптимизации Fixes: Allowed memory size of 268435456 bytes exhausted Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace App\Containers\Dashboard\Commands;
|
||||
|
||||
use App\Containers\Dashboard\Actions\FetchEmailNewsAction;
|
||||
use App\Containers\Dashboard\Actions\EmailNews\FetchEmailNewsAction;
|
||||
use App\Containers\Dashboard\Exceptions\EmailFetchException;
|
||||
use App\Containers\Dashboard\Jobs\ProcessEmailNewsJob;
|
||||
use App\Ship\Abstracts\Commands\ConsoleCommand;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Artisan команда для получения новостей из Email
|
||||
*
|
||||
* OPTIMIZATION: Поддерживает два режима:
|
||||
* 1. Синхронный (по умолчанию) — для backward compatibility
|
||||
* 2. Асинхронный (--async) — dispatch в queue worker (рекомендуется)
|
||||
*/
|
||||
class FetchEmailNewsCommand extends ConsoleCommand
|
||||
{
|
||||
@@ -17,9 +22,11 @@ class FetchEmailNewsCommand extends ConsoleCommand
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'email:fetch-news
|
||||
protected $signature = 'email:fetch-news
|
||||
{--force : Принудительный запуск, даже если отключено в конфиге}
|
||||
{--log : Выводить подробный лог в консоль}';
|
||||
{--log : Выводить подробный лог в консоль}
|
||||
{--async : Отправить задачу в очередь (рекомендуется для production)}
|
||||
{--queue= : Имя очереди (по умолчанию "default")}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -39,6 +46,8 @@ class FetchEmailNewsCommand extends ConsoleCommand
|
||||
|
||||
$force = $this->option('force');
|
||||
$verbose = $this->option('log');
|
||||
$async = $this->option('async');
|
||||
$queue = $this->option('queue') ?? 'default';
|
||||
|
||||
// Проверяем, включена ли функция
|
||||
if (!config('email-news.enabled', true) && !$force) {
|
||||
@@ -48,50 +57,13 @@ class FetchEmailNewsCommand extends ConsoleCommand
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $fetchEmailNewsAction->run();
|
||||
|
||||
// Вывод результатов
|
||||
$this->newLine();
|
||||
$this->line('📊 Результаты обработки:');
|
||||
$this->table(
|
||||
['Метрика', 'Значение'],
|
||||
[
|
||||
['Обработано писем', $result['processed_emails']],
|
||||
['Пропущено (не редактор)', $result['skipped_emails']],
|
||||
['Создано новостей', $result['created_posts']],
|
||||
['Ошибок', count($result['errors'])],
|
||||
]
|
||||
);
|
||||
|
||||
// Вывод созданных постов
|
||||
if (!empty($result['posts'])) {
|
||||
$this->newLine();
|
||||
$this->info('✅ Созданные новости:');
|
||||
foreach ($result['posts'] as $post) {
|
||||
$status = $post->status instanceof \BackedEnum ? $post->status->value : $post->status;
|
||||
$this->line(" • {$post->title} (ID: {$post->id}, статус: {$status})");
|
||||
}
|
||||
// OPTIMIZATION: Асинхронный режим через queue worker
|
||||
if ($async) {
|
||||
return $this->handleAsync($queue, $verbose);
|
||||
}
|
||||
|
||||
// Вывод ошибок
|
||||
if (!empty($result['errors'])) {
|
||||
$this->newLine();
|
||||
$this->error('❌ Ошибки:');
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->warn(" • {$error['email_subject']}: {$error['error']}");
|
||||
}
|
||||
}
|
||||
|
||||
// Подробный лог
|
||||
if ($verbose) {
|
||||
$this->newLine();
|
||||
$this->info('📝 Детальный лог доступен в storage/logs/laravel.log');
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('✅ Завершено!');
|
||||
|
||||
return 0;
|
||||
// Синхронный режим (backward compatibility)
|
||||
return $this->handleSync($fetchEmailNewsAction, $verbose);
|
||||
} catch (EmailFetchException $e) {
|
||||
$this->error('❌ Ошибка Email: ' . $e->getMessage());
|
||||
Log::error('[FetchEmailNewsCommand] EmailFetchException', [
|
||||
@@ -112,4 +84,102 @@ class FetchEmailNewsCommand extends ConsoleCommand
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Синхронная обработка (backward compatibility)
|
||||
*
|
||||
* @param FetchEmailNewsAction $fetchEmailNewsAction
|
||||
* @param bool $verbose
|
||||
* @return int
|
||||
*/
|
||||
private function handleSync(FetchEmailNewsAction $fetchEmailNewsAction, bool $verbose): int
|
||||
{
|
||||
$result = $fetchEmailNewsAction->run();
|
||||
|
||||
// Вывод результатов
|
||||
$this->displayResults($result, $verbose);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Асинхронная обработка через queue worker
|
||||
*
|
||||
* @param string $queue
|
||||
* @param bool $verbose
|
||||
* @return int
|
||||
*/
|
||||
private function handleAsync(string $queue, bool $verbose): int
|
||||
{
|
||||
$this->info("📤 Отправка задачи в очередь '{$queue}'...");
|
||||
|
||||
$job = ProcessEmailNewsJob::dispatch()->onQueue($queue);
|
||||
|
||||
$this->info('✅ Задача отправлена в очередь');
|
||||
$this->line(" Job ID: {$job->getJobId()}");
|
||||
$this->newLine();
|
||||
$this->info('📝 Мониторинг выполнения:');
|
||||
$this->line(" php artisan queue:monitor {$queue}");
|
||||
$this->line(" tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob");
|
||||
|
||||
if ($verbose) {
|
||||
$this->newLine();
|
||||
$this->info('ℹ️ Queue worker должен быть запущен:');
|
||||
$this->line(' docker exec ntspi-php php artisan queue:work --queue=' . $queue);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вывод результатов обработки
|
||||
*
|
||||
* @param array $result
|
||||
* @param bool $verbose
|
||||
* @return void
|
||||
*/
|
||||
private function displayResults(array $result, bool $verbose): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->line('📊 Результаты обработки:');
|
||||
$this->table(
|
||||
['Метрика', 'Значение'],
|
||||
[
|
||||
['Обработано писем', $result['processed_emails']],
|
||||
['Пропущено (не редактор)', $result['skipped_emails']],
|
||||
['Создано новостей', $result['created_posts']],
|
||||
['Ошибок', count($result['errors'])],
|
||||
['Batch-ей обработано', $result['batches_processed'] ?? 0],
|
||||
['Пик памяти', $result['peak_memory'] ?? 'N/A'],
|
||||
]
|
||||
);
|
||||
|
||||
// Вывод созданных постов
|
||||
if (!empty($result['posts'])) {
|
||||
$this->newLine();
|
||||
$this->info('✅ Созданные новости:');
|
||||
foreach ($result['posts'] as $post) {
|
||||
$status = $post->status instanceof \BackedEnum ? $post->status->value : $post->status;
|
||||
$this->line(" • {$post->title} (ID: {$post->id}, статус: {$status})");
|
||||
}
|
||||
}
|
||||
|
||||
// Вывод ошибок
|
||||
if (!empty($result['errors'])) {
|
||||
$this->newLine();
|
||||
$this->error('❌ Ошибки:');
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->warn(" • {$error['email_subject']}: {$error['error']}");
|
||||
}
|
||||
}
|
||||
|
||||
// Подробный лог
|
||||
if ($verbose) {
|
||||
$this->newLine();
|
||||
$this->info('📝 Детальный лог доступен в storage/logs/laravel.log');
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('✅ Завершено!');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user