2020-12-14 16:39:44 +00:00
< ? php
2022-11-14 10:01:41 +00:00
namespace Appwrite\Platform\Tasks ;
2020-12-14 16:39:44 +00:00
2022-04-19 13:13:55 +00:00
use Appwrite\Event\Delete ;
2026-04-10 08:58:31 +00:00
use Appwrite\Event\Publisher\Certificate ;
2025-06-10 12:40:02 +00:00
use DateInterval ;
use DateTime ;
2026-02-10 05:04:24 +00:00
use Utopia\Console ;
2022-07-13 07:02:55 +00:00
use Utopia\Database\Database ;
2025-06-10 12:12:24 +00:00
use Utopia\Database\DateTime as DatabaseDateTime ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Document ;
2022-04-10 09:38:22 +00:00
use Utopia\Database\Query ;
2022-07-13 07:02:55 +00:00
use Utopia\Platform\Action ;
2024-04-01 11:02:47 +00:00
use Utopia\System\System ;
2026-03-04 13:31:27 +00:00
use Utopia\Validator\WhiteList ;
2022-07-13 07:02:55 +00:00
2024-08-20 07:08:07 +00:00
class Maintenance extends Action
2022-07-14 02:04:31 +00:00
{
2022-08-02 01:58:36 +00:00
public static function getName () : string
{
return 'maintenance' ;
}
2022-07-13 07:02:55 +00:00
public function __construct ()
{
$this
2023-10-18 19:44:06 +00:00
-> desc ( 'Schedules maintenance tasks and publishes them to our queues' )
2026-03-04 13:31:27 +00:00
-> param ( 'type' , 'loop' , new WhiteList ([ 'loop' , 'trigger' ]), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2025-03-10 08:54:20 +00:00
-> inject ( 'console' )
2026-04-10 08:58:31 +00:00
-> inject ( 'publisherForCertificates' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForDeletes' )
2025-06-04 08:37:43 +00:00
-> callback ( $this -> action ( ... ));
2022-07-13 07:02:55 +00:00
}
2026-04-10 08:58:31 +00:00
public function action ( string $type , Database $dbForPlatform , Document $console , Certificate $publisherForCertificates , Delete $queueForDeletes ) : void
2022-07-13 07:02:55 +00:00
{
2021-01-26 20:49:13 +00:00
Console :: title ( 'Maintenance V1' );
2022-04-13 12:39:31 +00:00
Console :: success ( APP_NAME . ' maintenance process v1 has started' );
2021-01-26 20:49:13 +00:00
2025-05-08 07:49:27 +00:00
$interval = ( int ) System :: getEnv ( '_APP_MAINTENANCE_INTERVAL' , '86400' ); // 1 day
2024-04-01 11:02:47 +00:00
$usageStatsRetentionHourly = ( int ) System :: getEnv ( '_APP_MAINTENANCE_RETENTION_USAGE_HOURLY' , '8640000' ); //100 days
$cacheRetention = ( int ) System :: getEnv ( '_APP_MAINTENANCE_RETENTION_CACHE' , '2592000' ); // 30 days
$schedulesDeletionRetention = ( int ) System :: getEnv ( '_APP_MAINTENANCE_RETENTION_SCHEDULES' , '86400' ); // 1 Day
2025-05-08 07:49:27 +00:00
$jobInitTime = System :: getEnv ( '_APP_MAINTENANCE_START_TIME' , '00:00' ); // (hour:minutes)
$now = new \DateTime ();
$now -> setTimezone ( new \DateTimeZone ( date_default_timezone_get ()));
$next = new \DateTime ( $now -> format ( " Y-m-d $jobInitTime " ));
$next -> setTimezone ( new \DateTimeZone ( date_default_timezone_get ()));
$delay = $next -> getTimestamp () - $now -> getTimestamp ();
/**
* If time passed for the target day .
*/
if ( $delay <= 0 ) {
$next -> add ( \DateInterval :: createFromDateString ( '1 days' ));
$delay = $next -> getTimestamp () - $now -> getTimestamp ();
}
2026-04-10 08:58:31 +00:00
$action = function () use ( $interval , $cacheRetention , $schedulesDeletionRetention , $usageStatsRetentionHourly , $dbForPlatform , $console , $queueForDeletes , $publisherForCertificates ) {
2025-06-10 12:12:24 +00:00
$time = DatabaseDateTime :: now ();
2021-01-26 20:49:13 +00:00
2023-06-05 16:13:00 +00:00
Console :: info ( " [ { $time } ] Notifying workers with maintenance tasks every { $interval } seconds " );
2023-12-12 11:26:28 +00:00
2025-06-11 14:36:51 +00:00
// Iterate through project only if it was accessed in last 30 days
$dateInterval = DateInterval :: createFromDateString ( '30 days' );
$before30days = ( new DateTime ()) -> sub ( $dateInterval );
2025-06-10 12:12:24 +00:00
2025-04-11 14:52:19 +00:00
$dbForPlatform -> foreach (
'projects' ,
function ( Document $project ) use ( $queueForDeletes , $usageStatsRetentionHourly ) {
$queueForDeletes
-> setType ( DELETE_TYPE_MAINTENANCE )
-> setProject ( $project )
2025-06-10 12:12:24 +00:00
-> setUsageRetentionHourlyDateTime ( DatabaseDateTime :: addSeconds ( new \DateTime (), - 1 * $usageStatsRetentionHourly ))
2025-04-11 14:52:19 +00:00
-> trigger ();
},
[
2025-04-14 06:23:35 +00:00
Query :: equal ( 'region' , [ System :: getEnv ( '_APP_REGION' , 'default' )]),
2025-04-11 14:52:19 +00:00
Query :: limit ( 100 ),
2025-06-11 14:36:51 +00:00
Query :: greaterThanEqual ( 'accessedAt' , DatabaseDateTime :: format ( $before30days )),
2025-06-10 12:12:24 +00:00
Query :: orderAsc ( 'teamInternalId' ),
2025-04-11 14:52:19 +00:00
]
);
2023-12-12 11:26:28 +00:00
2025-03-10 08:54:20 +00:00
$queueForDeletes
-> setType ( DELETE_TYPE_MAINTENANCE )
-> setProject ( $console )
2025-06-10 12:12:24 +00:00
-> setUsageRetentionHourlyDateTime ( DatabaseDateTime :: addSeconds ( new \DateTime (), - 1 * $usageStatsRetentionHourly ))
2025-03-10 08:54:20 +00:00
-> trigger ();
2023-06-05 16:13:00 +00:00
$this -> notifyDeleteConnections ( $queueForDeletes );
2026-04-10 08:58:31 +00:00
$this -> renewCertificates ( $dbForPlatform , $publisherForCertificates );
2023-06-05 16:13:00 +00:00
$this -> notifyDeleteCache ( $cacheRetention , $queueForDeletes );
$this -> notifyDeleteSchedules ( $schedulesDeletionRetention , $queueForDeletes );
2025-11-14 02:55:41 +00:00
$this -> notifyDeleteCSVExports ( $queueForDeletes );
2026-03-04 13:31:27 +00:00
};
if ( $type === 'loop' ) {
Console :: info ( 'Setting loop start time to ' . $next -> format ( " Y-m-d H:i:s.v " ) . '. Delaying for ' . $delay . ' seconds.' );
Console :: loop ( function () use ( $action ) {
$action ();
}, $interval , $delay );
} elseif ( $type === 'trigger' ) {
$action ();
}
2023-06-05 16:13:00 +00:00
}
2021-01-26 20:49:13 +00:00
2023-06-05 16:13:00 +00:00
private function notifyDeleteConnections ( Delete $queueForDeletes ) : void
{
2024-01-11 03:15:11 +00:00
$queueForDeletes
2023-06-05 16:13:00 +00:00
-> setType ( DELETE_TYPE_REALTIME )
2025-06-10 12:12:24 +00:00
-> setDatetime ( DatabaseDateTime :: addSeconds ( new \DateTime (), - 60 ))
2023-06-05 16:13:00 +00:00
-> trigger ();
}
2022-04-10 09:38:22 +00:00
2025-11-14 02:55:41 +00:00
private function notifyDeleteCSVExports ( Delete $queueForDeletes ) : void
{
$queueForDeletes
-> setType ( DELETE_TYPE_CSV_EXPORTS )
-> trigger ();
}
2026-04-10 08:58:31 +00:00
private function renewCertificates ( Database $dbForPlatform , Certificate $publisherForCertificate ) : void
2023-06-05 16:13:00 +00:00
{
2025-06-10 12:12:24 +00:00
$time = DatabaseDateTime :: now ();
2023-06-05 16:13:00 +00:00
2024-12-12 10:30:26 +00:00
$certificates = $dbForPlatform -> find ( 'certificates' , [
2023-06-05 16:13:00 +00:00
Query :: lessThan ( 'attempts' , 5 ), // Maximum 5 attempts
2024-10-22 11:57:25 +00:00
Query :: isNotNull ( 'renewDate' ),
2023-06-05 16:13:00 +00:00
Query :: lessThanEqual ( 'renewDate' , $time ), // includes 60 days cooldown (we have 30 days to renew)
Query :: limit ( 200 ), // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains)
]);
2022-07-03 09:36:59 +00:00
2025-12-29 09:21:59 +00:00
if ( \count ( $certificates ) === 0 ) {
Console :: info ( " [ { $time } ] No certificates for renewal. " );
return ;
}
2022-11-16 12:51:43 +00:00
2025-12-29 09:21:59 +00:00
Console :: info ( " [ { $time } ] Found " . \count ( $certificates ) . " certificates for renewal, scheduling jobs. " );
2023-06-05 16:13:00 +00:00
2025-12-29 09:21:59 +00:00
$isMd5 = System :: getEnv ( '_APP_RULES_FORMAT' ) === 'md5' ;
$appRegion = System :: getEnv ( '_APP_REGION' , 'default' );
foreach ( $certificates as $certificate ) {
$domain = $certificate -> getAttribute ( 'domain' );
$rule = $isMd5 ?
$dbForPlatform -> getDocument ( 'rules' , md5 ( $domain )) :
$dbForPlatform -> findOne ( 'rules' , [
2025-07-29 11:23:55 +00:00
Query :: equal ( 'domain' , [ $domain ]),
2025-12-29 09:21:59 +00:00
Query :: limit ( 1 )
2025-07-29 11:23:55 +00:00
]);
2025-07-29 09:51:15 +00:00
2025-12-29 09:21:59 +00:00
if ( $rule -> isEmpty () || $rule -> getAttribute ( 'region' ) !== $appRegion ) {
continue ;
2023-06-05 16:13:00 +00:00
}
2025-12-29 09:21:59 +00:00
2026-04-10 08:58:31 +00:00
$publisherForCertificate -> enqueue ( new \Appwrite\Event\Message\Certificate (
project : new Document ([
'$id' => $rule -> getAttribute ( 'projectId' , '' ),
'$sequence' => $rule -> getAttribute ( 'projectInternalId' , 0 ),
]),
domain : new Document ([
2025-12-29 09:21:59 +00:00
'domain' => $rule -> getAttribute ( 'domain' ),
'domainType' => $rule -> getAttribute ( 'deploymentResourceType' , $rule -> getAttribute ( 'type' )),
2026-04-10 08:58:31 +00:00
]),
action : \Appwrite\Event\Certificate :: ACTION_GENERATION ,
));
2022-11-16 12:51:43 +00:00
}
2023-06-05 16:13:00 +00:00
}
2022-11-16 12:51:43 +00:00
2023-06-05 16:13:00 +00:00
private function notifyDeleteCache ( $interval , Delete $queueForDeletes ) : void
{
2024-01-15 06:52:40 +00:00
$queueForDeletes
2023-06-05 16:13:00 +00:00
-> setType ( DELETE_TYPE_CACHE_BY_TIMESTAMP )
2025-06-10 12:12:24 +00:00
-> setDatetime ( DatabaseDateTime :: addSeconds ( new \DateTime (), - 1 * $interval ))
2023-06-05 16:13:00 +00:00
-> trigger ();
}
2020-12-14 21:26:37 +00:00
2023-06-05 16:13:00 +00:00
private function notifyDeleteSchedules ( $interval , Delete $queueForDeletes ) : void
{
2024-01-15 06:52:40 +00:00
$queueForDeletes
2023-06-05 16:13:00 +00:00
-> setType ( DELETE_TYPE_SCHEDULES )
2025-06-10 12:12:24 +00:00
-> setDatetime ( DatabaseDateTime :: addSeconds ( new \DateTime (), - 1 * $interval ))
2023-06-05 16:13:00 +00:00
-> trigger ();
2022-07-13 07:02:55 +00:00
}
2022-07-14 02:04:31 +00:00
}