Optimize PushServerUpdateJob with batch updates and async jobs (#7639)

This commit is contained in:
Andras Bacsai 2025-12-16 12:22:24 +01:00 committed by GitHub
commit 2646fb81eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 182 additions and 109 deletions

View file

@ -0,0 +1,55 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Laravel\Horizon\Contracts\Silenced;
/**
* Asynchronously connects the coolify-proxy to Docker networks.
*
* This job is dispatched from PushServerUpdateJob when the proxy is found running
* to ensure it's connected to all required networks without blocking the status update.
*/
class ConnectProxyToNetworksJob implements ShouldBeEncrypted, ShouldQueue, Silenced
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public function middleware(): array
{
// Prevent overlapping executions for the same server and throttle to max once per 10 seconds
return [
(new WithoutOverlapping('connect-proxy-networks-'.$this->server->uuid))
->expireAfter(60)
->dontRelease(),
];
}
public function __construct(public Server $server) {}
public function handle()
{
if (! $this->server->isFunctional()) {
return;
}
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
if (empty($connectProxyToDockerNetworks)) {
return;
}
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
}

View file

@ -9,6 +9,7 @@
use App\Actions\Server\StartLogDrain; use App\Actions\Server\StartLogDrain;
use App\Actions\Shared\ComplexStatusCheck; use App\Actions\Shared\ComplexStatusCheck;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
@ -134,29 +135,29 @@ public function handle()
if ($this->containers->isEmpty()) { if ($this->containers->isEmpty()) {
return; return;
} }
$this->applications = $this->server->applications(); $this->applications = $this->server->applications();
$this->databases = $this->server->databases(); $this->databases = $this->server->databases();
$this->previews = $this->server->previews(); $this->previews = $this->server->previews();
$this->services = $this->server->services()->get(); // Eager load service applications and databases to avoid N+1 queries
$this->services = $this->server->services()
->with(['applications:id,service_id', 'databases:id,service_id'])
->get();
$this->allApplicationIds = $this->applications->filter(function ($application) { $this->allApplicationIds = $this->applications->filter(function ($application) {
return $application->additional_servers->count() === 0; return $application->additional_servers_count === 0;
})->pluck('id'); })->pluck('id');
$this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
return $application->additional_servers->count() > 0; return $application->additional_servers_count > 0;
}); });
$this->allApplicationPreviewsIds = $this->previews->map(function ($preview) { $this->allApplicationPreviewsIds = $this->previews->map(function ($preview) {
return $preview->application_id.':'.$preview->pull_request_id; return $preview->application_id.':'.$preview->pull_request_id;
}); });
$this->allDatabaseUuids = $this->databases->pluck('uuid'); $this->allDatabaseUuids = $this->databases->pluck('uuid');
$this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
$this->services->each(function ($service) { // Use eager-loaded relationships instead of querying in loop
$service->applications()->pluck('id')->each(function ($applicationId) { $this->allServiceApplicationIds = $this->services->flatMap(fn ($service) => $service->applications->pluck('id'));
$this->allServiceApplicationIds->push($applicationId); $this->allServiceDatabaseIds = $this->services->flatMap(fn ($service) => $service->databases->pluck('id'));
});
$service->databases()->pluck('id')->each(function ($databaseId) {
$this->allServiceDatabaseIds->push($databaseId);
});
});
foreach ($this->containers as $container) { foreach ($this->containers as $container) {
$containerStatus = data_get($container, 'state', 'exited'); $containerStatus = data_get($container, 'state', 'exited');
@ -402,66 +403,59 @@ private function updateApplicationPreviewStatus(string $applicationId, string $p
private function updateNotFoundApplicationStatus() private function updateNotFoundApplicationStatus()
{ {
$notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
if ($notFoundApplicationIds->isNotEmpty()) { if ($notFoundApplicationIds->isEmpty()) {
$notFoundApplicationIds->each(function ($applicationId) { return;
$application = Application::find($applicationId);
if ($application) {
// Don't mark as exited if already exited
if (str($application->status)->startsWith('exited')) {
return;
}
// Only protection: Verify we received any container data at all
// If containers collection is completely empty, Sentinel might have failed
if ($this->containers->isEmpty()) {
return;
}
if ($application->status !== 'exited') {
$application->status = 'exited';
$application->save();
}
}
});
} }
// Only protection: Verify we received any container data at all
// If containers collection is completely empty, Sentinel might have failed
if ($this->containers->isEmpty()) {
return;
}
// Batch update: mark all not-found applications as exited (excluding already exited ones)
Application::whereIn('id', $notFoundApplicationIds)
->where('status', 'not like', 'exited%')
->update(['status' => 'exited']);
} }
private function updateNotFoundApplicationPreviewStatus() private function updateNotFoundApplicationPreviewStatus()
{ {
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
if ($notFoundApplicationPreviewsIds->isNotEmpty()) { if ($notFoundApplicationPreviewsIds->isEmpty()) {
$notFoundApplicationPreviewsIds->each(function ($previewKey) { return;
// Parse the previewKey format "application_id:pull_request_id" }
$parts = explode(':', $previewKey);
if (count($parts) !== 2) {
return;
}
$applicationId = $parts[0]; // Only protection: Verify we received any container data at all
$pullRequestId = $parts[1]; // If containers collection is completely empty, Sentinel might have failed
if ($this->containers->isEmpty()) {
return;
}
$applicationPreview = $this->previews->where('application_id', $applicationId) // Collect IDs of previews that need to be marked as exited
->where('pull_request_id', $pullRequestId) $previewIdsToUpdate = collect();
->first(); foreach ($notFoundApplicationPreviewsIds as $previewKey) {
// Parse the previewKey format "application_id:pull_request_id"
$parts = explode(':', $previewKey);
if (count($parts) !== 2) {
continue;
}
if ($applicationPreview) { $applicationId = $parts[0];
// Don't mark as exited if already exited $pullRequestId = $parts[1];
if (str($applicationPreview->status)->startsWith('exited')) {
return;
}
// Only protection: Verify we received any container data at all $applicationPreview = $this->previews->where('application_id', $applicationId)
// If containers collection is completely empty, Sentinel might have failed ->where('pull_request_id', $pullRequestId)
if ($this->containers->isEmpty()) { ->first();
return; if ($applicationPreview && ! str($applicationPreview->status)->startsWith('exited')) {
} $previewIdsToUpdate->push($applicationPreview->id);
if ($applicationPreview->status !== 'exited') { }
$applicationPreview->status = 'exited'; }
$applicationPreview->save();
} // Batch update all collected preview IDs
} if ($previewIdsToUpdate->isNotEmpty()) {
}); ApplicationPreview::whereIn('id', $previewIdsToUpdate)->update(['status' => 'exited']);
} }
} }
@ -478,8 +472,8 @@ private function updateProxyStatus()
} catch (\Throwable $e) { } catch (\Throwable $e) {
} }
} else { } else {
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server); // Connect proxy to networks asynchronously to avoid blocking the status update
instant_remote_process($connectProxyToDockerNetworks, $this->server, false); ConnectProxyToNetworksJob::dispatch($this->server);
} }
} }
} }
@ -554,27 +548,19 @@ private function updateNotFoundServiceStatus()
{ {
$notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
$notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
// Batch update service applications
if ($notFoundServiceApplicationIds->isNotEmpty()) { if ($notFoundServiceApplicationIds->isNotEmpty()) {
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) { ServiceApplication::whereIn('id', $notFoundServiceApplicationIds)
$application = ServiceApplication::find($serviceApplicationId); ->where('status', '!=', 'exited')
if ($application) { ->update(['status' => 'exited']);
if ($application->status !== 'exited') {
$application->status = 'exited';
$application->save();
}
}
});
} }
// Batch update service databases
if ($notFoundServiceDatabaseIds->isNotEmpty()) { if ($notFoundServiceDatabaseIds->isNotEmpty()) {
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds)
$database = ServiceDatabase::find($serviceDatabaseId); ->where('status', '!=', 'exited')
if ($database) { ->update(['status' => 'exited']);
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
}
});
} }
} }

