140 Followers
·
Follow

Organize Realtime Job Management With Laravel and Vue Step by Step[Part 2]

<- Previous part

In previous part we created model for saving job execution progress. It should be applicable for any jobs. Now we needs scaffolding for create forms for run concrete jobs with its own parameters. Of course, we may create a table and eloquent model for each job, and in some cases it have a reason, but in this case we make create simple array with job definitions

Add in config/jobs.php some definitions

'definitions' => [
JobDefinition::create(DummyJob::class, 'jobs.forms.dummy')
->setTitle('Simple example job')
->setRules([
'delay' => 'integer|min:1|max:5',
'loop' => 'integer|min:1|max:5',
]),
JobDefinition::create(UrlChecker::class, 'jobs.forms.url')
->setTitle('Simple url checker')
->setRules([
'url' => 'url',
]),
JobDefinition::create(ContentChecker::class, 'jobs.forms.url')
->setTitle('Simple url content checker')
->setRules([
'url' => 'url',
]),
],

Prepare layout and views.

Create directory “jobs” in resources/views and add next views

As we keep in mind that exists different ways for store job definitions, we should create contract JobDefinitionRepository with a few methods

interface JobDefinitionRepository
{
/**
* @return Collection|\App\Lib\InteractiveJobs\JobDefinition[]
*/
public function jobDefinitions(): Collection;

public function findByName($jobName): ?JobDefinition;

public function exists($jobName): bool;
}

That is implementation, based on config values

Update InteractiveJobProvider

<?php
namespace App\Lib\InteractiveJobs;

use App\Lib\InteractiveJobs\Contracts\JobDefinitionRepository;
use Illuminate\Support\ServiceProvider;

class InteractiveJobsProvider extends ServiceProvider
{
public function boot()
{
/*We don't need subscription to queue events already, as
* we make own base class for interactive jobs
*/

}

public function provides()
{
return [JobDefinitionRepository::class];
}

public function register()
{
$this->app->singleton(
JobDefinitionRepository::class,
JobDefinitionConfigRepository::class
);
}
}

Lets go to update the JobController, (and routes)

Also we need to create ViewComposer for share definitions in jobs._menu

So now we can run http://your.app/jobs and check job forms from menu, but it didn’t works!
We needs observer for our Job Model where we can put the job command in a queue after the model has been created. Also we should update the DummyJob and make it inherited from the InteractiveJob class from previous part

<?php

namespace App\Lib\InteractiveJobs;

use App\Lib\InteractiveJobs\Models\Job;
use function dispatch;

class JobModelObserver
{
public function creating(Job $job)
{
$job
->fillDefaults();
}

public function created(Job $job)
{
try{
$command = app()->make(
$job->command, ['jobModel'=>$job]
);
dispatch($command)->onQueue($job->queue)->delay(2);
}catch (\Throwable $e){
$job
->delete();
}
}
}

register it in boot method of InteractiveJobsProvider `Job::observe(JobModelObserver::class);`

class DummyJob extends InteractiveJob
{

public function execute(DummyService $service)
{
$payload = $this->jobModel->payload;
$service->dummyJobLogic($payload['loop'], $payload['delay']);
}
}

Fix JobDbLogHandler class , because now we have put the Job Model as context, instead of array

//...
protected function write(array $record)
{
$jobContext = Config::get('jobs.context');
if($jobContext instanceof Job){
DB::table('job_logs')->insert([
'loggable_id'=>$jobContext->id,
'loggable_type'=>$jobContext->commandName(),
...

Oooh, seems it should be works now; Run it!

As you see, it works, but Job state is not updated. Put following methods in the model Job

public function updateProgress(int $value)
{
$this->progress = max($value, 100);
$this->save();
}

public function activate()
{
$this->state = JobState::PROCESSING;
$this->save();
}

public function retryAfterFail()
{
$this->state = JobState::RETRY;
$this->attempts += 1;
$this->save();
}

public function finish(bool $success = true)
{
$this->state = $success ? JobState::SUCCESS : JobState::FAIL;
$this->finished_at = Carbon::now();
$this->save();
}

And apply its in InteractiveJob class

protected function beforeStart()
{
$this->jobModel->activate();
}

protected function onFinish()
{
$this->jobModel->finish(true);
}

protected function onFail(\Throwable $e)
{
if ($this instanceof Reportable) {
$this->jobModel->report = collect(['error' => $e->getMessage()]);
}
if ($this->isWillBeRetry()) {
$this->jobModel->retryAfterFail();
} else {
$this->jobModel->finish(false);
}
$this->fail($e);
}

protected function isWillBeRetry(): bool
{
return is_null($this->job->maxTries())
|| $this->attempts() < $this->job->maxTries();
}

protected function isRetried(): bool
{
return $this->jobModel->attempts > 1;
}

You might have noticed that the code mentions the Reportable interface — it is just empty interface used as a flag that means implemented job generate report collection and it must be saved

Try to run a new job from site now, and ensure that the Job Model has changed state after job execution. You can play with DummyJob execution and touch updateProgress method of the Job Model

Well, it seems that now there is only lack of interactivity

For this purpose we need to install several nodejs packages

yarn add laravel-echo laravel-echo-server vue-echo vue-toasted

If you are not already familiar with the laravel-echo-server, look at github https://github.com/tlaverdure/laravel-echo-server

run laravel-echo-server init && laravel-echo-server client:add

next modify the newly generated laravel-echo-server.json and set actual values for “authHost” and “databaseConfig.redis”

I made three kinds of job notifications for broadcasting — about progress changing, state changing and simple message, and yet one for user notification

Now we will apply its in the Job model class.

Firstly, add Notifable trait

use Notifable and declare

public function receivesBroadcastNotificationsOn()
{
return 'Job.' . $this->id;
}

replace method updateProgress

public function updateProgress(int $value, $message = null)
{
if ($message) {
$this->notifyNow(new LogMessage($message));
}
$this->progress = max($value, 100);
$this->save();
$this->notifyNow(new Progress($value));
}

and add new one

protected function stateChanged()
{
$this->notifyNow(new StateMessage($this->state));
$this->owner->notifyNow(
$this->jobState()->notice($this->identity())
);
}

Add $this->stateChanged(); at the end of methods ‘activate’, ‘finish’, ’retryAfterFail’

I add notice method in the JobState class for generating state-based notices for user

//App/InteractiveJobs/JobState....protected static $notices
= [
self::FAIL => [Notificator::TYPE_ERROR, 'Job Execution {job} failed'],
self::PROCESSING => [Notificator::TYPE_INFO, 'Job Execution {job} started'],
self::SUCCESS => [Notificator::TYPE_SUCCESS, 'Job Execution {job} finished'],
self::RETRY => [Notificator::TYPE_WARNING, 'Job Execution {job} failed, job will be restarted'],
];
public function notice($job = ''): Notification
{
[$type, $message] = self::$notices[$this->value];
$message = strtr($message, ['job'=>$job]);
return new UserMessage($type, $message);
}

Finally we uncomment string ‘Illuminate\Broadcasting\BroadcastServiceProvider::class‘ in the providers array of your config/app.php configuration file.

add to routes/channels.php

use App\User;

Broadcast::channel('User.{id}', function ($user, $id) {
return (int)$user->id === (int)$id;
});
Broadcast::channel('Job.{id}', function (User $user, $id) {
return $user->hasJob($id);
});

and add in the User model method for define broadcasting route

public function receivesBroadcastNotificationsOn()
{
return 'User.' . $this->id;
}

Continuation in the next part ->

Written by

#php,#yii,#laravel,#javascript,#python,#linux,#archlinux,#vue

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store