DEV Community

Adam Mateusz Brożyński
Adam Mateusz Brożyński

Posted on

Cronless queue:work in Laravel executed in background

There are some approaches how to execute queue:work but I found them useless. Here is a solution for most shared hostings that:

  • does not require additional route
  • does not require remotely visiting website
  • does not require shell access
  • does not require cron access
  • requires bash with flock on server (for single execution protection)
  • requires PHP exec function available
  • requires shell php command (php-cli installed on server)
  • runs in the background so makes no website slowdowns
  • can be set to execute not more than once between minimum time span of $runEverySec seconds

1. Let's create new Middleware:

$ php artisan make:middleware QueueWorkMiddleware
Enter fullscreen mode Exit fullscreen mode

2. Use it as global middleware in bootstrap/app.php:

...
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(App\Http\Middleware\QueueWorkMiddleware::class);
    })
...
Enter fullscreen mode Exit fullscreen mode

3. Put contents to App/Middleware/QueueWorkMiddleware.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;


class QueueWorkMiddleware
{

    // lock file that prevents too many executions
    public $lockFile = 'queue.lock';
    // log from queue command
    public $logFile = 'queue.log';
    // log from background exec command
    public $execLogFile = 'exec.log';
    // pid of executed command
    public $pidFile = 'queue.pid';
    // php command path
    public $phpExec = '/usr/bin/php';
    // queue:work command
    public $queueCmd = 'artisan queue:work --stop-when-empty';
    // minimum time in seconds between executions
    public $runEverySec = 10;

    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        // get pid and lock file names
        $pidFile = base_path()."/".$this->pidFile;
        $lockFile = base_path()."/".$this->lockFile;

        if( 

            // if there is no lock file and
            !file_exists($lockFile) && 

            // there is no pid file (queue was never executed before)
            // or time between pidfile modification is more or equal $runEverySec
            (!file_exists($pidFile) || (time()-filemtime(base_path()."/{$this->pidFile}") >= $this->runEverySec))
          ) 
          { 
            // do the work
            $this->work(); 
          }

        return $next($request);
    }


    public function work() {     
        // file names
        $basePath = base_path();
        $lockFile = "{$basePath}/{$this->lockFile}";
        $logFile = "{$basePath}/{$this->logFile}";
        $execLogFile = "{$basePath}/{$this->execLogFile}";
        $pidFile = base_path()."/".$this->pidFile;

        // main queue command and lock file removal
        $cmd = "{ {$this->phpExec} {$this->queueCmd} > {$logFile} 2>&1; rm {$lockFile}; }";

        // go to base path and run command by flock (this guarantees single execution only!)
        $cmd = "cd {$basePath} && flock -n {$lockFile} --command '{$cmd}'";

        // execute command in background
        exec(sprintf("%s > {$execLogFile} 2>&1 & echo $! >> %s", $cmd, $pidFile));

        return true;
    }

}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)