1
0
Fork 0
mirror of synced 2024-07-03 13:41:01 +12:00
appwrite/app/workers/functions.php

394 lines
13 KiB
PHP
Raw Normal View History

2020-05-05 01:34:31 +12:00
<?php
2020-07-17 00:04:06 +12:00
use Appwrite\Database\Database;
use Appwrite\Database\Document;
2020-07-17 00:04:06 +12:00
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Database\Validator\Authorization;
2020-12-08 06:43:39 +13:00
use Appwrite\Event\Event;
use Appwrite\Resque\Worker;
2021-06-28 19:56:03 +12:00
use Appwrite\Utopia\Response\Model\Execution;
2021-01-17 12:38:13 +13:00
use Cron\CronExpression;
2020-11-03 19:53:32 +13:00
use Swoole\Runtime;
2020-07-20 02:43:59 +12:00
use Utopia\App;
2020-05-10 10:12:00 +12:00
use Utopia\CLI\Console;
2020-05-05 01:34:31 +12:00
use Utopia\Config\Config;
2020-05-09 18:26:18 +12:00
require_once __DIR__.'/../workers.php';
2020-05-10 04:39:50 +12:00
2021-04-17 04:50:16 +12:00
Runtime::enableCoroutine(0);
2020-05-10 04:39:50 +12:00
2021-04-15 01:07:26 +12:00
Console::title('Functions V1 Worker');
2020-05-10 04:39:50 +12:00
Console::success(APP_NAME.' functions worker v1 has started');
2021-04-21 23:02:54 +12:00
$runtimes = Config::getParam('runtimes');
2020-05-09 18:26:18 +12:00
2020-11-03 19:53:32 +13:00
/**
* Warmup Docker Images
*/
2020-07-20 18:43:25 +12:00
$warmupStart = \microtime(true);
2021-04-21 23:02:54 +12:00
Co\run(function() use ($runtimes) { // Warmup: make sure images are ready to run fast 🚀
2021-02-02 20:26:11 +13:00
2021-02-02 21:16:08 +13:00
$dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null);
$dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null);
2021-02-02 20:26:11 +13:00
2021-02-02 21:16:08 +13:00
if($dockerUser) {
$stdout = '';
$stderr = '';
Console::execute('docker login --username '.$dockerUser.' --password-stdin', $dockerPass, $stdout, $stderr);
Console::log('Docker Login'. $stdout.$stderr);
}
2021-02-02 20:26:11 +13:00
2021-04-21 23:02:54 +12:00
foreach($runtimes as $runtime) {
go(function() use ($runtime) {
2020-07-16 01:34:28 +12:00
$stdout = '';
$stderr = '';
2021-04-21 23:02:54 +12:00
Console::info('Warming up '.$runtime['name'].' '.$runtime['version'].' environment...');
2020-07-16 01:34:28 +12:00
2021-04-21 23:02:54 +12:00
Console::execute('docker pull '.$runtime['image'], '', $stdout, $stderr);
2020-07-16 01:34:28 +12:00
if(!empty($stdout)) {
Console::log($stdout);
}
if(!empty($stderr)) {
Console::error($stderr);
}
2021-04-17 04:50:16 +12:00
});
2020-05-10 10:12:00 +12:00
}
2021-04-17 04:50:16 +12:00
});
2020-05-05 01:34:31 +12:00
2020-07-20 18:43:25 +12:00
$warmupEnd = \microtime(true);
$warmupTime = $warmupEnd - $warmupStart;
Console::success('Finished warmup in '.$warmupTime.' seconds');
2020-11-03 19:53:32 +13:00
/**
* List function servers
*/
$stdout = '';
$stderr = '';
$executionStart = \microtime(true);
$exitCode = Console::execute('docker ps --all --format "name={{.Names}}&status={{.Status}}&labels={{.Labels}}" --filter label=appwrite-type=function'
, '', $stdout, $stderr, 30);
$executionEnd = \microtime(true);
$list = [];
$stdout = \explode("\n", $stdout);
\array_map(function($value) use (&$list) {
$container = [];
\parse_str($value, $container);
if(isset($container['name'])) {
$container = [
'name' => $container['name'],
'online' => (\substr($container['status'], 0, 2) === 'Up'),
'status' => $container['status'],
'labels' => $container['labels'],
];
\array_map(function($value) use (&$container) {
$value = \explode('=', $value);
if(isset($value[0]) && isset($value[1])) {
$container[$value[0]] = $value[1];
}
}, \explode(',', $container['labels']));
$list[$container['name']] = $container;
}
}, $stdout);
Console::info(count($list)." functions listed in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}");
/**
* 1. Get event args - DONE
* 2. Unpackage code in the isolated container - DONE
* 3. Execute in container with timeout
* + messure execution time - DONE
* + pass env vars - DONE
* + pass one-time api key
2020-07-20 18:43:25 +12:00
* 4. Update execution status - DONE
* 5. Update execution stdout & stderr - DONE
2020-07-23 17:52:03 +12:00
* 6. Trigger audit log - DONE
* 7. Trigger usage log - DONE
*/
//TODO aviod scheduled execution if delay is bigger than X offest
class FunctionsV1 extends Worker
2020-05-05 01:34:31 +12:00
{
public $args = [];
2020-07-22 08:10:31 +12:00
public $allowed = [];
public function init(): void
2020-05-05 01:34:31 +12:00
{
}
public function run(): void
2020-05-05 01:34:31 +12:00
{
global $register;
2020-05-10 22:30:07 +12:00
2021-06-28 19:19:33 +12:00
$db = $register->get('db');
$cache = $register->get('cache');
2020-11-03 19:53:32 +13:00
$projectId = $this->args['projectId'] ?? '';
$functionId = $this->args['functionId'] ?? '';
2021-05-17 23:32:37 +12:00
$webhooks = $this->args['webhooks'] ?? [];
2020-11-03 19:53:32 +13:00
$executionId = $this->args['executionId'] ?? '';
$trigger = $this->args['trigger'] ?? '';
$event = $this->args['event'] ?? '';
2021-01-17 12:38:13 +13:00
$scheduleOriginal = $this->args['scheduleOriginal'] ?? '';
$eventData = (!empty($this->args['eventData'])) ? json_encode($this->args['eventData']) : '';
$data = $this->args['data'] ?? '';
$userId = $this->args['userId'] ?? '';
$jwt = $this->args['jwt'] ?? '';
2020-07-16 08:29:55 +12:00
$database = new Database();
2021-06-28 19:19:33 +12:00
$database->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
$database->setNamespace('app_'.$projectId);
$database->setMocks(Config::getParam('collections', []));
2020-07-17 00:04:06 +12:00
switch ($trigger) {
case 'event':
$limit = 30;
$sum = 30;
$offset = 0;
$functions = []; /** @var Document[] $functions */
while ($sum >= $limit) {
Authorization::disable();
$functions = $database->getCollection([
'limit' => $limit,
'offset' => $offset,
'orderField' => 'name',
'orderType' => 'ASC',
'orderCast' => 'string',
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_FUNCTIONS,
],
]);
Authorization::reset();
$sum = \count($functions);
$offset = $offset + $limit;
Console::log('Fetched '.$sum.' functions...');
2020-08-05 17:18:45 +12:00
foreach($functions as $function) {
$events = $function->getAttribute('events', []);
$tag = $function->getAttribute('tag', []);
Console::success('Itterating function: '.$function->getAttribute('name'));
if(!\in_array($event, $events) || empty($tag)) {
continue;
}
Console::success('Triggered function: '.$event);
2021-05-17 23:32:37 +12:00
$this->execute('event', $projectId, '', $database, $function, $event, $eventData, $data, $webhooks, $userId, $jwt);
2020-08-05 17:18:45 +12:00
}
}
break;
case 'schedule':
2020-10-02 21:25:57 +13:00
/*
2020-11-03 19:53:32 +13:00
* 1. Get Original Task
* 2. Check for updates
* If has updates skip task and don't reschedule
* If status not equal to play skip task
* 3. Check next run date, update task and add new job at the given date
* 4. Execute task (set optional timeout)
* 5. Update task response to log
* On success reset error count
* On failure add error count
* If error count bigger than allowed change status to pause
*/
2021-01-17 12:38:13 +13:00
// Reschedule
Authorization::disable();
$function = $database->getDocument($functionId);
Authorization::reset();
if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) {
throw new Exception('Function not found ('.$functionId.')');
}
if($scheduleOriginal && $scheduleOriginal !== $function->getAttribute('schedule')) { // Schedule has changed from previous run, ignore this run.
return;
}
2021-02-22 10:37:22 +13:00
$cron = new CronExpression($function->getAttribute('schedule'));
2021-01-17 12:38:13 +13:00
$next = (int) $cron->getNextRunDate()->format('U');
$function
->setAttribute('scheduleNext', $next)
->setAttribute('schedulePrevious', \time())
;
2021-01-17 13:07:43 +13:00
Authorization::disable();
2021-01-17 12:38:13 +13:00
$function = $database->updateDocument(array_merge($function->getArrayCopy(), [
'scheduleNext' => $next,
]));
2021-01-17 13:07:43 +13:00
Authorization::reset();
2021-01-17 12:38:13 +13:00
ResqueScheduler::enqueueAt($next, 'v1-functions', 'FunctionsV1', [
'projectId' => $projectId,
2021-05-18 03:52:22 +12:00
'webhooks' => $webhooks,
2021-01-17 12:38:13 +13:00
'functionId' => $function->getId(),
'executionId' => null,
'trigger' => 'schedule',
'scheduleOriginal' => $function->getAttribute('schedule', ''),
]); // Async task rescheduale
2021-05-17 23:32:37 +12:00
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$eventData*/'', $data, $webhooks, $userId, $jwt);
break;
case 'http':
Authorization::disable();
$function = $database->getDocument($functionId);
Authorization::reset();
if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) {
2020-12-11 03:35:39 +13:00
throw new Exception('Function not found ('.$functionId.')');
}
2020-07-17 00:04:06 +12:00
2021-05-17 23:32:37 +12:00
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$eventData*/'', $data, $webhooks, $userId, $jwt);
break;
default:
# code...
break;
2020-07-17 00:04:06 +12:00
}
}
2020-11-03 19:53:32 +13:00
/**
* Execute function tag
*
* @param string $trigger
* @param string $projectId
* @param string $executionId
* @param Database $database
* @param Database $function
* @param string $event
2021-03-25 02:36:36 +13:00
* @param string $eventData
* @param string $data
2021-05-18 03:52:22 +12:00
* @param array $webhooks
* @param string $userId
* @param string $jwt
2020-11-03 19:53:32 +13:00
*
* @return void
*/
2021-05-17 23:32:37 +12:00
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): void
{
2021-08-24 21:32:27 +12:00
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://executor:8080/v1/execute");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'trigger' => $trigger,
'projectId' => $projectId,
'executionId' => $executionId,
2020-08-05 17:18:45 +12:00
'functionId' => $function->getId(),
2021-08-24 21:32:27 +12:00
'event' => $event,
'eventData' => $eventData,
'data' => $data,
'webhooks' => $webhooks,
'userId' => $userId,
'jwt' => $jwt,
2020-07-17 09:51:26 +12:00
]));
2021-08-24 21:32:27 +12:00
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + 200); // + 200 for safety margin
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
]);
2020-07-17 09:51:26 +12:00
2021-08-27 21:21:28 +12:00
\curl_exec($ch);
2020-07-17 09:51:26 +12:00
2021-08-24 21:32:27 +12:00
$error = \curl_error($ch);
if (!empty($error)) {
Console::error('Curl error: '.$error);
}
2020-07-21 22:33:23 +12:00
2021-08-24 21:32:27 +12:00
\curl_close($ch);
2020-11-03 19:53:32 +13:00
}
/**
* Cleanup any hanging containers above the allowed max containers.
*
* @return void
*/
public function cleanup(): void
{
global $list;
Console::success(count($list).' running containers counted');
2020-07-17 03:20:51 +12:00
2020-07-20 02:43:59 +12:00
$max = (int) App::getEnv('_APP_FUNCTIONS_CONTAINERS');
2020-07-20 18:43:25 +12:00
if(\count($list) > $max) {
Console::info('Starting containers cleanup');
2021-06-15 07:18:43 +12:00
\uasort($list, function ($item1, $item2) {
2020-12-30 23:44:33 +13:00
return (int)($item1['appwrite-created'] ?? 0) <=> (int)($item2['appwrite-created'] ?? 0);
});
2020-12-30 23:44:33 +13:00
while(\count($list) > $max) {
$first = \array_shift($list);
$stdout = '';
$stderr = '';
2020-12-30 23:44:33 +13:00
if(Console::execute("docker rm -f {$first['name']}", '', $stdout, $stderr, 30) !== 0) {
Console::error('Failed to remove container: '.$stderr);
}
2020-12-30 23:44:33 +13:00
else {
Console::info('Removed container: '.$first['name']);
}
}
}
2020-05-05 01:34:31 +12:00
}
2020-11-03 19:53:32 +13:00
/**
* Filter ENV vars
*
* @param string $string
*
* @return string
*/
2020-07-22 08:10:31 +12:00
public function filterEnvKey(string $string): string
{
if(empty($this->allowed)) {
$this->allowed = array_fill_keys(\str_split('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'), true);
2020-07-22 08:10:31 +12:00
}
$string = \str_split($string);
2020-07-22 08:10:31 +12:00
$output = '';
foreach ($string as $char) {
if(\array_key_exists($char, $this->allowed)) {
2020-07-22 08:10:31 +12:00
$output .= $char;
}
}
return $output;
}
public function shutdown(): void
2020-05-05 01:34:31 +12:00
{
}
}