Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When running as daemon worker, there are SSL errors when attempting to send email #4573

Closed
wyred opened this issue Jun 4, 2014 · 31 comments
Closed

Comments

@wyred
Copy link

wyred commented Jun 4, 2014

I upgraded from 4.1 to 4.2.

Running as queue:work would create SSL errors, but not when running queue:listen.

Using smtp driver with mailgun as the host.

[2014-06-04 02:26:19] production.ERROR: exception 'ErrorException' with message 'fwrite(): SSL operation failed with code 1. OpenSSL Error messages:
error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry' in /var/www/app.dev/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php:236
Stack trace:
#0 [internal function]: Illuminate\Exception\Handler->handleError(2, 'fwrite(): SSL o...', '/var/www/queues...', 236, Array)
#1 /var/www/app.dev/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php(236): fwrite(Resource id #3337, 'MAIL FROM: <con...')
#2 /var/www/app.dev/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/AbstractFilterableInputStream.php(175): Swift_Transport_StreamBuffer->_commit('MAIL FROM: <con...')
#3 /var/www/app.dev/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/AbstractFilterableInputStream.php(92): Swift_ByteStream_AbstractFilterableInputStream->_doWrite('MAIL FROM: <con...')
...
#30 /var/www/app.dev/artisan(59): Symfony\Component\Console\Application->run()
#31 {main} [] []
@taylorotwell
Copy link
Member

I would explore options besides it being a Laravel issue. It's a SwiftMailer exceptoin.

@reedmaniac
Copy link

Been noticing this on Forge, actually. Periodically the Queue will be unable to send an email and this error appears in my log and then it keeps on reappearing (as if the daemon never forgets it). And last night when a job failed, it was unable to make a MySQL connection. It is sporadic though and I have been unable to figure out why it sometimes fails to make these connections.

@mferrara
Copy link

mferrara commented Oct 9, 2014

Having this exact same problem, didn't start for me until I started running the queue as a daemon.

Hoping this is updated with a potential solution.

@maknz
Copy link

maknz commented Oct 9, 2014

Daemon workers are long running PHP processes, which can have side effects from code that wasn't built with that in mind.

It looks like SwiftMailer isn't built to be run long-term. To have a guess, it looks like it might not be catering for the fact the connection can drop and errors out.

There's nothing Laravel can do about it, it's probably best to follow the discussion here or switch to one of the API drivers which don't have this problem (e.g. the built-in Mandrill support).

@mferrara
Copy link

mferrara commented Oct 9, 2014

@maknz thanks for the response, it was very enlightening. I've since moved away from the daemon approach, but thank you for shedding some light on what the actual issue was!

@anaxamaxan
Copy link

While this is indeed a SwiftMailer issue, it might be noted in the Laravel Docs that SwiftMailer doesn't do well in a daemonized queue, or at least that we should be using API drivers when using a daemonized mail queue. The related SM thread is filled with comments from Laravel users.

@YOzaz
Copy link

YOzaz commented Feb 2, 2015

After some discussion with SwiftMailer developers, it looks like it should be solved from Laravel side - either by reinitializing Swift_Mailer object, either by closing Swift_Transport class object to close SMTP connection (or any other).

@GrahamCampbell
Copy link
Member

We're open to pull requests. :)

@YOzaz
Copy link

YOzaz commented Mar 30, 2015

@GrahamCampbell can you please at least mention in docs, that SwiftMailer should not be used with queue deamon worker, but used with queue listener instead?

@crynobone
Copy link
Member

@GrahamCampbell can you please at least mention in docs

If you feel this need to be addressed in the docs, feel free to send a Pull Request (as mentioned by @GrahamCampbell) to https://github.com/laravel/docs

@YOzaz
Copy link

YOzaz commented Mar 30, 2015

Made a PRs for code patch.
5.0: #8222
4.2: #8225

@mferrara
Copy link

@YOzaz <3

@YOzaz
Copy link

YOzaz commented Mar 30, 2015

