146 lines
3.9 KiB
PHP
146 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\OpsUx;
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Support\OperationCatalog;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
final class RunDurationInsights
|
|
{
|
|
public static function elapsedSeconds(OperationRun $run): ?int
|
|
{
|
|
$start = $run->started_at ?? $run->created_at;
|
|
|
|
if (! $start) {
|
|
return null;
|
|
}
|
|
|
|
$end = $run->completed_at ?? now();
|
|
|
|
$seconds = $end->diffInSeconds($start);
|
|
|
|
if (is_int($seconds)) {
|
|
return $seconds;
|
|
}
|
|
|
|
return (int) round((float) $seconds);
|
|
}
|
|
|
|
public static function elapsedHuman(OperationRun $run): string
|
|
{
|
|
$start = $run->started_at ?? $run->created_at;
|
|
|
|
if (! $start) {
|
|
return '—';
|
|
}
|
|
|
|
$end = $run->completed_at ?? now();
|
|
|
|
return $end->diffForHumans($start, true);
|
|
}
|
|
|
|
public static function expectedSeconds(OperationRun $run): ?int
|
|
{
|
|
$catalog = OperationCatalog::expectedDurationSeconds((string) $run->type);
|
|
|
|
if (is_int($catalog)) {
|
|
return $catalog;
|
|
}
|
|
|
|
$tenantId = (int) ($run->tenant_id ?? 0);
|
|
$type = (string) ($run->type ?? '');
|
|
|
|
if ($tenantId <= 0 || $type === '') {
|
|
return null;
|
|
}
|
|
|
|
$cacheKey = "opsux:expected-duration:tenant:{$tenantId}:type:".$type;
|
|
|
|
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($tenantId, $type): ?int {
|
|
$durations = OperationRun::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('type', $type)
|
|
->whereNotNull('started_at')
|
|
->whereNotNull('completed_at')
|
|
->where('created_at', '>=', now()->subDays(30))
|
|
->latest('id')
|
|
->limit(200)
|
|
->get(['started_at', 'completed_at'])
|
|
->map(function (OperationRun $run): ?int {
|
|
if (! $run->started_at || ! $run->completed_at) {
|
|
return null;
|
|
}
|
|
|
|
$seconds = $run->completed_at->diffInSeconds($run->started_at);
|
|
|
|
if (is_int($seconds)) {
|
|
return $seconds;
|
|
}
|
|
|
|
return (int) round((float) $seconds);
|
|
})
|
|
->filter(fn (?int $seconds): bool => is_int($seconds) && $seconds > 0)
|
|
->sort()
|
|
->values();
|
|
|
|
if ($durations->isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
$count = $durations->count();
|
|
$middle = intdiv($count - 1, 2);
|
|
|
|
$median = (int) $durations[$middle];
|
|
|
|
return $median > 0 ? $median : null;
|
|
});
|
|
}
|
|
|
|
public static function expectedHuman(OperationRun $run): ?string
|
|
{
|
|
$seconds = self::expectedSeconds($run);
|
|
|
|
if (! is_int($seconds) || $seconds <= 0) {
|
|
return null;
|
|
}
|
|
|
|
if ($seconds < 60) {
|
|
return 'Typically under 1 minute';
|
|
}
|
|
|
|
$minutes = (int) round($seconds / 60);
|
|
|
|
return "Typically ~{$minutes} min";
|
|
}
|
|
|
|
public static function stuckGuidance(OperationRun $run): ?string
|
|
{
|
|
$uxStatus = OperationStatusNormalizer::toUxStatus($run->status, $run->outcome);
|
|
|
|
if (! in_array($uxStatus, ['queued', 'running'], true)) {
|
|
return null;
|
|
}
|
|
|
|
$elapsed = self::elapsedSeconds($run);
|
|
|
|
if (! is_int($elapsed) || $elapsed <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$expected = self::expectedSeconds($run);
|
|
|
|
$isLikelyStuck = is_int($expected)
|
|
? ($elapsed > max(600, $expected * 2))
|
|
: ($elapsed > 900);
|
|
|
|
if (! $isLikelyStuck) {
|
|
return null;
|
|
}
|
|
|
|
return 'Taking longer than expected. If this does not complete soon, verify the queue worker is running and check logs for errors.';
|
|
}
|
|
}
|