mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Every released Sentinel with traffic analytics (1.0.0 and later) has the dashboard and series routes and the requests, bytes_in and app fields, so the fallbacks for builds without them are not needed. - Keep "no data" handling for Sentinel without traffic routes (0.0.x) - Keep the resource-scope fallback, which Sentinel 1.0.1 needs
172 lines
6.4 KiB
PHP
172 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Enums\GithubRunnerStatus;
|
|
use App\Models\GithubRunnerConfig;
|
|
use App\Models\GithubRunnerExecution;
|
|
use App\Models\Server;
|
|
use App\Services\GithubRunner\GithubRunnerContainer;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* Compares runner executions with the runner containers on each build server. It cleans up
|
|
* stopped and orphaned runners, applies the wait, idle, and job timeouts, and restarts
|
|
* provisioning for queued executions whose dispatch was lost.
|
|
*/
|
|
class ReconcileGithubRunnersJob implements ShouldBeUnique, ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public $tries = 1;
|
|
|
|
public $timeout = 300;
|
|
|
|
private const PROVISIONING_TIMEOUT_MINUTES = 20;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->onQueue('high');
|
|
}
|
|
|
|
public function uniqueFor(): int
|
|
{
|
|
return $this->timeout;
|
|
}
|
|
|
|
public function handle(): void
|
|
{
|
|
// Disabling keeps running jobs, so a disabled server is reconciled until its last runner is done.
|
|
Server::query()
|
|
->where(fn (Builder $query) => $query
|
|
->whereHas('githubRunnerConfig', fn (Builder $config) => $config->where('is_enabled', true))
|
|
->orWhereHas('githubRunnerExecutions', fn (Builder $execution) => $execution->whereIn('status', GithubRunnerStatus::occupying())))
|
|
->with('settings')
|
|
->get()
|
|
->filter(fn (Server $server) => $server->isFunctional())
|
|
->each(fn (Server $server) => $this->reconcileServer($server));
|
|
|
|
$this->removeRunnersOfDeletedServers();
|
|
$this->reconcileQueued();
|
|
}
|
|
|
|
/**
|
|
* Removes executions that still occupy a runner of a deleted server (for example a server deleted
|
|
* without model events), because they block deleting the GitHub App.
|
|
*/
|
|
private function removeRunnersOfDeletedServers(): void
|
|
{
|
|
GithubRunnerExecution::deleteAndDeregister(
|
|
GithubRunnerExecution::query()
|
|
->whereNull('server_id')
|
|
->whereIn('status', GithubRunnerStatus::occupying())
|
|
);
|
|
}
|
|
|
|
private function reconcileServer(Server $server): void
|
|
{
|
|
$output = instant_remote_process([GithubRunnerContainer::listCommand()], $server, false);
|
|
if ($output === null) {
|
|
return;
|
|
}
|
|
$containers = $this->parseContainers($output);
|
|
|
|
$executions = GithubRunnerExecution::query()
|
|
->where('server_id', $server->id)
|
|
->whereIn('status', GithubRunnerStatus::occupying())
|
|
->with('config')
|
|
->get();
|
|
|
|
foreach ($executions as $execution) {
|
|
$failure = $this->failureFor($execution, $containers->get($execution->uuid, []));
|
|
if ($failure) {
|
|
$execution->finish(...$failure);
|
|
CleanupGithubRunnerJob::dispatch($execution->id);
|
|
}
|
|
}
|
|
|
|
$containers->keys()
|
|
->diff($executions->pluck('uuid'))
|
|
->each(fn (string $uuid) => instant_remote_process(GithubRunnerContainer::cleanupCommands($uuid), $server, false));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $containerStates container name => state
|
|
* @return array{0: GithubRunnerStatus, 1: string}|null
|
|
*/
|
|
private function failureFor(GithubRunnerExecution $execution, array $containerStates): ?array
|
|
{
|
|
if ($execution->status === GithubRunnerStatus::Provisioning) {
|
|
return $execution->updated_at->lt(now()->subMinutes(self::PROVISIONING_TIMEOUT_MINUTES))
|
|
? [GithubRunnerStatus::Failed, 'Provisioning did not finish in '.self::PROVISIONING_TIMEOUT_MINUTES.' minutes.']
|
|
: null;
|
|
}
|
|
|
|
$state = $containerStates[GithubRunnerContainer::nameFor($execution->uuid)] ?? null;
|
|
if (! in_array($state, ['running', 'restarting', 'paused'], true)) {
|
|
return [GithubRunnerStatus::Failed, 'The runner container stopped before GitHub reported a job result.'];
|
|
}
|
|
|
|
$config = $execution->config;
|
|
if ($execution->status === GithubRunnerStatus::Idle && $config
|
|
&& ($execution->ready_at ?? $execution->updated_at)->lt(now()->subMinutes($config->idle_timeout))) {
|
|
return [GithubRunnerStatus::Cancelled, "No job was assigned to the runner within {$config->idle_timeout} minutes."];
|
|
}
|
|
|
|
if ($execution->status === GithubRunnerStatus::Running && $config
|
|
&& ($execution->started_at ?? $execution->updated_at)->lt(now()->subMinutes($config->job_timeout))) {
|
|
return [GithubRunnerStatus::TimedOut, "The job ran longer than {$config->job_timeout} minutes."];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return Collection<string, array<string, string>> execution uuid => [container name => state]
|
|
*/
|
|
private function parseContainers(string $output): Collection
|
|
{
|
|
$containers = collect();
|
|
foreach (preg_split('/\R/', trim($output)) as $line) {
|
|
$parts = preg_split('/\s+/', trim($line));
|
|
if (count($parts) !== 3 || ! preg_match('/\A[a-z0-9]+\z/', $parts[0])) {
|
|
continue;
|
|
}
|
|
[$uuid, $name, $state] = $parts;
|
|
$containers->put($uuid, [...$containers->get($uuid, []), $name => $state]);
|
|
}
|
|
|
|
return $containers;
|
|
}
|
|
|
|
private function reconcileQueued(): void
|
|
{
|
|
$queued = GithubRunnerExecution::query()
|
|
->where('status', GithubRunnerStatus::Queued)
|
|
->get()
|
|
->groupBy('github_app_id');
|
|
|
|
foreach ($queued as $githubAppId => $executions) {
|
|
$timeout = (int) (GithubRunnerConfig::query()
|
|
->where('github_app_id', $githubAppId)
|
|
->where('is_enabled', true)
|
|
->max('capacity_wait_timeout') ?? 0);
|
|
|
|
foreach ($executions as $execution) {
|
|
if (($execution->queued_at ?? $execution->created_at)->lt(now()->subMinutes($timeout))) {
|
|
$execution->finish(GithubRunnerStatus::TimedOut, "No build server had free capacity within {$timeout} minutes.");
|
|
}
|
|
}
|
|
|
|
ProvisionGithubRunnerJob::dispatchQueued($githubAppId);
|
|
}
|
|
}
|
|
}
|