optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Jobs\RefreshCollectionHealthJob;
use App\Jobs\RefreshCollectionQualityJob;
use App\Jobs\RefreshCollectionRecommendationJob;
use App\Jobs\ScanCollectionDuplicateCandidatesJob;
use App\Models\Collection;
use App\Models\User;
use Illuminate\Support\Collection as SupportCollection;
class CollectionBackgroundJobService
{
public function dispatchQualityRefresh(Collection $collection, ?User $actor = null): array
{
RefreshCollectionQualityJob::dispatch((int) $collection->id, $actor?->id)->afterCommit();
return [
'status' => 'queued',
'job' => 'quality_refresh',
'scope' => 'single',
'count' => 1,
'collection_ids' => [(int) $collection->id],
'message' => 'Quality refresh queued.',
];
}
public function dispatchHealthRefresh(?Collection $collection = null, ?User $actor = null): array
{
$targets = $collection ? collect([$collection]) : $this->healthTargets();
$targets->each(fn (Collection $item) => RefreshCollectionHealthJob::dispatch((int) $item->id, $actor?->id, 'programming-eligibility')->afterCommit());
return $this->queuedPayload('health_refresh', $targets, 'Health and eligibility refresh queued.');
}
public function dispatchRecommendationRefresh(?Collection $collection = null, ?User $actor = null, string $context = 'default'): array
{
$targets = $collection ? collect([$collection]) : $this->recommendationTargets();
$targets->each(fn (Collection $item) => RefreshCollectionRecommendationJob::dispatch((int) $item->id, $actor?->id, $context)->afterCommit());
return $this->queuedPayload('recommendation_refresh', $targets, 'Recommendation refresh queued.');
}
public function dispatchDuplicateScan(?Collection $collection = null, ?User $actor = null): array
{
$targets = $collection ? collect([$collection]) : $this->duplicateTargets();
$targets->each(fn (Collection $item) => ScanCollectionDuplicateCandidatesJob::dispatch((int) $item->id, $actor?->id)->afterCommit());
return $this->queuedPayload('duplicate_scan', $targets, 'Duplicate scan queued.');
}
public function dispatchScheduledMaintenance(bool $health = true, bool $recommendations = true, bool $duplicates = true): array
{
$summary = [];
if ($health) {
$summary['health'] = $this->dispatchHealthRefresh();
}
if ($recommendations) {
$summary['recommendations'] = $this->dispatchRecommendationRefresh();
}
if ($duplicates) {
$summary['duplicates'] = $this->dispatchDuplicateScan();
}
return $summary;
}
/** @return SupportCollection<int, Collection> */
private function healthTargets(): SupportCollection
{
$cutoff = now()->subHours(max(1, (int) config('collections.v5.queue.health_stale_after_hours', 24)));
return Collection::query()
->where(function ($query) use ($cutoff): void {
$query->whereNull('last_health_check_at')
->orWhere('last_health_check_at', '<=', $cutoff)
->orWhereColumn('updated_at', '>', 'last_health_check_at');
})
->orderBy('last_health_check_at')
->orderByDesc('updated_at')
->limit(max(1, (int) config('collections.v5.queue.health_batch_size', 40)))
->get(['id']);
}
/** @return SupportCollection<int, Collection> */
private function recommendationTargets(): SupportCollection
{
$cutoff = now()->subHours(max(1, (int) config('collections.v5.queue.recommendation_stale_after_hours', 12)));
return Collection::query()
->where('placement_eligibility', true)
->where(function ($query) use ($cutoff): void {
$query->whereNull('last_recommendation_refresh_at')
->orWhere('last_recommendation_refresh_at', '<=', $cutoff)
->orWhereColumn('updated_at', '>', 'last_recommendation_refresh_at');
})
->orderBy('last_recommendation_refresh_at')
->orderByDesc('ranking_score')
->limit(max(1, (int) config('collections.v5.queue.recommendation_batch_size', 40)))
->get(['id']);
}
/** @return SupportCollection<int, Collection> */
private function duplicateTargets(): SupportCollection
{
$cutoff = now()->subHours(max(1, (int) config('collections.v5.queue.duplicate_stale_after_hours', 24)));
return Collection::query()
->whereNull('canonical_collection_id')
->where(function ($query) use ($cutoff): void {
$query->where('updated_at', '>=', $cutoff)
->orWhereDoesntHave('mergeActionsAsSource', function ($mergeQuery) use ($cutoff): void {
$mergeQuery->where('action_type', 'suggested')
->where('updated_at', '>=', $cutoff);
});
})
->orderByDesc('updated_at')
->limit(max(1, (int) config('collections.v5.queue.duplicate_batch_size', 30)))
->get(['id']);
}
/**
* @param SupportCollection<int, Collection> $targets
*/
private function queuedPayload(string $job, SupportCollection $targets, string $message): array
{
$ids = $targets->pluck('id')->map(static fn ($id): int => (int) $id)->values()->all();
return [
'status' => 'queued',
'job' => $job,
'scope' => count($ids) === 1 ? 'single' : 'batch',
'count' => count($ids),
'collection_ids' => $ids,
'items' => [],
'message' => $ids === [] ? 'No collections needed this refresh.' : $message,
];
}
}