TenantAtlas/app/Support/SystemConsole/StuckRunClassifier.php
ahmido 0cf612826f feat(114): system console control tower (merged) (#139)
Feature branch PR for Spec 114.

This branch contains the merged agent session work (see merge commit on branch).

Tests
- `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #139
2026-02-28 00:15:31 +00:00

87 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\SystemConsole;
use App\Models\OperationRun;
use App\Support\OperationRunStatus;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
final class StuckRunClassifier
{
public function __construct(
private readonly int $queuedThresholdMinutes = 0,
private readonly int $runningThresholdMinutes = 0,
) {}
public function queuedThresholdMinutes(): int
{
if ($this->queuedThresholdMinutes > 0) {
return $this->queuedThresholdMinutes;
}
return max(1, (int) config('tenantpilot.system_console.stuck_thresholds.queued_minutes', 15));
}
public function runningThresholdMinutes(): int
{
if ($this->runningThresholdMinutes > 0) {
return $this->runningThresholdMinutes;
}
return max(1, (int) config('tenantpilot.system_console.stuck_thresholds.running_minutes', 30));
}
public function apply(Builder $query, ?CarbonImmutable $now = null): Builder
{
$now ??= CarbonImmutable::now();
$queuedCutoff = $now->subMinutes($this->queuedThresholdMinutes());
$runningCutoff = $now->subMinutes($this->runningThresholdMinutes());
return $query->where(function (Builder $stuckQuery) use ($queuedCutoff, $runningCutoff): void {
$stuckQuery
->where(function (Builder $queuedQuery) use ($queuedCutoff): void {
$queuedQuery
->where('status', OperationRunStatus::Queued->value)
->whereNull('started_at')
->where('created_at', '<=', $queuedCutoff);
})
->orWhere(function (Builder $runningQuery) use ($runningCutoff): void {
$runningQuery
->where('status', OperationRunStatus::Running->value)
->where('started_at', '<=', $runningCutoff);
});
});
}
public function classify(OperationRun $run, ?CarbonImmutable $now = null): ?string
{
$now ??= CarbonImmutable::now();
if ($run->status === OperationRunStatus::Queued->value) {
if ($run->started_at !== null) {
return null;
}
if ($run->created_at === null) {
return null;
}
return $run->created_at->lte($now->subMinutes($this->queuedThresholdMinutes()))
? OperationRunStatus::Queued->value
: null;
}
if ($run->status === OperationRunStatus::Running->value && $run->started_at !== null) {
return $run->started_at->lte($now->subMinutes($this->runningThresholdMinutes()))
? OperationRunStatus::Running->value
: null;
}
return null;
}
}