diff --git a/README.md b/README.md index 1c04350..7999fc9 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,13 @@ php artisan migrate You can publish the config file with: +```bash +php artisan vendor:publish --tag="filament-job-manager-config" ``` -php artisan vendor:publish --tag="filament-jobs-monitor-config" -``` - - This is the content of the published config file: -``` +```php setProgress($count); + $count = $count + $steps; + sleep(10); + } + } +} +``` -## Authorization - outdated! +## Authorization - outdated! Outdated. Use Shield instead. diff --git a/composer.json b/composer.json index ef96986..9878c6e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "adrolli/filament-job-manager", - "description": "A Filament manager for job queues including failed jobs and batches.", + "description": "A Filament panel for managing job queues including failed jobs and batches.", "keywords": [ "laravel", "filament", @@ -42,7 +42,7 @@ "Adrolli\\FilamentJobManager\\JobMonitorProvider" ], "aliases": { - "JobMonitor": "Adrolli\\FilamentJobManager\\QueueMonitorProvider\\Facade" + "JobMonitor": "Adrolli\\FilamentJobManager\\JobMonitorProvider\\Facade" } } }, diff --git a/database/migrations/create_filament-job-manager_table.php.stub b/database/migrations/create_filament-job-manager_table.php.stub new file mode 100644 index 0000000..c9bb2bd --- /dev/null +++ b/database/migrations/create_filament-job-manager_table.php.stub @@ -0,0 +1,36 @@ +id(); + $table->string('job_id')->index(); + $table->string('name')->nullable(); + $table->string('queue')->nullable(); + $table->timestamp('started_at')->nullable()->index(); + $table->timestamp('finished_at')->nullable(); + $table->boolean('failed')->default(false)->index(); + $table->integer('attempt')->default(0); + $table->integer('progress')->nullable(); + $table->text('exception_message')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_manager'); + } +}; \ No newline at end of file diff --git a/src/JobManagerProvider.php b/src/JobManagerProvider.php new file mode 100644 index 0000000..9faf443 --- /dev/null +++ b/src/JobManagerProvider.php @@ -0,0 +1,107 @@ +job); + }); + + Queue::after(static function (JobProcessed $event) { + self::jobFinished($event->job); + }); + + Queue::failing(static function (JobFailed $event) { + self::jobFinished($event->job, true, $event->exception); + }); + + Queue::exceptionOccurred(static function (JobExceptionOccurred $event) { + self::jobFinished($event->job, true, $event->exception); + }); + } + + /** + * Get Job ID. + */ + public static function getJobId(JobContract $job): string|int + { + return JobManager::getJobId($job); + } + + /** + * Start Queue Monitoring for Job. + */ + protected static function jobStarted(JobContract $job): void + { + $now = now(); + $jobId = self::getJobId($job); + + $monitor = JobManager::query()->create([ + 'job_id' => $jobId, + 'name' => $job->resolveName(), + 'queue' => $job->getQueue(), + 'started_at' => $now, + 'attempt' => $job->attempts(), + 'progress' => 0, + ]); + + JobManager::query() + ->where('id', '!=', $monitor->id) + ->where('job_id', $jobId) + ->where('failed', false) + ->whereNull('finished_at') + ->each(function (JobManager $monitor) { + $monitor->finished_at = now(); + $monitor->failed = true; + $monitor->save(); + }); + } + + /** + * Finish Queue Monitoring for Job. + */ + protected static function jobFinished(JobContract $job, bool $failed = false, ?\Throwable $exception = null): void + { + $monitor = JobManager::query() + ->where('job_id', self::getJobId($job)) + ->where('attempt', $job->attempts()) + ->orderByDesc('started_at') + ->first(); + + if (null === $monitor) { + return; + } + + $attributes = [ + 'progress' => 100, + 'finished_at' => now(), + 'failed' => $failed, + ]; + + if (null !== $exception) { + $attributes += [ + 'exception_message' => mb_strcut($exception->getMessage(), 0, 65535), + ]; + } + + $monitor->update($attributes); + } +} \ No newline at end of file diff --git a/src/Models/Job.php b/src/Models/Job.php deleted file mode 100644 index c5ed9d5..0000000 --- a/src/Models/Job.php +++ /dev/null @@ -1,9 +0,0 @@ - 'bool', + 'started_at' => 'datetime', + 'finished_at' => 'datetime', + ]; + + /* + *-------------------------------------------------------------------------- + * Mutators + *-------------------------------------------------------------------------- + */ + public function status(): Attribute + { + return Attribute::make( + get: function () { + if ($this->isFinished()) { + return $this->failed ? 'failed' : 'succeeded'; + } + + return 'running'; + }, + ); + } + + /* + *-------------------------------------------------------------------------- + * Methods + *-------------------------------------------------------------------------- + */ + + public static function getJobId(JobContract $job): string|int + { + if ($jobId = $job->getJobId()) { + return $jobId; + } + + return Hash::make($job->getRawBody()); + } + + /** + * check if the job is finished. + */ + public function isFinished(): bool + { + if ($this->hasFailed()) { + return true; + } + + return null !== $this->finished_at; + } + + /** + * Check if the job has failed. + */ + public function hasFailed(): bool + { + return $this->failed; + } + + /** + * check if the job has succeeded. + */ + public function hasSucceeded(): bool + { + if (! $this->isFinished()) { + return false; + } + + return ! $this->hasFailed(); + } + + /** + * Get the prunable model query. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function prunable() + { + if (config('filament-job-manager.pruning.activate')) { + return static::where('created_at', '<=', now()->subDays(config('filament-job-manager.pruning.retention_days'))); + } + + return false; + } +} \ No newline at end of file diff --git a/src/Resources/JobsResource.php b/src/Resources/JobsResource.php index baf76e8..1869492 100644 --- a/src/Resources/JobsResource.php +++ b/src/Resources/JobsResource.php @@ -3,7 +3,7 @@ namespace Adrolli\FilamentJobManager\Resources; use Adrolli\FilamentJobManager\FilamentJobsPlugin; -use Adrolli\FilamentJobManager\Models\Job; +use Adrolli\FilamentJobManager\Models\JobManager; use Adrolli\FilamentJobManager\Resources\JobsResource\Pages\ListJobs; use Adrolli\FilamentJobManager\Resources\JobsResource\Widgets\JobStatsOverview; use Filament\Forms\Components\DateTimePicker; @@ -19,7 +19,7 @@ class JobsResource extends Resource { - protected static ?string $model = Job::class; + protected static ?string $model = JobManager::class; public static function getNavigationBadge(): ?string { @@ -94,27 +94,27 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('status') ->badge() - ->label(__('filament-jobs-monitor::translations.status')) + ->label(__('filament-job-manager::translations.status')) ->sortable() - ->formatStateUsing(fn (string $state): string => __("filament-jobs-monitor::translations.{$state}")) + ->formatStateUsing(fn (string $state): string => __("filament-job-manager::translations.{$state}")) ->color(fn (string $state): string => match ($state) { 'running' => 'primary', 'succeeded' => 'success', 'failed' => 'danger', }), TextColumn::make('name') - ->label(__('filament-jobs-monitor::translations.name')) + ->label(__('filament-job-manager::translations.name')) ->sortable(), TextColumn::make('queue') - ->label(__('filament-jobs-monitor::translations.queue')) + ->label(__('filament-job-manager::translations.queue')) ->sortable(), TextColumn::make('progress') - ->label(__('filament-jobs-monitor::translations.progress')) + ->label(__('filament-job-manager::translations.progress')) ->formatStateUsing(fn (string $state) => "{$state}%") ->sortable(), - // ProgressColumn::make('progress')->label(__('filament-jobs-monitor::translations.progress'))->color('warning'), + // ProgressColumn::make('progress')->label(__('filament-job-manager::translations.progress'))->color('warning'), TextColumn::make('started_at') - ->label(__('filament-jobs-monitor::translations.started_at')) + ->label(__('filament-job-manager::translations.started_at')) ->since() ->sortable(), ]) diff --git a/src/Resources/JobsResource/Pages/ListJobs.php b/src/Resources/JobsResource/Pages/ListJobs.php index b3bbe9c..1c85ba4 100644 --- a/src/Resources/JobsResource/Pages/ListJobs.php +++ b/src/Resources/JobsResource/Pages/ListJobs.php @@ -24,6 +24,6 @@ public function getHeaderWidgets(): array public function getTitle(): string { - return __('filament-jobs-monitor::translations.title'); + return __('filament-job-manager::translations.title'); } } diff --git a/src/Resources/JobsResource/Widgets/JobStatsOverview.php b/src/Resources/JobsResource/Widgets/JobStatsOverview.php index 82b5fb2..e039857 100644 --- a/src/Resources/JobsResource/Widgets/JobStatsOverview.php +++ b/src/Resources/JobsResource/Widgets/JobStatsOverview.php @@ -22,9 +22,9 @@ protected function getCards(): array ->first(); return [ - Stat::make(__('filament-jobs-monitor::translations.total_jobs'), $aggregatedInfo->count ?? 0), - Stat::make(__('filament-jobs-monitor::translations.execution_time'), ($aggregatedInfo->total_time_elapsed ?? 0).'s'), - Stat::make(__('filament-jobs-monitor::translations.average_time'), ceil((float) $aggregatedInfo->average_time_elapsed).'s' ?? 0), + Stat::make(__('filament-job-manager::translations.total_jobs'), $aggregatedInfo->count ?? 0), + Stat::make(__('filament-job-manager::translations.execution_time'), ($aggregatedInfo->total_time_elapsed ?? 0).'s'), + Stat::make(__('filament-job-manager::translations.average_time'), ceil((float) $aggregatedInfo->average_time_elapsed).'s' ?? 0), ]; } }