So, as PR didn't go through - made a package:
https://github.com/YOzaz/Laravel-SwiftMailer
Feel free to test it.
Updated: compatible with Laravel 4 and 5 now.

@lukepolo
Copy link
Contributor

@YOzaz Why didn't the PR go through?

@YOzaz
Copy link

YOzaz commented Mar 30, 2016

@lukepolo I think because it may be undesired functionality. E.g. you may not want to do auto-restart for SMTP because of specific server configuration. But Laravel authors may answer better here ;)

@YOzaz
Copy link

YOzaz commented Apr 7, 2016

@lukepolo actually, it was implemented in L5 with commit af8eb1f. For L4 you need to use my package.

@d4vidmiller
Copy link

@YOzaz I actually tried your fix and still didn't work - gave up and moved back to regular queue, not daemon and it is such a CPU hog as it reloads framework every 5 seconds. Have you have any further feedback on your fix? Get same errors.

@YOzaz
Copy link

YOzaz commented Apr 14, 2016

@d4vidmiller which Laravel version are you using?

@schanzel
Copy link
Contributor

Even in L5 there are still problems with this.

[2016-04-18 11:56:16] live.ERROR: exception 'ErrorException' with message 'fwrite(): SSL operation failed with code 1. OpenSSL Error messages:
error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry' in  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php:232
Stack trace:
#0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(2, 'fwrite(): SSL o...', '[...]', 232, Array)
#1  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php(232): fwrite(Resource id #413, 'QUIT??QUIT??')
#2  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/AbstractFilterableInputStream.php(171): Swift_Transport_StreamBuffer->_commit('QUIT??QUIT??')
#3  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/AbstractFilterableInputStream.php(90): Swift_ByteStream_AbstractFilterableInputStream->_doWrite('QUIT??QUIT??')
#4  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php(276): Swift_ByteStream_AbstractFilterableInputStream->write('QUIT??')
#5  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpTransport.php(243): Swift_Transport_AbstractSmtpTransport->executeCommand('QUIT??', Array, Array)
#6  [...]vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php(216): Swift_Transport_EsmtpTransport->executeCommand('QUIT??', Array)
#7  [...]vendor/laravel/framework/src/Illuminate/Mail/Mailer.php(315): Swift_Transport_AbstractSmtpTransport->stop()
#8  [...]vendor/laravel/framework/src/Illuminate/Mail/Mailer.php(159): Illuminate\Mail\Mailer->forceReconnection()

There is an ErrorException thrown while Swift\Transport\AbstractSmtpTransport tries to write "QUIT" on the broken stream and SwiftMailer only catches a Swift_TransportException.

A possible solution would be to put a try-catch around the

$this->forceReconnection();

call in Illuminate\Mail\Mailer

Example:

