From 1dacb948603525441e59f3abbf36df26df17a451 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:16:12 +0100 Subject: [PATCH] fix(performance): eliminate N+1 query in CheckTraefikVersionJob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes a critical N+1 query issue in CheckTraefikVersionJob that was loading ALL proxy servers into memory then filtering in PHP, causing potential OOM errors with thousands of servers. Changes: - Added scopeWhereProxyType() query scope to Server model for database-level filtering using JSON column arrow notation - Updated CheckTraefikVersionJob to use new scope instead of collection filter, moving proxy type filtering into the SQL query - Added comprehensive unit tests for the new query scope Performance impact: - Before: SELECT * FROM servers WHERE proxy IS NOT NULL (all servers) - After: SELECT * FROM servers WHERE proxy->>'type' = 'TRAEFIK' (filtered) - Eliminates memory overhead of loading non-Traefik servers - Critical for cloud instances with thousands of connected servers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Jobs/CheckTraefikVersionJob.php | 4 +- app/Models/Server.php | 5 +++ tests/Unit/ServerQueryScopeTest.php | 62 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/ServerQueryScopeTest.php diff --git a/app/Jobs/CheckTraefikVersionJob.php b/app/Jobs/CheckTraefikVersionJob.php index 925c8ba7d..cb4c94695 100644 --- a/app/Jobs/CheckTraefikVersionJob.php +++ b/app/Jobs/CheckTraefikVersionJob.php @@ -47,10 +47,10 @@ public function handle(): void // Query all servers with Traefik proxy that are reachable $servers = Server::whereNotNull('proxy') + ->whereProxyType(ProxyTypes::TRAEFIK->value) ->whereRelation('settings', 'is_reachable', true) ->whereRelation('settings', 'is_usable', true) - ->get() - ->filter(fn ($server) => $server->proxyType() === ProxyTypes::TRAEFIK->value); + ->get(); $serverCount = $servers->count(); Log::info("CheckTraefikVersionJob: Found {$serverCount} server(s) with Traefik proxy"); diff --git a/app/Models/Server.php b/app/Models/Server.php index 52dcce44f..157666d66 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -523,6 +523,11 @@ public function scopeWithProxy(): Builder return $this->proxy->modelScope(); } + public function scopeWhereProxyType(Builder $query, string $proxyType): Builder + { + return $query->where('proxy->type', $proxyType); + } + public function isLocalhost() { return $this->ip === 'host.docker.internal' || $this->id === 0; diff --git a/tests/Unit/ServerQueryScopeTest.php b/tests/Unit/ServerQueryScopeTest.php new file mode 100644 index 000000000..8ab0b8b10 --- /dev/null +++ b/tests/Unit/ServerQueryScopeTest.php @@ -0,0 +1,62 @@ +shouldReceive('where') + ->once() + ->with('proxy->type', ProxyTypes::TRAEFIK->value) + ->andReturnSelf(); + + // Create a server instance and call the scope + $server = new Server; + $result = $server->scopeWhereProxyType($mockBuilder, ProxyTypes::TRAEFIK->value); + + // Assert the builder is returned + expect($result)->toBe($mockBuilder); +}); + +it('can chain whereProxyType scope with other query methods', function () { + // Mock the Builder + $mockBuilder = Mockery::mock(Builder::class); + + // Expect multiple chained calls + $mockBuilder->shouldReceive('where') + ->once() + ->with('proxy->type', ProxyTypes::CADDY->value) + ->andReturnSelf(); + + // Create a server instance and call the scope + $server = new Server; + $result = $server->scopeWhereProxyType($mockBuilder, ProxyTypes::CADDY->value); + + // Assert the builder is returned for chaining + expect($result)->toBe($mockBuilder); +}); + +it('accepts any proxy type string value', function () { + // Mock the Builder + $mockBuilder = Mockery::mock(Builder::class); + + // Test with a custom proxy type + $customProxyType = 'custom-proxy'; + + $mockBuilder->shouldReceive('where') + ->once() + ->with('proxy->type', $customProxyType) + ->andReturnSelf(); + + // Create a server instance and call the scope + $server = new Server; + $result = $server->scopeWhereProxyType($mockBuilder, $customProxyType); + + // Assert the builder is returned + expect($result)->toBe($mockBuilder); +});