1
0
Fork 0
mirror of synced 2024-06-29 19:50:26 +12:00
appwrite/app/executor.php

781 lines
29 KiB
PHP
Raw Normal View History

2021-08-24 21:32:27 +12:00
<?php
2022-05-24 02:54:50 +12:00
2021-08-24 21:32:27 +12:00
require_once __DIR__ . '/../vendor/autoload.php';
2022-02-15 05:37:56 +13:00
use Appwrite\Runtimes\Runtimes;
use Swoole\ConnectionPool;
2021-08-24 21:32:27 +12:00
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
2022-01-24 06:21:23 +13:00
use Swoole\Http\Server;
use Swoole\Process;
2022-02-15 10:26:31 +13:00
use Swoole\Runtime;
use Swoole\Timer;
2022-01-24 06:21:23 +13:00
use Utopia\App;
use Utopia\CLI\Console;
2022-01-19 22:33:48 +13:00
use Utopia\Logger\Log;
2022-02-19 04:58:04 +13:00
use Utopia\Logger\Logger;
2022-01-24 06:21:23 +13:00
use Utopia\Orchestration\Adapter\DockerCLI;
use Utopia\Orchestration\Orchestration;
2022-02-18 00:40:02 +13:00
use Utopia\Storage\Device;
2022-01-24 06:21:23 +13:00
use Utopia\Storage\Device\Local;
2022-05-10 23:15:56 +12:00
use Utopia\Storage\Device\Backblaze;
2022-02-18 00:40:02 +13:00
use Utopia\Storage\Device\DOSpaces;
2022-03-19 05:03:04 +13:00
use Utopia\Storage\Device\Linode;
2022-03-19 06:17:43 +13:00
use Utopia\Storage\Device\Wasabi;
2022-02-18 00:40:02 +13:00
use Utopia\Storage\Device\S3;
2022-01-24 06:21:23 +13:00
use Utopia\Storage\Storage;
use Utopia\Swoole\Request;
2022-02-14 01:03:25 +13:00
use Utopia\Swoole\Response;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
2022-02-18 13:43:04 +13:00
use Utopia\Validator\Boolean;
2022-03-04 10:14:44 +13:00
use Utopia\Validator\Range;
2022-01-24 06:21:23 +13:00
use Utopia\Validator\Text;
2021-08-27 21:21:28 +12:00
2022-02-15 05:37:56 +13:00
2022-02-16 04:59:26 +13:00
Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
2022-01-21 00:34:50 +13:00
2022-02-15 13:23:20 +13:00
/** Constants */
2022-03-04 10:14:44 +13:00
const MAINTENANCE_INTERVAL = 3600; // 3600 seconds = 1 hour
2022-02-15 13:23:20 +13:00
2022-02-15 11:15:08 +13:00
/**
2022-05-24 02:54:50 +12:00
* Create a Swoole table to store runtime information
2022-02-15 11:15:08 +13:00
*/
2022-02-15 13:23:20 +13:00
$activeRuntimes = new Swoole\Table(1024);
$activeRuntimes->column('id', Swoole\Table::TYPE_STRING, 256);
2022-02-15 13:23:20 +13:00
$activeRuntimes->column('created', Swoole\Table::TYPE_INT, 8);
$activeRuntimes->column('updated', Swoole\Table::TYPE_INT, 8);
$activeRuntimes->column('name', Swoole\Table::TYPE_STRING, 128);
$activeRuntimes->column('status', Swoole\Table::TYPE_STRING, 128);
$activeRuntimes->column('key', Swoole\Table::TYPE_STRING, 256);
2022-02-15 13:23:20 +13:00
$activeRuntimes->create();
2022-02-15 11:15:08 +13:00
/**
* Create orchestration pool
*/
$orchestrationPool = new ConnectionPool(function () {
$dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null);
$dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null);
$orchestration = new Orchestration(new DockerCLI($dockerUser, $dockerPass));
return $orchestration;
}, 10);
2022-02-19 04:58:04 +13:00
/**
* Create logger instance
*/
$providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
$logger = null;
2022-05-24 02:54:50 +12:00
if (!empty($providerName) && !empty($providerConfig) && Logger::hasProvider($providerName)) {
$classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName);
2022-02-19 04:58:04 +13:00
$adapter = new $classname($providerConfig);
$logger = new Logger($adapter);
}
2022-01-21 23:42:12 +13:00
function logError(Throwable $error, string $action, Utopia\Route $route = null)
{
2022-02-19 04:58:04 +13:00
global $logger;
2022-01-21 00:34:50 +13:00
2022-01-21 23:42:12 +13:00
if ($logger) {
2022-01-19 05:55:53 +13:00
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$log = new Log();
$log->setNamespace("executor");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
2021-08-27 21:21:28 +12:00
2022-01-21 23:42:12 +13:00
if ($route) {
2022-01-19 05:55:53 +13:00
$log->addTag('method', $route->getMethod());
2022-05-24 02:54:50 +12:00
$log->addTag('url', $route->getPath());
2022-01-19 05:55:53 +13:00
}
2022-01-19 05:55:53 +13:00
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
2022-01-19 05:55:53 +13:00
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
2022-01-19 05:55:53 +13:00
$log->setAction($action);
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$responseCode = $logger->addLog($log);
2022-01-21 23:42:12 +13:00
Console::info('Executor log pushed with status code: ' . $responseCode);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $error->getMessage());
Console::error('[Error] File: ' . $error->getFile());
Console::error('[Error] Line: ' . $error->getLine());
2022-05-31 05:41:25 +12:00
}
2022-01-17 23:18:31 +13:00
2022-05-24 02:54:50 +12:00
function getStorageDevice($root): Device
{
2022-02-18 00:40:02 +13:00
switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) {
2022-05-24 02:54:50 +12:00
case Storage::DEVICE_LOCAL:
default:
2022-02-18 00:40:02 +13:00
return new Local($root);
case Storage::DEVICE_S3:
2022-02-23 01:33:35 +13:00
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
2022-02-18 00:40:02 +13:00
$s3Acl = 'private';
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
case Storage::DEVICE_DO_SPACES:
2022-02-23 01:33:35 +13:00
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
2022-02-18 00:40:02 +13:00
$doSpacesAcl = 'private';
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
2022-05-13 10:01:53 +12:00
case Storage::DEVICE_BACKBLAZE:
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
$backblazeAcl = 'private';
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
2022-03-19 05:03:04 +13:00
case Storage::DEVICE_LINODE:
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
$linodeAcl = 'private';
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
2022-03-19 06:17:43 +13:00
case Storage::DEVICE_WASABI:
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
$wasabiAcl = 'private';
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
2022-02-18 00:40:02 +13:00
}
}
2022-02-14 01:31:44 +13:00
App::post('/v1/runtimes')
->desc("Create a new runtime server")
2022-02-19 13:15:03 +13:00
->param('runtimeId', '', new Text(64), 'Unique runtime ID.')
2022-02-14 01:31:44 +13:00
->param('source', '', new Text(0), 'Path to source files.')
2022-02-18 13:43:04 +13:00
->param('destination', '', new Text(0), 'Destination folder to store build files into.', true)
2022-04-25 19:23:25 +12:00
->param('vars', [], new Assoc(), 'Environment Variables required for the build.')
2022-05-03 20:47:50 +12:00
->param('commands', [], new ArrayList(new Text(1024), 100), 'Commands required to build the container. Maximum of 100 commands are allowed, each 1024 characters long.')
2022-04-25 19:23:25 +12:00
->param('runtime', '', new Text(128), 'Runtime for the cloud function.')
->param('baseImage', '', new Text(128), 'Base image name of the runtime.')
->param('entrypoint', '', new Text(256), 'Entrypoint of the code file.', true)
->param('remove', false, new Boolean(), 'Remove a runtime after execution.')
->param('workdir', '', new Text(256), 'Working directory.', true)
->inject('orchestrationPool')
2022-02-15 13:23:20 +13:00
->inject('activeRuntimes')
2022-02-14 01:31:44 +13:00
->inject('response')
->action(function (string $runtimeId, string $source, string $destination, array $vars, array $commands, string $runtime, string $baseImage, string $entrypoint, bool $remove, string $workdir, $orchestrationPool, $activeRuntimes, Response $response) {
2022-02-19 13:15:03 +13:00
if ($activeRuntimes->exists($runtimeId)) {
if ($activeRuntimes->get($runtimeId)['status'] == 'pending') {
throw new \Exception('A runtime with the same ID is already being created. Attempt a execution soon.', 500);
}
throw new Exception('Runtime already exists.', 409);
}
2022-02-14 01:31:44 +13:00
2022-02-19 04:58:04 +13:00
$container = [];
$containerId = '';
$stdout = '';
$stderr = '';
$startTime = \time();
$endTime = 0;
2022-03-01 03:15:10 +13:00
$orchestration = $orchestrationPool->get();
2022-02-14 01:31:44 +13:00
$secret = \bin2hex(\random_bytes(16));
if (!$remove) {
$activeRuntimes->set($runtimeId, [
'id' => $containerId,
'name' => $runtimeId,
'created' => $startTime,
'updated' => $endTime,
'status' => 'pending',
'key' => $secret,
]);
}
2022-02-14 01:31:44 +13:00
try {
2022-02-19 13:15:03 +13:00
Console::info('Building container : ' . $runtimeId);
2022-05-24 02:54:50 +12:00
/**
* Temporary file paths in the executor
2022-02-14 01:31:44 +13:00
*/
2022-02-24 01:22:18 +13:00
$tmpSource = "/tmp/$runtimeId/src/code.tar.gz";
2022-02-14 01:31:44 +13:00
$tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz";
/**
2022-02-14 10:26:36 +13:00
* Copy code files from source to a temporary location on the executor
2022-02-14 01:31:44 +13:00
*/
2022-02-22 18:56:50 +13:00
$sourceDevice = getStorageDevice("/");
$localDevice = new Local();
$buffer = $sourceDevice->read($source);
2022-05-24 02:54:50 +12:00
if (!$localDevice->write($tmpSource, $buffer)) {
throw new Exception('Failed to copy source code to temporary directory', 500);
2022-02-14 10:26:36 +13:00
};
2022-02-14 01:31:44 +13:00
/**
2022-02-18 10:40:28 +13:00
* Create the mount folder
2022-02-14 01:31:44 +13:00
*/
2022-02-18 10:40:28 +13:00
if (!\file_exists(\dirname($tmpBuild))) {
if (!@\mkdir(\dirname($tmpBuild), 0755, true)) {
throw new Exception("Failed to create temporary directory", 500);
2022-02-14 01:31:44 +13:00
}
}
2021-08-24 21:32:27 +12:00
2022-02-14 01:31:44 +13:00
/**
* Create container
*/
2022-02-18 13:43:04 +13:00
$vars = \array_merge($vars, [
2022-02-19 12:32:09 +13:00
'INTERNAL_RUNTIME_KEY' => $secret,
'INTERNAL_RUNTIME_ENTRYPOINT' => $entrypoint,
2022-02-18 13:43:04 +13:00
]);
2022-02-14 01:31:44 +13:00
$vars = array_map(fn ($v) => strval($v), $vars);
$orchestration
->setCpus((int) App::getEnv('_APP_FUNCTIONS_CPUS', 0))
->setMemory((int) App::getEnv('_APP_FUNCTIONS_MEMORY', 0))
->setSwap((int) App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 0));
2022-05-24 02:54:50 +12:00
2022-02-22 01:15:09 +13:00
/** Keep the container alive if we have commands to be executed */
$entrypoint = !empty($commands) ? [
2022-02-21 09:25:35 +13:00
'tail',
'-f',
'/dev/null'
2022-02-22 01:15:09 +13:00
] : [];
2022-02-22 00:08:58 +13:00
2022-02-19 04:58:04 +13:00
$containerId = $orchestration->run(
2022-02-14 01:31:44 +13:00
image: $baseImage,
2022-02-19 13:15:03 +13:00
name: $runtimeId,
hostname: $runtimeId,
2022-02-14 01:31:44 +13:00
vars: $vars,
2022-02-21 09:25:35 +13:00
command: $entrypoint,
2022-02-14 01:31:44 +13:00
labels: [
'openruntimes-id' => $runtimeId,
2022-02-19 04:25:54 +13:00
'openruntimes-type' => 'runtime',
2022-02-19 04:58:04 +13:00
'openruntimes-created' => strval($startTime),
2022-02-14 01:31:44 +13:00
'openruntimes-runtime' => $runtime,
],
2022-02-18 10:40:28 +13:00
workdir: $workdir,
2022-02-14 01:31:44 +13:00
volumes: [
2022-05-24 02:54:50 +12:00
\dirname($tmpSource) . ':/tmp:rw',
\dirname($tmpBuild) . ':/usr/code:rw'
2022-02-14 01:31:44 +13:00
]
);
2021-08-27 22:55:22 +12:00
2022-02-19 04:58:04 +13:00
if (empty($containerId)) {
2022-02-14 01:31:44 +13:00
throw new Exception('Failed to create build container', 500);
}
2021-09-21 03:52:12 +12:00
2022-04-04 20:14:14 +12:00
$orchestration->networkConnect($runtimeId, App::getEnv('OPEN_RUNTIMES_NETWORK', 'appwrite_runtimes'));
2022-02-18 13:43:04 +13:00
2022-05-24 02:54:50 +12:00
/**
2022-02-18 10:40:28 +13:00
* Execute any commands if they were provided
2022-02-14 01:31:44 +13:00
*/
2022-02-18 10:40:28 +13:00
if (!empty($commands)) {
$status = $orchestration->execute(
2022-02-19 13:15:03 +13:00
name: $runtimeId,
2022-02-18 10:40:28 +13:00
command: $commands,
2022-02-19 04:58:04 +13:00
stdout: $stdout,
stderr: $stderr,
timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900)
2022-02-18 10:40:28 +13:00
);
if (!$status) {
2022-02-19 04:58:04 +13:00
throw new Exception('Failed to build dependenices ' . $stderr, 500);
2022-02-18 10:40:28 +13:00
}
2022-02-14 01:31:44 +13:00
}
/**
2022-02-14 10:26:36 +13:00
* Move built code to expected build directory
2022-02-14 01:31:44 +13:00
*/
2022-02-18 10:40:28 +13:00
if (!empty($destination)) {
// Check if the build was successful by checking if file exists
if (!\file_exists($tmpBuild)) {
throw new Exception('Something went wrong during the build process', 500);
2022-02-14 01:31:44 +13:00
}
2022-02-18 10:40:28 +13:00
$destinationDevice = getStorageDevice($destination);
2022-02-22 18:56:50 +13:00
$outputPath = $destinationDevice->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
2022-02-19 12:32:09 +13:00
2022-02-22 18:56:50 +13:00
$buffer = $localDevice->read($tmpBuild);
2022-05-24 02:54:50 +12:00
if (!$destinationDevice->write($outputPath, $buffer, $localDevice->getFileMimeType($tmpBuild))) {
2022-02-19 12:32:09 +13:00
throw new Exception('Failed to move built code to storage', 500);
};
2022-02-18 10:40:28 +13:00
2022-02-19 04:58:04 +13:00
$container['outputPath'] = $outputPath;
2022-02-14 01:31:44 +13:00
}
2022-02-19 04:58:04 +13:00
if (empty($stdout)) {
$stdout = 'Build Successful!';
2022-02-14 01:31:44 +13:00
}
2022-02-19 04:58:04 +13:00
$endTime = \time();
$container = array_merge($container, [
2022-02-14 01:31:44 +13:00
'status' => 'ready',
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
2022-05-05 21:01:08 +12:00
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
2022-02-19 04:58:04 +13:00
'startTime' => $startTime,
'endTime' => $endTime,
'duration' => $endTime - $startTime,
2022-02-18 10:40:28 +13:00
]);
2022-02-14 01:31:44 +13:00
2022-02-18 13:43:04 +13:00
if (!$remove) {
2022-02-19 13:15:03 +13:00
$activeRuntimes->set($runtimeId, [
2022-02-19 04:58:04 +13:00
'id' => $containerId,
2022-02-19 13:15:03 +13:00
'name' => $runtimeId,
2022-02-19 04:58:04 +13:00
'created' => $startTime,
'updated' => $endTime,
'status' => 'Up ' . \round($endTime - $startTime, 2) . 's',
2022-02-18 13:43:04 +13:00
'key' => $secret,
]);
}
2022-02-19 04:58:04 +13:00
Console::success('Build Stage completed in ' . ($endTime - $startTime) . ' seconds');
2022-02-14 01:31:44 +13:00
} catch (Throwable $th) {
Console::error('Build failed: ' . $th->getMessage() . $stdout);
2022-06-09 23:46:08 +12:00
throw new Exception($th->getMessage() . $stdout, 500);
2022-02-14 01:31:44 +13:00
} finally {
2022-03-19 00:42:13 +13:00
// Container cleanup
2022-05-24 02:54:50 +12:00
if ($remove) {
2022-03-19 00:42:13 +13:00
if (!empty($containerId)) {
// If container properly created
$orchestration->remove($containerId, true);
2022-06-09 23:46:08 +12:00
$activeRuntimes->del($runtimeId);
2022-03-19 00:42:13 +13:00
} else {
// If whole creation failed, but container might have been initialized
try {
// Try to remove with contaier name instead of ID
$orchestration->remove($runtimeId, true);
2022-06-09 23:46:08 +12:00
$activeRuntimes->del($runtimeId);
2022-03-19 00:42:13 +13:00
} catch (Throwable $th) {
// If fails, means initialization also failed.
// Contianer is not there, no need to remove
}
}
2022-02-14 01:31:44 +13:00
}
2022-03-19 00:42:13 +13:00
// Release orchestration back to pool, we are done with it
$orchestrationPool->put($orchestration);
2022-02-14 10:26:36 +13:00
}
2022-02-11 23:31:53 +13:00
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
2022-02-19 04:58:04 +13:00
->json($container);
2022-02-11 23:31:53 +13:00
});
App::get('/v1/runtimes')
2022-02-15 06:52:44 +13:00
->desc("List currently active runtimes")
2022-02-15 13:23:20 +13:00
->inject('activeRuntimes')
2022-02-11 23:31:53 +13:00
->inject('response')
2022-02-15 13:23:20 +13:00
->action(function ($activeRuntimes, Response $response) {
2022-02-11 23:31:53 +13:00
$runtimes = [];
2022-05-24 02:54:50 +12:00
foreach ($activeRuntimes as $runtime) {
2022-02-15 06:52:44 +13:00
$runtimes[] = $runtime;
}
2022-02-11 23:31:53 +13:00
$response
2022-02-19 04:58:04 +13:00
->setStatusCode(Response::STATUS_CODE_OK)
2022-02-11 23:31:53 +13:00
->json($runtimes);
});
App::get('/v1/runtimes/:runtimeId')
->desc("Get a runtime by its ID")
2022-02-19 13:15:03 +13:00
->param('runtimeId', '', new Text(64), 'Runtime unique ID.')
2022-02-15 13:23:20 +13:00
->inject('activeRuntimes')
2022-02-11 23:31:53 +13:00
->inject('response')
2022-02-15 13:23:20 +13:00
->action(function ($runtimeId, $activeRuntimes, Response $response) {
2022-02-15 06:52:44 +13:00
2022-05-24 02:54:50 +12:00
if (!$activeRuntimes->exists($runtimeId)) {
2022-02-15 06:52:44 +13:00
throw new Exception('Runtime not found', 404);
}
2022-02-19 13:15:03 +13:00
$runtime = $activeRuntimes->get($runtimeId);
2022-02-11 23:31:53 +13:00
$response
2022-02-19 04:58:04 +13:00
->setStatusCode(Response::STATUS_CODE_OK)
2022-02-11 23:31:53 +13:00
->json($runtime);
});
2022-02-13 11:34:16 +13:00
App::delete('/v1/runtimes/:runtimeId')
->desc('Delete a runtime')
2022-02-19 13:15:03 +13:00
->param('runtimeId', '', new Text(64), 'Runtime unique ID.', false)
->inject('orchestrationPool')
2022-02-15 13:23:20 +13:00
->inject('activeRuntimes')
2022-01-24 06:21:23 +13:00
->inject('response')
->action(function (string $runtimeId, $orchestrationPool, $activeRuntimes, Response $response) {
2022-05-24 02:54:50 +12:00
if (!$activeRuntimes->exists($runtimeId)) {
throw new Exception('Runtime not found', 404);
}
2022-02-19 13:15:03 +13:00
Console::info('Deleting runtime: ' . $runtimeId);
try {
$orchestration = $orchestrationPool->get();
2022-02-19 13:15:03 +13:00
$orchestration->remove($runtimeId, true);
$activeRuntimes->del($runtimeId);
Console::success('Removed runtime container: ' . $runtimeId);
} finally {
$orchestrationPool->put($orchestration);
}
2022-02-13 11:34:16 +13:00
// Remove all the build containers with that same ID
2022-02-13 14:29:28 +13:00
// TODO:: Delete build containers
// foreach ($buildIds as $buildId) {
// try {
// Console::info('Deleting build container : ' . $buildId);
// $status = $orchestration->remove('build-' . $buildId, true);
// } catch (Throwable $th) {
// Console::error($th->getMessage());
// }
// }
$response
->setStatusCode(Response::STATUS_CODE_OK)
->send();
2022-01-24 06:21:23 +13:00
});
2022-02-13 11:34:16 +13:00
App::post('/v1/execution')
->desc('Create an execution')
2022-04-25 19:23:25 +12:00
->param('runtimeId', '', new Text(64), 'The runtimeID to execute.')
->param('vars', [], new Assoc(), 'Environment variables required for the build.')
2022-06-10 22:31:37 +12:00
->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
2022-03-04 10:14:44 +13:00
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.')
2022-02-15 13:23:20 +13:00
->inject('activeRuntimes')
2022-02-13 11:34:16 +13:00
->inject('response')
->action(
2022-02-25 03:31:30 +13:00
function (string $runtimeId, array $vars, string $data, $timeout, $activeRuntimes, Response $response) {
2022-02-19 13:15:03 +13:00
if (!$activeRuntimes->exists($runtimeId)) {
2022-02-15 05:37:56 +13:00
throw new Exception('Runtime not found. Please create the runtime.', 404);
}
for ($i = 0; $i < 5; $i++) {
if ($activeRuntimes->get($runtimeId)['status'] === 'pending') {
Console::info('Waiting for runtime to be ready...');
sleep(1);
} else {
break;
}
if ($i === 4) {
throw new Exception('Runtime failed to launch in allocated time.', 500);
}
}
2022-02-19 13:15:03 +13:00
$runtime = $activeRuntimes->get($runtimeId);
2022-02-15 12:22:16 +13:00
$secret = $runtime['key'];
2022-02-15 05:37:56 +13:00
if (empty($secret)) {
2022-02-17 09:11:05 +13:00
throw new Exception('Runtime secret not found. Please re-create the runtime.', 500);
2022-02-15 05:37:56 +13:00
}
Console::info('Executing Runtime: ' . $runtimeId);
2022-02-17 00:52:54 +13:00
$execution = [];
2022-02-15 05:37:56 +13:00
$executionStart = \microtime(true);
$stdout = '';
$stderr = '';
$statusCode = 0;
$errNo = -1;
$executorResponse = '';
2022-03-04 23:37:56 +13:00
$timeout ??= (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900);
2022-03-04 10:14:44 +13:00
2022-02-17 00:52:54 +13:00
$ch = \curl_init();
$body = \json_encode([
'env' => $vars,
'payload' => $data,
2022-03-04 10:14:44 +13:00
'timeout' => $timeout
2022-02-17 00:52:54 +13:00
]);
2022-02-19 13:15:03 +13:00
\curl_setopt($ch, CURLOPT_URL, "http://" . $runtimeId . ":3000/");
2022-02-17 00:52:54 +13:00
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
2022-03-04 10:14:44 +13:00
\curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
2022-02-17 00:52:54 +13:00
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
'x-internal-challenge: ' . $secret,
'host: null'
]);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$executorResponse = \curl_exec($ch);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$error = \curl_error($ch);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$errNo = \curl_errno($ch);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
\curl_close($ch);
switch (true) {
/** No Error. */
2022-05-24 02:54:50 +12:00
case $errNo === 0:
2022-03-16 03:51:32 +13:00
break;
2022-03-16 01:04:43 +13:00
/** Runtime not ready for requests yet. 111 is the swoole error code for Connection Refused - see https://openswoole.com/docs/swoole-error-code */
case $errNo === 111:
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 406);
2022-03-16 01:04:43 +13:00
/** Any other CURL error */
2022-05-24 02:54:50 +12:00
default:
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500);
2022-02-17 00:52:54 +13:00
}
switch (true) {
case $statusCode >= 500:
$stderr = $executorResponse ?? 'Internal Runtime error.';
break;
case $statusCode >= 100:
$stdout = $executorResponse;
break;
default:
$stderr = $executorResponse ?? 'Execution failed.';
break;
2022-02-15 05:37:56 +13:00
}
2022-02-17 00:52:54 +13:00
$executionEnd = \microtime(true);
$executionTime = ($executionEnd - $executionStart);
$functionStatus = ($statusCode >= 500) ? 'failed' : 'completed';
2022-02-17 00:52:54 +13:00
Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus);
2022-04-28 01:57:03 +12:00
2022-02-17 00:52:54 +13:00
$execution = [
'status' => $functionStatus,
'statusCode' => $statusCode,
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
2022-04-28 01:57:03 +12:00
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
2022-02-17 00:52:54 +13:00
'time' => $executionTime,
];
/** Update swoole table */
$runtime['updated'] = \time();
2022-02-19 13:15:03 +13:00
$activeRuntimes->set($runtimeId, $runtime);
2022-02-13 11:34:16 +13:00
$response
->setStatusCode(Response::STATUS_CODE_OK)
->json($execution);
}
);
2022-01-19 05:55:53 +13:00
App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
2022-01-24 06:21:23 +13:00
$http = new Server("0.0.0.0", 80);
2021-08-24 21:32:27 +12:00
/** Set Resources */
App::setResource('orchestrationPool', fn() => $orchestrationPool);
2022-02-15 13:23:20 +13:00
App::setResource('activeRuntimes', fn() => $activeRuntimes);
/** Set callbacks */
2022-02-19 04:58:04 +13:00
App::error(function ($utopia, $error, $request, $response) {
$route = $utopia->match($request);
logError($error, "httpError", $route);
2022-05-24 02:54:50 +12:00
2022-02-16 12:39:09 +13:00
switch ($error->getCode()) {
case 400: // Error allowed publicly
case 401: // Error allowed publicly
case 402: // Error allowed publicly
case 403: // Error allowed publicly
case 404: // Error allowed publicly
2022-02-18 13:43:04 +13:00
case 406: // Error allowed publicly
2022-02-16 12:39:09 +13:00
case 409: // Error allowed publicly
case 412: // Error allowed publicly
2022-02-18 13:43:04 +13:00
case 425: // Error allowed publicly
2022-02-16 12:39:09 +13:00
case 429: // Error allowed publicly
case 501: // Error allowed publicly
case 503: // Error allowed publicly
$code = $error->getCode();
break;
default:
$code = 500; // All other errors get the generic 500 server error status code
}
2022-05-24 02:54:50 +12:00
$output = [
'message' => $error->getMessage(),
'code' => $error->getCode(),
'file' => $error->getFile(),
'line' => $error->getLine(),
'trace' => $error->getTrace(),
2022-02-22 00:08:58 +13:00
'version' => App::getEnv('_APP_VERSION', 'UNKNOWN')
];
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
2022-02-16 12:39:09 +13:00
->setStatusCode($code);
$response->json($output);
2022-02-19 04:58:04 +13:00
}, ['utopia', 'error', 'request', 'response']);
2022-02-15 05:37:56 +13:00
App::init(function ($request, $response) {
$secretKey = $request->getHeader('x-appwrite-executor-key', '');
2022-05-24 02:54:50 +12:00
if (empty($secretKey)) {
throw new Exception('Missing executor key', 401);
}
2022-02-15 05:37:56 +13:00
2022-05-24 02:54:50 +12:00
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
2022-02-15 05:37:56 +13:00
throw new Exception('Missing executor key', 401);
2022-05-24 02:54:50 +12:00
}
2022-02-15 05:37:56 +13:00
}, ['request', 'response']);
2022-02-15 10:26:31 +13:00
2022-02-15 12:22:16 +13:00
$http->on('start', function ($http) {
2022-02-15 13:40:16 +13:00
global $orchestrationPool;
global $activeRuntimes;
2022-05-17 01:05:58 +12:00
2022-05-24 02:54:50 +12:00
/**
2022-02-15 11:15:08 +13:00
* Warmup: make sure images are ready to run fast 🚀
*/
2022-05-17 01:05:58 +12:00
$runtimes = new Runtimes('v1');
2022-02-15 11:15:08 +13:00
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
$runtimes = $runtimes->getAll(true, $allowList);
foreach ($runtimes as $runtime) {
go(function () use ($runtime, $orchestrationPool) {
try {
$orchestration = $orchestrationPool->get();
Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...');
2022-02-28 07:41:43 +13:00
$response = $orchestration->pull($runtime['image']);
if ($response) {
Console::success("Successfully Warmed up {$runtime['name']} {$runtime['version']}!");
} else {
Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!");
}
2022-02-15 11:15:08 +13:00
} catch (\Throwable $th) {
} finally {
$orchestrationPool->put($orchestration);
}
});
}
/**
2022-02-15 13:40:16 +13:00
* Remove residual runtimes
2022-02-15 11:15:08 +13:00
*/
2022-02-15 13:40:16 +13:00
Console::info('Removing orphan runtimes...');
2022-02-15 11:15:08 +13:00
try {
$orchestration = $orchestrationPool->get();
2022-02-19 04:25:54 +13:00
$orphans = $orchestration->list(['label' => 'openruntimes-type=runtime']);
2022-02-15 11:15:08 +13:00
} finally {
$orchestrationPool->put($orchestration);
}
2022-02-15 13:40:16 +13:00
foreach ($orphans as $runtime) {
go(function () use ($runtime, $orchestrationPool) {
try {
$orchestration = $orchestrationPool->get();
$orchestration->remove($runtime->getName(), true);
Console::success("Successfully removed {$runtime->getName()}");
} catch (\Throwable $th) {
Console::error('Orphan runtime deletion failed: ' . $th->getMessage());
} finally {
$orchestrationPool->put($orchestration);
}
});
2022-02-15 11:15:08 +13:00
}
2022-02-15 10:26:31 +13:00
/**
* Register handlers for shutdown
*/
2022-01-21 23:42:12 +13:00
@Process::signal(SIGINT, function () use ($http) {
$http->shutdown();
});
2022-01-21 23:42:12 +13:00
@Process::signal(SIGQUIT, function () use ($http) {
$http->shutdown();
});
2022-01-21 23:42:12 +13:00
@Process::signal(SIGKILL, function () use ($http) {
$http->shutdown();
});
2022-01-21 23:42:12 +13:00
@Process::signal(SIGTERM, function () use ($http) {
$http->shutdown();
});
2022-02-15 10:26:31 +13:00
/**
2022-02-15 13:23:20 +13:00
* Run a maintenance worker every MAINTENANCE_INTERVAL seconds to remove inactive runtimes
2022-02-15 10:26:31 +13:00
*/
2022-02-15 13:23:20 +13:00
Timer::tick(MAINTENANCE_INTERVAL * 1000, function () use ($orchestrationPool, $activeRuntimes) {
Console::warning("Running maintenance task ...");
foreach ($activeRuntimes as $runtime) {
2022-02-26 03:34:43 +13:00
$inactiveThreshold = \time() - App::getEnv('_APP_FUNCTIONS_INACTIVE_THRESHOLD', 60);
2022-02-15 13:23:20 +13:00
if ($runtime['updated'] < $inactiveThreshold) {
go(function () use ($runtime, $orchestrationPool, $activeRuntimes) {
try {
$orchestration = $orchestrationPool->get();
$orchestration->remove($runtime['name'], true);
$activeRuntimes->del($runtime['name']);
Console::success("Successfully removed {$runtime['name']}");
} catch (\Throwable $th) {
Console::error('Inactive Runtime deletion failed: ' . $th->getMessage());
} finally {
$orchestrationPool->put($orchestration);
}
});
}
}
});
});
2022-02-15 10:26:31 +13:00
2022-05-24 02:54:50 +12:00
$http->on('beforeShutdown', function () {
2022-02-15 10:26:31 +13:00
global $orchestrationPool;
Console::info('Cleaning up containers before shutdown...');
$orchestration = $orchestrationPool->get();
2022-02-19 04:25:54 +13:00
$functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=runtime']);
2022-02-15 10:26:31 +13:00
$orchestrationPool->put($orchestration);
foreach ($functionsToRemove as $container) {
2022-05-24 02:54:50 +12:00
go(function () use ($orchestrationPool, $container) {
2022-02-15 10:26:31 +13:00
try {
$orchestration = $orchestrationPool->get();
$orchestration->remove($container->getId(), true);
Console::info('Removed container ' . $container->getName());
} catch (\Throwable $th) {
Console::error('Failed to remove container: ' . $container->getName());
} finally {
$orchestrationPool->put($orchestration);
}
});
}
});
2022-01-21 23:42:12 +13:00
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) {
2021-08-24 21:32:27 +12:00
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
$app = new App('UTC');
try {
$app->run($request, $response);
2022-02-15 05:37:56 +13:00
} catch (\Throwable $th) {
2022-02-19 04:58:04 +13:00
logError($th, "serverError");
2022-02-15 05:37:56 +13:00
$swooleResponse->setStatusCode(500);
$output = [
2022-05-24 02:54:50 +12:00
'message' => 'Error: ' . $th->getMessage(),
2022-02-15 05:37:56 +13:00
'code' => 500,
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTrace()
];
$swooleResponse->end(\json_encode($output));
2021-08-24 21:32:27 +12:00
}
});
2022-05-05 21:01:08 +12:00
$http->start();