I am having trouble setting up my delayed mailing with laravel 5.1
In App\Exceptions\Handler in the render method I want to mail any occuring errors to myself: Like this
Mail::send('emails.servererror', ['exception' => $e, 'request' => $request] ,function($message)
{
...
});
This works perfectly, except that I would like to delay the sending of the email With Mail::later. E.G.
Mail::later(5, 'emails.servererror', ['exception' => $e, 'request' => $request] ,function($message)
{
...
});
The part where I am having trouble is the following:
This method will automatically take care of pushing a job onto the queue to send the mail message in the background. Of course, you will need to configure your queues before using this feature.
I have read the documentation several times. I still can't figure out what precisely I should do to make the Mail::later work.
Related
I am trying to debug some bizarre behaviour of my PHP application. It is running Laravel 6 + AWS SQS. The program downloads call recordings from a VoIP provider's API using a job. The API has a heavy rate limit of 10req/minute, so I'm throttling the requests on my side. The job is configured to try to complete within 24 hours using retryUntil method. However, the job disappears from the queue after 4 tries. It doesn't fail. The job's failed method never gets executed (I've put logging and Sentry::capture in there). It's not on the failed_jobs table. The last log says "Cannot complete job, retrying in ... seconds", which is right before the release call. However, the job simply disappears from the queue and never gets executed again.
I am logging the number of attempts, max tries, timeoutAt, etc. Everything seems to be configured properly. Here's (the essence of) my code:
public function handle()
{
/** #var Track $track */
$track = Track::idOrUuId($this->trackId);
$this->logger->info('Downloading track', [
'trackId' => $track->getId(),
'attempt' => $this->attempts(),
'retryUntil' => $this->job->timeoutAt(),
'maxTries' => $this->job->maxTries(),
]);
$throttleKey = sprintf('track.download.%s', $track->getUser()->getTeamId());
if (!$this->rateLimiter->tooManyAttempts($throttleKey, self::MAX_ALLOWED_JOBS)) {
$this->downloadTrack($track);
$this->rateLimiter->hit($throttleKey, 60);
} else {
$delay = random_int(10, 100) + $this->rateLimiter->availableIn($throttleKey);
$this->logger->info('Throttling track download.', [
'trackId' => $track->getId(),
'delay' => $delay,
]);
$this->release($delay);
}
}
public function retryUntil(): DateTimeInterface
{
return now()->addHours(24);
}
public function failed(Exception $exception)
{
$this->logger->info('Job failed', ['exception' => $exception->getMessage()];
Sentry::captureException($exception);
}
I found the problem and I'm posting it here for anyone who might struggle in the future. It all came down to a simple configuration. In AWS SQS the queue I am working with has a configured DLQ (Dead-Letter Queue) and Maximum receives set to 4. According to the SQS docs
The Maximum receives value determines when a message will be sent to the DLQ. If the ReceiveCount for a message exceeds the maximum receive count for the queue, Amazon SQS moves the message to the associated DLQ (with its original message ID).
Since this is an infra configuration, it overwrites any Laravel parameters you might pass to the job. And because the message is simply removed from the queue, the processing job does not actually fail, so the failed method is not executed.
Ive been running into issues with Laravel Cashier when i deployed my app to heroku.
One my local environment everything is fine but on my staging server , no POST request body is ever sent to stripe.
I tried swapping api keys as i thought maybe the api version on stripe differs between the two but that doesn't work (see screenshots below)
Things i know are correct
API creds , they wont show up on stripe logs if it wasent
Composer version matches both environments (Laravel Cashier 10.5.2, Laravel 5.8.36, Stripe-php 17.7.0)
I cant seem to find anything that logs out going api requests. Ive even tried manually calling the stripe functions as low as i can get in the stack still no POST body.
Im sure some one else has ran into this. Google search on laravel cashier ALWAYS sends me back to the laravel website, like WTF.
this is my stripe method on my User model. All other code is from Cashier
public function activateSubscription() {
if ($this->hasStripeId() &&
$this->has_default_payment_method &&
$this->has_active_subscription) {
return;
}
try {
$this->newSubscription(env('STRIPE_SUBSCRIPTION_NAME'), env('STRIPE_PLAN_ID'))
->create(null, [
'name' => $this->fullname,
'email' => $this->email,
]);
$this->notify(new UserRegistered());
} catch (\Stripe\Exception\InvalidRequestException $e) {
Log::debug('Invalid Request', [
'body' => $e->getHttpBody(),
'headers' => $e->getHttpHeaders(),
'json' => $e->getJsonBody(),
'error_code' => $e->getStripeCode(),
]);
}
}
Edit
Ive removed some personal details from the POST request body
I figured it out , i had a \n at the end of my stripe secret api key on heroku environment variables.
For some reason that caused all requests to stripe to strip the POST body.
Removed that, ran a php artisan config:clear and it worked
I have 2 simple questions overall. Im currently looking into some event handling in Laravel and would like to use RabbitMQ as my event store. Therefor i installed this package to start with: https://github.com/php-enqueue/enqueue-dev
To get started i registered it and i am able to push messages on to RabbitMQ:
$job = (new Sendemail())->onQueue('email')->onConnection('interop');
dispatch($job);
The problem however is that Laravel pushes a certain format on the queue and i can't figure out how to change that. An example message would be:
{
"job":"Illuminate\\\\Queue\\\\CallQueuedHandler#call",
"data":{
"command":"O:29:\\"Acme\\Jobs\\FooJob\\":4:{s:11:\\"fooBar\\";s:7:\\"abc-123\\";s:5:\\"queue\\";N;s:5:\\"delay\\";N;s:6:\\"\\u0000*\\u0000job\\";N;}"
}
}
So the question is, how can i change this? The main reason on this is that the consumer side is not even a PHP application which also can not interpret the PHP serialized model. Therefor im looking for a way to push a plain JSON object instead.
From the other hand i would also like to understand how you could build a custom listener? For the listener the same thing happens. Laravel tries to read the method but when i push plain JSON this will never work. Isn't there a way to register a handler on a topic and do further handling of the payload of the message within the handler itself?
There is a simple way for your purpose:
First install this package for rabbit:
vladimir-yuldashev/laravel-queue-rabbitmq
and in controller:
Queue::connection('rabbitmq')->pushRaw('{you can generate a json format here}', 'queue_name');
you can generate a json and put in this command.
There's a laravel-queue library that works with the php-enqueue library you linked to make it compatible with Laravel's built in queue system that Florian mentioned.
By default, it will still use a serialized object, but I think that can be overridden. If you look in Queue.php, createObjectPayload() on line 130 in the core Laravel Framework, that's where the job is being serialized.
If you extend the Queue class in the laravel-queue library, you should be able to change createObjectPayload to look something like this:
protected function createObjectPayload($job, $queue)
{
$payload = $this->withCreatePayloadHooks($queue, [
'displayName' => $this->getDisplayName($job),
'job' => 'Illuminate\Queue\CallQueuedHandler#call',
'maxTries' => $job->tries ?? null,
'timeout' => $job->timeout ?? null,
'timeoutAt' => $this->getJobExpiration($job),
'data' => [
'commandName' => $job,
'command' => $job,
],
]);
return array_merge($payload, [
'data' => [
'commandName' => get_class($job),
'command' => json_encode(clone $job),
],
]);
}
That should JSON encode the job data instead of serializing it. You may even be able to remove the encoding altogether, as I think it's already JSON encoded somewhere up the chain.
Our emails are failing to send using Laravel with a Redis Queue.
The code that triggers the error is this: ->onQueue('emails')
$job = (new SendNewEmail($sender, $recipients))->onQueue('emails');
$job_result = $this->dispatch($job);
In combination with this in the job:
use InteractsWithQueue;
Our error message is:
Feb 09 17:15:57 laravel: message repeated 7947 times: [ production.ERROR: exception 'Swift_TransportException' with message 'Expected response code 354 but got code "550", with message "550 5.7.0 Requested action not taken: too many emails per second "' in /home/laravel/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php:383 Stack trace: #0 /home/laravel/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php(281):
Our error only happens using Sendgrid and not Mailtrap, which spoofs emailing sending. I've talked with Sendgrid and the emails never touched their servers and their service was fully active when my error occurred. So, the error appears to be on my end.
Any thoughts?
Seems like only Mailtrap sends this error, so either open another account or upgrade to a paid plan.
I finally figured out how to set up the entire Laravel app to throttle mail based on a config.
In the boot() function of AppServiceProvider,
$throttleRate = config('mail.throttleToMessagesPerMin');
if ($throttleRate) {
$throttlerPlugin = new \Swift_Plugins_ThrottlerPlugin($throttleRate, \Swift_Plugins_ThrottlerPlugin::MESSAGES_PER_MINUTE);
Mail::getSwiftMailer()->registerPlugin($throttlerPlugin);
}
In config/mail.php, add this line:
'throttleToMessagesPerMin' => env('MAIL_THROTTLE_TO_MESSAGES_PER_MIN', null), //https://mailtrap.io has a rate limit of 2 emails/sec per inbox, but consider being even more conservative.
In your .env files, add a line like:
MAIL_THROTTLE_TO_MESSAGES_PER_MIN=50
The only problem is that it doesn't seem to affect mail sent via the later() function if QUEUE_DRIVER=sync.
For debugging only!
If you don't expect more then 5 emails and don't have the option to change mailtrap, try:
foreach ($emails as $email) {
...
Mail::send(... $email);
if(env('MAIL_HOST', false) == 'smtp.mailtrap.io'){
sleep(1); //use usleep(500000) for half a second or less
}
}
Using sleep() is a really bad practice. In theory this code should only execute in test environment or in debug mode.
Maybe you should make sure it was really sent via Sendgrid and not mailtrap. Their hard rate limit seems currently to be 3k requests per second against 3 requests per second for mailtrap on free plan :)
I used sleep(5) to wait five seconds before using mailtrap again.
foreach ($this->suscriptores as $suscriptor) {
\Mail::to($suscriptor->email)
->send(new BoletinMail($suscriptor, $sermones, $entradas));
sleep(5);
}
I used the sleep(5) inside a foreach. The foreach traverses all emails stored in the database and the sleep(5) halts the loop for five seconds before continue with the next email.
I achieved this on Laravel v5.8 by setting the Authentication routes manually. The routes are located on the file routes\web.php. Below are the routes that needs to be added to that file:
Auth::routes();
Route::get('email/verify', 'Auth\VerificationController#show')->name('verification.notice');
Route::get('email/verify/{id}', 'Auth\VerificationController#verify')->name('verification.verify');
Route::group(['middleware' => 'throttle:1,1'], function(){
Route::get('email/resend', 'Auth\VerificationController#resend')->name('verification.resend');
});
Explanation:
Do not pass any parameter to the route Auth::routes(); to let configure the authentication routes manually.
Wrap the route email/resend inside a Route::group with the middleware throttle:1,1 (the two numbers represents the max retry attempts and the time in minutes for those max retries)
I also removed a line of code in the file app\Http\Controllers\Auth\VerificationController.php in the __construct function.
I removed this:
$this->middleware('throttle:6,1')->only('verify', 'resend');
You need to rate limit emails queue.
The "official" way is to setup Redis queue driver. But it's hard and time consuming.
So I wrote custom queue worker mxl/laravel-queue-rate-limit that uses Illuminate\Cache\RateLimiter to rate limit job execution (the same one that used internally by Laravel to rate limit HTTP requests).
In config/queue.php specify rate limit for emails queue (for example 2 emails per second):
'rateLimit' => [
'emails' => [
'allows' => 2,
'every' => 1
]
]
And run worker for this queue:
$ php artisan queue:work --queue emails
I had this problem when working with mail trap. I was sending 10 mails in one second and it was treated as spam. I had to make delay between each job. Take look at solution (its Laravel - I have system_jobs table and use queue=database)
Make a static function to check last job time, then add to it self::DELAY_IN_SECONDS - how many seconds you want to have between jobs:
public static function addSecondsToQueue() {
$job = SystemJobs::orderBy('available_at', 'desc')->first();
if($job) {
$now = Carbon::now()->timestamp;
$jobTimestamp = $job->available_at + self::DELAY_IN_SECONDS;
$result = $jobTimestamp - $now;
return $result;
} else {
return 0;
}
}
Then use it to make sending messages with delay (taking in to consideration last job in queue)
Mail::to($mail)->later(SystemJobs::addSecondsToQueue(), new SendMailable($params));
I'm trying to queue mail using Laravel-5. The code I am using is below. I was expecting the mail to get stored in the database in the 'jobs' table but it just gets sent instantly.
Mail::queue('emails.orderthankyou', ['first_name' => 'My Name'], function ($m) {
$m->to('me#myemail.com')->subject('Test');
});
Any idea what could be going wrong here?
You probably want to use Mail::later instead.
http://laravel.com/docs/5.1/mail
Mail::later(5, 'emails.orderthankyou', ['first_name' => 'My Name'], function ($m) {
$m->to('me#myemail.com')->subject('Test');
});
You might be using sync driver ( in config/queue.php ). sync stands for synchronous. Therefore, all jobs are executed instantly.
Delayed Message Queueing
If you wish to delay the delivery of a queued e-mail message, you may
use the later method. To get started, simply pass the number of
seconds by which you wish to delay the sending of the message as the
first argument to the method:
Mail::later(5, 'emails.welcome', $data, function ($message) {
//here 5 is the number of seconds
});