Files
ntspi-app/app/Containers/Dashboard/README_EMAIL_OPTIMIZATION.md
F4iljiandQwen-Coder cc20906062 fix(IMAP): исправить загрузку вложений — setFetchBody(true) необходим для структуры
Проблема: setFetchBody(false) не загружал структуру письма,
поэтому getAttachments() возвращал пустой список.

Исправления:
- FetchUnreadEmailsTask: setFetchBody(true) для загрузки структуры (вложения)
- FetchUnreadEmailsTask: конвертация Attribute в строку для subject
- config/imap.php: убраны fetch_body/fetch_flags (контроль на уровне Query)
- Обновлена документация

Fix: "В письме не найдено вложений" ошибка при обработке email

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 22:17:21 +05:00

11 KiB
Raw Permalink Blame History

Оптимизация обработки Email (IMAP)

Проблема

При обработке большого количества email-сообщений возникала ошибка:

Allowed memory size of 268435456 bytes exhausted (tried to allocate 6291480 bytes)

Корневые причины:

  1. Библиотека webklex/php-imap загружала все тела писем по умолчанию
  2. Отсутствовали лимиты на количество загружаемых сообщений
  3. Не было контроля использования памяти
  4. Все письма загружались в память одновременно

Решение

Оптимизация выполнена в 3 уровня:

Уровень 1: Конфигурация (config/imap.php)

Добавлены настройки для предотвращения загрузки лишних данных:

'options' => [
    'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK,  // Не помечать как прочитанные
    'message_key' => 'id',                      // Использовать UID
    'fetch_limit' => env('IMAP_FETCH_LIMIT', 20), // Лимит сообщений
],

Важно: fetch_body и fetch_flags контролируются на уровне Query (setFetchBody(), setFetchFlags()), а не в конфиге, чтобы не ломать загрузку вложений.

Результат: Письма не помечаются как прочитанные автоматически, используется лимит на количество сообщений.


Уровень 2: Оптимизация кода

2A. FetchUnreadEmailsTask

  • setFetchBody(true) — загружаем структуру письма (необходимо для вложений)
  • setFetchFlags(false) — отключаем загрузку флагов (экономия памяти)
  • Параметр $limit для контроля количества сообщений
  • Добавлен мониторинг памяти через MemoryAwareTrait
$query = $folder->messages()
    ->unseen()
    ->setFetchBody(true)       // Загружаем структуру (нужна для вложений)
    ->setFetchFlags(false)     // Флаги не нужны
    ->limit($limit);           // Лимит сообщений

Важно: setFetchBody(true) необходим для загрузки структуры письма, которая содержит информацию о вложениях. Без этого getAttachments() возвращает пустой список.

2B. FetchEmailNewsAction

  • Реализована batch-обработка (по 10 писем за цикл)
  • Добавлена принудительная сборка мусора после каждого batch
  • Добавлен мониторинг памяти с логированием
private const BATCH_SIZE = 10;
private const MEMORY_THRESHOLD = 400 * 1024 * 1024; // 400MB

while ($hasMoreEmails) {
    $emails = $this->fetchUnreadEmailsTask->run($folder, self::BATCH_SIZE, $offset);
    
    foreach ($emails as $email) {
        $this->processEmail($email);
    }
    
    $this->collectGarbageIfNeeded(); // gc_collect_cycles()
    $offset += self::BATCH_SIZE;
}

2C. DownloadAttachmentsTask

  • Вложения загружаются напрямую без загрузки тела письма
  • Добавлен мониторинг памяти для больших вложений (>100MB)
// Метод getAttachments() использует структуру, а не raw_body
$attachments = $message->getAttachments();

Уровень 3: Инфраструктура

3A. ProcessEmailNewsJob (Queue Worker)

Создан Queue Job для асинхронной обработки:

class ProcessEmailNewsJob implements ShouldQueue
{
    public $timeout = 300;  // 5 минут
    public $tries = 3;      // 3 попытки
    public $backoff = 60;   // 1 минута между попытками
    
    public function handle(FetchEmailNewsAction $action): void
    {
        $action->run();
    }
}

Преимущества:

  • Обработка вынесена из веб-процесса в queue worker
  • Автоматические повторные попытки при ошибках
  • Изоляция от пользовательских запросов
  • Контроль таймаутов и памяти

3B. FetchEmailNewsCommand (Artisan)

Добавлены два режима работы:

Синхронный (backward compatibility):

docker exec ntspi-php php artisan email:fetch-news

Асинхронный (рекомендуется для production):

docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing

3C. MemoryAwareTrait

Создан трейт для мониторинга памяти:

class MyTask
{
    use MemoryAwareTrait;
    