View file

@ -76,8 +76,7 @@ public function handle()
} else { } else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save(); $this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server); ConnectProxyToNetworksJob::dispatchSync($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} }
} }
} }

View file

@ -2,6 +2,7 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Jobs\ConnectProxyToNetworksJob;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
@ -29,8 +30,7 @@ public function mount(string $server_uuid)
private function createNetworkAndAttachToProxy() private function createNetworkAndAttachToProxy()
{ {
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server); ConnectProxyToNetworksJob::dispatchSync($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} }
public function add($name) public function add($name)

View file

@ -831,34 +831,67 @@ public function hasDefinedResources()
public function databases() public function databases()
{ {
return $this->destinations()->map(function ($standaloneDocker) { // Get destination IDs for this server in two efficient queries
$postgresqls = data_get($standaloneDocker, 'postgresqls', collect([])); $standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id');
$redis = data_get($standaloneDocker, 'redis', collect([])); $swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id');
$mongodbs = data_get($standaloneDocker, 'mongodbs', collect([]));
$mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
$keydbs = data_get($standaloneDocker, 'keydbs', collect([]));
$dragonflies = data_get($standaloneDocker, 'dragonflies', collect([]));
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); $destinationCondition = function ($query) use ($standaloneDockerIds, $swarmDockerIds) {
})->flatten()->filter(function ($item) { $query->where(function ($q) use ($standaloneDockerIds) {
return data_get($item, 'name') !== 'coolify-db'; $q->where('destination_type', StandaloneDocker::class)
}); ->whereIn('destination_id', $standaloneDockerIds);
})->orWhere(function ($q) use ($swarmDockerIds) {
$q->where('destination_type', SwarmDocker::class)
->whereIn('destination_id', $swarmDockerIds);
});
};
// Query each database type with the destination condition
$postgresqls = StandalonePostgresql::where($destinationCondition)->get();
$redis = StandaloneRedis::where($destinationCondition)->get();
$mongodbs = StandaloneMongodb::where($destinationCondition)->get();
$mysqls = StandaloneMysql::where($destinationCondition)->get();
$mariadbs = StandaloneMariadb::where($destinationCondition)->get();
$keydbs = StandaloneKeydb::where($destinationCondition)->get();
$dragonflies = StandaloneDragonfly::where($destinationCondition)->get();
$clickhouses = StandaloneClickhouse::where($destinationCondition)->get();
return $postgresqls
->concat($redis)
->concat($mongodbs)
->concat($mysqls)
->concat($mariadbs)
->concat($keydbs)
->concat($dragonflies)
->concat($clickhouses)
->filter(fn ($item) => data_get($item, 'name') !== 'coolify-db');
} }
public function applications() public function applications()
{ {
$applications = $this->destinations()->map(function ($standaloneDocker) { // Get destination IDs for this server in two efficient queries
return $standaloneDocker->applications; $standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id');
})->flatten(); $swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id');
$additionalApplicationIds = DB::table('additional_destinations')->where('server_id', $this->id)->get('application_id');
$additionalApplicationIds = collect($additionalApplicationIds)->map(function ($item) { // Query all applications in a single query using polymorphic conditions
return $item->application_id; $applications = Application::where(function ($query) use ($standaloneDockerIds, $swarmDockerIds) {
}); $query->where(function ($q) use ($standaloneDockerIds) {
Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) { $q->where('destination_type', StandaloneDocker::class)
$applications->push($application); ->whereIn('destination_id', $standaloneDockerIds);
}); })->orWhere(function ($q) use ($swarmDockerIds) {
$q->where('destination_type', SwarmDocker::class)
->whereIn('destination_id', $swarmDockerIds);
});
})->get();
// Get additional server applications
$additionalApplicationIds = DB::table('additional_destinations')
->where('server_id', $this->id)
->pluck('application_id');
if ($additionalApplicationIds->isNotEmpty()) {
$additionalApps = Application::whereIn('id', $additionalApplicationIds)->get();
$applications = $applications->concat($additionalApps);
}
return $applications; return $applications;
} }

View file

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Jobs\ConnectProxyToNetworksJob;
use App\Traits\HasSafeStringAttribute; use App\Traits\HasSafeStringAttribute;
class StandaloneDocker extends BaseModel class StandaloneDocker extends BaseModel
@ -18,8 +19,7 @@ protected static function boot()
instant_remote_process([ instant_remote_process([
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null", "docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
], $server, false); ], $server, false);
$connectProxyToDockerNetworks = connectProxyToNetworks($server); ConnectProxyToNetworksJob::dispatchSync($server);
instant_remote_process($connectProxyToDockerNetworks, $server, false);
}); });
} }