From c67b77bca09317bc9c7f00f9ade6214363481360 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 16 Jan 2026 15:06:35 +0530 Subject: [PATCH 1/6] update: implement proper logs cleanup! --- app/controllers/general.php | 38 +++++--- app/init/constants.php | 1 + app/init/resources.php | 8 ++ app/worker.php | 8 ++ .../Functions/Http/Executions/Create.php | 14 +++ src/Appwrite/Platform/Workers/Deletes.php | 93 ++++++++++++++++++- 6 files changed, 148 insertions(+), 14 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index e335f284b7..685ab14aea 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -6,6 +6,7 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Auth\Key; use Appwrite\Event\Certificate; +use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\StatsUsage; @@ -59,7 +60,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey) +function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { $host = $request->getHostname() ?? ''; if (!empty($previewHostname)) { @@ -802,6 +803,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ->setProject($project) ->trigger(); + /* cleanup */ + if ($executionsRetentionCount > 0) { + $queueForDeletes + ->setProject($project) + ->setResource($resource->getSequence()) + ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) + ->trigger(); + } + return true; } elseif ($type === 'api') { return false; @@ -812,8 +822,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } else { throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView); } - - return false; } App::init() @@ -863,7 +871,9 @@ App::init() ->inject('apiKey') ->inject('cors') ->inject('authorization') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization) { + ->inject('queueForDeletes') + ->inject('executionsRetentionCount') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { /* * Appwrite Router */ @@ -871,7 +881,7 @@ App::init() $platformHostnames = $platform['hostnames'] ?? []; // Only run Router when external domain if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } @@ -1144,14 +1154,16 @@ App::options() ->inject('apiKey') ->inject('cors') ->inject('authorization') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization) { + ->inject('queueForDeletes') + ->inject('executionsRetentionCount') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { /* * Appwrite Router */ $platformHostnames = $platform['hostnames'] ?? []; // Only run Router when external domain if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } @@ -1535,13 +1547,15 @@ App::get('/robots.txt') ->inject('previewHostname') ->inject('apiKey') ->inject('authorization') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization) { + ->inject('queueForDeletes') + ->inject('executionsRetentionCount') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { $platformHostnames = $platform['hostnames'] ?? []; if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } @@ -1568,13 +1582,15 @@ App::get('/humans.txt') ->inject('previewHostname') ->inject('apiKey') ->inject('authorization') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization) { + ->inject('queueForDeletes') + ->inject('executionsRetentionCount') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { $platformHostnames = $platform['hostnames'] ?? []; if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } diff --git a/app/init/constants.php b/app/init/constants.php index d51cb6b7af..e6215b3a43 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -194,6 +194,7 @@ const DELETE_TYPE_DEPLOYMENTS = 'deployments'; const DELETE_TYPE_USERS = 'users'; const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects'; const DELETE_TYPE_EXECUTIONS = 'executions'; +const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit'; const DELETE_TYPE_AUDIT = 'audit'; const DELETE_TYPE_ABUSE = 'abuse'; const DELETE_TYPE_USAGE = 'usage'; diff --git a/app/init/resources.php b/app/init/resources.php index a9d46a17be..2f43ee008b 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1156,3 +1156,11 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request, A App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) { return new TransactionState($dbForProject, $authorization); }, ['dbForProject', 'authorization']); + +App::setResource('executionsRetentionCount', function (Document $project, array $plan) { + if ($project->getId() === 'console' || empty($plan)) { + return 0; + } + + return (int) ($plan['executionsRetentionCount'] ?? 100); +}, ['project', 'plan']); diff --git a/app/worker.php b/app/worker.php index ba8bf98568..39f0695bb3 100644 --- a/app/worker.php +++ b/app/worker.php @@ -490,6 +490,14 @@ Server::setResource('getAudit', function (Database $dbForPlatform, callable $get }; }, ['dbForPlatform', 'getProjectDB']); +Server::setResource('executionsRetentionCount', function (Document $project, array $plan) { + if ($project->getId() === 'console' || empty($plan)) { + return 0; + } + + return (int) ($plan['executionsRetentionCount'] ?? 100); +}, ['project', 'plan']); + $pools = $register->get('pools'); $platform = new Appwrite(); $args = $platform->getEnv('argv'); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 1a265298d3..5e8b6d9e02 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Executions; use Ahc\Jwt\JWT; +use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\StatsUsage; @@ -101,6 +102,8 @@ class Create extends Base ->inject('executor') ->inject('platform') ->inject('authorization') + ->inject('queueForDeletes') + ->inject('executionsRetentionCount') ->callback($this->action(...)); } @@ -127,6 +130,8 @@ class Create extends Base Executor $executor, array $platform, Authorization $authorization, + DeleteEvent $queueForDeletes, + int $executionsRetentionCount, ) { $async = \strval($async) === 'true' || \strval($async) === '1'; @@ -513,6 +518,15 @@ class Create extends Base } } + /* cleanup */ + if ($executionsRetentionCount > 0) { + $queueForDeletes + ->setProject($project) + ->setResource($function->getSequence()) + ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) + ->trigger(); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($execution, Response::MODEL_EXECUTION); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 9687f4f4bb..0bbd7b7e66 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -30,6 +30,8 @@ use Utopia\Queue\Message; use Utopia\Storage\Device; use Utopia\System\System; +use function Swoole\Coroutine\batch; + class Deletes extends Action { protected array $selects = ['$sequence', '$id', '$collection', '$permissions', '$updatedAt']; @@ -59,6 +61,7 @@ class Deletes extends Action ->inject('certificates') ->inject('executor') ->inject('executionRetention') + ->inject('executionsRetentionCount') ->inject('auditRetention') ->inject('log') ->inject('getAudit') @@ -83,6 +86,7 @@ class Deletes extends Action CertificatesAdapter $certificates, Executor $executor, string $executionRetention, + int $executionsRetentionCount, string $auditRetention, Log $log, callable $getAudit, @@ -144,6 +148,17 @@ class Deletes extends Action case DELETE_TYPE_EXECUTIONS: $this->deleteExecutionLogs($project, $getProjectDB, $executionRetention); break; + case DELETE_TYPE_EXECUTIONS_LIMIT: + $resourceInternalId = $payload['resource'] ?? null; + if ($resourceInternalId) { + $this->deleteExecutionsByLimit( + $project, + $getProjectDB, + $executionsRetentionCount, + $resourceInternalId + ); + } + break; case DELETE_TYPE_AUDIT: if (!$project->isEmpty()) { $this->deleteAuditLogs($project, $getAudit, $auditRetention); @@ -694,14 +709,15 @@ class Deletes extends Action } /** - * @param database $dbForPlatform + * @param Document $project * @param callable $getProjectDB * @param string $datetime * @return void - * @throws Exception + * @throws Exception|DatabaseException */ private function deleteExecutionLogs(Document $project, callable $getProjectDB, string $datetime): void { + /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); // Delete Executions @@ -711,10 +727,81 @@ class Deletes extends Action Query::orderDesc('$createdAt'), Query::orderDesc(), ], $dbForProject); + + /* delete based on custom retention, if any */ + $this->deleteExecutionsByLimit($project, $getProjectDB); } /** - * @param Database $dbForPlatform + * @param Document $project + * @param callable $getProjectDB + * @param int|null $executionsRetentionCount + * @param string|null $resourceInternalId + * @return void + * @throws DatabaseException + */ + protected function deleteExecutionsByLimit( + Document $project, + callable $getProjectDB, + ?int $executionsRetentionCount = 0, + ?string $resourceInternalId = null + ): void { + if ($executionsRetentionCount <= 0) { + return; + } + + /** @var Database $dbForProject */ + $dbForProject = $getProjectDB($project); + + /* delete log for a given $resourceInternalId */ + $deleteExecDocuments = function (Database $dbForProject, string $resourceInternalId) use ($executionsRetentionCount) { + // get the execution at position `N+1` + $execution = $dbForProject->findOne('executions', [ + Query::select(['$createdAt']), + Query::equal('resourceInternalId', [$resourceInternalId]), + Query::orderDesc('$createdAt'), + Query::offset($executionsRetentionCount), + ]); + + if (!$execution->isEmpty()) { + // delete everything older + $cutoffTime = $execution->getAttribute('$createdAt'); + + $this->deleteByGroup('executions', [ + Query::select([...$this->selects, '$createdAt']), + Query::equal('resourceInternalId', [$resourceInternalId]), + Query::lessThan('$createdAt', $cutoffTime), + Query::orderDesc('$createdAt'), + Query::orderDesc(), + ], $dbForProject); + } + }; + + if (!empty($resourceInternalId)) { + // fast path, no need to list anything! + $deleteExecDocuments($dbForProject, $resourceInternalId); + } else { + $processResource = function (string $type) use ($dbForProject, $deleteExecDocuments) { + $this->listByGroup( + collection: $type, + queries: [Query::select(['$id'])], + database: $dbForProject, + callback: function (Document $resource) use ($dbForProject, $deleteExecDocuments) { + $deleteExecDocuments($dbForProject, $resource->getSequence()); + } + ); + }; + + /* perform processing in parallel */ + batch([ + fn () => $processResource('sites'), + fn () => $processResource('functions'), + ]); + } + } + + /** + * @param Document $project * @param callable $getProjectDB * @return void * @throws Exception|Throwable From 0ef4bf21cce9809cd41852dcd1576dbc20fa2bb1 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 16 Jan 2026 15:48:21 +0530 Subject: [PATCH 2/6] address comments. --- app/config/collections/projects.php | 7 +++++ app/controllers/general.php | 1 + .../Functions/Http/Executions/Create.php | 2 +- src/Appwrite/Platform/Workers/Deletes.php | 29 +++++++++++-------- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index dae0337dc9..86346d2672 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2098,6 +2098,13 @@ return [ 'lengths' => [], 'orders' => [], ], + [ + '$id' => ID::custom('_key_resourceType'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['resourceType'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], ], ], diff --git a/app/controllers/general.php b/app/controllers/general.php index 685ab14aea..4222d18ff1 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -807,6 +807,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($executionsRetentionCount > 0) { $queueForDeletes ->setProject($project) + ->setResourceType($type) ->setResource($resource->getSequence()) ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) ->trigger(); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 5e8b6d9e02..8c4b68edb6 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -63,7 +63,6 @@ class Create extends Base ->label('scope', 'execution.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].executions.[executionId].create') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', group: 'executions', @@ -523,6 +522,7 @@ class Create extends Base $queueForDeletes ->setProject($project) ->setResource($function->getSequence()) + ->setResourceType(RESOURCE_TYPE_FUNCTIONS) ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) ->trigger(); } diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 0bbd7b7e66..3c66eef277 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -150,12 +150,14 @@ class Deletes extends Action break; case DELETE_TYPE_EXECUTIONS_LIMIT: $resourceInternalId = $payload['resource'] ?? null; + $resourceType = $payload['resourceType'] ?? null; if ($resourceInternalId) { $this->deleteExecutionsByLimit( $project, $getProjectDB, $executionsRetentionCount, - $resourceInternalId + $resourceInternalId, + $resourceType ); } break; @@ -214,16 +216,15 @@ class Deletes extends Action * @param Database $dbForPlatform * @param callable $getProjectDB * @param string $datetime - * @param Document|null $document * @return void * @throws Conflict * @throws Restricted * @throws Structure - * @throws DatabaseException + * @throws DatabaseException|Exception */ private function deleteSchedules(Database $dbForPlatform, callable $getProjectDB, string $datetime): void { - // Temporarly accepting both 'fra' and 'default' + // Temporarily accepting both 'fra' and 'default' // When all migrated, only use _APP_REGION with 'default' as default value $regions = [System::getEnv('_APP_REGION', 'default')]; if (!in_array('default', $regions)) { @@ -737,6 +738,7 @@ class Deletes extends Action * @param callable $getProjectDB * @param int|null $executionsRetentionCount * @param string|null $resourceInternalId + * @param string|null $resourceType * @return void * @throws DatabaseException */ @@ -744,7 +746,8 @@ class Deletes extends Action Document $project, callable $getProjectDB, ?int $executionsRetentionCount = 0, - ?string $resourceInternalId = null + ?string $resourceInternalId = null, + ?string $resourceType = null ): void { if ($executionsRetentionCount <= 0) { return; @@ -754,11 +757,12 @@ class Deletes extends Action $dbForProject = $getProjectDB($project); /* delete log for a given $resourceInternalId */ - $deleteExecDocuments = function (Database $dbForProject, string $resourceInternalId) use ($executionsRetentionCount) { + $delete = function (Database $dbForProject, string $resourceInternalId, string $resourceType) use ($executionsRetentionCount) { // get the execution at position `N+1` $execution = $dbForProject->findOne('executions', [ Query::select(['$createdAt']), Query::equal('resourceInternalId', [$resourceInternalId]), + Query::equal('resourceType', [$resourceType]), Query::orderDesc('$createdAt'), Query::offset($executionsRetentionCount), ]); @@ -770,6 +774,7 @@ class Deletes extends Action $this->deleteByGroup('executions', [ Query::select([...$this->selects, '$createdAt']), Query::equal('resourceInternalId', [$resourceInternalId]), + Query::equal('resourceType', [$resourceType]), Query::lessThan('$createdAt', $cutoffTime), Query::orderDesc('$createdAt'), Query::orderDesc(), @@ -779,23 +784,23 @@ class Deletes extends Action if (!empty($resourceInternalId)) { // fast path, no need to list anything! - $deleteExecDocuments($dbForProject, $resourceInternalId); + $delete($dbForProject, $resourceInternalId, $resourceType); } else { - $processResource = function (string $type) use ($dbForProject, $deleteExecDocuments) { + $processResource = function (string $type) use ($dbForProject, $delete, $resourceType) { $this->listByGroup( collection: $type, queries: [Query::select(['$id'])], database: $dbForProject, - callback: function (Document $resource) use ($dbForProject, $deleteExecDocuments) { - $deleteExecDocuments($dbForProject, $resource->getSequence()); + callback: function (Document $resource) use ($dbForProject, $delete, $type) { + $delete($dbForProject, $resource->getSequence(), $type); } ); }; /* perform processing in parallel */ batch([ - fn () => $processResource('sites'), - fn () => $processResource('functions'), + fn () => $processResource(RESOURCE_TYPE_SITES), + fn () => $processResource(RESOURCE_TYPE_FUNCTIONS), ]); } } From beee5e721ee6de0fe77867d7844a46af6823d143 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 16 Jan 2026 15:55:45 +0530 Subject: [PATCH 3/6] upate: run on maintenance as well. --- src/Appwrite/Platform/Workers/Deletes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 3c66eef277..62230ed5c6 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -206,6 +206,7 @@ class Deletes extends Action $this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime); $this->deleteExpiredSessions($project, $getProjectDB); $this->deleteExpiredTransactions($project, $getProjectDB); + $this->deleteExecutionsByLimit($project, $getProjectDB, $executionsRetentionCount); break; default: throw new \Exception('No delete operation for type: ' . \strval($type)); From 79e150b7b29cdb588ae200b5fb4dfd9d99756266 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 16 Jan 2026 16:00:59 +0530 Subject: [PATCH 4/6] fix: type. --- app/controllers/general.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 4222d18ff1..6f01e256c4 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -805,9 +805,13 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw /* cleanup */ if ($executionsRetentionCount > 0) { + $resourceType = $type === 'function' + ? RESOURCE_TYPE_FUNCTIONS + : RESOURCE_TYPE_SITES; + $queueForDeletes ->setProject($project) - ->setResourceType($type) + ->setResourceType($resourceType) ->setResource($resource->getSequence()) ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) ->trigger(); From b5e9c1786ad1c97d4d500c5d69ccc3105b339b6c Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 16 Jan 2026 16:04:54 +0530 Subject: [PATCH 5/6] fix: maintenance logic. --- src/Appwrite/Platform/Workers/Deletes.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 62230ed5c6..0dae78f31d 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -201,12 +201,11 @@ class Deletes extends Action break; case DELETE_TYPE_MAINTENANCE: $this->deleteExpiredTargets($project, $getProjectDB); - $this->deleteExecutionLogs($project, $getProjectDB, $executionRetention); + $this->deleteExecutionLogs($project, $getProjectDB, $executionRetention, $executionsRetentionCount); $this->deleteAuditLogs($project, $getAudit, $auditRetention); $this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime); $this->deleteExpiredSessions($project, $getProjectDB); $this->deleteExpiredTransactions($project, $getProjectDB); - $this->deleteExecutionsByLimit($project, $getProjectDB, $executionsRetentionCount); break; default: throw new \Exception('No delete operation for type: ' . \strval($type)); @@ -714,10 +713,11 @@ class Deletes extends Action * @param Document $project * @param callable $getProjectDB * @param string $datetime + * @param int|null $executionsRetentionCount * @return void * @throws Exception|DatabaseException */ - private function deleteExecutionLogs(Document $project, callable $getProjectDB, string $datetime): void + private function deleteExecutionLogs(Document $project, callable $getProjectDB, string $datetime, ?int $executionsRetentionCount = 0): void { /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); @@ -731,7 +731,7 @@ class Deletes extends Action ], $dbForProject); /* delete based on custom retention, if any */ - $this->deleteExecutionsByLimit($project, $getProjectDB); + $this->deleteExecutionsByLimit($project, $getProjectDB, $executionsRetentionCount); } /** From ccaea5d0107c7b9a166c1427194f3adc33e3a3ec Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 16 Jan 2026 16:11:38 +0530 Subject: [PATCH 6/6] add: constant. --- app/controllers/general.php | 2 +- app/init/constants.php | 4 +++- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 6f01e256c4..8fc5a11503 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -804,7 +804,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ->trigger(); /* cleanup */ - if ($executionsRetentionCount > 0) { + if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) { $resourceType = $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES; diff --git a/app/init/constants.php b/app/init/constants.php index e6215b3a43..e05f31e078 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -181,8 +181,10 @@ const BUILD_TYPE_DEPLOYMENT = 'deployment'; const BUILD_TYPE_RETRY = 'retry'; // Deletion Types -const DELETE_TYPE_DATABASES = 'databases'; +const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false; + +const DELETE_TYPE_DATABASES = 'databases'; const DELETE_TYPE_DOCUMENT = 'document'; const DELETE_TYPE_COLLECTIONS = 'collections'; const DELETE_TYPE_TRANSACTION = 'transaction'; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 0dae78f31d..654b083a98 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -790,7 +790,7 @@ class Deletes extends Action $processResource = function (string $type) use ($dbForProject, $delete, $resourceType) { $this->listByGroup( collection: $type, - queries: [Query::select(['$id'])], + queries: [Query::select(['$id', '$sequence'])], database: $dbForProject, callback: function (Document $resource) use ($dbForProject, $delete, $type) { $delete($dbForProject, $resource->getSequence(), $type);