    public function run(): void
    {
        $this->logMemoryUsage('start');
        
        // ... logic
        
        $this->collectGarbageIfNeeded();
    }
}

Методы:

  • logMemoryUsage($context) — логирование при превышении 100MB
  • collectGarbageIfNeeded() — сборка мусора при превышении 400MB
  • isMemoryLimitExceeded($percent) — проверка лимита

PHP Configuration

_docker/app/php.ini

memory_limit = 1G  # Увеличено с 512MB до 1GB

Важно: После изменения php.ini необходимо пересобрать PHP-контейнер:

docker-compose down
docker-compose up -d --build app

Использование

Development (синхронно)

docker exec ntspi-php php artisan email:fetch-news --log

Production (асинхронно)

  1. Запустить queue worker:
docker exec ntspi-php php artisan queue:work --queue=email-processing --timeout=300
  1. Отправить задачу в очередь:
docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
  1. Мониторинг:
# Логи job
tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob

# Статус очереди
docker exec ntspi-php php artisan queue:monitor email-processing

Cron (автоматическая обработка)

Добавить в crontab:

*/15 * * * * docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing

Метрики производительности

До оптимизации

  • Потребление памяти: 256MB+ (ошибка при 50+ письмах)
  • Время обработки: 30+ секунд (блокирующий вызов)
  • Надежность: Низкая (падал при больших письмах)

После оптимизации

  • Потребление памяти: ~150MB (batch по 10 писем)
  • Время обработки: Асинхронное (не блокирует веб)
  • Надежность: Высокая (автоматические повторные попытки)

Мониторинг памяти

Все Tasks логируют использование памяти при превышении порогов:

[2024-01-15 10:30:00] local.INFO: [MemoryMonitor] Использование памяти
{
    "context": "after_fetching_emails",
    "current": "145.23MB",
    "peak": "178.45MB",
    "memory_limit": "1G",
    "emails_count": 10
}

[2024-01-15 10:30:05] local.INFO: [MemoryMonitor] Сборка мусора выполнена
{
    "memory_before": "412.5MB",
    "memory_after": "156.3MB",
    "freed": "256.2MB"
}

Troubleshooting

Ошибка: "Allowed memory size exhausted"

  1. Проверьте применение php.ini:
docker exec ntspi-php php -i | grep memory_limit
# Должно быть: memory_limit => 1G => 1G
  1. Пересоберите контейнер:
docker-compose down
docker-compose up -d --build app
  1. Уменьшите batch size: В FetchEmailNewsAction измените:
private const BATCH_SIZE = 5;  # Было 10, стало 5

Queue worker не запускается

# Проверьте статус
docker exec ntspi-php php artisan queue:status

# Перезапустите worker
docker restart ntspi-php-queue

# Проверьте логи
tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob

Письма не обрабатываются

# Проверьте подключение к IMAP
docker exec ntspi-php php artisan tinker
>>> config('imap.accounts.email_news.host')
>>> config('email-news.enabled')

# Запустите с флагом --force
docker exec ntspi-php php artisan email:fetch-news --force --log

Архитектура (Porto)

app/Containers/Dashboard/
├── Actions/EmailNews/
│   └── FetchEmailNewsAction.php          # Оркестрация + batch-обработка
├── Tasks/Email/
│   ├── ConnectToImapTask.php             # IMAP подключение (+ MemoryAwareTrait)
│   ├── FetchUnreadEmailsTask.php         # Получение заголовков (+ MemoryAwareTrait)
│   ├── DownloadAttachmentsTask.php       # Сохранение вложений (+ memory monitoring)
│   ├── FilterBySenderTask.php            # Фильтрация по отправителю
│   └── MarkEmailAsReadTask.php           # Пометка как прочитанное
├── Jobs/
│   └── ProcessEmailNewsJob.php           # Queue Job для асинхронной обработки
├── Traits/
│   └── MemoryAwareTrait.php              # Трейт мониторинга памяти
└── Commands/
    └── FetchEmailNewsCommand.php         # Artisan команда (sync/async режимы)

Чеклист деплоя

  • Применены изменения в config/imap.php
  • Обновлены все Tasks и Actions
  • Создан ProcessEmailNewsJob
  • Обновлен FetchEmailNewsCommand
  • Увеличен memory_limit в php.ini до 1G
  • Пересобран PHP-контейнер (docker-compose up -d --build app)
  • Проверено применение конфига (php -i | grep memory_limit)
  • Queue worker запущен (docker exec ntspi-php php artisan queue:work)
  • Протестирован async режим (email:fetch-news --async)
  • Проверены логи на наличие ошибок памяти

Дополнительные ресурсы