Search code examples
laravellaravel-queuelaravel-schedulerlaravel-jobs

Laravel: Check if new items added to db table and schedule a job to email user


I want to trigger an email when new rows are added to a table in my Laravel application. However I want to add a buffer of sorts, so if 5 rows are added in quick succession then only 1 email is sent.

The method I've chosen is to schedule a check every 15 minutes and see if there are new rows added. If there are then I will queue an email.

Currently I'm getting an error on the schedule. I'll run through my code below:

In Kernel.php where we setup schedules I have:

        $schedule->job(new ProcessActivity)
        ->everyFifteenMinutes()
        ->when(function () {
            return \App\JobItem::whereBetween('created_at', array(Carbon::now()->subMinutes(15), Carbon::now()))->exists();
        })
        ->onSuccess(function () {
            Log::debug(
                'Success'
            );
        })
        ->onFailure(function () {
            Log::debug(
                'Fail'
            );
        });

Which I use to trigger the Job found in: App\Jobs\ProcessActivity.php :

 public function __construct()
{

    $this->jobs = \App\JobItem::whereBetween('created_at', array(Carbon::now()->subMinutes(15), Carbon::now()))->get();
}

/**
 * Execute the job.
 *
 * @return void
 */
public function handle()
{


    Log::debug('Activity Job Run',  ['jobs' => $this->jobs]);

    $this->jobs->each(function ($item, $key) {
        Log::debug('loop');

        // get project
        $project = $item->project;

        // get project email
        $user_id = $project->user_id;
        $email = \App\User::find($user_id)->email;

        // get project UUID
        $projectUuid = $project->public_id;

        // emails
        $subscriberEmails = \App\ProjectSubscription::where('project_id', $project->id)->get();

        // create activity email
        Notification::route('mail', $subscriberEmails)->notify(new Activity($project, $projectUuid));
    });

    return true;
}

I've posted my full code above which also shows a relationship between my JobItems and Project models. I won't elaborate on that as I've commented in the code.

The problem

When I add a new row to my JobItem table I can see the job is scheduled and processed (using Laravel Telescope to inspect this).

However, I can also see in my log that for each job I get two log messages:

First: 'Fail' and then 'Activity Job Run'

My email is not sent and I'm uncertain how to determine why this is failing.

So it seems that onFailure is being triggered and there is a problem with my ProcessActivity.

Any clues on where I am going wrong and how to determine the error would be much appreciated.


Solution

  • I have a fix, but first, here are some things I learnt that hampered my progress:

    I was using this artisan command to process my scheduled jobs:

    php artisan queue:work
    

    The problem with developing while using that command is that if there are code changes then those changes are not recognised.

    So you can either Command+C to return to the console and use this every time there is a code change:

    php artisan queue:restart
    php artisan queue:work
    

    Or you can just use this and it will allow code changes:

    php artisan queue:listen
    

    As you can imagine without knowing this you will have a slow debugging process!

    As a result of this and adding an exception to my Job I made some progress. I'll paste in the code below to compare against the original code:

        public function __construct()
    {
    }
    
    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        try {
    
            $jobs = \App\JobItem::whereBetween('created_at', array(Carbon::now()->subMinutes(20), Carbon::now()))->get();
    
            Log::debug('Activity Job',  ['jobs' => $jobs]);
    
            // collection start
            $collection = collect();
    
            // loop jobs to get emails
            foreach ($jobs as $key => $value) {
    
                // get project UUID
                $project = $value->project;
                $projectUuid = $project->public_id;
    
                // get emails subscribed to projects
                $subscriberEmails = \App\ProjectSubscription::where('project_id', $project->id)->get();
    
                // merge into a single collection via the loop
                if ($key != 0) {
                    $merge = $collection->merge($subscriberEmails);
                    $collection = collect($merge);
                } else {
                    $collection = $subscriberEmails;
                }
    
                // Log::debug('emails_project in loop', ['emails' => $subscriberEmails]);
            };
    
            // clean object with uniques only
            $subscriberEmailsCleaned = $collection->unique();
    
            // debug
            Log::debug('Project Emails to Notify', ['emails' => $subscriberEmailsCleaned]);
    
            // create activity email
            Notification::route('mail', $subscriberEmailsCleaned)->notify(new Activity($project, $projectUuid));
        } catch (\Exception $e) {
            \Log::info($e->getMessage());
        }
    }
    

    First thing to note, is that as __construct() is run initially and is serialised. Then the handle method is called when the job is processed. So I had to move my eloquent query into the handle method.

    I also used a foreach rather than .each to loop through and create a new collection of emails. Perhaps there is a more elegant way, but I needed to create a collection of emails and this way allowed me to move the variables in the loop outside to be used in the method.

    You can see me merge these at the bottom of the loop.

    I have also added a few Log:: items which is useful for debugging.

    Not fixed 100% With this code I can now auto schedule an email every x minutes when new items are added. However, I am still getting the log Fail from the onFailure()from my Kernal.php file:

            ->onFailure(function () {
            Log::debug(
                'Fail'
            );
    

    I am still confused as to what that indicates and how I can determine more information about how this has failed and what that means. However, it does work so I will cautiously move forward (with one eye open on the comments, in case someone has an idea that can help!)