/**
     * Send a new message using a view.
     *
     * @param  string|array  $view
     * @param  array  $data
     * @param  \Closure|string  $callback
     * @return void
     */
    public function send($view, array $data, $callback)
    {
        try {
            $this->forceReconnection();
        } catch (\ErrorException $e) {
        }

//...

Any thoughts on this?

It it worth a pull request?

@YOzaz
Copy link

YOzaz commented Apr 19, 2016

From my experience, ideal code should look something like this:

$transport = $this->getSwiftMailerTransport();

if ( ! $transport->isStarted() )
{
    $transport->start();
    return;
}
try
{
    // Send RESET to restart the SMTP status and check if it's ready for running
    $transport->reset();
}
catch (Exception $e)
{
    // In case of failure - let's try to stop it
    try
    {
        $transport->stop();
    }
    catch (Exception $e)
    {
        // Just start it then...
    }
    $transport->start();
}

... because SwiftMailer may fail in various places.

@schanzel
Copy link
Contributor

@YOzaz This is your suggested solution for Laravel 4. The problem should be fixed by af8eb1f as of Version 5. But this does not seem to be the case because of further similar exceptions due to the

$this->forceReconnection();

call.

@YOzaz
Copy link

YOzaz commented Apr 19, 2016

@schanzel that's my point - stop() may throw an Exception, if:

  • Transport has not started yet,
  • There is internal failure like in your example.

Proper implementation would be to use reset() if transport is not started yet, or just start() if it's already started.
That's why my code is so complicated with lots of try...catch blocks. Actually, I'm not the author of this - it's SwiftMailer author who wrote it.

@lukepolo
Copy link
Contributor

lukepolo commented May 2, 2016

Yah im still having these issues with 5.2

@alizons
Copy link

alizons commented May 3, 2016

having the same issue here with laravel 5.1, any updates on that ?

@lifeofguenter
Copy link
Contributor

Any updates?

@mpreclik
Copy link

Same issue here, v5.1.35 - tried scheduling an hourly restart of daemonized queue workers, but the problem persists.

@lukepolo
Copy link
Contributor

I had to restart every 15 min and seems to have been a good temp fix.

@drcreazy
Copy link

drcreazy commented May 13, 2016

Well, I tested all solutions include described in this thread

Seems the only appropriate solution is stopping transport after each task (it's looks like the transport connects automatically on next task):

\Mail::getSwiftMailer()->getTransport()->stop();

I tried to reset daemons after couple failed attempts, but after some time I got timeouts errors from mailgun so it didn't help me.

This is all my code for the job handler (worked fine in prod for a week, php7). Catching errors/stopping daemon instance seems unnecessary, but I keep it for now (hope they can helps somebody) :

public function sendOrderCompleted(RabbitMQJob $job, array $data)
    {
        try {
            \Mail::send(
                $data['template'],
                $data['body'],
                function ($message) use ($data) {
                    $message
                        ->to($data['to'])
                        ->subject($data['subject']);
                }
            );
        } catch (\ErrorException $e) {
            /**
             * Workaround
             *
             */
            if ($job->attempts() >= self::ATTEMPTS_BEFORE_EXIT) {
                $this->logger->info(
                    'Resetting daemon after ErrorException:'. $e->getMessage()
                    . 'Job: sendOrderCompleted. Job data: ' . json_encode($data)
                );
                // Attempts++
                $job->release();
                // Kill current daemon instance
                exit(0);
            }
            $this->logger->info('Catches ErrorException in sendOrderCompleted: ' . $e->getMessage());
            throw new \RuntimeException('ErrorException in sendOrderCompleted');
        } catch (\Throwable $e) {
            $this->logger->info(
                'Throwable in sendOrderCompleted: '
                . $e->getMessage()
                . ' Data: '
                . json_encode($data)
            );
            throw $e;
        }
        $job->delete();
        // stopping mail transport
        \Mail::getSwiftMailer()->getTransport()->stop();
    }

@GrahamCampbell
Copy link
Member

Maybe we just need the worker daemon to keep track of the number of swift exceptions, and kill itself for us?

@lifeofguenter
Copy link
Contributor

Is there a reason why the SMTP connection needs to be kept alive after the job has ran? Sounds like the equivalent of keeping a HTTP connection alive.

E.g. Mail::send() should only connect to the SMTP provider when called, and disconnect again - or am I misunderstanding something?

@schanzel
Copy link
Contributor

schanzel commented May 17, 2016

I think closing the connection after sending an email would be the correct way to fix this. At least it sounds more sane than trying to close the connection before sending an email and calling that a forced reconnection.

As @drcreazy's solution seems to work I've submitted a corresponding pull request for the 5.1 branch (see #13583). If approved I'll submit the the changes for 5.0 and 5.2 as well.

taylorotwell pushed a commit that referenced this issue Jun 11, 2016
…ssue #4573) (#13583)

* Issue #4573
    - close mailer connection after sending message
    - fix return statements and related phpdoc

* Minor
    Fixed code style according to StyleCI

* Revert return type fixes

* Minor
    fixed code style according to StyleCI

* Minor
    StyleCI still not happy... :-(

* - Reverted removal of Mailer::forceReconnection() method
- wrap try-finally around swift->send(); transport->stop();

* Minor: remove temporary result variable

* Use private field instead of public getter for swift mailer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests