Skip to content

[Mailer] Memory leaking when sending many emails #45211

@alexkingdom

Description

@alexkingdom

Symfony version(s) affected

5.3, 5.4

Description

This problem I had before (https://stackoverflow.com/questions/67248353/symfony-5-memory-leaking-when-send-multiple-emails-through-mailer) but it fixed after upgrading Symfony.

Now I started to have again problems related mailer when sending many emails through command.

Code inside execute method of command:

    // Total Users
    $totalRecords = $this->getUserRepository()->count(['newsletter' => true]);

    if ($totalRecords === 0) {
        $output->writeln('No records found');

        return Command::SUCCESS;
    }

    $offers = $this->fetchOffersByType($type);
    $totalOffers = count($offers);

    // Check if we have popular offers
    if ($totalOffers === 0) {
        $output->writeln('No Offers was found');

        return Command::SUCCESS;
    }

    $totalPages = ceil($totalRecords / self::BUFFER_SIZE);

    // Initializing one time and assign to users
    $newsletter = (new Newsletter())
        ->setSentAt(new DateTime())
        ->setType(NewsletterType::MAP[$type]);

    $totalSuccessSent = 0;
    $total = 0;
    for ($page = 1; $page <= $totalPages; $page++) {
        // Get users to who we will send newsletters
        $users = $this->getUserRepository()
            ->findBy(['newsletter' => true], null, self::BUFFER_SIZE, self::BUFFER_SIZE * ($page - 1));

        foreach ($users as $user) {
            $total++;
            if (empty($user->getEmail())) {
                continue;
            }

            if ($this->emailService->sendNewsletter($user, $type, $offers)) {
                $user->addNewsletter($newsletter);

                $this->em->persist($user);
                $totalSuccessSent++;
            }
        }

        $this->em->flush();

        // Make clean up after specific number of users
        if ($total % self::CLEAN_UP_AFTER === 0) {
            $output->writeln('Clean Up');
            $this->em->clear();
            gc_collect_cycles();
        }
    }

And here is the piece of method sendNewsletter:

     try {
        $email = (new TemplatedEmail())
            ->from(new Address($this->parameterBag->get('noReplayEmail'), $this->parameterBag->get('noReplayEmailName')))
            ->to($user->getEmail())
            ->priority(Email::PRIORITY_NORMAL)
            ->subject($subject)
            ->htmlTemplate($template)
            ->context([
                'offers' => $offers,
            ]);

        $this->mailer->send($email);

        return true;
    } catch (TransportExceptionInterface | RfcComplianceException | JsonException $e) {
        return false;
    }

If to comment $this->mailer->send($email) no problem at all. For testing I'm using dsn: null://null

Upgrading / downgrading of symfony not helped me.
I'm using right now Symfony 5.4 and php 7.4

Note: Memory limit that is used for command is 512 MB.

How to reproduce

Just create a command send many emails and on each sent email track the memory peak and you will see how it's increasing.

Possible Solution

After investigation and debugging I found that so called MessageLoggerListener (vendor/symfony/mailer/EventListener/MessageLoggerListener.php) is collecting logs but don't reset the logs.

So I decorated this listener and just reset after ever 50 events. I don't like this solution so for this reason I wrote here, maybe there is better solution.

Here is the code:

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;

/**
 * Decorating the listener that save logs related to message events but it not reset the data
 * and this creates memory leaking.
 *
 * Temporary solution.
 */
class MessageLoggerListenerDecorator implements EventSubscriberInterface
{
    private const BUFFER_LOG = 50;

    private MessageLoggerListener $messageLoggerListener;

    public function __construct(MessageLoggerListener $messageLoggerListener)
    {
        $this->messageLoggerListener = $messageLoggerListener;
    }

    /**
     * @inheritDoc
     */
    public static function getSubscribedEvents(): array
    {
        return [
            MessageEvent::class => ['onMessage', -255],
        ];
    }

    public function onMessage(MessageEvent $event): void
    {
        if (count($this->messageLoggerListener->getEvents()->getEvents()) >= self::BUFFER_LOG) {
            $this->messageLoggerListener->reset();
        }

        $this->messageLoggerListener->onMessage($event);
    }
}

And service.yml:

    App\EventSubscriber\MessageLoggerListenerDecorator:
        decorates: mailer.message_logger_listener
        arguments: ['@.inner']

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy