From 3a203c27defa680811c2de101f30998186491ea5 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Wed, 2 Feb 2022 22:58:03 +0400 Subject: [PATCH 01/61] feat: remove database dependencies from the create runtime endpoint --- app/executor.php | 358 +++++++++++------------------------------ app/workers/builds.php | 311 +++++++++++++++++++++++++++-------- composer.lock | 40 ++--- tests/e2e/Client.php | 4 +- 4 files changed, 361 insertions(+), 352 deletions(-) diff --git a/app/executor.php b/app/executor.php index 982a9b8cee..22e379335c 100644 --- a/app/executor.php +++ b/app/executor.php @@ -32,6 +32,7 @@ use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\Swoole\Request; use Utopia\Validator\ArrayList; +use Utopia\Validator\Assoc; use Utopia\Validator\JSON; use Utopia\Validator\Text; @@ -152,93 +153,33 @@ try { call_user_func($logError, $error, "startupError"); } -function createRuntimeServer(string $functionId, string $projectId, string $deploymentId, Database $database): void +function createRuntimeServer(string $projectId, string $deploymentId, array $build, array $vars, string $baseImage, string $runtime): array { global $orchestrationPool; - global $runtimes; global $activeFunctions; + $orchestration = $orchestrationPool->get(); + try { - $orchestration = $orchestrationPool->get(); - $function = $database->getDocument('functions', $functionId); - $deployment = $database->getDocument('deployments', $deploymentId); - - if ($deployment->getAttribute('buildId') === null) { - throw new Exception('Deployment has no buildId'); - } - - // Grab Build Document - $build = $database->getDocument('builds', $deployment->getAttribute('buildId')); - - // Check if function isn't already created - $functions = $orchestration->list(['label' => 'appwrite-type=function', 'name' => 'appwrite-function-' . $deployment->getId()]); - - if (\count($functions) > 0) { - return; - } - - // Generate random secret key - $secret = \bin2hex(\random_bytes(16)); - - // Check if runtime is active - $runtime = $runtimes[$function->getAttribute('runtime', '')] ?? null; - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception('deployment not found', 404); - } - - if (\is_null($runtime)) { - throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); - } - - // Process environment variables - $vars = \array_merge($function->getAttribute('vars', []), [ - 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), - 'APPWRITE_FUNCTION_ID' => $function->getId(), - 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), - 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], - 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], - 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, - 'INTERNAL_RUNTIME_KEY' => $secret - ]); - - $container = 'appwrite-function-' . $deployment->getId(); + $container = 'appwrite-function-' . $deploymentId; if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online // If container is online then stop and remove it try { $orchestration->remove($container, true); } catch (Exception $e) { - try { - throw new Exception('Failed to remove container: ' . $e->getMessage()); - } catch (Throwable $error) { - logError($error, "createRuntimeServer"); - } + throw new Exception('Failed to remove container: ' . $e->getMessage()); } $activeFunctions->del($container); } - // Check if deployment hasn't failed - if ($build->getAttribute('status') === 'failed') { - throw new Exception('Deployment build failed, please check your logs.', 500); - } - - // Check if deployment is built yet. - if ($build->getAttribute('status') !== 'ready') { - throw new Exception('Deployment is not built yet', 500); - } - - // Grab Deployment Files - $deploymentPath = $build->getAttribute('outputPath', ''); - - $deploymentPathTarget = '/tmp/project-' . $projectId . '/' . $build->getId() . '/builtCode/code.tar.gz'; + /** Storage stuff */ + $deploymentPath = $build['outputPath']; + $deploymentPathTarget = '/tmp/project-' . $projectId . '/' . $build['$id'] . '/builtCode/code.tar.gz'; $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); - $container = 'appwrite-function-' . $deployment->getId(); $device = Storage::getDevice('builds'); - if (!\file_exists($deploymentPathTargetDir)) { if (@\mkdir($deploymentPathTargetDir, 0777, true)) { \chmod($deploymentPathTargetDir, 0777); @@ -257,38 +198,43 @@ function createRuntimeServer(string $functionId, string $projectId, string $depl \file_put_contents($deploymentPathTarget, $buffer); } }; + /** End Storage stuff */ - /** - * Limit CPU Usage - DONE - * Limit Memory Usage - DONE - * Limit Network Usage - * Limit Storage Usage (//--storage-opt size=120m \) - * Make sure no access to redis, mariadb, influxdb or other system services - * Make sure no access to NFS server / storage volumes - * Access Appwrite REST from internal network for improved performance - */ - if (!$activeFunctions->exists($container)) { // Create contianer if not ready + // Generate random secret key + $secret = \bin2hex(\random_bytes(16)); + $vars = \array_merge($vars, [ + // 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), + // 'APPWRITE_FUNCTION_ID' => $function->getId(), + // 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + // 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + // 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], + // 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], + 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, + 'INTERNAL_RUNTIME_KEY' => $secret + ]); + + /** Launch Runtime */ + if (!$activeFunctions->exists($container)) { $executionStart = \microtime(true); $executionTime = \time(); + $vars = array_map(fn ($v) => strval($v), $vars); + $orchestration ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1')) ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256')) ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256')); - $vars = array_map(fn ($v) => strval($v), $vars); - - // Launch runtime server $id = $orchestration->run( - image: $runtime['image'], + image: $baseImage, name: $container, vars: $vars, labels: [ 'appwrite-type' => 'function', 'appwrite-created' => strval($executionTime), - 'appwrite-runtime' => $function->getAttribute('runtime', ''), + 'appwrite-runtime' => $runtime, 'appwrite-project' => $projectId, - 'appwrite-deployment' => $deployment->getId(), + 'appwrite-deployment' => $deploymentId, ], hostname: $container, mountFolder: $deploymentPathTargetDir, @@ -309,17 +255,17 @@ function createRuntimeServer(string $functionId, string $projectId, string $depl 'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', 'key' => $secret, ]); - - Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); - } else { - Console::success('Runtime server is ready to run'); } + /** End Launch Runtime */ + + Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); } catch (\Throwable $th) { - Console::error($th->getMessage()); - $orchestrationPool->put($orchestration ?? null); - throw $th; + $build['status'] = 'failed'; + Console::error('Runtime Server Creation Failed: '. $th->getMessage()); + } finally { + $orchestrationPool->put($orchestration); + return $build; } - $orchestrationPool->put($orchestration); }; function execute(string $trigger, string $projectId, string $executionId, string $functionId, Database $database, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): array @@ -649,53 +595,23 @@ function execute(string $trigger, string $projectId, string $executionId, string ]; }; -function runBuildStage(string $buildId, string $deploymentId, string $projectID): Document +function runBuildStage(string $buildId, string $projectID, string $path, array $vars, string $baseImage, string $runtime): array { - global $runtimes; + global $orchestrationPool; - global $register; - - /** @var Orchestration $orchestration */ $orchestration = $orchestrationPool->get(); + $build = []; + $id = ''; $buildStdout = ''; $buildStderr = ''; - - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - $cache = new Cache(new RedisCache($redis)); - - $database = new Database(new MariaDB($db), $cache); - $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); - $database->setNamespace('_project_' . $projectID); - - // Check if build has already been run - $build = $database->getDocument('builds', $buildId); - $deployment = $database->getDocument('deployments', $deploymentId); - - - // Start tracking time $buildStart = \time(); + $buildEnd = 0; try { - // If we already have a built package ready there is no need to rebuild. - if ($build->getAttribute('status') === 'ready' && \file_exists($build->getAttribute('outputPath'))) { - return $build; - } - - // Update deployment Status - $build->setAttribute('status', 'building'); - $database->updateDocument('builds', $buildId, $build); - - // Check if runtime is active - $runtime = $runtimes[$build->getAttribute('runtime', '')] ?? null; - - if (\is_null($runtime)) { - throw new Exception('Runtime "' . $build->getAttribute('runtime', '') . '" is not supported'); - } - + Console::info('Running build stage: ' . $buildId); // Grab Deployment Files - $deploymentPath = $build->getAttribute('source', ''); + $deploymentPath = $path; $device = Storage::getDevice('builds'); $deploymentPathTarget = '/tmp/project-' . $projectID . '/' . $buildId . '/code.tar.gz'; @@ -724,34 +640,9 @@ function runBuildStage(string $buildId, string $deploymentId, string $projectID) } if (!$device->exists($deploymentPath)) { - throw new Exception('Code is not readable: ' . $build->getAttribute('source', '')); + throw new Exception('Code is not readable: ' . $path); } - $deployment = $database->getDocument('deployments', $build->getAttribute('deploymentId', '')); - $resourceId = $deployment->getAttribute('resourceId', ''); - $resourceType = $deployment->getAttribute('resourceType', ''); - - if (empty($resourceId)) { - throw new Exception('Invalid resource ID on build ' . $build->getId()); - } - - if (empty($resourceType)) { - throw new Exception('Invalid resource type on build' . $build->getId()); - } - - $resource = $database->getDocument($resourceType, $resourceId); - - if ($resource->isEmpty()) { - throw new Exception('Resource not found on build ' . $build->getId()); - } - - $vars = $resource->getAttribute('vars', []); - - $orchestration - ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) - ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) - ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); - $vars = array_map(fn ($v) => strval($v), $vars); $path = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode'; @@ -763,16 +654,20 @@ function runBuildStage(string $buildId, string $deploymentId, string $projectID) } } - // Launch build container + $orchestration + ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) + ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) + ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); + $id = $orchestration->run( - image: $runtime['base'], + image: $baseImage, name: $container, vars: $vars, workdir: '/usr/code', labels: [ 'appwrite-type' => 'function', 'appwrite-created' => strval($buildStart), - 'appwrite-runtime' => $build->getAttribute('runtime', ''), + 'appwrite-runtime' => $runtime, 'appwrite-project' => $projectID, 'appwrite-build' => $buildId, ], @@ -829,7 +724,7 @@ function runBuildStage(string $buildId, string $deploymentId, string $projectID) $compressStdout = ''; $compressStderr = ''; - $builtCodePath = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode/code.tar.gz'; + $builtCodePath = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode/code.tar.gz'; $compressSuccess = $orchestration->execute( name: $container, @@ -845,9 +740,6 @@ function runBuildStage(string $buildId, string $deploymentId, string $projectID) throw new Exception('Failed to compress built code: ' . $compressStderr); } - // Remove Container - $orchestration->remove($id, true); - // Check if the build was successful by checking if file exists if (!\file_exists($builtCodePath)) { throw new Exception('Something went wrong during the build process.'); @@ -880,49 +772,39 @@ function runBuildStage(string $buildId, string $deploymentId, string $projectID) $buildStdout = 'Build Successful!'; } - $build - ->setAttribute('outputPath', $path) - ->setAttribute('status', 'ready') - ->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096))) - ->setAttribute('stderr', \utf8_encode(\mb_substr($buildStderr, -4096))) - ->setAttribute('startTime', $buildStart) - ->setAttribute('endTime', \time()) - ->setAttribute('duration', \time() - $buildStart); - - // Update build with built code attribute - $build = $database->updateDocument('builds', $buildId, $build); - $buildEnd = \time(); - - Console::info('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds'); - } catch (Exception $e) { - $build - ->setAttribute('status', 'failed') - ->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096))) - ->setAttribute('stderr', \utf8_encode(\mb_substr($e->getMessage(), -4096))) - ->setAttribute('startTime', $buildStart) - ->setAttribute('endTime', \time()) - ->setAttribute('duration', \time() - $buildStart); - - $build = $database->updateDocument('builds', $buildId, $build); - - // also remove the container if it exists - if (isset($id)) { + $build = [ + '$id' => $buildId, + 'outputPath' => $path, + 'status' => 'ready', + 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), + 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), + 'startTime' => $buildStart, + 'endTime' => $buildEnd, + 'duration' => $buildEnd - $buildStart, + ]; + + Console::success('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds'); + } catch (Throwable $th) { + $buildEnd = \time(); + $buildStderr = $th->getMessage(); + $build = [ + '$id' => $buildId, + 'status' => 'failed', + 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), + 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), + 'startTime' => $buildStart, + 'endTime' => $buildEnd, + 'duration' => $buildEnd - $buildStart, + ]; + Console::error('Build failed: ' . $th->getMessage()); + } finally { + if (!empty($id)) { $orchestration->remove($id, true); } - - $register->get('dbPool')->put($db); - $register->get('redisPool')->put($redis); - - throw new Exception('Build failed: ' . $e->getMessage()); + $orchestrationPool->put($orchestration); + return $build; } - - $orchestrationPool->put($orchestration); - - $register->get('dbPool')->put($db); - $register->get('redisPool')->put($redis); - - return $build; } App::post('/v1/functions/:functionId/executions') @@ -1093,74 +975,24 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->param('functionId', '', new UID(), 'Function unique ID.', false) ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) ->param('buildId', '', new UID(), 'Build unique ID.', false) + ->param('path', '', new Text(0), 'Path to source files.', false) + ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) + ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) + ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) ->inject('response') ->inject('dbForProject') ->inject('projectId') - ->action(function (string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, string $projectId) { - - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception('Function not found', 404); + ->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, Response $response, Database $dbForProject, string $projectId) { + + $build = runBuildStage($buildId, $projectId, $path, $vars, $baseImage, $runtime); + + if ( $build['status'] === 'ready') { + $build = createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); } - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception('Deployment not found', 404); - } - - $build = $dbForProject->getDocument('builds', $buildId); - if ($build->isEmpty()) { - throw new Exception('Build not found', 404); - } - - if ($build->getAttribute('status') === 'building') { - throw new Exception('Build is already running', 409); - } - - // Check if build is already finished - if ($build->getAttribute('status') === 'ready') { - throw new Exception('Build is already finished', 409); - } - - go(function() use ($functionId, $deploymentId, $buildId, $projectId, $dbForProject, $function, $deployment) { - Console::info('Starting build for deployment ' . $deployment['$id']); - runBuildStage($buildId, $deploymentId, $projectId); - - // Update the schedule - $schedule = $function->getAttribute('schedule', ''); - $cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null; - $next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0; - - // Grab build - $build = $dbForProject->getDocument('builds', $buildId); - - // If the build failed, it won't be possible to deploy - if ($build->getAttribute('status') !== 'ready') { - throw new Exception('Build failed', 500); - } - - if ($deployment->getAttribute('deploy') === true) { - // Update the function document setting the deployment as the active one - $function - ->setAttribute('deployment', $deployment->getId()) - ->setAttribute('scheduleNext', (int)$next); - - $function = $dbForProject->updateDocument('functions', $functionId, $function); - } - - // Deploy Runtime Server - try { - Console::info("Creating runtime server"); - createRuntimeServer($functionId, $projectId, $deploymentId, $dbForProject); - } catch (\Throwable $th) { - Console::error($th->getMessage()); - throw $th; - } - }); - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->send(); + ->setStatusCode(201) + ->json($build); }); App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode diff --git a/app/workers/builds.php b/app/workers/builds.php index acb8ed6633..c754f49d25 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -20,6 +20,21 @@ Console::success(APP_NAME.' build worker v1 has started'); // TODO: Executor should return appropriate response codes. class BuildsV1 extends Worker { + const METHOD_GET = 'GET'; + const METHOD_POST = 'POST'; + const METHOD_PUT = 'PUT'; + const METHOD_PATCH = 'PATCH'; + const METHOD_DELETE = 'DELETE'; + const METHOD_HEAD = 'HEAD'; + const METHOD_OPTIONS = 'OPTIONS'; + const METHOD_CONNECT = 'CONNECT'; + const METHOD_TRACE = 'TRACE'; + + protected $selfSigned = false; + private $endpoint = 'http://appwrite-executor/v1'; + protected $headers = [ + 'content-type' => '', + ]; public function getName(): string { @@ -41,13 +56,13 @@ class BuildsV1 extends Worker $this->buildDeployment($projectId, $functionId, $deploymentId); break; - case BUILD_TYPE_RETRY: - $buildId = $this->args['buildId'] ?? ''; - $functionId = $this->args['functionId'] ?? ''; - $deploymentId = $this->args['deploymentId'] ?? ''; - Console::info("Retrying build for id: $buildId"); - $this->createBuild($projectId, $functionId, $deploymentId, $buildId); - break; + // case BUILD_TYPE_RETRY: + // $buildId = $this->args['buildId'] ?? ''; + // $functionId = $this->args['functionId'] ?? ''; + // $deploymentId = $this->args['deploymentId'] ?? ''; + // Console::info("Retrying build for id: $buildId"); + // $this->createBuild($projectId, $functionId, $deploymentId, $buildId); + // break; default: throw new \Exception('Invalid build type'); @@ -55,34 +70,30 @@ class BuildsV1 extends Worker } } - protected function createBuild(string $projectId, string $functionId, string $deploymentId, string $buildId) + + protected function createBuild(string $projectId, string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage) { - // TODO: What is a reasonable time to wait for a build to complete? - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/$functionId/deployments/$deploymentId/builds/$buildId"); - \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_TIMEOUT, 900); - \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - \curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'x-appwrite-project: '.$projectId, - 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') - ]); + $route = "/functions/$functionId/deployments/$deploymentId/builds/$buildId"; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'path' => $path, + 'vars' => $vars, + 'runtime' => $runtime, + 'baseImage' => $baseImage + ]; - $response = \curl_exec($ch); - $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); - $error = \curl_error($ch); - if (!empty($error)) { - throw new \Exception($error); - } + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception('Error creating build: ', $status); + } - \curl_close($ch); - - if ($responseStatus >= 400) { - throw new \Exception("Build failed with status code: $responseStatus"); - } + return $response['body']; } protected function buildDeployment(string $projectId, string $functionId, string $deploymentId) @@ -94,7 +105,6 @@ class BuildsV1 extends Worker throw new Exception('Function not found', 404); } - // Get deployment document $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { throw new Exception('Deployment not found', 404); @@ -108,47 +118,214 @@ class BuildsV1 extends Worker } $buildId = $deployment->getAttribute('buildId', ''); - - // If build ID is empty, create a new build + $build = null; if (empty($buildId)) { - try { - $buildId = $dbForProject->getId(); - $dbForProject->createDocument('builds', new Document([ - '$id' => $buildId, - '$read' => [], - '$write' => [], - 'startTime' => time(), - 'deploymentId' => $deploymentId, - 'status' => 'processing', - 'outputPath' => '', - 'runtime' => $function->getAttribute('runtime'), - 'source' => $deployment->getAttribute('path'), - 'sourceType' => Storage::DEVICE_LOCAL, - 'stdout' => '', - 'stderr' => '', - 'endTime' => 0, - 'duration' => 0 - ])); - } catch (\Throwable $th) { - $deployment->setAttribute('buildId', ''); - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); - Console::error($th->getMessage()); - throw $th; - } - } - - // Build the Code - try { + $buildId = $dbForProject->getId(); + $build = $dbForProject->createDocument('builds', new Document([ + '$id' => $buildId, + '$read' => [], + '$write' => [], + 'startTime' => time(), + 'deploymentId' => $deploymentId, + 'status' => 'processing', + 'outputPath' => '', + 'runtime' => $function->getAttribute('runtime'), + 'source' => $deployment->getAttribute('path'), + 'sourceType' => Storage::DEVICE_LOCAL, + 'stdout' => '', + 'stderr' => '', + 'endTime' => 0, + 'duration' => 0 + ])); $deployment->setAttribute('buildId', $buildId); $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); - $this->createBuild($projectId, $functionId, $deploymentId, $buildId); - } catch (\Throwable $th) { - Console::error($th->getMessage()); - throw $th; + } else { + $build = $dbForProject->getDocument('builds', $buildId); } - Console::success("Build id: $buildId started"); + /** Request the executor to build the code... */ + $build->setAttribute('status', 'building'); + $build = $dbForProject->updateDocument('builds', $buildId, $build); + + $path = $deployment->getAttribute('path'); + $vars = $function->getAttribute('vars', []); + $baseImage = $runtime['image']; + $response = $this->createBuild($projectId, $functionId, $deploymentId, $buildId, $path, $vars, $key, $baseImage); + + /** Update the build document */ + $build->setAttribute('endTime', $response['endTime']); + $build->setAttribute('duration', $response['duration']); + $build->setAttribute('status', $response['status']); + $build->setAttribute('outputPath', $response['outputPath']); + $build->setAttribute('stderr', $response['stderr']); + $build->setAttribute('stdout', $response['stdout']); + $build = $dbForProject->updateDocument('builds', $buildId, $build); + + /** Set auto deploy */ + if ($deployment->getAttribute('deploy') === true) { + $function->setAttribute('deployment', $deployment->getId()); + $function = $dbForProject->updateDocument('functions', $functionId, $function); + } + + /** Update function schedule */ + $schedule = $function->getAttribute('schedule', ''); + $cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null; + $next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0; + $function->setAttribute('scheduleNext', (int)$next); + $function = $dbForProject->updateDocument('functions', $functionId, $function); + + // /** Create runtime server */ + + + Console::success("Build id: $buildId created"); } public function shutdown(): void {} + + /** + * Call + * + * Make an API call + * + * @param string $method + * @param string $path + * @param array $params + * @param array $headers + * @param bool $decode + * @return array|string + * @throws Exception + */ + public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) + { + $headers = array_merge($this->headers, $headers); + $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + $responseHeaders = []; + $responseStatus = -1; + $responseType = ''; + $responseBody = ''; + + switch ($headers['content-type']) { + case 'application/json': + $query = json_encode($params); + break; + + case 'multipart/form-data': + $query = $this->flatten($params); + break; + + default: + $query = http_build_query($params); + break; + } + + foreach ($headers as $i => $header) { + $headers[] = $i . ':' . $header; + unset($headers[$i]); + } + + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', $header, 2); + + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + + return $len; + }); + + if ($method != self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + + // Allow self signed certificates + if ($this->selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + $responseBody = curl_exec($ch); + $responseType = $responseHeaders['content-type'] ?? ''; + $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if($decode) { + switch (substr($responseType, 0, strpos($responseType, ';'))) { + case 'application/json': + $json = json_decode($responseBody, true); + + if ($json === null) { + throw new Exception('Failed to parse response: '.$responseBody); + } + + $responseBody = $json; + $json = null; + break; + } + } + + if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { + throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); + } + + curl_close($ch); + + $responseHeaders['status-code'] = $responseStatus; + + if ($responseStatus === 500) { + echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n"; + } + + return [ + 'headers' => $responseHeaders, + 'body' => $responseBody + ]; + } + + /** + * Parse Cookie String + * + * @param string $cookie + * @return array + */ + public function parseCookie(string $cookie): array + { + $cookies = []; + + parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies); + + return $cookies; + } + + /** + * Flatten params array to PHP multiple format + * + * @param array $data + * @param string $prefix + * @return array + */ + protected function flatten(array $data, string $prefix = ''): array + { + $output = []; + + foreach ($data as $key => $value) { + $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; + + if (is_array($value)) { + $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed + } else { + $output[$finalKey] = $value; + } + } + + return $output; + } } diff --git a/composer.lock b/composer.lock index 5d9a38a9b0..a8640d648b 100644 --- a/composer.lock +++ b/composer.lock @@ -119,7 +119,7 @@ "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "96a536e0ad7e3788cd2493e39cdf94b44206411d" + "reference": "991a8674c75f78644b557c9723869c370b3290b5" }, "require": { "php": ">=8.0", @@ -160,7 +160,7 @@ "php", "runtimes" ], - "time": "2022-01-19T09:26:05+00:00" + "time": "2022-02-01T10:58:43+00:00" }, { "name": "chillerlan/php-qrcode", @@ -2135,16 +2135,16 @@ }, { "name": "utopia-php/database", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "2f2527bb080cf578fba327ea2ec637064561d403" + "reference": "ecc143f2cfe16b23675407035c6b5375ba263285" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/2f2527bb080cf578fba327ea2ec637064561d403", - "reference": "2f2527bb080cf578fba327ea2ec637064561d403", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ecc143f2cfe16b23675407035c6b5375ba263285", + "reference": "ecc143f2cfe16b23675407035c6b5375ba263285", "shasum": "" }, "require": { @@ -2192,9 +2192,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.14.0" + "source": "https://github.com/utopia-php/database/tree/0.14.1" }, - "time": "2022-01-21T16:34:34+00:00" + "time": "2022-01-25T13:01:20+00:00" }, { "name": "utopia-php/domains", @@ -5715,16 +5715,16 @@ }, { "name": "symfony/console", - "version": "v6.0.2", + "version": "v6.0.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dd434fa8d69325e5d210f63070014d889511fcb3" + "reference": "22e8efd019c3270c4f79376234a3f8752cd25490" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dd434fa8d69325e5d210f63070014d889511fcb3", - "reference": "dd434fa8d69325e5d210f63070014d889511fcb3", + "url": "https://api.github.com/repos/symfony/console/zipball/22e8efd019c3270c4f79376234a3f8752cd25490", + "reference": "22e8efd019c3270c4f79376234a3f8752cd25490", "shasum": "" }, "require": { @@ -5790,7 +5790,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.2" + "source": "https://github.com/symfony/console/tree/v6.0.3" }, "funding": [ { @@ -5806,7 +5806,7 @@ "type": "tidelift" } ], - "time": "2021-12-27T21:05:08+00:00" + "time": "2022-01-26T17:23:29+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -6216,16 +6216,16 @@ }, { "name": "symfony/string", - "version": "v6.0.2", + "version": "v6.0.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "bae261d0c3ac38a1f802b4dfed42094296100631" + "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/bae261d0c3ac38a1f802b4dfed42094296100631", - "reference": "bae261d0c3ac38a1f802b4dfed42094296100631", + "url": "https://api.github.com/repos/symfony/string/zipball/522144f0c4c004c80d56fa47e40e17028e2eefc2", + "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2", "shasum": "" }, "require": { @@ -6281,7 +6281,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.2" + "source": "https://github.com/symfony/string/tree/v6.0.3" }, "funding": [ { @@ -6297,7 +6297,7 @@ "type": "tidelift" } ], - "time": "2021-12-16T22:13:01+00:00" + "time": "2022-01-02T09:55:41+00:00" }, { "name": "textalk/websocket", diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 4e0c138b9e..2fd337a532 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -155,7 +155,7 @@ class Client * @return array|string * @throws Exception */ - public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true) + public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) { $headers = array_merge($this->headers, $headers); $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); @@ -189,7 +189,7 @@ class Client curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); - curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { $len = strlen($header); $header = explode(':', $header, 2); From 803e4ccbaffa8d2269f43aab910f3c185a85953d Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Wed, 2 Feb 2022 23:38:47 +0400 Subject: [PATCH 02/61] feat: remove database dependencies from the delete deployment endpoint --- app/executor.php | 55 +++++------ app/workers/deletes.php | 206 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 207 insertions(+), 54 deletions(-) diff --git a/app/executor.php b/app/executor.php index 22e379335c..0c4b5c8f04 100644 --- a/app/executor.php +++ b/app/executor.php @@ -926,44 +926,33 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/runtime') App::delete('/v1/deployments/:deploymentId') ->desc('Delete a deployment') - ->param('deploymentId', '', new UID(), 'Deployment unique ID.') - ->inject('projectId') + ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) + ->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false) ->inject('response') - ->action(function (string $deploymentId, string $projectId, Response $response) use ($orchestrationPool) { + ->action(function (string $deploymentId, array $buildIds, Response $response) use ($orchestrationPool) { + Console::info('Deleting deployment: ' . $deploymentId); - global $register; - go(function () use ($projectId, $orchestrationPool, $register, $deploymentId) { - try { - $orchestration = $orchestrationPool->get(); - // Remove the container of the deployment - $orchestration->remove('appwrite-function-' . $deploymentId , true); - Console::success('Removed container for deployment: ' . $deploymentId); + $orchestration = $orchestrationPool->get(); - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - $cache = new Cache(new RedisCache($redis)); - $dbForProject = new Database(new MariaDB($db), $cache); - $dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); - $dbForProject->setNamespace('_project_' . $projectId); + // Remove the container of the deployment + $status = $orchestration->remove('appwrite-function-' . $deploymentId , true); + if ($status) { + Console::success('Removed container for deployment: ' . $deploymentId); + } else { + Console::error('Failed to remove container for deployment: ' . $deploymentId); + } - $builds = $dbForProject->find('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]), - new Query('status', Query::TYPE_EQUAL, ['building']) - ], 999); - - // Remove all the build containers - foreach ($builds as $build) { - $orchestration->remove('build-stage-' . $build['$id'], true); - Console::success("Removed build container: $build for deployment: " . $deploymentId); - } - } catch (\Throwable $th) { - Console::error($th->getMessage()); - } finally { - $orchestrationPool->put($orchestration); - $register->get('dbPool')->put($db); - $register->get('redisPool')->put($redis); + // Remove all the build containers + foreach ($buildIds as $buildId) { + $status = $orchestration->remove('build-stage-' . $buildId, true); + if ($status) { + Console::success("Removed build container: $buildId for deployment: " . $deploymentId); + } else { + Console::error("Failed to remove build container: $buildId for deployment: " . $deploymentId); } - }); + } + + $orchestrationPool->put($orchestration); $response ->setStatusCode(Response::STATUS_CODE_OK) diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 7e46325cc3..af244906e9 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -389,33 +389,34 @@ class DeletesV1 extends Worker { $dbForProject = $this->getProjectDB($projectId); + $builds = $dbForProject->find('builds', [ + new Query('deploymentId', Query::TYPE_EQUAL, [$document->getId()]) + ], 999); + + $buildIds = []; + foreach ($builds as $build) { + $buildIds[] = $build['$id']; + } /** * Request executor to delete the deployment containers */ try { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - // TODO: Implement coroutines. - \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/deployments/{$document->getId()}"); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'x-appwrite-project: '. $projectId, - 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') - ]); + $route = "/deployments/{$document->getId()}"; + $headers = [ + 'content-Type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'buildIds' => $buildIds ?? [] + ]; - $executorResponse = \curl_exec($ch); - $error = \curl_error($ch); - if (!empty($error)) { - throw new Exception($error, 500); - } + $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status); + } - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($statusCode >= 400) { - throw new Exception('Executor error: ' . $executorResponse, $statusCode); - } - - \curl_close($ch); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -565,4 +566,167 @@ class DeletesV1 extends Worker Console::info("No certificate files found for {$domain}"); } } + + const METHOD_GET = 'GET'; + const METHOD_POST = 'POST'; + const METHOD_PUT = 'PUT'; + const METHOD_PATCH = 'PATCH'; + const METHOD_DELETE = 'DELETE'; + const METHOD_HEAD = 'HEAD'; + const METHOD_OPTIONS = 'OPTIONS'; + const METHOD_CONNECT = 'CONNECT'; + const METHOD_TRACE = 'TRACE'; + + protected $selfSigned = false; + private $endpoint = 'http://appwrite-executor/v1'; + protected $headers = [ + 'content-type' => '', + ]; + + /** + * Call + * + * Make an API call + * + * @param string $method + * @param string $path + * @param array $params + * @param array $headers + * @param bool $decode + * @return array|string + * @throws Exception + */ + public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) + { + $headers = array_merge($this->headers, $headers); + $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + $responseHeaders = []; + $responseStatus = -1; + $responseType = ''; + $responseBody = ''; + + switch ($headers['content-type']) { + case 'application/json': + $query = json_encode($params); + break; + + case 'multipart/form-data': + $query = $this->flatten($params); + break; + + default: + $query = http_build_query($params); + break; + } + + foreach ($headers as $i => $header) { + $headers[] = $i . ':' . $header; + unset($headers[$i]); + } + + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', $header, 2); + + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + + return $len; + }); + + if ($method != self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + + // Allow self signed certificates + if ($this->selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + $responseBody = curl_exec($ch); + $responseType = $responseHeaders['content-type'] ?? ''; + $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if($decode) { + switch (substr($responseType, 0, strpos($responseType, ';'))) { + case 'application/json': + $json = json_decode($responseBody, true); + + if ($json === null) { + throw new Exception('Failed to parse response: '.$responseBody); + } + + $responseBody = $json; + $json = null; + break; + } + } + + if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { + throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); + } + + curl_close($ch); + + $responseHeaders['status-code'] = $responseStatus; + + if ($responseStatus === 500) { + echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n"; + } + + return [ + 'headers' => $responseHeaders, + 'body' => $responseBody + ]; + } + + /** + * Parse Cookie String + * + * @param string $cookie + * @return array + */ + public function parseCookie(string $cookie): array + { + $cookies = []; + + parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies); + + return $cookies; + } + + /** + * Flatten params array to PHP multiple format + * + * @param array $data + * @param string $prefix + * @return array + */ + protected function flatten(array $data, string $prefix = ''): array + { + $output = []; + + foreach ($data as $key => $value) { + $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; + + if (is_array($value)) { + $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed + } else { + $output[$finalKey] = $value; + } + } + + return $output; + } } From c239240cbc2e7b452907dbc95149a5863dda887b Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 3 Feb 2022 01:36:21 +0400 Subject: [PATCH 03/61] feat: remove database dependencies from the delete function endpoint --- app/executor.php | 62 -------------------- app/workers/deletes.php | 126 ++++++++++++++++++---------------------- 2 files changed, 57 insertions(+), 131 deletions(-) diff --git a/app/executor.php b/app/executor.php index 0c4b5c8f04..9c237d6a07 100644 --- a/app/executor.php +++ b/app/executor.php @@ -830,68 +830,6 @@ App::post('/v1/functions/:functionId/executions') } ); -App::delete('/v1/functions/:functionId') - ->desc('Delete a function') - ->param('functionId', '', new UID()) - ->inject('projectId') - ->inject('response') - ->inject('dbForProject') - ->action( - function (string $functionId, string $projectId, Response $response, Database $dbForProject) use ($orchestrationPool) { - - $results = $dbForProject->find('deployments', [new Query('resourceId', Query::TYPE_EQUAL, [$functionId])], 999); - - // If amount is 0 then we simply return true - if (count($results) === 0) { - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->send(); - } - - Console::info('Deleting function: ' . $functionId); - // Delete the containers of all deployments - global $register; - foreach ($results as $deployment) { - go(function () use ($orchestrationPool, $deployment, $register, $projectId) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - $cache = new Cache(new RedisCache($redis)); - $dbForProject = new Database(new MariaDB($db), $cache); - $dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); - $dbForProject->setNamespace('_project_' . $projectId); - - try { - $orchestration = $orchestrationPool->get(); - // Remove the container of the deployment - $orchestration->remove('appwrite-function-' . $deployment['$id'], true); - Console::success('Removed container for deployment: ' . $deployment['$id']); - - $builds = $dbForProject->find('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$deployment['$id']]), - new Query('status', Query::TYPE_EQUAL, ['building']) - ], 999); - - // Remove all the build containers - foreach ($builds as $build) { - $orchestration->remove('build-stage-' . $build['$id'], true); - Console::success("Removed build contanier: $build for deployment: " . $deployment['$id']); - } - } catch (\Throwable $th) { - Console::error($th->getMessage()); - } finally { - $orchestrationPool->put($orchestration); - $register->get('dbPool')->put($db); - $register->get('redisPool')->put($redis); - } - }); - } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->send(); - } - ); - App::post('/v1/functions/:functionId/deployments/:deploymentId/runtime') ->desc('Create a new runtime server for a deployment') ->param('functionId', '', new UID(), 'Function unique ID.') diff --git a/app/workers/deletes.php b/app/workers/deletes.php index af244906e9..0341eae2cd 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -312,36 +312,6 @@ class DeletesV1 extends Worker { $dbForProject = $this->getProjectDB($projectId); - /** - * Request executor to delete all deployment containers - */ - try { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$document->getId()}"); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'x-appwrite-project: '. $projectId, - 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') - ]); - - $executorResponse = \curl_exec($ch); - $error = \curl_error($ch); - if (!empty($error)) { - throw new Exception($error, 500); - } - - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($statusCode >= 400) { - throw new Exception('Executor error: ' . $executorResponse, $statusCode); - } - - \curl_close($ch); - } catch (Throwable $th) { - Console::error($th->getMessage()); - } - /** * Delete Deployments */ @@ -361,11 +331,13 @@ class DeletesV1 extends Worker /** * Delete builds */ - if (!empty($deploymentIds)) { - $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); + $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); + $buildIds = []; + foreach ($deploymentIds as $deploymentId) { $this->deleteByGroup('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, $deploymentIds) - ], $dbForProject, function (Document $document) use ($storageBuilds) { + new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]) + ], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId, &$buildIds) { + $buildIds[$deploymentId][] = $document->getId(); if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) { Console::success('Deleted build files: ' . $document->getAttribute('outputPath', '')); } else { @@ -379,6 +351,30 @@ class DeletesV1 extends Worker new Query('functionId', Query::TYPE_EQUAL, [$document->getId()]) ], $dbForProject); + /** + * Request executor to delete all deployment containers + */ + foreach ($deploymentIds as $deploymentId) { + try { + $route = "/deployments/$deploymentId"; + $headers = [ + 'content-Type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'buildIds' => $buildIds[$deploymentId] ?? [], + ]; + $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status); + } + } catch (Throwable $th) { + Console::error($th->getMessage()); + } + } + } /** @@ -389,16 +385,34 @@ class DeletesV1 extends Worker { $dbForProject = $this->getProjectDB($projectId); - $builds = $dbForProject->find('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$document->getId()]) - ], 999); - - $buildIds = []; - foreach ($builds as $build) { - $buildIds[] = $build['$id']; - } /** - * Request executor to delete the deployment containers + * Delete deployment files + */ + $storageFunctions = new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); + if ($storageFunctions->delete($document->getAttribute('path', ''), true)) { + Console::success('Deleted deployment files: ' . $document->getAttribute('path', '')); + } else { + Console::error('Failed to delete deployment files: ' . $document->getAttribute('path', '')); + } + + /** + * Delete builds + */ + $buildIds = []; + $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); + $this->deleteByGroup('builds', [ + new Query('deploymentId', Query::TYPE_EQUAL, [$document->getId()]) + ], $dbForProject, function (Document $document) use ($storageBuilds) { + $buildIds[] = $document->getId(); + if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) { + Console::success('Deleted build files: ' . $document->getAttribute('outputPath', '')); + } else { + Console::error('Failed to delete build files: ' . $document->getAttribute('outputPath', '')); + } + }); + + /** + * Request executor to delete the deployment container */ try { $route = "/deployments/{$document->getId()}"; @@ -416,35 +430,9 @@ class DeletesV1 extends Worker if ($status >= 400) { throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status); } - } catch (Throwable $th) { Console::error($th->getMessage()); } - - /** - * Delete deployment files - */ - $storageFunctions = new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); - if ($storageFunctions->delete($document->getAttribute('path', ''), true)) { - Console::success('Deleted deployment files: ' . $document->getAttribute('path', '')); - } else { - Console::error('Failed to delete deployment files: ' . $document->getAttribute('path', '')); - } - - /** - * Delete builds - */ - $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); - $this->deleteByGroup('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$document->getId()]) - ], $dbForProject, function (Document $document) use ($storageBuilds) { - if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) { - Console::success('Deleted build files: ' . $document->getAttribute('outputPath', '')); - } else { - Console::error('Failed to delete build files: ' . $document->getAttribute('outputPath', '')); - } - }); - } From 7f1df839ec80ac6102148bdfec7176377198a0af Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 3 Feb 2022 04:05:03 +0400 Subject: [PATCH 04/61] feat: remove database dependencies from the create execution endpoint --- app/controllers/api/functions.php | 51 ++++- app/executor.php | 317 ++++++------------------------ app/workers/functions.php | 23 ++- 3 files changed, 124 insertions(+), 267 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 6290abd81e..04654c671f 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -769,6 +769,16 @@ App::post('/v1/functions/:functionId/executions') throw new Exception('Deployment not found. Deploy deployment before trying to execute a function', 404); } + /** Check if build has completed */ + $build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + if ($build->isEmpty()) { + throw new Exception('Build not found', 404); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new Exception('Build not completed', 400); + } + $validator = new Authorization('execute'); if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function @@ -817,13 +827,20 @@ App::post('/v1/functions/:functionId/executions') if ($async) { Resque::enqueue('v1-functions', 'FunctionsV1', [ 'projectId' => $project->getId(), + 'deploymentId' => $deployment->getId(), + 'buildId' => $deployment->getAttribute('buildId', ''), + 'path' => $build->getAttribute('outputPath', ''), + 'vars' => $function->getAttribute('vars', []), + 'data' => $data, + 'runtime' => $function->getAttribute('runtime', ''), + 'timeout' => $function->getAttribute('timeout', 0), + 'baseImage' => '', 'webhooks' => $project->getAttribute('webhooks', []), + 'userId' => $user->getId(), 'functionId' => $function->getId(), 'executionId' => $execution->getId(), 'trigger' => 'http', - 'data' => $data, - 'userId' => $user->getId(), - 'jwt' => $jwt + 'jwt' => $jwt, ]); $response->setStatusCode(Response::STATUS_CODE_CREATED); @@ -831,18 +848,38 @@ App::post('/v1/functions/:functionId/executions') return $response; } + /** Send variables */ + // $vars = \array_merge($function->getAttribute('vars', []), [ + // 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), + // 'APPWRITE_FUNCTION_ID' => $function->getId(), + // 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + // 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + // 'APPWRITE_FUNCTION_TRIGGER' => 'http', + // 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], + // 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], + // 'APPWRITE_FUNCTION_EVENT' => $event, + // 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, + // 'APPWRITE_FUNCTION_DATA' => $data, + // 'APPWRITE_FUNCTION_USER_ID' => $userId, + // 'APPWRITE_FUNCTION_JWT' => $jwt, + // 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId + // ]); + // Directly execute function. $ch = \curl_init(); \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$function->getId()}/executions"); \curl_setopt($ch, CURLOPT_POST, true); \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ - 'trigger' => 'http', - 'projectId' => $project->getId(), - 'executionId' => $execution->getId(), + 'deploymentId' => $deployment->getId(), + 'buildId' => $deployment->getAttribute('buildId', ''), + 'path' => $build->getAttribute('outputPath', ''), + 'vars' => $function->getAttribute('vars', []), 'data' => $data, + 'runtime' => $function->getAttribute('runtime', ''), + 'timeout' => $function->getAttribute('timeout', 0), + 'baseImage' => '', 'webhooks' => $project->getAttribute('webhooks', []), 'userId' => $user->getId(), - 'jwt' => $jwt, ])); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); \curl_setopt($ch, CURLOPT_TIMEOUT, App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + 200); // + 200 for safety margin diff --git a/app/executor.php b/app/executor.php index 9c237d6a07..ba257f0a03 100644 --- a/app/executor.php +++ b/app/executor.php @@ -7,6 +7,7 @@ use Appwrite\Stats\Stats; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Execution; use Cron\CronExpression; +use LanguageServerProtocol\Range; use Swoole\ConnectionPool; use Swoole\Coroutine as Co; use Swoole\Http\Request as SwooleRequest; @@ -34,6 +35,7 @@ use Utopia\Swoole\Request; use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; use Utopia\Validator\JSON; +use Utopia\Validator\Range as ValidatorRange; use Utopia\Validator\Text; require_once __DIR__ . '/init.php'; @@ -161,7 +163,6 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui $orchestration = $orchestrationPool->get(); try { - $container = 'appwrite-function-' . $deploymentId; if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online // If container is online then stop and remove it @@ -170,7 +171,6 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui } catch (Exception $e) { throw new Exception('Failed to remove container: ' . $e->getMessage()); } - $activeFunctions->del($container); } @@ -268,172 +268,38 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui } }; -function execute(string $trigger, string $projectId, string $executionId, string $functionId, Database $database, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): array +function execute(string $projectId, string $functionId, string $deploymentId, array $build, array $vars, string $data, string $userId, string $baseImage, string $runtime, int $timeout, array $webhooks = []): array { + Console::info('Executing function: ' . $functionId); global $activeFunctions; - global $runtimes; global $register; + $container = 'appwrite-function-' . $deploymentId; - $function = $database->getDocument('functions', $functionId); - $deployment = $database->getDocument('deployments', $function->getAttribute('deployment', '')); - $build = $database->getDocument('builds', $deployment->getAttribute('buildId', '')); - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception('Deployment not found', 404); + /** Create a new runtime server if there's none running */ + if (!$activeFunctions->exists($container)) { + createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); } - // Grab execution document if exists - // It it doesn't exist, create a new one. - $execution = !empty($executionId) - ? $database->getDocument('executions', $executionId) - : $database->createDocument('executions', new Document([ - '$id' => $executionId, - '$read' => ($userId !== '') ? ['user:' . $userId] : [], - '$write' => ['role:all'], - 'dateCreated' => time(), - 'functionId' => $function->getId(), - 'deploymentId' => $deployment->getId(), - 'trigger' => $trigger, // http / schedule / event - 'status' => 'processing', // waiting / processing / completed / failed - 'statusCode' => 0, - 'stdout' => '', - 'stderr' => '', - 'time' => 0.0, - 'search' => implode(' ', [$functionId, $executionId]), - ])); - - if (false === $execution || ($execution instanceof Document && $execution->isEmpty())) { - throw new Exception('Failed to create or read execution'); - } - - - if ($build->getAttribute('status') === 'building') { - - $execution - ->setAttribute('status', 'failed') - ->setAttribute('statusCode', 500) - ->setAttribute('stderr', 'Deployment is still being built.') - ->setAttribute('time', 0); - - $database->updateDocument('executions', $execution->getId(), $execution); - - throw new Exception('Execution Failed. Reason: Deployment is still being built.'); - } - - // Check if runtime is active - $runtime = $runtimes[$function->getAttribute('runtime', '')] ?? null; - - if (\is_null($runtime)) { - throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); - } + $key = $activeFunctions->get('appwrite-function-' . $deploymentId, 'key'); // Process environment variables - $vars = \array_merge($function->getAttribute('vars', []), [ - 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), - 'APPWRITE_FUNCTION_ID' => $function->getId(), - 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), - 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - 'APPWRITE_FUNCTION_TRIGGER' => $trigger, - 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], - 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], - 'APPWRITE_FUNCTION_EVENT' => $event, - 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, - 'APPWRITE_FUNCTION_DATA' => $data, - 'APPWRITE_FUNCTION_USER_ID' => $userId, - 'APPWRITE_FUNCTION_JWT' => $jwt, - 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, - ]); - - $container = 'appwrite-function-' . $deployment->getId(); - - try { - if ($build->getAttribute('status') !== 'ready') { - // Create a new build entry - $buildId = $database->getId(); - $database->createDocument('builds', new Document([ - '$id' => $buildId, - '$read' => [], - '$write' => [], - 'startTime' => time(), - 'deploymentId' => $deployment->getId(), - 'status' => 'processing', - 'outputPath' => '', - 'runtime' => $function->getAttribute('runtime', ''), - 'source' => $deployment->getAttribute('path'), - 'sourceType' => Storage::DEVICE_LOCAL, - 'stdout' => '', - 'stderr' => '', - 'endTime' => 0, - 'duration' => 0 - ])); - - $deployment->setAttribute('buildId', $buildId); - - $database->updateDocument('deployments', $deployment->getId(), $deployment); - - runBuildStage($buildId, $deployment->getId(), $projectId); - } - } catch (Exception $e) { - $execution - ->setAttribute('status', 'failed') - ->setAttribute('statusCode', 500) - ->setAttribute('stderr', \utf8_encode(\mb_substr($e->getMessage(), -4000))) // log last 4000 chars output - ->setAttribute('time', 0); - - $database->updateDocument('executions', $execution->getId(), $execution); - - throw new Error('Something went wrong building the code. ' . $e->getMessage()); - } - - try { - if (!$activeFunctions->exists($container)) { // Create container if not ready - createRuntimeServer($functionId, $projectId, $deployment->getId(), $database); - } else if ($activeFunctions->get($container)['status'] === 'Down') { - sleep(1); - } else { - Console::info('Container is ready to run'); - } - } catch (Exception $e) { - $execution->setAttribute('status', 'failed') - ->setAttribute('statusCode', 500) - ->setAttribute('stderr', \utf8_encode(\mb_substr($e->getMessage(), -4000))) // log last 4000 chars output - ->setAttribute('time', 0); - - $execution = $database->updateDocument('executions', $execution->getId(), $execution); - - try { - throw new Exception('Something went wrong building the runtime server. ' . $e->getMessage()); - } catch (\Exception $error) { - logError($error, 'execution'); - } - - return [ - 'status' => 'failed', - 'response' => \utf8_encode(\mb_substr($e->getMessage(), -4000)), // log last 4000 chars output - 'time' => 0 - ]; - } - - $key = $activeFunctions->get('appwrite-function-' . $deployment->getId(), 'key'); - - // Process environment variables - $vars = \array_merge($function->getAttribute('vars', []), [ - 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), - 'APPWRITE_FUNCTION_ID' => $function->getId(), - 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), - 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - 'APPWRITE_FUNCTION_TRIGGER' => $trigger, - 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], - 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], - 'APPWRITE_FUNCTION_EVENT' => $event, - 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, - 'APPWRITE_FUNCTION_DATA' => $data, - 'APPWRITE_FUNCTION_USER_ID' => $userId, - 'APPWRITE_FUNCTION_JWT' => $jwt, - 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId - ]); + // $vars = \array_merge($function->getAttribute('vars', []), [ + // 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), + // 'APPWRITE_FUNCTION_ID' => $function->getId(), + // 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + // 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + // 'APPWRITE_FUNCTION_TRIGGER' => $trigger, + // 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], + // 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], + // 'APPWRITE_FUNCTION_EVENT' => $event, + // 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, + // 'APPWRITE_FUNCTION_DATA' => $data, + // 'APPWRITE_FUNCTION_USER_ID' => $userId, + // 'APPWRITE_FUNCTION_JWT' => $jwt, + // 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId + // ]); $stdout = ''; $stderr = ''; @@ -458,7 +324,7 @@ function execute(string $trigger, string $projectId, string $executionId, string 'file' => $vars['ENTRYPOINT_NAME'], 'env' => $vars, 'payload' => $data, - 'timeout' => $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)) + 'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) ]); \curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/"); @@ -466,7 +332,7 @@ function execute(string $trigger, string $projectId, string $executionId, string \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_TIMEOUT, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); + \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)); \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); \curl_setopt($ch, CURLOPT_HTTPHEADER, [ @@ -537,34 +403,32 @@ function execute(string $trigger, string $projectId, string $executionId, string $executionTime = ($executionEnd - $executionStart); $functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed'; - Console::success('Function executed in ' . ($executionEnd - $executionStart) . ' seconds, status: ' . $functionStatus); + Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus); - $execution->setAttribute('deploymentId', $deployment->getId()) - ->setAttribute('status', $functionStatus) - ->setAttribute('statusCode', $statusCode) - ->setAttribute('stdout', \utf8_encode(\mb_substr($stdout, -8000))) - ->setAttribute('stderr', \utf8_encode(\mb_substr($stderr, -8000))) - ->setAttribute('time', $executionTime); - - $execution = $database->updateDocument('executions', $execution->getId(), $execution); + $execution = [ + 'status' => $functionStatus, + 'statusCode' => $statusCode, + 'stdout' => \utf8_encode(\mb_substr($stdout, -8000)), + 'stderr' => \utf8_encode(\mb_substr($stderr, -8000)), + 'time' => $executionTime, + ]; + /** Trigger event */ $executionModel = new Execution(); $executionUpdate = new Event('v1-webhooks', 'WebhooksV1'); - $executionUpdate ->setParam('projectId', $projectId) ->setParam('userId', $userId) ->setParam('webhooks', $webhooks) ->setParam('event', 'functions.executions.update') - ->setParam('eventData', $execution->getArrayCopy(array_keys($executionModel->getRules()))); - + ->setParam('eventData', (new Document($execution))->getArrayCopy(array_keys($executionModel->getRules()))); $executionUpdate->trigger(); - $target = Realtime::fromPayload('functions.executions.update', $execution); - + /** Trigger realtime event */ + $target = Realtime::fromPayload('functions.executions.update', new Document($execution)); Realtime::send( projectId: $projectId, - payload: $execution->getArrayCopy(), + payload: $execution, event: 'functions.executions.update', channels: $target['channels'], roles: $target['roles'] @@ -572,27 +436,21 @@ function execute(string $trigger, string $projectId, string $executionId, string if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') { $statsd = $register->get('statsd'); - $usage = new Stats($statsd); - $usage ->setParam('projectId', $projectId) - ->setParam('functionId', $function->getId()) + ->setParam('functionId', $functionId) ->setParam('functionExecution', 1) ->setParam('functionStatus', $functionStatus) ->setParam('functionExecutionTime', $executionTime * 1000) // ms ->setParam('networkRequestSize', 0) ->setParam('networkResponseSize', 0) ->submit(); - $usage->submit(); } - return [ - 'status' => $functionStatus, - 'response' => ($functionStatus !== 'completed') ? $stderr : $stdout, - 'time' => $executionTime - ]; + return $execution; + }; function runBuildStage(string $buildId, string $projectID, string $path, array $vars, string $baseImage, string $runtime): array @@ -809,59 +667,36 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ App::post('/v1/functions/:functionId/executions') ->desc('Execute a function') - ->param('trigger', '', new Text(1024), 'What triggered this execution, can be http / schedule / event') - ->param('projectId', '', new Text(1024), 'The ProjectID this execution belongs to') - ->param('executionId', '', new Text(1024), 'An optional execution ID, If not specified a new execution document is created.', true) ->param('functionId', '', new Text(1024), 'The FunctionID to execute') - ->param('event', '', new Text(1024), 'The event that triggered this execution', true) - ->param('eventData', '', new Text(0), 'Extra Data for the event', true) - ->param('data', '', new Text(1024), 'Data to be forwarded to the function, this is user specified.', true) + ->param('deploymentId', '', new Text(1024), 'The deployment ID to execute') + ->param('buildId', '', new Text(1024), 'The build ID of the function') + ->param('path', '', new Text(0), 'Path to built files.', false) + ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) + ->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true) + ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) + ->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true) + ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) ->param('webhooks', [], new ArrayList(new JSON()), 'Any webhooks that need to be triggered after this execution', true) ->param('userId', '', new Text(1024), 'The UserID of the user who triggered the execution if it was called from a client SDK', true) - ->param('jwt', '', new Text(1024), 'A JWT of the user who triggered the execution if it was called from a client SDK', true) + ->inject('projectId') ->inject('response') - ->inject('dbForProject') ->action( - function (string $trigger, string $projectId, string $executionId, string $functionId, string $event, string $eventData, string $data, array $webhooks, string $userId, string $jwt, Response $response, Database $dbForProject) { - $data = execute($trigger, $projectId, $executionId, $functionId, $dbForProject, $event, $eventData, $data, $webhooks, $userId, $jwt); + function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $data, string $runtime, $timeout, string $baseImage, array $webhooks, string $userId, string $projectId, Response $response) { + + $build = [ + '$id' => $buildId, + 'outputPath' => $path, + ]; + + // Send both data and vars from the caller + $execution = execute($projectId, $functionId, $deploymentId, $build, $vars, $data, $userId, $baseImage, $runtime, $timeout, $webhooks); + $response ->setStatusCode(Response::STATUS_CODE_OK) - ->json($data); + ->json($execution); } ); -App::post('/v1/functions/:functionId/deployments/:deploymentId/runtime') - ->desc('Create a new runtime server for a deployment') - ->param('functionId', '', new UID(), 'Function unique ID.') - ->param('deploymentId', '', new UID(), 'Deployment unique ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('projectId') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, string $projectID) use ($runtimes) { - // Get function document - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception('Function not found', 404); - } - - // Get deployment document - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception('Deployment not found', 404); - } - - $runtime = $runtimes[$function->getAttribute('runtime')] ?? null; - if (\is_null($runtime)) { - throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" not found.', 404); - } - - createRuntimeServer($functionId, $projectID, $deploymentId, $dbForProject); - - $response - ->setStatusCode(201) - ->send(); - }); - App::delete('/v1/deployments/:deploymentId') ->desc('Delete a deployment') ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) @@ -906,10 +741,9 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) - ->inject('response') - ->inject('dbForProject') ->inject('projectId') - ->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, Response $response, Database $dbForProject, string $projectId) { + ->inject('response') + ->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) { $build = runBuildStage($buildId, $projectId, $path, $vars, $baseImage, $runtime); @@ -1007,12 +841,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $response = new Response($swooleResponse); $app = new App('UTC'); - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - - App::setResource('db', fn () => $db); - App::setResource('cache', fn () => $redis); - $projectId = $request->getHeader('x-appwrite-project', ''); Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId)); @@ -1031,16 +859,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo return $swooleResponse->end('401: Authentication Error'); } - App::setResource('dbForProject', function ($db, $cache) use ($projectId) { - $cache = new Cache(new RedisCache($cache)); - - $database = new Database(new MariaDB($db), $cache); - $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); - $database->setNamespace('_project_' . $projectId); - - return $database; - }, ['db', 'cache']); - App::error(function ($error, $utopia, $request, $response) { /** @var Exception $error */ /** @var Utopia\App $utopia */ @@ -1094,13 +912,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo logError($e, "serverError"); $swooleResponse->end('500: Server Error'); } finally { - /** @var PDOPool $dbPool */ - $dbPool = $register->get('dbPool'); - $dbPool->put($db); - - /** @var RedisPool $redisPool */ - $redisPool = $register->get('redisPool'); - $redisPool->put($redis); } }); diff --git a/app/workers/functions.php b/app/workers/functions.php index cbcb598f9e..bf5779f1e6 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -189,10 +189,18 @@ class FunctionsV1 extends Worker throw new Exception('Function not found ('.$functionId.')'); } + $deployment = Authorization::skip(fn() => $database->getDocument('deployments', $function->getAttribute('deployment'))); + + $build = Authorization::skip(fn() => $database->getDocument('builds', $deployment->getAttribute('build'))); + $path = $build->getAttribute('path', ''); + $this->execute( trigger: $trigger, projectId: $projectId, executionId: $executionId, + path: $path, + buildId: $deployment->getAttribute('buildId', ''), + deploymentId: $deployment->getId(), database: $database, function: $function, data: $data, @@ -221,21 +229,22 @@ class FunctionsV1 extends Worker * * @return void */ - 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 + public function execute(string $trigger, string $path, string $projectId, string $deploymentId, string $buildId, string $executionId, Database $database, Document $function, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): void { $ch = \curl_init(); \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$function->getId()}/executions"); \curl_setopt($ch, CURLOPT_POST, true); \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ - 'trigger' => $trigger, - 'projectId' => $projectId, - 'executionId' => $executionId, - 'event' => $event, - 'eventData' => $eventData, + 'deploymentId' => $deploymentId, + 'buildId' => $buildId, + 'path' => $path, + 'vars' => $function->getAttribute('vars', []), 'data' => $data, + 'runtime' => $function->getAttribute('runtime', ''), + 'timeout' => $function->getAttribute('timeout', 0), + 'baseImage' => '', 'webhooks' => $webhooks, 'userId' => $userId, - 'jwt' => $jwt, ])); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); \curl_setopt($ch, CURLOPT_TIMEOUT, App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + 200); // + 200 for safety margin From 0b36c7e21cf634e98762421d7a59185e9f06891c Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 3 Feb 2022 20:22:56 +0400 Subject: [PATCH 05/61] feat: remove handle shutdown method --- app/executor.php | 99 +++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 55 deletions(-) diff --git a/app/executor.php b/app/executor.php index ba257f0a03..76f3bc4ff0 100644 --- a/app/executor.php +++ b/app/executor.php @@ -6,8 +6,6 @@ use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Stats\Stats; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Execution; -use Cron\CronExpression; -use LanguageServerProtocol\Range; use Swoole\ConnectionPool; use Swoole\Coroutine as Co; use Swoole\Http\Request as SwooleRequest; @@ -16,19 +14,13 @@ use Swoole\Http\Server; use Swoole\Process; use Utopia\App; use Utopia\CLI\Console; -use Utopia\Cache\Adapter\Redis as RedisCache; -use Utopia\Cache\Cache; use Utopia\Config\Config; -use Utopia\Database\Adapter\MariaDB; -use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Orchestration\Adapter\DockerCLI; use Utopia\Orchestration\Orchestration; -use Utopia\Registry\Registry; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\Swoole\Request; @@ -760,83 +752,81 @@ App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode $http = new Server("0.0.0.0", 80); -function handleShutdown() -{ - global $orchestrationPool; - global $register; +// function handleShutdown() +// { +// global $orchestrationPool; +// global $register; - try { - Console::info('Cleaning up containers before shutdown...'); +// try { +// Console::info('Cleaning up containers before shutdown...'); - // Remove all containers. +// // Remove all containers. - /** @var Orchestration $orchestration */ - $orchestration = $orchestrationPool->get(); +// /** @var Orchestration $orchestration */ +// $orchestration = $orchestrationPool->get(); - $functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']); +// $functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']); - foreach ($functionsToRemove as $container) { - go(fn () => $orchestration->remove($container->getId(), true)); +// foreach ($functionsToRemove as $container) { +// go(fn () => $orchestration->remove($container->getId(), true)); - // Get a database instance - $db = $register->get('dbPool')->get(); - $cache = $register->get('redisPool')->get(); +// // Get a database instance +// $db = $register->get('dbPool')->get(); +// $cache = $register->get('redisPool')->get(); - $cache = new Cache(new RedisCache($cache)); - $database = new Database(new MariaDB($db), $cache); - $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); - $database->setNamespace('_project_' . $container->getLabels()["appwrite-project"]); +// $cache = new Cache(new RedisCache($cache)); +// $database = new Database(new MariaDB($db), $cache); +// $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); +// $database->setNamespace('_project_' . $container->getLabels()["appwrite-project"]); - // Get list of all processing executions - $executions = $database->find('executions', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$container->getLabels()["appwrite-deployment"]]), - new Query('status', Query::TYPE_EQUAL, ['waiting']) - ]); +// // Get list of all processing executions +// $executions = $database->find('executions', [ +// new Query('deploymentId', Query::TYPE_EQUAL, [$container->getLabels()["appwrite-deployment"]]), +// new Query('status', Query::TYPE_EQUAL, ['waiting']) +// ]); - // Mark all processing executions as failed - foreach ($executions as $execution) { - $execution - ->setAttribute('status', 'failed') - ->setAttribute('statusCode', 1) - ->setAttribute('stderr', 'Appwrite was shutdown during execution'); +// // Mark all processing executions as failed +// foreach ($executions as $execution) { +// $execution +// ->setAttribute('status', 'failed') +// ->setAttribute('statusCode', 1) +// ->setAttribute('stderr', 'Appwrite was shutdown during execution'); - $database->updateDocument('executions', $execution->getId(), $execution); - } +// $database->updateDocument('executions', $execution->getId(), $execution); +// } - Console::info('Removed container ' . $container->getName()); - } - } catch (\Throwable $error) { - logError($error, 'shutdownError'); - } finally { - $orchestrationPool->put($orchestration); - } -}; +// Console::info('Removed container ' . $container->getName()); +// } +// } catch (\Throwable $error) { +// logError($error, 'shutdownError'); +// } finally { +// $orchestrationPool->put($orchestration); +// } +// }; $http->on('start', function ($http) { @Process::signal(SIGINT, function () use ($http) { - handleShutdown(); + // handleShutdown(); $http->shutdown(); }); @Process::signal(SIGQUIT, function () use ($http) { - handleShutdown(); + // handleShutdown(); $http->shutdown(); }); @Process::signal(SIGKILL, function () use ($http) { - handleShutdown(); + // handleShutdown(); $http->shutdown(); }); @Process::signal(SIGTERM, function () use ($http) { - handleShutdown(); + // handleShutdown(); $http->shutdown(); }); }); $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { - global $register; - $request = new Request($swooleRequest); $response = new Response($swooleResponse); $app = new App('UTC'); @@ -911,7 +901,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo } catch (Exception $e) { logError($e, "serverError"); $swooleResponse->end('500: Server Error'); - } finally { } }); From 6610bf9873a8f15b47e87e5609691b2bbc0a39fd Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 4 Feb 2022 05:29:40 +0400 Subject: [PATCH 06/61] feat: add executor class --- app/controllers/api/functions.php | 54 +++--- app/executor.php | 19 +- app/workers/builds.php | 210 ++-------------------- app/workers/deletes.php | 197 +-------------------- composer.json | 3 +- src/Executor/Executor.php | 278 ++++++++++++++++++++++++++++++ 6 files changed, 340 insertions(+), 421 deletions(-) create mode 100644 src/Executor/Executor.php diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 04654c671f..034ab79a39 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -759,6 +759,13 @@ App::post('/v1/functions/:functionId/executions') throw new Exception('Function not found', 404); } + $runtimes = Config::getParam('runtimes', []); + $key = $function->getAttribute('runtime', ''); + $runtime = isset($runtimes[$key]) ? $runtimes[$key] : null; + if (\is_null($runtime)) { + throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400); + } + $deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $function->getAttribute('deployment'))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { @@ -834,7 +841,7 @@ App::post('/v1/functions/:functionId/executions') 'data' => $data, 'runtime' => $function->getAttribute('runtime', ''), 'timeout' => $function->getAttribute('timeout', 0), - 'baseImage' => '', + 'baseImage' => $runtime['image'], 'webhooks' => $project->getAttribute('webhooks', []), 'userId' => $user->getId(), 'functionId' => $function->getId(), @@ -849,21 +856,19 @@ App::post('/v1/functions/:functionId/executions') } /** Send variables */ - // $vars = \array_merge($function->getAttribute('vars', []), [ - // 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), - // 'APPWRITE_FUNCTION_ID' => $function->getId(), - // 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), - // 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - // 'APPWRITE_FUNCTION_TRIGGER' => 'http', - // 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], - // 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], - // 'APPWRITE_FUNCTION_EVENT' => $event, - // 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, - // 'APPWRITE_FUNCTION_DATA' => $data, - // 'APPWRITE_FUNCTION_USER_ID' => $userId, - // 'APPWRITE_FUNCTION_JWT' => $jwt, - // 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId - // ]); + $vars = \array_merge($function->getAttribute('vars', []), [ + 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), + 'APPWRITE_FUNCTION_ID' => $function->getId(), + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + 'APPWRITE_FUNCTION_TRIGGER' => 'http', + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], + 'APPWRITE_FUNCTION_DATA' => $data, + 'APPWRITE_FUNCTION_USER_ID' => $user->getId(), + 'APPWRITE_FUNCTION_JWT' => $jwt, + 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId() + ]); // Directly execute function. $ch = \curl_init(); @@ -873,11 +878,11 @@ App::post('/v1/functions/:functionId/executions') 'deploymentId' => $deployment->getId(), 'buildId' => $deployment->getAttribute('buildId', ''), 'path' => $build->getAttribute('outputPath', ''), - 'vars' => $function->getAttribute('vars', []), + 'vars' => $vars, 'data' => $data, 'runtime' => $function->getAttribute('runtime', ''), 'timeout' => $function->getAttribute('timeout', 0), - 'baseImage' => '', + 'baseImage' => $runtime['image'], 'webhooks' => $project->getAttribute('webhooks', []), 'userId' => $user->getId(), ])); @@ -896,12 +901,21 @@ App::post('/v1/functions/:functionId/executions') if (!empty($error)) { Console::error('Curl error: '.$error); } - \curl_close($ch); + $responseExecute = json_decode($responseExecute, true); + $execution->setAttribute('status', $responseExecute['status']); + $execution->setAttribute('statusCode', $responseExecute['statusCode']); + $execution->setAttribute('stdout', $responseExecute['stdout']); + $execution->setAttribute('stderr', $responseExecute['stderr']); + $execution->setAttribute('time', $responseExecute['time']); + Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution)); + + $responseExecute['response'] = ($responseExecute['status'] !== 'completed') ? $responseExecute['stderr'] : $responseExecute['stdout']; + $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic(new Document(json_decode($responseExecute, true)), Response::MODEL_SYNC_EXECUTION); + ->dynamic(new Document($responseExecute), Response::MODEL_SYNC_EXECUTION); }); App::get('/v1/functions/:functionId/executions') diff --git a/app/executor.php b/app/executor.php index 76f3bc4ff0..2e923479f8 100644 --- a/app/executor.php +++ b/app/executor.php @@ -276,23 +276,6 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar $key = $activeFunctions->get('appwrite-function-' . $deploymentId, 'key'); - // Process environment variables - // $vars = \array_merge($function->getAttribute('vars', []), [ - // 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), - // 'APPWRITE_FUNCTION_ID' => $function->getId(), - // 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), - // 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - // 'APPWRITE_FUNCTION_TRIGGER' => $trigger, - // 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], - // 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], - // 'APPWRITE_FUNCTION_EVENT' => $event, - // 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, - // 'APPWRITE_FUNCTION_DATA' => $data, - // 'APPWRITE_FUNCTION_USER_ID' => $userId, - // 'APPWRITE_FUNCTION_JWT' => $jwt, - // 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId - // ]); - $stdout = ''; $stderr = ''; @@ -365,6 +348,8 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500); } + var_dump($executorResponse); + $executionData = []; if (!empty($executorResponse)) { diff --git a/app/workers/builds.php b/app/workers/builds.php index c754f49d25..9ebfb0518c 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -2,6 +2,7 @@ use Appwrite\Resque\Worker; use Cron\CronExpression; +use Executor\Executor; use Utopia\Database\Validator\Authorization; use Utopia\App; use Utopia\CLI\Console; @@ -20,28 +21,19 @@ Console::success(APP_NAME.' build worker v1 has started'); // TODO: Executor should return appropriate response codes. class BuildsV1 extends Worker { - const METHOD_GET = 'GET'; - const METHOD_POST = 'POST'; - const METHOD_PUT = 'PUT'; - const METHOD_PATCH = 'PATCH'; - const METHOD_DELETE = 'DELETE'; - const METHOD_HEAD = 'HEAD'; - const METHOD_OPTIONS = 'OPTIONS'; - const METHOD_CONNECT = 'CONNECT'; - const METHOD_TRACE = 'TRACE'; - - protected $selfSigned = false; - private $endpoint = 'http://appwrite-executor/v1'; - protected $headers = [ - 'content-type' => '', - ]; + /** + * @var Executor + */ + private $executor = null; public function getName(): string { return "builds"; } - public function init(): void {} + public function init(): void { + $this->executor = new Executor(); + } public function run(): void { @@ -70,32 +62,6 @@ class BuildsV1 extends Worker } } - - protected function createBuild(string $projectId, string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage) - { - $route = "/functions/$functionId/deployments/$deploymentId/builds/$buildId"; - $headers = [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') - ]; - $params = [ - 'path' => $path, - 'vars' => $vars, - 'runtime' => $runtime, - 'baseImage' => $baseImage - ]; - - $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); - - $status = $response['headers']['status-code']; - if ($status >= 400) { - throw new \Exception('Error creating build: ', $status); - } - - return $response['body']; - } - protected function buildDeployment(string $projectId, string $functionId, string $deploymentId) { $dbForProject = $this->getProjectDB($projectId); @@ -150,8 +116,17 @@ class BuildsV1 extends Worker $path = $deployment->getAttribute('path'); $vars = $function->getAttribute('vars', []); $baseImage = $runtime['image']; - $response = $this->createBuild($projectId, $functionId, $deploymentId, $buildId, $path, $vars, $key, $baseImage); - + $response = $this->executor->createRuntime( + projectId: $projectId, + functionId: $functionId, + deploymentId: $deploymentId, + buildId: $buildId, + path: $path, + vars: $vars, + runtime: $key, + baseImage: $baseImage + ); + /** Update the build document */ $build->setAttribute('endTime', $response['endTime']); $build->setAttribute('duration', $response['duration']); @@ -181,151 +156,4 @@ class BuildsV1 extends Worker } public function shutdown(): void {} - - /** - * Call - * - * Make an API call - * - * @param string $method - * @param string $path - * @param array $params - * @param array $headers - * @param bool $decode - * @return array|string - * @throws Exception - */ - public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) - { - $headers = array_merge($this->headers, $headers); - $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); - $responseHeaders = []; - $responseStatus = -1; - $responseType = ''; - $responseBody = ''; - - switch ($headers['content-type']) { - case 'application/json': - $query = json_encode($params); - break; - - case 'multipart/form-data': - $query = $this->flatten($params); - break; - - default: - $query = http_build_query($params); - break; - } - - foreach ($headers as $i => $header) { - $headers[] = $i . ':' . $header; - unset($headers[$i]); - } - - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { - $len = strlen($header); - $header = explode(':', $header, 2); - - if (count($header) < 2) { // ignore invalid headers - return $len; - } - - $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); - - return $len; - }); - - if ($method != self::METHOD_GET) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $query); - } - - // Allow self signed certificates - if ($this->selfSigned) { - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - } - - $responseBody = curl_exec($ch); - $responseType = $responseHeaders['content-type'] ?? ''; - $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - if($decode) { - switch (substr($responseType, 0, strpos($responseType, ';'))) { - case 'application/json': - $json = json_decode($responseBody, true); - - if ($json === null) { - throw new Exception('Failed to parse response: '.$responseBody); - } - - $responseBody = $json; - $json = null; - break; - } - } - - if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { - throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); - } - - curl_close($ch); - - $responseHeaders['status-code'] = $responseStatus; - - if ($responseStatus === 500) { - echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n"; - } - - return [ - 'headers' => $responseHeaders, - 'body' => $responseBody - ]; - } - - /** - * Parse Cookie String - * - * @param string $cookie - * @return array - */ - public function parseCookie(string $cookie): array - { - $cookies = []; - - parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies); - - return $cookies; - } - - /** - * Flatten params array to PHP multiple format - * - * @param array $data - * @param string $prefix - * @return array - */ - protected function flatten(array $data, string $prefix = ''): array - { - $output = []; - - foreach ($data as $key => $value) { - $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; - - if (is_array($value)) { - $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed - } else { - $output[$finalKey] = $value; - } - } - - return $output; - } } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 0341eae2cd..0760a67886 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -5,6 +5,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Appwrite\Resque\Worker; +use Executor\Executor; use Utopia\Storage\Device\Local; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; @@ -354,22 +355,10 @@ class DeletesV1 extends Worker /** * Request executor to delete all deployment containers */ + $executor = new Executor(); foreach ($deploymentIds as $deploymentId) { try { - $route = "/deployments/$deploymentId"; - $headers = [ - 'content-Type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') - ]; - $params = [ - 'buildIds' => $buildIds[$deploymentId] ?? [], - ]; - $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); - $status = $response['headers']['status-code']; - if ($status >= 400) { - throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status); - } + $executor->deleteRuntime($deploymentId, $projectId); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -415,21 +404,8 @@ class DeletesV1 extends Worker * Request executor to delete the deployment container */ try { - $route = "/deployments/{$document->getId()}"; - $headers = [ - 'content-Type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') - ]; - $params = [ - 'buildIds' => $buildIds ?? [] - ]; - - $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); - $status = $response['headers']['status-code']; - if ($status >= 400) { - throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status); - } + $executor = new Executor(); + $executor->deleteRuntime($document->getId(), $projectId); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -554,167 +530,4 @@ class DeletesV1 extends Worker Console::info("No certificate files found for {$domain}"); } } - - const METHOD_GET = 'GET'; - const METHOD_POST = 'POST'; - const METHOD_PUT = 'PUT'; - const METHOD_PATCH = 'PATCH'; - const METHOD_DELETE = 'DELETE'; - const METHOD_HEAD = 'HEAD'; - const METHOD_OPTIONS = 'OPTIONS'; - const METHOD_CONNECT = 'CONNECT'; - const METHOD_TRACE = 'TRACE'; - - protected $selfSigned = false; - private $endpoint = 'http://appwrite-executor/v1'; - protected $headers = [ - 'content-type' => '', - ]; - - /** - * Call - * - * Make an API call - * - * @param string $method - * @param string $path - * @param array $params - * @param array $headers - * @param bool $decode - * @return array|string - * @throws Exception - */ - public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) - { - $headers = array_merge($this->headers, $headers); - $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); - $responseHeaders = []; - $responseStatus = -1; - $responseType = ''; - $responseBody = ''; - - switch ($headers['content-type']) { - case 'application/json': - $query = json_encode($params); - break; - - case 'multipart/form-data': - $query = $this->flatten($params); - break; - - default: - $query = http_build_query($params); - break; - } - - foreach ($headers as $i => $header) { - $headers[] = $i . ':' . $header; - unset($headers[$i]); - } - - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { - $len = strlen($header); - $header = explode(':', $header, 2); - - if (count($header) < 2) { // ignore invalid headers - return $len; - } - - $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); - - return $len; - }); - - if ($method != self::METHOD_GET) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $query); - } - - // Allow self signed certificates - if ($this->selfSigned) { - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - } - - $responseBody = curl_exec($ch); - $responseType = $responseHeaders['content-type'] ?? ''; - $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - if($decode) { - switch (substr($responseType, 0, strpos($responseType, ';'))) { - case 'application/json': - $json = json_decode($responseBody, true); - - if ($json === null) { - throw new Exception('Failed to parse response: '.$responseBody); - } - - $responseBody = $json; - $json = null; - break; - } - } - - if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { - throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); - } - - curl_close($ch); - - $responseHeaders['status-code'] = $responseStatus; - - if ($responseStatus === 500) { - echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n"; - } - - return [ - 'headers' => $responseHeaders, - 'body' => $responseBody - ]; - } - - /** - * Parse Cookie String - * - * @param string $cookie - * @return array - */ - public function parseCookie(string $cookie): array - { - $cookies = []; - - parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies); - - return $cookies; - } - - /** - * Flatten params array to PHP multiple format - * - * @param array $data - * @param string $prefix - * @return array - */ - protected function flatten(array $data, string $prefix = ''): array - { - $output = []; - - foreach ($data as $key => $value) { - $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; - - if (is_array($value)) { - $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed - } else { - $output[$finalKey] = $value; - } - } - - return $output; - } } diff --git a/composer.json b/composer.json index 02daadc7d5..5a46b5758f 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ ], "autoload": { "psr-4": { - "Appwrite\\": "src/Appwrite" + "Appwrite\\": "src/Appwrite", + "Executor\\": "src/Executor" } }, "autoload-dev": { diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php new file mode 100644 index 0000000000..91a34a11f6 --- /dev/null +++ b/src/Executor/Executor.php @@ -0,0 +1,278 @@ + '', + ]; + + public function __construct(string $endpoint = 'http://appwrite-executor/v1') + { + $this->endpoint = $endpoint; + } + + public function createRuntime( + string $functionId, + string $deploymentId, + string $buildId, + string $projectId, + string $path, + array $vars, + string $runtime, + string $baseImage) + { + $route = "/functions/$functionId/deployments/$deploymentId/builds/$buildId"; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'path' => $path, + 'vars' => $vars, + 'runtime' => $runtime, + 'baseImage' => $baseImage + ]; + + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); + + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception('Error creating build: ', $status); + } + + return $response['body']; + } + + public function deleteRuntime(string $deploymentId, string $projectId) + { + $route = "/deployments/$deploymentId"; + $headers = [ + 'content-Type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'buildIds' => $buildIds[$deploymentId] ?? [], + ]; + + $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); + + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception('Error deleting deplyoment: ' . $deploymentId , $status); + } + + return $response['body']; + } + + public function createExecution( + string $projectId, + string $functionId, + string $deploymentId, + string $buildId, + string $path, + array $vars, + string $data, + string $runtime, + string $baseImage, + $timeout, + $webhooks, + string $userId + ) + { + $route = "/functions/$functionId/executions"; + $headers = [ + 'content-Type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'deploymentId' => $deploymentId, + 'buildId' => $buildId, + 'path' => $path, + 'vars' => $vars, + 'data' => $data, + 'runtime' => $runtime, + 'timeout' => $timeout, + 'baseImage' => $baseImage, + 'webhooks' => $webhooks, + 'userId' => $userId, + ]; + + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); + + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception('Error creating execution: ', $status); + } + + return $response['body']; + } + + /** + * Call + * + * Make an API call + * + * @param string $method + * @param string $path + * @param array $params + * @param array $headers + * @param bool $decode + * @return array|string + * @throws Exception + */ + public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) + { + $headers = array_merge($this->headers, $headers); + $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + $responseHeaders = []; + $responseStatus = -1; + $responseType = ''; + $responseBody = ''; + + switch ($headers['content-type']) { + case 'application/json': + $query = json_encode($params); + break; + + case 'multipart/form-data': + $query = $this->flatten($params); + break; + + default: + $query = http_build_query($params); + break; + } + + foreach ($headers as $i => $header) { + $headers[] = $i . ':' . $header; + unset($headers[$i]); + } + + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', $header, 2); + + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + + return $len; + }); + + if ($method != self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + + // Allow self signed certificates + if ($this->selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + $responseBody = curl_exec($ch); + $responseType = $responseHeaders['content-type'] ?? ''; + $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if($decode) { + switch (substr($responseType, 0, strpos($responseType, ';'))) { + case 'application/json': + $json = json_decode($responseBody, true); + + if ($json === null) { + throw new Exception('Failed to parse response: '.$responseBody); + } + + $responseBody = $json; + $json = null; + break; + } + } + + if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { + throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); + } + + curl_close($ch); + + $responseHeaders['status-code'] = $responseStatus; + + if ($responseStatus === 500) { + echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n"; + } + + return [ + 'headers' => $responseHeaders, + 'body' => $responseBody + ]; + } + + /** + * Parse Cookie String + * + * @param string $cookie + * @return array + */ + public function parseCookie(string $cookie): array + { + $cookies = []; + + parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies); + + return $cookies; + } + + /** + * Flatten params array to PHP multiple format + * + * @param array $data + * @param string $prefix + * @return array + */ + protected function flatten(array $data, string $prefix = ''): array + { + $output = []; + + foreach ($data as $key => $value) { + $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; + + if (is_array($value)) { + $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed + } else { + $output[$finalKey] = $value; + } + } + + return $output; + } +} From f83fdc92de2a7b46874963f1fd97bee7e8ff38ae Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sat, 5 Feb 2022 14:08:05 +0400 Subject: [PATCH 07/61] feat: fix executor issues --- app/controllers/api/functions.php | 51 +++++++++++-------------------- app/executor.php | 2 +- src/Executor/Executor.php | 4 +-- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 034ab79a39..8776a3d1d8 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -25,6 +25,7 @@ use Utopia\Validator\Range; use Utopia\Validator\WhiteList; use Utopia\Config\Config; use Cron\CronExpression; +use Executor\Executor; use Utopia\CLI\Console; use Utopia\Validator\Boolean; @@ -865,45 +866,29 @@ App::post('/v1/functions/:functionId/executions') 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], 'APPWRITE_FUNCTION_DATA' => $data, + 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_USER_ID' => $user->getId(), 'APPWRITE_FUNCTION_JWT' => $jwt, - 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId() ]); // Directly execute function. - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$function->getId()}/executions"); - \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ - 'deploymentId' => $deployment->getId(), - 'buildId' => $deployment->getAttribute('buildId', ''), - 'path' => $build->getAttribute('outputPath', ''), - 'vars' => $vars, - 'data' => $data, - 'runtime' => $function->getAttribute('runtime', ''), - 'timeout' => $function->getAttribute('timeout', 0), - 'baseImage' => $runtime['image'], - 'webhooks' => $project->getAttribute('webhooks', []), - 'userId' => $user->getId(), - ])); - \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', - 'x-appwrite-project: '.$project->getId(), - 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') - ]); - - $responseExecute = \curl_exec($ch); - - $error = \curl_error($ch); - if (!empty($error)) { - Console::error('Curl error: '.$error); - } - \curl_close($ch); + $executor = new Executor(); + + $responseExecute = $executor->createExecution( + projectId: $project->getId(), + functionId: $function->getId(), + deploymentId: $deployment->getId(), + buildId: $deployment->getAttribute('buildId', ''), + path: $build->getAttribute('outputPath', ''), + vars: $vars, + data: $data, + runtime: $function->getAttribute('runtime', ''), + timeout: $function->getAttribute('timeout', 0), + baseImage: $runtime['image'], + webhooks: $project->getAttribute('webhooks', []), + userId: $user->getId(), + ); - $responseExecute = json_decode($responseExecute, true); $execution->setAttribute('status', $responseExecute['status']); $execution->setAttribute('statusCode', $responseExecute['statusCode']); $execution->setAttribute('stdout', $responseExecute['stdout']); diff --git a/app/executor.php b/app/executor.php index 2e923479f8..eafc634e4b 100644 --- a/app/executor.php +++ b/app/executor.php @@ -349,7 +349,7 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar } var_dump($executorResponse); - + $executionData = []; if (!empty($executorResponse)) { diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 91a34a11f6..8cbb1f17fb 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -67,7 +67,7 @@ class Executor { $route = "/deployments/$deploymentId"; $headers = [ - 'content-Type' => 'application/json', + 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; @@ -102,7 +102,7 @@ class Executor { $route = "/functions/$functionId/executions"; $headers = [ - 'content-Type' => 'application/json', + 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; From 1b0a02e20d654bfcb9a83babecbb0df2cf92009e Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sat, 5 Feb 2022 23:49:57 +0400 Subject: [PATCH 08/61] feat: fix executor issues --- app/controllers/api/functions.php | 35 ++-- app/executor.php | 52 +----- app/workers/functions.php | 295 ++++++++++++++++++++---------- src/Executor/Executor.php | 2 + 4 files changed, 219 insertions(+), 165 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 8776a3d1d8..b899f9ed5d 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -332,7 +332,7 @@ App::put('/v1/functions/:functionId') ]))); if ($next && $schedule !== $original) { - ResqueScheduler::enqueueAt($next, 'v1-functions', 'FunctionsV1', [ + ResqueScheduler::enqueueAt($next, Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME, [ 'projectId' => $project->getId(), 'webhooks' => $project->getAttribute('webhooks', []), 'functionId' => $function->getId(), @@ -761,13 +761,14 @@ App::post('/v1/functions/:functionId/executions') } $runtimes = Config::getParam('runtimes', []); - $key = $function->getAttribute('runtime', ''); - $runtime = isset($runtimes[$key]) ? $runtimes[$key] : null; + + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; + if (\is_null($runtime)) { throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400); } - $deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $function->getAttribute('deployment'))); + $deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception('Deployment not found. Deploy deployment before trying to execute a function', 404); @@ -784,7 +785,7 @@ App::post('/v1/functions/:functionId/executions') } if ($build->getAttribute('status') !== 'ready') { - throw new Exception('Build not completed', 400); + throw new Exception('Build not ready', 400); } $validator = new Authorization('execute'); @@ -798,7 +799,7 @@ App::post('/v1/functions/:functionId/executions') $execution = Authorization::skip(fn() => $dbForProject->createDocument('executions', new Document([ '$id' => $executionId, '$read' => (!$user->isEmpty()) ? ['user:' . $user->getId()] : [], - '$write' => ['role:all'], + '$write' => [], 'dateCreated' => time(), 'functionId' => $function->getId(), 'deploymentId' => $deployment->getId(), @@ -833,32 +834,23 @@ App::post('/v1/functions/:functionId/executions') } if ($async) { - Resque::enqueue('v1-functions', 'FunctionsV1', [ + Resque::enqueue(Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME, [ 'projectId' => $project->getId(), - 'deploymentId' => $deployment->getId(), - 'buildId' => $deployment->getAttribute('buildId', ''), - 'path' => $build->getAttribute('outputPath', ''), - 'vars' => $function->getAttribute('vars', []), - 'data' => $data, - 'runtime' => $function->getAttribute('runtime', ''), - 'timeout' => $function->getAttribute('timeout', 0), - 'baseImage' => $runtime['image'], - 'webhooks' => $project->getAttribute('webhooks', []), - 'userId' => $user->getId(), 'functionId' => $function->getId(), + 'webhooks' => $project->getAttribute('webhooks', []), 'executionId' => $execution->getId(), 'trigger' => 'http', + 'data' => $data, + 'userId' => $user->getId(), 'jwt' => $jwt, ]); $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($execution, Response::MODEL_EXECUTION); - return $response; } /** Send variables */ $vars = \array_merge($function->getAttribute('vars', []), [ - 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), 'APPWRITE_FUNCTION_ID' => $function->getId(), 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), @@ -871,9 +863,8 @@ App::post('/v1/functions/:functionId/executions') 'APPWRITE_FUNCTION_JWT' => $jwt, ]); - // Directly execute function. + /** Execute function */ $executor = new Executor(); - $responseExecute = $executor->createExecution( projectId: $project->getId(), functionId: $function->getId(), @@ -882,6 +873,7 @@ App::post('/v1/functions/:functionId/executions') path: $build->getAttribute('outputPath', ''), vars: $vars, data: $data, + entrypoint: $deployment->getAttribute('entrypoint', ''), runtime: $function->getAttribute('runtime', ''), timeout: $function->getAttribute('timeout', 0), baseImage: $runtime['image'], @@ -889,6 +881,7 @@ App::post('/v1/functions/:functionId/executions') userId: $user->getId(), ); + /** Update execution status */ $execution->setAttribute('status', $responseExecute['status']); $execution->setAttribute('statusCode', $responseExecute['statusCode']); $execution->setAttribute('stdout', $responseExecute['stdout']); diff --git a/app/executor.php b/app/executor.php index eafc634e4b..050800b5c8 100644 --- a/app/executor.php +++ b/app/executor.php @@ -1,11 +1,7 @@ '/usr/code', - 'file' => $vars['ENTRYPOINT_NAME'], + 'file' => $entrypoint, 'env' => $vars, 'payload' => $data, 'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) @@ -348,8 +344,6 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500); } - var_dump($executorResponse); - $executionData = []; if (!empty($executorResponse)) { @@ -390,44 +384,7 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar 'time' => $executionTime, ]; - /** Trigger event */ - $executionModel = new Execution(); - $executionUpdate = new Event('v1-webhooks', 'WebhooksV1'); - $executionUpdate - ->setParam('projectId', $projectId) - ->setParam('userId', $userId) - ->setParam('webhooks', $webhooks) - ->setParam('event', 'functions.executions.update') - ->setParam('eventData', (new Document($execution))->getArrayCopy(array_keys($executionModel->getRules()))); - $executionUpdate->trigger(); - - /** Trigger realtime event */ - $target = Realtime::fromPayload('functions.executions.update', new Document($execution)); - Realtime::send( - projectId: $projectId, - payload: $execution, - event: 'functions.executions.update', - channels: $target['channels'], - roles: $target['roles'] - ); - - if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') { - $statsd = $register->get('statsd'); - $usage = new Stats($statsd); - $usage - ->setParam('projectId', $projectId) - ->setParam('functionId', $functionId) - ->setParam('functionExecution', 1) - ->setParam('functionStatus', $functionStatus) - ->setParam('functionExecutionTime', $executionTime * 1000) // ms - ->setParam('networkRequestSize', 0) - ->setParam('networkResponseSize', 0) - ->submit(); - $usage->submit(); - } - return $execution; - }; function runBuildStage(string $buildId, string $projectID, string $path, array $vars, string $baseImage, string $runtime): array @@ -651,6 +608,7 @@ App::post('/v1/functions/:functionId/executions') ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) ->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true) ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) + ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file') ->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true) ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) ->param('webhooks', [], new ArrayList(new JSON()), 'Any webhooks that need to be triggered after this execution', true) @@ -658,7 +616,7 @@ App::post('/v1/functions/:functionId/executions') ->inject('projectId') ->inject('response') ->action( - function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $data, string $runtime, $timeout, string $baseImage, array $webhooks, string $userId, string $projectId, Response $response) { + function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, array $webhooks, string $userId, string $projectId, Response $response) { $build = [ '$id' => $buildId, @@ -666,7 +624,7 @@ App::post('/v1/functions/:functionId/executions') ]; // Send both data and vars from the caller - $execution = execute($projectId, $functionId, $deploymentId, $build, $vars, $data, $userId, $baseImage, $runtime, $timeout, $webhooks); + $execution = execute($projectId, $functionId, $deploymentId, $build, $vars, $data, $userId, $baseImage, $runtime, $entrypoint, $timeout, $webhooks); $response ->setStatusCode(Response::STATUS_CODE_OK) diff --git a/app/workers/functions.php b/app/workers/functions.php index bf5779f1e6..52c3df4fb1 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -1,7 +1,12 @@ executor = new Executor(); } public function run(): void @@ -70,13 +81,7 @@ class FunctionsV1 extends Worker /** @var Document[] $functions */ while ($sum >= $limit) { - - Authorization::disable(); - - $functions = $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC]); - - Authorization::reset(); - + $functions = Authorization::skip(fn() => $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC])); $sum = \count($functions); $offset = $offset + $limit; @@ -84,31 +89,31 @@ class FunctionsV1 extends Worker foreach ($functions as $function) { $events = $function->getAttribute('events', []); - $deployment = $function->getAttribute('deployment', []); - Console::success('Itterating function: ' . $function->getAttribute('name')); - - if (!\in_array($event, $events) || empty($deployment)) { + if (!\in_array($event, $events)) { continue; } - Console::success('Triggered function: ' . $event); + Console::success('Iterating function: ' . $function->getAttribute('name')); $this->execute( - trigger: 'event', projectId: $projectId, - executionId: '', - database: $database, function: $function, + dbForProject: $database, + executionId: $executionId, + webhooks: $webhooks, + trigger: $trigger, event: $event, eventData: $eventData, data: $data, - webhooks: $webhooks, userId: $userId, jwt: $jwt ); + + Console::success('Triggered function: ' . $event); } } + break; case 'schedule': @@ -126,9 +131,7 @@ class FunctionsV1 extends Worker */ // Reschedule - Authorization::disable(); - $function = $database->getDocument('functions', $functionId); - Authorization::reset(); + $function = Authorization::skip(fn() => $database->getDocument('functions', $functionId)); if (empty($function->getId())) { throw new Exception('Function not found ('.$functionId.')'); @@ -145,19 +148,18 @@ class FunctionsV1 extends Worker ->setAttribute('scheduleNext', $next) ->setAttribute('schedulePrevious', \time()); - Authorization::disable(); + $function = Authorization::skip(function() use ($database, $function, $next, $functionId) { + $function = $database->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ + 'scheduleNext' => (int)$next, + ]))); + + if ($function === false) { + throw new Exception('Function update failed (' . $functionId . ')'); + } + return $function; + }); - $function = $database->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ - 'scheduleNext' => (int)$next, - ]))); - - if ($function === false) { - throw new Exception('Function update failed (' . $functionId . ')'); - } - - Authorization::reset(); - - ResqueScheduler::enqueueAt($next, 'v1-functions', 'FunctionsV1', [ + ResqueScheduler::enqueueAt($next, Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME, [ 'projectId' => $projectId, 'webhooks' => $webhooks, 'functionId' => $function->getId(), @@ -168,101 +170,200 @@ class FunctionsV1 extends Worker ]); // Async task reschedule $this->execute( - trigger: $trigger, projectId: $projectId, - executionId: $executionId, - database: $database, function: $function, - data: $data, + dbForProject: $database, + executionId: $executionId, webhooks: $webhooks, + trigger: $trigger, + event: $event, + eventData: $eventData, + data: $data, userId: $userId, jwt: $jwt ); break; case 'http': - Authorization::disable(); - $function = $database->getDocument('functions', $functionId); - Authorization::reset(); + $function = Authorization::skip(fn() => $database->getDocument('functions', $functionId)); if (empty($function->getId())) { throw new Exception('Function not found ('.$functionId.')'); } - $deployment = Authorization::skip(fn() => $database->getDocument('deployments', $function->getAttribute('deployment'))); - - $build = Authorization::skip(fn() => $database->getDocument('builds', $deployment->getAttribute('build'))); - $path = $build->getAttribute('path', ''); - $this->execute( - trigger: $trigger, projectId: $projectId, - executionId: $executionId, - path: $path, - buildId: $deployment->getAttribute('buildId', ''), - deploymentId: $deployment->getId(), - database: $database, function: $function, - data: $data, + dbForProject: $database, + executionId: $executionId, webhooks: $webhooks, + trigger: $trigger, + event: $event, + eventData: $eventData, + data: $data, userId: $userId, jwt: $jwt ); + break; } } - /** - * Execute function deployment - * - * @param string $trigger - * @param string $projectId - * @param string $executionId - * @param Database $database - * @param Document $function - * @param string $event - * @param string $eventData - * @param string $data - * @param array $webhooks - * @param string $userId - * @param string $jwt - * - * @return void - */ - public function execute(string $trigger, string $path, string $projectId, string $deploymentId, string $buildId, string $executionId, Database $database, Document $function, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): void - { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$function->getId()}/executions"); - \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ - 'deploymentId' => $deploymentId, - 'buildId' => $buildId, - 'path' => $path, - 'vars' => $function->getAttribute('vars', []), - 'data' => $data, - 'runtime' => $function->getAttribute('runtime', ''), - 'timeout' => $function->getAttribute('timeout', 0), - 'baseImage' => '', - 'webhooks' => $webhooks, - 'userId' => $userId, - ])); - \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', - 'x-appwrite-project: '.$projectId, - 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') - ]); + private function execute( + string $projectId, + Document $function, + Database $dbForProject, + string $executionId, + array $webhooks, + string $trigger, + string $event, + string $eventData, + string $data, + string $userId, + string $jwt + ) { - \curl_exec($ch); + $functionId = $function->getId(); + $deploymentId = $function->getAttribute('deployment', ''); - $error = \curl_error($ch); - if (!empty($error)) { - Console::error('Curl error: '.$error); + /** Check if deployment exists */ + $deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $deploymentId)); + + if ($deployment->getAttribute('resourceId') !== $functionId) { + throw new Exception('Deployment not found. Create deployment before trying to execute a function', 404); } - \curl_close($ch); + if ($deployment->isEmpty()) { + throw new Exception('Deployment not found. Create deployment before trying to execute a function', 404); + } + + /** Check if build has exists */ + $build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + if ($build->isEmpty()) { + throw new Exception('Build not found', 404); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new Exception('Build not ready', 400); + } + + /** Check if runtime is supported */ + $runtimes = Config::getParam('runtimes', []); + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; + + if (\is_null($runtime)) { + throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400); + } + + /** Create execution or update execution status */ + $execution = Authorization::skip(function() use ($dbForProject, &$executionId, $functionId, $deploymentId, $trigger, $userId) { + $execution = $dbForProject->getDocument('executions', $executionId); + if ($execution->isEmpty()) { + $executionId = $dbForProject->getId(); + $execution = $dbForProject->createDocument('executions', new Document([ + '$id' => $executionId, + '$read' => $userId ? ['user:' . $userId] : [], + '$write' => [], + 'dateCreated' => time(), + 'functionId' => $functionId, + 'deploymentId' => $deploymentId, + 'trigger' => $trigger, + 'status' => 'waiting', + 'statusCode' => 0, + 'stdout' => '', + 'stderr' => '', + 'time' => 0.0, + 'search' => implode(' ', [$functionId, $executionId]), + ])); + + if ($execution->isEmpty()) { + throw new Exception('Failed to create or read execution'); + } + } + $execution->setAttribute('status', 'processing'); + $execution = $dbForProject->updateDocument('executions', $executionId, $execution); + return $execution; + }); + + /** Collect environment variables */ + $vars = [ + 'APPWRITE_FUNCTION_ID' => $functionId, + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + 'APPWRITE_FUNCTION_DEPLOYMENT' => $deploymentId, + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], + 'APPWRITE_FUNCTION_TRIGGER' => $trigger, + 'APPWRITE_FUNCTION_EVENT' => $event, + 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData, + 'APPWRITE_FUNCTION_DATA' => $data, + 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, + 'APPWRITE_FUNCTION_USER_ID' => $userId, + 'APPWRITE_FUNCTION_JWT' => $jwt, + ]; + $vars = \array_merge($function->getAttribute('vars', []), $vars); + + /** Execute function */ + $executionResponse = $this->executor->createExecution( + projectId: $projectId, + functionId: $functionId, + deploymentId: $deploymentId, + buildId: $deployment->getAttribute('buildId', ''), + path: $build->getAttribute('outputPath', ''), + vars: $vars, + entrypoint: $deployment->getAttribute('entrypoint', ''), + data: $vars['APPWRITE_FUNCTION_DATA'], + runtime: $function->getAttribute('runtime', ''), + timeout: $function->getAttribute('timeout', 0), + baseImage: $runtime['image'], + webhooks: $webhooks, + userId: $userId, + ); + + /** Update execution status */ + $execution->setAttribute('status', $executionResponse['status']); + $execution->setAttribute('statusCode', $executionResponse['statusCode']); + $execution->setAttribute('stdout', $executionResponse['stdout']); + $execution->setAttribute('stderr', $executionResponse['stderr']); + $execution->setAttribute('time', $executionResponse['time']); + $execution = Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution)); + + /** Trigger Webhook */ + $executionModel = new Execution(); + $executionUpdate = new Event(Event::WEBHOOK_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME); + $executionUpdate + ->setParam('projectId', $projectId) + ->setParam('userId', $userId) + ->setParam('webhooks', $webhooks) + ->setParam('event', 'functions.executions.update') + ->setParam('eventData', $execution->getArrayCopy(array_keys($executionModel->getRules()))); + $executionUpdate->trigger(); + + /** Trigger realtime event */ + $target = Realtime::fromPayload('functions.executions.update', $execution); + Realtime::send( + projectId: $projectId, + payload: $execution->getArrayCopy(), + event: 'functions.executions.update', + channels: $target['channels'], + roles: $target['roles'] + ); + + /** Update usage stats */ + global $register; + if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') { + $statsd = $register->get('statsd'); + $usage = new Stats($statsd); + $usage + ->setParam('projectId', $projectId) + ->setParam('functionId', $function->getId()) + ->setParam('functionExecution', 1) + ->setParam('functionStatus', $execution->getAttribute('status', '')) + ->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000) // ms + ->setParam('networkRequestSize', 0) + ->setParam('networkResponseSize', 0) + ->submit(); + $usage->submit(); + } } public function shutdown(): void diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 8cbb1f17fb..e38ef0585f 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -92,6 +92,7 @@ class Executor string $buildId, string $path, array $vars, + string $entrypoint, string $data, string $runtime, string $baseImage, @@ -117,6 +118,7 @@ class Executor 'baseImage' => $baseImage, 'webhooks' => $webhooks, 'userId' => $userId, + 'entrypoint' => $entrypoint ]; $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); From a96c6c84ee2e488ffda2eef8d2fd29f1b43cb2b8 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 13:15:52 +0400 Subject: [PATCH 09/61] feat: update comment --- app/controllers/api/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b899f9ed5d..a60051406c 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -849,7 +849,7 @@ App::post('/v1/functions/:functionId/executions') $response->dynamic($execution, Response::MODEL_EXECUTION); } - /** Send variables */ + /** Environment variables */ $vars = \array_merge($function->getAttribute('vars', []), [ 'APPWRITE_FUNCTION_ID' => $function->getId(), 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), From ae073f5369606b0491f03cdad2c5a46bac529f56 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 13:18:44 +0400 Subject: [PATCH 10/61] feat: update comment --- tests/e2e/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 2fd337a532..4e0c138b9e 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -155,7 +155,7 @@ class Client * @return array|string * @throws Exception */ - public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15) + public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true) { $headers = array_merge($this->headers, $headers); $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); @@ -189,7 +189,7 @@ class Client curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { $len = strlen($header); $header = explode(':', $header, 2); From a9c1b96de5522344ffc4db04c6af04f7cdc1b10b Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 13:19:11 +0400 Subject: [PATCH 11/61] feat: update comment --- app/controllers/api/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index a60051406c..0df59e3e5b 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -849,7 +849,7 @@ App::post('/v1/functions/:functionId/executions') $response->dynamic($execution, Response::MODEL_EXECUTION); } - /** Environment variables */ + /** Collect environment variables */ $vars = \array_merge($function->getAttribute('vars', []), [ 'APPWRITE_FUNCTION_ID' => $function->getId(), 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), From 20415aaafd0894c3808d6d2f83e046f6c79b1ed0 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 13:25:10 +0400 Subject: [PATCH 12/61] feat: update creation endpoint --- app/executor.php | 2 +- src/Executor/Executor.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/executor.php b/app/executor.php index 050800b5c8..24f75ea653 100644 --- a/app/executor.php +++ b/app/executor.php @@ -599,7 +599,7 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ } } -App::post('/v1/functions/:functionId/executions') +App::post('/v1/execution') ->desc('Execute a function') ->param('functionId', '', new Text(1024), 'The FunctionID to execute') ->param('deploymentId', '', new Text(1024), 'The deployment ID to execute') diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index e38ef0585f..41225c6a50 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -101,13 +101,14 @@ class Executor string $userId ) { - $route = "/functions/$functionId/executions"; + $route = "/execution"; $headers = [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; $params = [ + 'functionId' => $functionId, 'deploymentId' => $deploymentId, 'buildId' => $buildId, 'path' => $path, From 132011102e60315241cdf6b1909c837535d9b559 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 13:25:33 +0400 Subject: [PATCH 13/61] feat: update creation endpoint --- app/executor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/executor.php b/app/executor.php index 24f75ea653..f39ef01923 100644 --- a/app/executor.php +++ b/app/executor.php @@ -600,7 +600,7 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ } App::post('/v1/execution') - ->desc('Execute a function') + ->desc('Create a function execution') ->param('functionId', '', new Text(1024), 'The FunctionID to execute') ->param('deploymentId', '', new Text(1024), 'The deployment ID to execute') ->param('buildId', '', new Text(1024), 'The build ID of the function') From dea2873845a14e83a4e8f39a74b7219678e273db Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 13:37:50 +0400 Subject: [PATCH 14/61] feat: update creation endpoint --- app/executor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/executor.php b/app/executor.php index f39ef01923..55120c2f4b 100644 --- a/app/executor.php +++ b/app/executor.php @@ -262,11 +262,11 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar Console::info('Executing function: ' . $functionId); global $activeFunctions; - global $register; $container = 'appwrite-function-' . $deploymentId; /** Create a new runtime server if there's none running */ if (!$activeFunctions->exists($container)) { + Console::info("Runtime server for $deploymentId not running. Creating new one..."); createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); } From 6ab1095280d77dec09a8d2c2a5d4770679658f01 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 14:00:44 +0400 Subject: [PATCH 15/61] feat: catch docker remove errors elegantly --- app/executor.php | 12 ++++++------ app/workers/deletes.php | 9 +++++---- src/Executor/Executor.php | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/executor.php b/app/executor.php index 55120c2f4b..78acb606bf 100644 --- a/app/executor.php +++ b/app/executor.php @@ -652,14 +652,14 @@ App::delete('/v1/deployments/:deploymentId') // Remove all the build containers foreach ($buildIds as $buildId) { - $status = $orchestration->remove('build-stage-' . $buildId, true); - if ($status) { - Console::success("Removed build container: $buildId for deployment: " . $deploymentId); - } else { - Console::error("Failed to remove build container: $buildId for deployment: " . $deploymentId); + try { + Console::info('Deleting build container : ' . $buildId); + $status = $orchestration->remove('build-stage-' . $buildId, true); + } catch (Throwable $th) { + Console::error($th->getMessage()); } } - + $orchestrationPool->put($orchestration); $response diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 0760a67886..45414434e9 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -358,7 +358,7 @@ class DeletesV1 extends Worker $executor = new Executor(); foreach ($deploymentIds as $deploymentId) { try { - $executor->deleteRuntime($deploymentId, $projectId); + $executor->deleteRuntime($deploymentId, $buildIds[$deploymentId], $projectId); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -373,6 +373,7 @@ class DeletesV1 extends Worker protected function deleteDeployment(Document $document, string $projectId): void { $dbForProject = $this->getProjectDB($projectId); + $deploymentId = $document->getId(); /** * Delete deployment files @@ -390,8 +391,8 @@ class DeletesV1 extends Worker $buildIds = []; $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); $this->deleteByGroup('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$document->getId()]) - ], $dbForProject, function (Document $document) use ($storageBuilds) { + new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]) + ], $dbForProject, function (Document $document) use ($storageBuilds, &$buildIds) { $buildIds[] = $document->getId(); if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) { Console::success('Deleted build files: ' . $document->getAttribute('outputPath', '')); @@ -405,7 +406,7 @@ class DeletesV1 extends Worker */ try { $executor = new Executor(); - $executor->deleteRuntime($document->getId(), $projectId); + $executor->deleteRuntime($deploymentId, $buildIds, $projectId); } catch (Throwable $th) { Console::error($th->getMessage()); } diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 41225c6a50..e297c30d7e 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -63,7 +63,7 @@ class Executor return $response['body']; } - public function deleteRuntime(string $deploymentId, string $projectId) + public function deleteRuntime(string $deploymentId, array $buildIds, string $projectId) { $route = "/deployments/$deploymentId"; $headers = [ @@ -72,14 +72,14 @@ class Executor 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; $params = [ - 'buildIds' => $buildIds[$deploymentId] ?? [], + 'buildIds' => $buildIds, ]; $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); $status = $response['headers']['status-code']; if ($status >= 400) { - throw new \Exception('Error deleting deplyoment: ' . $deploymentId , $status); + throw new \Exception('Error deleting deployment: ' . $deploymentId , $status); } return $response['body']; From 7735b67a3faea8431c13697ea5969e842d4d4e5e Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 6 Feb 2022 15:19:33 +0400 Subject: [PATCH 16/61] feat: fix tests --- app/workers/functions.php | 6 +++--- tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index 52c3df4fb1..f1970b1962 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -334,16 +334,16 @@ class FunctionsV1 extends Worker ->setParam('projectId', $projectId) ->setParam('userId', $userId) ->setParam('webhooks', $webhooks) - ->setParam('event', 'functions.executions.update') + ->setParam('event', $event) ->setParam('eventData', $execution->getArrayCopy(array_keys($executionModel->getRules()))); $executionUpdate->trigger(); /** Trigger realtime event */ - $target = Realtime::fromPayload('functions.executions.update', $execution); + $target = Realtime::fromPayload($event, $execution); Realtime::send( projectId: $projectId, payload: $execution->getArrayCopy(), - event: 'functions.executions.update', + event: $event, channels: $target['channels'], roles: $target['roles'] ); diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index 4f930ed6b5..94db8cc15a 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -476,7 +476,7 @@ class WebhooksCustomServerTest extends Scope $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), ['executionId' => 'unique()',]); + ], $this->getHeaders()), []); $this->assertEquals($execution['headers']['status-code'], 201); $this->assertNotEmpty($execution['body']['$id']); From f18f96e816fd6da519cf9f48ab140776395067df Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 10 Feb 2022 00:20:28 +0400 Subject: [PATCH 17/61] feat: fix tests --- app/controllers/api/functions.php | 3 +-- app/workers/functions.php | 6 +++--- src/Executor/Executor.php | 1 - tests/e2e/Services/Realtime/RealtimeCustomClientTest.php | 9 +++++++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 0df59e3e5b..3e9caed315 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -369,7 +369,6 @@ App::patch('/v1/functions/:functionId/deployment') $function = $dbForProject->getDocument('functions', $functionId); $deployment = $dbForProject->getDocument('deployments', $deployment); - var_dump($deployment); $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); if ($function->isEmpty()) { @@ -846,7 +845,7 @@ App::post('/v1/functions/:functionId/executions') ]); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($execution, Response::MODEL_EXECUTION); + return $response->dynamic($execution, Response::MODEL_EXECUTION); } /** Collect environment variables */ diff --git a/app/workers/functions.php b/app/workers/functions.php index f1970b1962..52c3df4fb1 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -334,16 +334,16 @@ class FunctionsV1 extends Worker ->setParam('projectId', $projectId) ->setParam('userId', $userId) ->setParam('webhooks', $webhooks) - ->setParam('event', $event) + ->setParam('event', 'functions.executions.update') ->setParam('eventData', $execution->getArrayCopy(array_keys($executionModel->getRules()))); $executionUpdate->trigger(); /** Trigger realtime event */ - $target = Realtime::fromPayload($event, $execution); + $target = Realtime::fromPayload('functions.executions.update', $execution); Realtime::send( projectId: $projectId, payload: $execution->getArrayCopy(), - event: $event, + event: 'functions.executions.update', channels: $target['channels'], roles: $target['roles'] ); diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index e297c30d7e..fa4ff4d7c4 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -176,7 +176,6 @@ class Executor curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 95b4dd4423..8f8e467ea3 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1049,6 +1049,15 @@ class RealtimeCustomClientTest extends Scope $this->assertNotEmpty($responseUpdate['data']['payload']); $client->close(); + + // Cleanup : Delete function + $response = $this->client->call(Client::METHOD_DELETE, '/functions/'. $functionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], []); + + $this->assertEquals(204, $response['headers']['status-code']); } public function testChannelTeams(): array From b77c698b4fb99a86e422a5f19e0e7fb9668e3714 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 11 Feb 2022 14:31:53 +0400 Subject: [PATCH 18/61] feat: add remaninig endpoints --- app/executor.php | 80 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/app/executor.php b/app/executor.php index 78acb606bf..2805ec97b4 100644 --- a/app/executor.php +++ b/app/executor.php @@ -599,6 +599,61 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ } } +// POST /v1/runtimes +App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') + ->desc("Create a new build") + ->param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) + ->param('buildId', '', new UID(), 'Build unique ID.', false) + ->param('path', '', new Text(0), 'Path to source files.', false) + ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) + ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) + ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) + ->inject('projectId') + ->inject('response') + ->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) { + + $build = runBuildStage($buildId, $projectId, $path, $vars, $baseImage, $runtime); + + if ( $build['status'] === 'ready') { + $build = createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); + } + + $response + ->setStatusCode(201) + ->json($build); + }); + + +// GET /v1/runtimes +App::get('/v1/runtimes') + ->desc("Get the list of currently active runtimes") + ->inject('response') + ->action(function (Response $response) { + // TODO : Get list of active runtimes from swoole table + $runtimes = []; + + $response + ->setStatusCode(200) + ->json($runtimes); + }); + +// GET /v1/runtimes/:runtimeId (projectId + functionId) +App::get('/v1/runtimes/:runtimeId') + ->desc("Get a runtime by its ID") + ->param('runtimeId', '', new UID(), 'Runtime unique ID.') + ->inject('response') + ->action(function (Response $response) { + + // Get a runtime by its ID + $runtime = []; + + $response + ->setStatusCode(200) + ->json($runtime); + }); + +// POST /v1/execution (get runtime as param, if 404 or 501/503, go and create a runtime first) App::post('/v1/execution') ->desc('Create a function execution') ->param('functionId', '', new Text(1024), 'The FunctionID to execute') @@ -632,6 +687,7 @@ App::post('/v1/execution') } ); +// DELETE /v1/runtimes/:runtimeId (projectId + functionId) App::delete('/v1/deployments/:deploymentId') ->desc('Delete a deployment') ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) @@ -667,30 +723,6 @@ App::delete('/v1/deployments/:deploymentId') ->send(); }); -App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') - ->desc("Create a new build") - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) - ->param('buildId', '', new UID(), 'Build unique ID.', false) - ->param('path', '', new Text(0), 'Path to source files.', false) - ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) - ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) - ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) - ->inject('projectId') - ->inject('response') - ->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) { - - $build = runBuildStage($buildId, $projectId, $path, $vars, $baseImage, $runtime); - - if ( $build['status'] === 'ready') { - $build = createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); - } - - $response - ->setStatusCode(201) - ->json($build); - }); - App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode $http = new Server("0.0.0.0", 80); From 0276d94ef109c16a595f7ddce9231842685d6503 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 13 Feb 2022 02:34:16 +0400 Subject: [PATCH 19/61] feat: add remaninig endpoints --- app/controllers/api/functions.php | 5 +- app/executor.php | 175 +++++++++++++----------------- app/workers/builds.php | 4 - app/workers/deletes.php | 21 +++- app/workers/functions.php | 5 +- src/Executor/Executor.php | 23 ++-- 6 files changed, 105 insertions(+), 128 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 3e9caed315..b42e495fb7 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -868,16 +868,13 @@ App::post('/v1/functions/:functionId/executions') projectId: $project->getId(), functionId: $function->getId(), deploymentId: $deployment->getId(), - buildId: $deployment->getAttribute('buildId', ''), path: $build->getAttribute('outputPath', ''), vars: $vars, data: $data, entrypoint: $deployment->getAttribute('entrypoint', ''), runtime: $function->getAttribute('runtime', ''), timeout: $function->getAttribute('timeout', 0), - baseImage: $runtime['image'], - webhooks: $project->getAttribute('webhooks', []), - userId: $user->getId(), + baseImage: $runtime['image'] ); /** Update execution status */ diff --git a/app/executor.php b/app/executor.php index 2805ec97b4..b482642747 100644 --- a/app/executor.php +++ b/app/executor.php @@ -12,8 +12,6 @@ use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Document; -use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Orchestration\Adapter\DockerCLI; use Utopia\Orchestration\Orchestration; @@ -28,7 +26,6 @@ use Utopia\Validator\Text; require_once __DIR__ . '/init.php'; -Authorization::disable(); Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); function logError(Throwable $error, string $action, Utopia\Route $route = null) @@ -120,7 +117,7 @@ try { try { $orchestration = $orchestrationPool->get(); $executionStart = \microtime(true); - $residueList = $orchestration->list(['label' => 'appwrite-type=function']); + $residueList = $orchestration->list(['label' => 'openruntimes-type=function']); } catch (\Throwable $th) { } finally { $orchestrationPool->put($orchestration); @@ -143,7 +140,7 @@ try { call_user_func($logError, $error, "startupError"); } -function createRuntimeServer(string $projectId, string $deploymentId, array $build, array $vars, string $baseImage, string $runtime): array +function createRuntimeServer(string $runtimeId, array $build, array $vars, string $baseImage, string $runtime): array { global $orchestrationPool; global $activeFunctions; @@ -151,7 +148,7 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui $orchestration = $orchestrationPool->get(); try { - $container = 'appwrite-function-' . $deploymentId; + $container = 'runtime-' . $runtimeId; if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online // If container is online then stop and remove it try { @@ -164,7 +161,7 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui /** Storage stuff */ $deploymentPath = $build['outputPath']; - $deploymentPathTarget = '/tmp/project-' . $projectId . '/' . $build['$id'] . '/builtCode/code.tar.gz'; + $deploymentPathTarget = "/tmp/$runtimeId/builtCode/code.tar.gz"; $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); $device = Storage::getDevice('builds'); @@ -191,13 +188,6 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui // Generate random secret key $secret = \bin2hex(\random_bytes(16)); $vars = \array_merge($vars, [ - // 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''), - // 'APPWRITE_FUNCTION_ID' => $function->getId(), - // 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), - // 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - // 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], - // 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], - 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, 'INTERNAL_RUNTIME_KEY' => $secret ]); @@ -218,11 +208,10 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui name: $container, vars: $vars, labels: [ - 'appwrite-type' => 'function', - 'appwrite-created' => strval($executionTime), - 'appwrite-runtime' => $runtime, - 'appwrite-project' => $projectId, - 'appwrite-deployment' => $deploymentId, + 'openruntimes-id' => $runtimeId, + 'openruntimes-type' => 'function', + 'openruntimes-created' => strval($executionTime), + 'openruntimes-runtime' => $runtime ], hostname: $container, mountFolder: $deploymentPathTargetDir, @@ -256,21 +245,21 @@ function createRuntimeServer(string $projectId, string $deploymentId, array $bui } }; -function execute(string $projectId, string $functionId, string $deploymentId, array $build, array $vars, string $data, string $userId, string $baseImage, string $runtime, string $entrypoint, int $timeout, array $webhooks = []): array +function execute(string $runtimeId, array $build, array $vars, string $data, string $baseImage, string $runtime, string $entrypoint, int $timeout): array { - Console::info('Executing function: ' . $functionId); + Console::info('Executing Runtime: ' . $runtimeId); global $activeFunctions; - $container = 'appwrite-function-' . $deploymentId; + $container = 'runtime-' . $runtimeId; /** Create a new runtime server if there's none running */ if (!$activeFunctions->exists($container)) { - Console::info("Runtime server for $deploymentId not running. Creating new one..."); - createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); + Console::info("Runtime server for $runtimeId not running. Creating new one..."); + createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); } - $key = $activeFunctions->get('appwrite-function-' . $deploymentId, 'key'); + $key = $activeFunctions->get('runtime-' . $runtimeId, 'key'); $stdout = ''; $stderr = ''; @@ -387,7 +376,7 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar return $execution; }; -function runBuildStage(string $buildId, string $projectID, string $path, array $vars, string $baseImage, string $runtime): array +function runBuildStage(string $runtimeId, string $path, array $vars, string $baseImage, string $runtime): array { global $orchestrationPool; @@ -401,15 +390,16 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ $buildEnd = 0; try { - Console::info('Running build stage: ' . $buildId); + Console::info('Building runtime with ID : ' . $runtimeId); + // Grab Deployment Files $deploymentPath = $path; $device = Storage::getDevice('builds'); - $deploymentPathTarget = '/tmp/project-' . $projectID . '/' . $buildId . '/code.tar.gz'; + $deploymentPathTarget = "/tmp/$runtimeId/code.tar.gz"; $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); - $container = 'build-stage-' . $buildId; + $container = 'build-' . $runtimeId; // Perform various checks if (!\file_exists($deploymentPathTargetDir)) { @@ -435,17 +425,18 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ throw new Exception('Code is not readable: ' . $path); } - $vars = array_map(fn ($v) => strval($v), $vars); - $path = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode'; + $path = "/tmp/$runtimeId/builtCode"; if (!\file_exists($path)) { if (@\mkdir($path, 0777, true)) { \chmod($path, 0777); } else { - throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $buildId . '/builtCode'); + throw new Exception("Can't create directory : /tmp/$runtimeId/builtCode"); } } + $vars = array_map(fn ($v) => strval($v), $vars); + $orchestration ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) @@ -457,11 +448,10 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ vars: $vars, workdir: '/usr/code', labels: [ - 'appwrite-type' => 'function', - 'appwrite-created' => strval($buildStart), - 'appwrite-runtime' => $runtime, - 'appwrite-project' => $projectID, - 'appwrite-build' => $buildId, + 'openruntimes-id' => $runtimeId, + 'openruntimes-type' => 'build', + 'openruntimes-created' => strval($buildStart), + 'openruntimes-runtime' => $runtime, ], command: [ 'tail', @@ -471,7 +461,7 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ hostname: $container, mountFolder: $deploymentPathTargetDir, volumes: [ - '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode' . ':/usr/builtCode:rw' + "/tmp/$runtimeId/builtCode" . ':/usr/builtCode:rw' ] ); @@ -515,9 +505,6 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ // Repackage Code and Save. $compressStdout = ''; $compressStderr = ''; - - $builtCodePath = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode/code.tar.gz'; - $compressSuccess = $orchestration->execute( name: $container, command: [ @@ -533,6 +520,7 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ } // Check if the build was successful by checking if file exists + $builtCodePath = "/tmp/$runtimeId/builtCode/code.tar.gz"; if (!\file_exists($builtCodePath)) { throw new Exception('Something went wrong during the build process.'); } @@ -566,7 +554,6 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ $buildEnd = \time(); $build = [ - '$id' => $buildId, 'outputPath' => $path, 'status' => 'ready', 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), @@ -581,7 +568,6 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ $buildEnd = \time(); $buildStderr = $th->getMessage(); $build = [ - '$id' => $buildId, 'status' => 'failed', 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), @@ -600,23 +586,23 @@ function runBuildStage(string $buildId, string $projectID, string $path, array $ } // POST /v1/runtimes -App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') - ->desc("Create a new build") - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) - ->param('buildId', '', new UID(), 'Build unique ID.', false) +App::post('/v1/runtimes') + ->desc("Create a new runtime server") + ->param('runtimeId', '', new Text(128), 'Unique runtime ID.', false) ->param('path', '', new Text(0), 'Path to source files.', false) ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) ->inject('projectId') ->inject('response') - ->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) { - - $build = runBuildStage($buildId, $projectId, $path, $vars, $baseImage, $runtime); + ->action(function (string $runtimeId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) { + // TODO: Check if runtime already exists.. + $build = runBuildStage($runtimeId, $path, $vars, $baseImage, $runtime); if ( $build['status'] === 'ready') { - $build = createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime); + $build = createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); + } else { + throw new Exception('Failed to build runtime: ' . $build['stderr'], 500); } $response @@ -641,7 +627,7 @@ App::get('/v1/runtimes') // GET /v1/runtimes/:runtimeId (projectId + functionId) App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") - ->param('runtimeId', '', new UID(), 'Runtime unique ID.') + ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') ->inject('response') ->action(function (Response $response) { @@ -653,64 +639,30 @@ App::get('/v1/runtimes/:runtimeId') ->json($runtime); }); -// POST /v1/execution (get runtime as param, if 404 or 501/503, go and create a runtime first) -App::post('/v1/execution') - ->desc('Create a function execution') - ->param('functionId', '', new Text(1024), 'The FunctionID to execute') - ->param('deploymentId', '', new Text(1024), 'The deployment ID to execute') - ->param('buildId', '', new Text(1024), 'The build ID of the function') - ->param('path', '', new Text(0), 'Path to built files.', false) - ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) - ->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true) - ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) - ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file') - ->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true) - ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) - ->param('webhooks', [], new ArrayList(new JSON()), 'Any webhooks that need to be triggered after this execution', true) - ->param('userId', '', new Text(1024), 'The UserID of the user who triggered the execution if it was called from a client SDK', true) - ->inject('projectId') - ->inject('response') - ->action( - function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, array $webhooks, string $userId, string $projectId, Response $response) { - - $build = [ - '$id' => $buildId, - 'outputPath' => $path, - ]; - - // Send both data and vars from the caller - $execution = execute($projectId, $functionId, $deploymentId, $build, $vars, $data, $userId, $baseImage, $runtime, $entrypoint, $timeout, $webhooks); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->json($execution); - } - ); - // DELETE /v1/runtimes/:runtimeId (projectId + functionId) -App::delete('/v1/deployments/:deploymentId') - ->desc('Delete a deployment') - ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) +App::delete('/v1/runtimes/:runtimeId') + ->desc('Delete a runtime') + ->param('runtimeId', '', new Text(128), 'Runtime unique ID.', false) ->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false) ->inject('response') - ->action(function (string $deploymentId, array $buildIds, Response $response) use ($orchestrationPool) { + ->action(function (string $runtimeId, array $buildIds, Response $response) use ($orchestrationPool) { - Console::info('Deleting deployment: ' . $deploymentId); + Console::info('Deleting runtime: ' . $runtimeId); $orchestration = $orchestrationPool->get(); // Remove the container of the deployment - $status = $orchestration->remove('appwrite-function-' . $deploymentId , true); + $status = $orchestration->remove('runtime-' . $runtimeId , true); if ($status) { - Console::success('Removed container for deployment: ' . $deploymentId); + Console::success('Removed runtime container: ' . $runtimeId); } else { - Console::error('Failed to remove container for deployment: ' . $deploymentId); + Console::error('Failed to remove runtime container: ' . $runtimeId); } - // Remove all the build containers + // Remove all the build containers with that same ID foreach ($buildIds as $buildId) { try { Console::info('Deleting build container : ' . $buildId); - $status = $orchestration->remove('build-stage-' . $buildId, true); + $status = $orchestration->remove('build-' . $buildId, true); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -723,6 +675,35 @@ App::delete('/v1/deployments/:deploymentId') ->send(); }); + +// POST /v1/execution (get runtime as param, if 404 or 501/503, go and create a runtime first) +App::post('/v1/execution') + ->desc('Create an execution') + ->param('runtimeId', '', new Text(1024), 'The runtimeID to execute') + ->param('path', '', new Text(0), 'Path to built files.', false) + ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) + ->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true) + ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) + ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file') + ->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true) + ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) + ->inject('response') + ->action( + function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, Response $response) { + + $build = [ + 'outputPath' => $path, + ]; + + // Send both data and vars from the caller + $execution = execute($runtimeId, $build, $vars, $data, $baseImage, $runtime, $entrypoint, $timeout); + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->json($execution); + } + ); + App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode $http = new Server("0.0.0.0", 80); diff --git a/app/workers/builds.php b/app/workers/builds.php index 9ebfb0518c..9233ea680c 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -120,7 +120,6 @@ class BuildsV1 extends Worker projectId: $projectId, functionId: $functionId, deploymentId: $deploymentId, - buildId: $buildId, path: $path, vars: $vars, runtime: $key, @@ -149,9 +148,6 @@ class BuildsV1 extends Worker $function->setAttribute('scheduleNext', (int)$next); $function = $dbForProject->updateDocument('functions', $functionId, $function); - // /** Create runtime server */ - - Console::success("Build id: $buildId created"); } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 45414434e9..1b3494f058 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -312,14 +312,16 @@ class DeletesV1 extends Worker protected function deleteFunction(Document $document, string $projectId): void { $dbForProject = $this->getProjectDB($projectId); + $functionId = $document->getId(); /** * Delete Deployments */ + Console::info("Deleting deployments for function " . $functionId); $storageFunctions = new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); $deploymentIds = []; $this->deleteByGroup('deployments', [ - new Query('resourceId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('resourceId', Query::TYPE_EQUAL, [$functionId]) ], $dbForProject, function (Document $document) use ($storageFunctions, &$deploymentIds) { $deploymentIds[] = $document->getId(); if ($storageFunctions->delete($document->getAttribute('path', ''), true)) { @@ -332,6 +334,7 @@ class DeletesV1 extends Worker /** * Delete builds */ + Console::info("Deleting builds for function " . $functionId); $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); $buildIds = []; foreach ($deploymentIds as $deploymentId) { @@ -347,18 +350,22 @@ class DeletesV1 extends Worker }); } - // Delete Executions + /** + * Delete Executions + */ + Console::info("Deleting executions for function " . $functionId); $this->deleteByGroup('executions', [ - new Query('functionId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('functionId', Query::TYPE_EQUAL, [$functionId]) ], $dbForProject); /** * Request executor to delete all deployment containers */ + Console::info("Requesting executor to delete all deployment containers for function " . $functionId); $executor = new Executor(); foreach ($deploymentIds as $deploymentId) { try { - $executor->deleteRuntime($deploymentId, $buildIds[$deploymentId], $projectId); + $executor->deleteRuntime($projectId, $functionId, $deploymentId, $buildIds[$deploymentId]); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -374,10 +381,12 @@ class DeletesV1 extends Worker { $dbForProject = $this->getProjectDB($projectId); $deploymentId = $document->getId(); + $functionId = $document->getAttribute('resourceId'); /** * Delete deployment files */ + Console::info("Deleting deployment files for deployment " . $deploymentId); $storageFunctions = new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); if ($storageFunctions->delete($document->getAttribute('path', ''), true)) { Console::success('Deleted deployment files: ' . $document->getAttribute('path', '')); @@ -388,6 +397,7 @@ class DeletesV1 extends Worker /** * Delete builds */ + Console::info("Deleting builds for deployment " . $deploymentId); $buildIds = []; $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); $this->deleteByGroup('builds', [ @@ -404,9 +414,10 @@ class DeletesV1 extends Worker /** * Request executor to delete the deployment container */ + Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId); try { $executor = new Executor(); - $executor->deleteRuntime($deploymentId, $buildIds, $projectId); + $executor->deleteRuntime($projectId, $functionId, $deploymentId, $buildIds); } catch (Throwable $th) { Console::error($th->getMessage()); } diff --git a/app/workers/functions.php b/app/workers/functions.php index 52c3df4fb1..dc5b661f04 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -307,16 +307,13 @@ class FunctionsV1 extends Worker projectId: $projectId, functionId: $functionId, deploymentId: $deploymentId, - buildId: $deployment->getAttribute('buildId', ''), path: $build->getAttribute('outputPath', ''), vars: $vars, entrypoint: $deployment->getAttribute('entrypoint', ''), data: $vars['APPWRITE_FUNCTION_DATA'], runtime: $function->getAttribute('runtime', ''), timeout: $function->getAttribute('timeout', 0), - baseImage: $runtime['image'], - webhooks: $webhooks, - userId: $userId, + baseImage: $runtime['image'] ); /** Update execution status */ diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index fa4ff4d7c4..6c72826476 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -33,20 +33,20 @@ class Executor public function createRuntime( string $functionId, string $deploymentId, - string $buildId, string $projectId, string $path, array $vars, string $runtime, string $baseImage) { - $route = "/functions/$functionId/deployments/$deploymentId/builds/$buildId"; + $route = "/runtimes"; $headers = [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; $params = [ + 'runtimeId' => "$projectId-$deploymentId", 'path' => $path, 'vars' => $vars, 'runtime' => $runtime, @@ -63,14 +63,16 @@ class Executor return $response['body']; } - public function deleteRuntime(string $deploymentId, array $buildIds, string $projectId) + public function deleteRuntime(string $projectId, string $functionId, string $deploymentId, array $buildIds) { - $route = "/deployments/$deploymentId"; + $runtimeId = "$projectId-$deploymentId"; + $route = "/runtimes/$runtimeId"; $headers = [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; + $params = [ 'buildIds' => $buildIds, ]; @@ -89,16 +91,13 @@ class Executor string $projectId, string $functionId, string $deploymentId, - string $buildId, string $path, array $vars, string $entrypoint, string $data, string $runtime, string $baseImage, - $timeout, - $webhooks, - string $userId + $timeout ) { $route = "/execution"; @@ -108,18 +107,14 @@ class Executor 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; $params = [ - 'functionId' => $functionId, - 'deploymentId' => $deploymentId, - 'buildId' => $buildId, + 'runtimeId' => "$projectId-$deploymentId", 'path' => $path, 'vars' => $vars, 'data' => $data, 'runtime' => $runtime, + 'entrypoint' => $entrypoint, 'timeout' => $timeout, 'baseImage' => $baseImage, - 'webhooks' => $webhooks, - 'userId' => $userId, - 'entrypoint' => $entrypoint ]; $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); From 24c1467e68ead0de25211c6776ba4e5adc918bd2 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 13 Feb 2022 05:29:28 +0400 Subject: [PATCH 20/61] feat: decouple storage --- app/executor.php | 131 ++++++++++++++++---------------------- app/workers/builds.php | 2 +- src/Executor/Executor.php | 4 +- 3 files changed, 58 insertions(+), 79 deletions(-) diff --git a/app/executor.php b/app/executor.php index b482642747..684f74bb30 100644 --- a/app/executor.php +++ b/app/executor.php @@ -161,10 +161,10 @@ function createRuntimeServer(string $runtimeId, array $build, array $vars, strin /** Storage stuff */ $deploymentPath = $build['outputPath']; - $deploymentPathTarget = "/tmp/$runtimeId/builtCode/code.tar.gz"; + $deploymentPathTarget = "/tmp/$runtimeId/builds/code.tar.gz"; $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); - $device = Storage::getDevice('builds'); + $device = new Local(); if (!\file_exists($deploymentPathTargetDir)) { if (@\mkdir($deploymentPathTargetDir, 0777, true)) { \chmod($deploymentPathTargetDir, 0777); @@ -376,9 +376,8 @@ function execute(string $runtimeId, array $build, array $vars, string $data, str return $execution; }; -function runBuildStage(string $runtimeId, string $path, array $vars, string $baseImage, string $runtime): array +function runBuildStage(string $runtimeId, string $source, array $vars, string $baseImage, string $runtime): array { - global $orchestrationPool; $orchestration = $orchestrationPool->get(); @@ -391,57 +390,44 @@ function runBuildStage(string $runtimeId, string $path, array $vars, string $bas try { Console::info('Building runtime with ID : ' . $runtimeId); + + /** + * Move code files from source to temporary destination + */ + $device = new Local(); + $destination = "/tmp/$runtimeId/code.tar.gz"; + if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { + if(!$device->move($source, $destination)) { + throw new Exception('Failed to move file to destination.', 500); + }; + } else { + $buffer = $device->read($source); + if(!$device->write($destination, $buffer)) { + throw new Exception('Failed to write file to destination.', 500); + }; + } - // Grab Deployment Files - $deploymentPath = $path; - $device = Storage::getDevice('builds'); - - $deploymentPathTarget = "/tmp/$runtimeId/code.tar.gz"; - $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); - - $container = 'build-' . $runtimeId; - - // Perform various checks - if (!\file_exists($deploymentPathTargetDir)) { - if (@\mkdir($deploymentPathTargetDir, 0777, true)) { - \chmod($deploymentPathTargetDir, 0777); - } else { - throw new Exception('Can\'t create directory ' . $deploymentPathTargetDir); + /** + * Create folder to store builds + */ + $builds = "/tmp/$runtimeId/builds"; + if (!\file_exists($builds)) { + if (!@\mkdir($builds, 0755, true)) { + throw new Exception("Can't create directory : $builds", 500); } } - if (!\file_exists($deploymentPathTarget)) { - if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if (!\copy($deploymentPath, $deploymentPathTarget)) { - throw new Exception('Can\'t create temporary code file ' . $deploymentPathTarget); - } - } else { - $buffer = $device->read($deploymentPath); - \file_put_contents($deploymentPathTarget, $buffer); - } - } - - if (!$device->exists($deploymentPath)) { - throw new Exception('Code is not readable: ' . $path); - } - - $path = "/tmp/$runtimeId/builtCode"; - - if (!\file_exists($path)) { - if (@\mkdir($path, 0777, true)) { - \chmod($path, 0777); - } else { - throw new Exception("Can't create directory : /tmp/$runtimeId/builtCode"); - } - } - - $vars = array_map(fn ($v) => strval($v), $vars); - + /** + * Create container + */ $orchestration ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); + $container = 'build-' . $runtimeId; + $vars = array_map(fn ($v) => strval($v), $vars); + $id = $orchestration->run( image: $baseImage, name: $container, @@ -459,17 +445,19 @@ function runBuildStage(string $runtimeId, string $path, array $vars, string $bas '/dev/null' ], hostname: $container, - mountFolder: $deploymentPathTargetDir, + mountFolder: \dirname($destination), volumes: [ - "/tmp/$runtimeId/builtCode" . ':/usr/builtCode:rw' + "$builds:/usr/builds:rw" ] ); if (empty($id)) { - throw new Exception('Failed to start build container'); + throw new Exception('Failed to create build container', 500); } - // Extract user code into build container + /** + * Extract user code into build container + */ $untarStdout = ''; $untarStderr = ''; @@ -489,7 +477,9 @@ function runBuildStage(string $runtimeId, string $path, array $vars, string $bas throw new Exception('Failed to extract tar: ' . $untarStderr); } - // Build Code / Install Dependencies + /** + * Build code and install dependenices + */ $buildSuccess = $orchestration->execute( name: $container, command: ['sh', '-c', 'cd /usr/local/src && ./build.sh'], @@ -508,7 +498,7 @@ function runBuildStage(string $runtimeId, string $path, array $vars, string $bas $compressSuccess = $orchestration->execute( name: $container, command: [ - 'tar', '-C', '/usr/code', '-czvf', '/usr/builtCode/code.tar.gz', './' + 'tar', '-C', '/usr/code', '-czvf', '/usr/builds/code.tar.gz', './' ], stdout: $compressStdout, stderr: $compressStderr, @@ -520,14 +510,12 @@ function runBuildStage(string $runtimeId, string $path, array $vars, string $bas } // Check if the build was successful by checking if file exists - $builtCodePath = "/tmp/$runtimeId/builtCode/code.tar.gz"; + $builtCodePath = "$builds/code.tar.gz"; if (!\file_exists($builtCodePath)) { throw new Exception('Something went wrong during the build process.'); } // Upload new code - $device = Storage::getDevice('builds'); - $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists @@ -589,16 +577,15 @@ function runBuildStage(string $runtimeId, string $path, array $vars, string $bas App::post('/v1/runtimes') ->desc("Create a new runtime server") ->param('runtimeId', '', new Text(128), 'Unique runtime ID.', false) - ->param('path', '', new Text(0), 'Path to source files.', false) + ->param('source', '', new Text(0), 'Path to source files.', false) ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) - ->inject('projectId') ->inject('response') - ->action(function (string $runtimeId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) { + ->action(function (string $runtimeId, string $source, array $vars, string $runtime, string $baseImage, Response $response) { // TODO: Check if runtime already exists.. - $build = runBuildStage($runtimeId, $path, $vars, $baseImage, $runtime); + $build = runBuildStage($runtimeId, $source, $vars, $baseImage, $runtime); if ( $build['status'] === 'ready') { $build = createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); } else { @@ -659,14 +646,15 @@ App::delete('/v1/runtimes/:runtimeId') } // Remove all the build containers with that same ID - foreach ($buildIds as $buildId) { - try { - Console::info('Deleting build container : ' . $buildId); - $status = $orchestration->remove('build-' . $buildId, true); - } catch (Throwable $th) { - Console::error($th->getMessage()); - } - } + // 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()); + // } + // } $orchestrationPool->put($orchestration); @@ -787,11 +775,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $response = new Response($swooleResponse); $app = new App('UTC'); - $projectId = $request->getHeader('x-appwrite-project', ''); - - Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId)); - Storage::setDevice('builds', new Local(APP_STORAGE_BUILDS . '/app-' . $projectId)); - // Check environment variable key $secretKey = $request->getHeader('x-appwrite-executor-key', ''); @@ -848,10 +831,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo ); }, ['error', 'utopia', 'request', 'response']); - App::setResource('projectId', function () use ($projectId) { - return $projectId; - }); - try { $app->run($request, $response); } catch (Exception $e) { diff --git a/app/workers/builds.php b/app/workers/builds.php index 9233ea680c..c68340398f 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -120,7 +120,7 @@ class BuildsV1 extends Worker projectId: $projectId, functionId: $functionId, deploymentId: $deploymentId, - path: $path, + source: $path, vars: $vars, runtime: $key, baseImage: $baseImage diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 6c72826476..98942a88ef 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -34,7 +34,7 @@ class Executor string $functionId, string $deploymentId, string $projectId, - string $path, + string $source, array $vars, string $runtime, string $baseImage) @@ -47,7 +47,7 @@ class Executor ]; $params = [ 'runtimeId' => "$projectId-$deploymentId", - 'path' => $path, + 'source' => $source, 'vars' => $vars, 'runtime' => $runtime, 'baseImage' => $baseImage From a665def876b5bb8d9392d90574af11350808019b Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 13 Feb 2022 05:42:22 +0400 Subject: [PATCH 21/61] feat: decouple storage --- app/executor.php | 31 +++++++++++++------------------ src/Executor/Executor.php | 2 ++ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/executor.php b/app/executor.php index 684f74bb30..601e5bc2fe 100644 --- a/app/executor.php +++ b/app/executor.php @@ -376,7 +376,7 @@ function execute(string $runtimeId, array $build, array $vars, string $data, str return $execution; }; -function runBuildStage(string $runtimeId, string $source, array $vars, string $baseImage, string $runtime): array +function runBuildStage(string $runtimeId, string $source, string $buildDir, array $vars, string $baseImage, string $runtime): array { global $orchestrationPool; $orchestration = $orchestrationPool->get(); @@ -394,7 +394,7 @@ function runBuildStage(string $runtimeId, string $source, array $vars, string $b /** * Move code files from source to temporary destination */ - $device = new Local(); + $device = new Local($buildDir); $destination = "/tmp/$runtimeId/code.tar.gz"; if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { if(!$device->move($source, $destination)) { @@ -515,17 +515,11 @@ function runBuildStage(string $runtimeId, string $source, array $vars, string $b throw new Exception('Something went wrong during the build process.'); } - // Upload new code + /** + * Upload built code to expected build directory + */ $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists - if (@\mkdir(\dirname($path), 0777, true)) { - \chmod(\dirname($path), 0777); - } else { - throw new Exception('Can\'t create directory: ' . \dirname($path)); - } - } - if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { if (!$device->move($builtCodePath, $path)) { throw new Exception('Failed to upload built code upload to storage', 500); @@ -576,16 +570,17 @@ function runBuildStage(string $runtimeId, string $source, array $vars, string $b // POST /v1/runtimes App::post('/v1/runtimes') ->desc("Create a new runtime server") - ->param('runtimeId', '', new Text(128), 'Unique runtime ID.', false) - ->param('source', '', new Text(0), 'Path to source files.', false) - ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) - ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) - ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) + ->param('runtimeId', '', new Text(128), 'Unique runtime ID.') + ->param('source', '', new Text(0), 'Path to source files.') + ->param('destination', '', new Text(0), 'Destination folder to store build files into.') + ->param('vars', '', new Assoc(), 'Environment Variables required for the build') + ->param('runtime', '', new Text(128), 'Runtime for the cloud function') + ->param('baseImage', '', new Text(128), 'Base image name of the runtime') ->inject('response') - ->action(function (string $runtimeId, string $source, array $vars, string $runtime, string $baseImage, Response $response) { + ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, Response $response) { // TODO: Check if runtime already exists.. - $build = runBuildStage($runtimeId, $source, $vars, $baseImage, $runtime); + $build = runBuildStage($runtimeId, $source, $destination, $vars, $baseImage, $runtime); if ( $build['status'] === 'ready') { $build = createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); } else { diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 98942a88ef..05e36fd1fa 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -4,6 +4,7 @@ namespace Executor; use Exception; use Utopia\App; +use Utopia\Storage\Storage; class Executor { @@ -48,6 +49,7 @@ class Executor $params = [ 'runtimeId' => "$projectId-$deploymentId", 'source' => $source, + 'destination' => APP_STORAGE_BUILDS . "/app-$projectId", 'vars' => $vars, 'runtime' => $runtime, 'baseImage' => $baseImage From 9262bad8bf8fd37947e06b9883d6473f1ecb4244 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 13 Feb 2022 12:25:48 +0400 Subject: [PATCH 22/61] feat: decouple storage --- app/executor.php | 65 ++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/app/executor.php b/app/executor.php index 601e5bc2fe..02d1534190 100644 --- a/app/executor.php +++ b/app/executor.php @@ -376,7 +376,7 @@ function execute(string $runtimeId, array $build, array $vars, string $data, str return $execution; }; -function runBuildStage(string $runtimeId, string $source, string $buildDir, array $vars, string $baseImage, string $runtime): array +function runBuildStage(string $runtimeId, string $source, string $destination, array $vars, string $baseImage, string $runtime): array { global $orchestrationPool; $orchestration = $orchestrationPool->get(); @@ -390,44 +390,48 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra try { Console::info('Building runtime with ID : ' . $runtimeId); - - /** - * Move code files from source to temporary destination + + /** + * Temporary file paths in the executor */ - $device = new Local($buildDir); - $destination = "/tmp/$runtimeId/code.tar.gz"; + $tmpSource = "/tmp/$runtimeId/code.tar.gz"; + $tmpBuildDir = "/tmp/$runtimeId/builds"; + $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; + + /** + * Move code files from source to a temporary location on the executor + */ + $device = new Local($destination); + if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if(!$device->move($source, $destination)) { - throw new Exception('Failed to move file to destination.', 500); + if(!$device->move($source, $tmpSource)) { + throw new Exception('Failed to move source code to temporary location.', 500); }; } else { $buffer = $device->read($source); - if(!$device->write($destination, $buffer)) { - throw new Exception('Failed to write file to destination.', 500); + if(!$device->write($tmpSource, $buffer)) { + throw new Exception('Failed to write source code to temporary location.', 500); }; } /** - * Create folder to store builds + * Create a temporary folder to store builds */ - $builds = "/tmp/$runtimeId/builds"; - if (!\file_exists($builds)) { - if (!@\mkdir($builds, 0755, true)) { - throw new Exception("Can't create directory : $builds", 500); + if (!\file_exists($tmpBuildDir)) { + if (!@\mkdir($tmpBuildDir, 0755, true)) { + throw new Exception("Can't create directory : $tmpBuildDir", 500); } } /** * Create container */ + $container = 'build-' . $runtimeId; + $vars = array_map(fn ($v) => strval($v), $vars); $orchestration ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); - - $container = 'build-' . $runtimeId; - $vars = array_map(fn ($v) => strval($v), $vars); - $id = $orchestration->run( image: $baseImage, name: $container, @@ -445,9 +449,9 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra '/dev/null' ], hostname: $container, - mountFolder: \dirname($destination), + mountFolder: \dirname($tmpSource), volumes: [ - "$builds:/usr/builds:rw" + "$tmpBuildDir:/usr/builds:rw" ] ); @@ -460,7 +464,6 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra */ $untarStdout = ''; $untarStderr = ''; - $untarSuccess = $orchestration->execute( name: $container, command: [ @@ -492,7 +495,9 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra throw new Exception('Failed to build dependencies: ' . $buildStderr); } - // Repackage Code and Save. + /** + * Repackage code and save + */ $compressStdout = ''; $compressStderr = ''; $compressSuccess = $orchestration->execute( @@ -510,8 +515,7 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra } // Check if the build was successful by checking if file exists - $builtCodePath = "$builds/code.tar.gz"; - if (!\file_exists($builtCodePath)) { + if (!\file_exists($tmpBuild)) { throw new Exception('Something went wrong during the build process.'); } @@ -521,11 +525,11 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if (!$device->move($builtCodePath, $path)) { - throw new Exception('Failed to upload built code upload to storage', 500); + if (!$device->move($tmpBuild, $path)) { + throw new Exception('Failed to move built code to storage', 500); } } else { - if (!$device->upload($builtCodePath, $path)) { + if (!$device->upload($tmpBuild, $path)) { throw new Exception('Failed to upload built code upload to storage', 500); } } @@ -557,6 +561,7 @@ function runBuildStage(string $runtimeId, string $source, string $buildDir, arra 'endTime' => $buildEnd, 'duration' => $buildEnd - $buildStart, ]; + Console::error('Build failed: ' . $th->getMessage()); } finally { if (!empty($id)) { @@ -606,7 +611,7 @@ App::get('/v1/runtimes') ->json($runtimes); }); -// GET /v1/runtimes/:runtimeId (projectId + functionId) +// GET /v1/runtimes/:runtimeId App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') @@ -621,7 +626,7 @@ App::get('/v1/runtimes/:runtimeId') ->json($runtime); }); -// DELETE /v1/runtimes/:runtimeId (projectId + functionId) +// DELETE /v1/runtimes/:runtimeId App::delete('/v1/runtimes/:runtimeId') ->desc('Delete a runtime') ->param('runtimeId', '', new Text(128), 'Runtime unique ID.', false) From fc8ce14009018dc9c1a40bb293958dbf1ab9a148 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 13 Feb 2022 16:03:25 +0400 Subject: [PATCH 23/61] feat: remove more dependencies --- app/executor.php | 11 ++--------- docker-compose.yml | 19 ------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/app/executor.php b/app/executor.php index 02d1534190..d095759772 100644 --- a/app/executor.php +++ b/app/executor.php @@ -1,7 +1,6 @@ on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo ->addHeader('Pragma', 'no-cache') ->setStatusCode(500); - $response->dynamic( - new Document($output), - $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR - ); + $response->json($output); }, ['error', 'utopia', 'request', 'response']); try { diff --git a/docker-compose.yml b/docker-compose.yml index a73b34a7b5..3c6ffb5a56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -399,24 +399,8 @@ services: - appwrite-functions:/storage/functions:rw - appwrite-builds:/storage/builds:rw - /tmp:/tmp:rw - - ./app:/usr/src/code/app - - ./src:/usr/src/code/src - - ./dev:/usr/local/dev - depends_on: - - redis - - mariadb environment: - _APP_ENV - - _APP_OPENSSL_KEY_V1 - - _APP_REDIS_HOST - - _APP_REDIS_PORT - - _APP_REDIS_USER - - _APP_REDIS_PASS - - _APP_DB_HOST - - _APP_DB_PORT - - _APP_DB_SCHEMA - - _APP_DB_USER - - _APP_DB_PASS - _APP_FUNCTIONS_TIMEOUT - _APP_FUNCTIONS_BUILD_TIMEOUT - _APP_FUNCTIONS_CONTAINERS @@ -426,9 +410,6 @@ services: - _APP_FUNCTIONS_MEMORY_SWAP - _APP_EXECUTOR_SECRET - _APP_EXECUTOR_RUNTIME_NETWORK - - _APP_USAGE_STATS - - _APP_STATSD_HOST - - _APP_STATSD_PORT - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG - DOCKERHUB_PULL_USERNAME From 22700daa078e9a4155c98dd6d0b912bef0fcde60 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Sun, 13 Feb 2022 16:31:44 +0400 Subject: [PATCH 24/61] feat: refactor build function --- app/executor.php | 392 +++++++++++++++++++++++------------------------ 1 file changed, 196 insertions(+), 196 deletions(-) diff --git a/app/executor.php b/app/executor.php index d095759772..d936d5d646 100644 --- a/app/executor.php +++ b/app/executor.php @@ -372,201 +372,6 @@ function execute(string $runtimeId, array $build, array $vars, string $data, str return $execution; }; -function runBuildStage(string $runtimeId, string $source, string $destination, array $vars, string $baseImage, string $runtime): array -{ - global $orchestrationPool; - $orchestration = $orchestrationPool->get(); - - $build = []; - $id = ''; - $buildStdout = ''; - $buildStderr = ''; - $buildStart = \time(); - $buildEnd = 0; - - try { - Console::info('Building runtime with ID : ' . $runtimeId); - - /** - * Temporary file paths in the executor - */ - $tmpSource = "/tmp/$runtimeId/code.tar.gz"; - $tmpBuildDir = "/tmp/$runtimeId/builds"; - $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; - - /** - * Move code files from source to a temporary location on the executor - */ - $device = new Local($destination); - - if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if(!$device->move($source, $tmpSource)) { - throw new Exception('Failed to move source code to temporary location.', 500); - }; - } else { - $buffer = $device->read($source); - if(!$device->write($tmpSource, $buffer)) { - throw new Exception('Failed to write source code to temporary location.', 500); - }; - } - - /** - * Create a temporary folder to store builds - */ - if (!\file_exists($tmpBuildDir)) { - if (!@\mkdir($tmpBuildDir, 0755, true)) { - throw new Exception("Can't create directory : $tmpBuildDir", 500); - } - } - - /** - * Create container - */ - $container = 'build-' . $runtimeId; - $vars = array_map(fn ($v) => strval($v), $vars); - $orchestration - ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) - ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) - ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); - $id = $orchestration->run( - image: $baseImage, - name: $container, - vars: $vars, - workdir: '/usr/code', - labels: [ - 'openruntimes-id' => $runtimeId, - 'openruntimes-type' => 'build', - 'openruntimes-created' => strval($buildStart), - 'openruntimes-runtime' => $runtime, - ], - command: [ - 'tail', - '-f', - '/dev/null' - ], - hostname: $container, - mountFolder: \dirname($tmpSource), - volumes: [ - "$tmpBuildDir:/usr/builds:rw" - ] - ); - - if (empty($id)) { - throw new Exception('Failed to create build container', 500); - } - - /** - * Extract user code into build container - */ - $untarStdout = ''; - $untarStderr = ''; - $untarSuccess = $orchestration->execute( - name: $container, - command: [ - 'sh', - '-c', - 'mkdir -p /usr/code && cp /tmp/code.tar.gz /usr/workspace/code.tar.gz && cd /usr/workspace/ && tar -zxf /usr/workspace/code.tar.gz -C /usr/code && rm /usr/workspace/code.tar.gz' - ], - stdout: $untarStdout, - stderr: $untarStderr, - timeout: 60 - ); - - if (!$untarSuccess) { - throw new Exception('Failed to extract tar: ' . $untarStderr); - } - - /** - * Build code and install dependenices - */ - $buildSuccess = $orchestration->execute( - name: $container, - command: ['sh', '-c', 'cd /usr/local/src && ./build.sh'], - stdout: $buildStdout, - stderr: $buildStderr, - timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900) - ); - - if (!$buildSuccess) { - throw new Exception('Failed to build dependencies: ' . $buildStderr); - } - - /** - * Repackage code and save - */ - $compressStdout = ''; - $compressStderr = ''; - $compressSuccess = $orchestration->execute( - name: $container, - command: [ - 'tar', '-C', '/usr/code', '-czvf', '/usr/builds/code.tar.gz', './' - ], - stdout: $compressStdout, - stderr: $compressStderr, - timeout: 60 - ); - - if (!$compressSuccess) { - throw new Exception('Failed to compress built code: ' . $compressStderr); - } - - // 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.'); - } - - /** - * Upload built code to expected build directory - */ - $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - - if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if (!$device->move($tmpBuild, $path)) { - throw new Exception('Failed to move built code to storage', 500); - } - } else { - if (!$device->upload($tmpBuild, $path)) { - throw new Exception('Failed to upload built code upload to storage', 500); - } - } - - if ($buildStdout === '') { - $buildStdout = 'Build Successful!'; - } - - $buildEnd = \time(); - $build = [ - 'outputPath' => $path, - 'status' => 'ready', - 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), - 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), - 'startTime' => $buildStart, - 'endTime' => $buildEnd, - 'duration' => $buildEnd - $buildStart, - ]; - - Console::success('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds'); - } catch (Throwable $th) { - $buildEnd = \time(); - $buildStderr = $th->getMessage(); - $build = [ - 'status' => 'failed', - 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), - 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), - 'startTime' => $buildStart, - 'endTime' => $buildEnd, - 'duration' => $buildEnd - $buildStart, - ]; - - Console::error('Build failed: ' . $th->getMessage()); - } finally { - if (!empty($id)) { - $orchestration->remove($id, true); - } - $orchestrationPool->put($orchestration); - return $build; - } -} // POST /v1/runtimes App::post('/v1/runtimes') @@ -581,7 +386,202 @@ App::post('/v1/runtimes') ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, Response $response) { // TODO: Check if runtime already exists.. - $build = runBuildStage($runtimeId, $source, $destination, $vars, $baseImage, $runtime); + + // TODO: Move orchestration pool to a utopia resource + global $orchestrationPool; + $orchestration = $orchestrationPool->get(); + + $build = []; + $id = ''; + $buildStdout = ''; + $buildStderr = ''; + $buildStart = \time(); + $buildEnd = 0; + + try { + Console::info('Building runtime with ID : ' . $runtimeId); + + /** + * Temporary file paths in the executor + */ + $tmpSource = "/tmp/$runtimeId/code.tar.gz"; + $tmpBuildDir = "/tmp/$runtimeId/builds"; + $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; + + /** + * Move code files from source to a temporary location on the executor + */ + $device = new Local($destination); + + if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { + if(!$device->move($source, $tmpSource)) { + throw new Exception('Failed to move source code to temporary location.', 500); + }; + } else { + $buffer = $device->read($source); + if(!$device->write($tmpSource, $buffer)) { + throw new Exception('Failed to write source code to temporary location.', 500); + }; + } + + /** + * Create a temporary folder to store builds + */ + if (!\file_exists($tmpBuildDir)) { + if (!@\mkdir($tmpBuildDir, 0755, true)) { + throw new Exception("Can't create directory : $tmpBuildDir", 500); + } + } + + /** + * Create container + */ + $container = 'build-' . $runtimeId; + $vars = array_map(fn ($v) => strval($v), $vars); + $orchestration + ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) + ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) + ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); + $id = $orchestration->run( + image: $baseImage, + name: $container, + vars: $vars, + workdir: '/usr/code', + labels: [ + 'openruntimes-id' => $runtimeId, + 'openruntimes-type' => 'build', + 'openruntimes-created' => strval($buildStart), + 'openruntimes-runtime' => $runtime, + ], + command: [ + 'tail', + '-f', + '/dev/null' + ], + hostname: $container, + mountFolder: \dirname($tmpSource), + volumes: [ + "$tmpBuildDir:/usr/builds:rw" + ] + ); + + if (empty($id)) { + throw new Exception('Failed to create build container', 500); + } + + /** + * Extract user code into build container + */ + $untarStdout = ''; + $untarStderr = ''; + $untarSuccess = $orchestration->execute( + name: $container, + command: [ + 'sh', + '-c', + 'mkdir -p /usr/code && cp /tmp/code.tar.gz /usr/workspace/code.tar.gz && cd /usr/workspace/ && tar -zxf /usr/workspace/code.tar.gz -C /usr/code && rm /usr/workspace/code.tar.gz' + ], + stdout: $untarStdout, + stderr: $untarStderr, + timeout: 60 + ); + + if (!$untarSuccess) { + throw new Exception('Failed to extract tar: ' . $untarStderr); + } + + /** + * Build code and install dependenices + */ + $buildSuccess = $orchestration->execute( + name: $container, + command: ['sh', '-c', 'cd /usr/local/src && ./build.sh'], + stdout: $buildStdout, + stderr: $buildStderr, + timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900) + ); + + if (!$buildSuccess) { + throw new Exception('Failed to build dependencies: ' . $buildStderr); + } + + /** + * Repackage code and save + */ + $compressStdout = ''; + $compressStderr = ''; + $compressSuccess = $orchestration->execute( + name: $container, + command: [ + 'tar', '-C', '/usr/code', '-czvf', '/usr/builds/code.tar.gz', './' + ], + stdout: $compressStdout, + stderr: $compressStderr, + timeout: 60 + ); + + if (!$compressSuccess) { + throw new Exception('Failed to compress built code: ' . $compressStderr); + } + + // 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.'); + } + + /** + * Upload built code to expected build directory + */ + $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + + if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { + if (!$device->move($tmpBuild, $path)) { + throw new Exception('Failed to move built code to storage', 500); + } + } else { + if (!$device->upload($tmpBuild, $path)) { + throw new Exception('Failed to upload built code upload to storage', 500); + } + } + + if ($buildStdout === '') { + $buildStdout = 'Build Successful!'; + } + + $buildEnd = \time(); + $build = [ + 'outputPath' => $path, + 'status' => 'ready', + 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), + 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), + 'startTime' => $buildStart, + 'endTime' => $buildEnd, + 'duration' => $buildEnd - $buildStart, + ]; + + Console::success('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds'); + + } catch (Throwable $th) { + $buildEnd = \time(); + $buildStderr = $th->getMessage(); + $build = [ + 'status' => 'failed', + 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), + 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), + 'startTime' => $buildStart, + 'endTime' => $buildEnd, + 'duration' => $buildEnd - $buildStart, + ]; + + Console::error('Build failed: ' . $th->getMessage()); + + } finally { + if (!empty($id)) { + $orchestration->remove($id, true); + } + $orchestrationPool->put($orchestration); + } + if ( $build['status'] === 'ready') { $build = createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); } else { From 36bd50a1ffb35e1e151b186465756b2d509a8818 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 01:26:36 +0400 Subject: [PATCH 25/61] feat: refactor build function --- app/executor.php | 67 +++++++++++++++++++++--------------------- app/workers/builds.php | 50 +++++++++++++++++++------------ 2 files changed, 64 insertions(+), 53 deletions(-) diff --git a/app/executor.php b/app/executor.php index d936d5d646..274b89e1d7 100644 --- a/app/executor.php +++ b/app/executor.php @@ -136,7 +136,7 @@ try { call_user_func($logError, $error, "startupError"); } -function createRuntimeServer(string $runtimeId, array $build, array $vars, string $baseImage, string $runtime): array +function createRuntimeServer(string $runtimeId, string $destination, array $vars, string $baseImage, string $runtime): bool { global $orchestrationPool; global $activeFunctions; @@ -155,8 +155,10 @@ function createRuntimeServer(string $runtimeId, array $build, array $vars, strin $activeFunctions->del($container); } - /** Storage stuff */ - $deploymentPath = $build['outputPath']; + /** + * Copy built code from build destination to a temporary directory + */ + $deploymentPath = $destination; $deploymentPathTarget = "/tmp/$runtimeId/builds/code.tar.gz"; $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); @@ -179,7 +181,10 @@ function createRuntimeServer(string $runtimeId, array $build, array $vars, strin \file_put_contents($deploymentPathTarget, $buffer); } }; - /** End Storage stuff */ + + /** + * Launch Runtime + */ // Generate random secret key $secret = \bin2hex(\random_bytes(16)); @@ -187,7 +192,6 @@ function createRuntimeServer(string $runtimeId, array $build, array $vars, strin 'INTERNAL_RUNTIME_KEY' => $secret ]); - /** Launch Runtime */ if (!$activeFunctions->exists($container)) { $executionStart = \microtime(true); $executionTime = \time(); @@ -229,15 +233,13 @@ function createRuntimeServer(string $runtimeId, array $build, array $vars, strin 'key' => $secret, ]); } - /** End Launch Runtime */ - Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); + return true; } catch (\Throwable $th) { - $build['status'] = 'failed'; Console::error('Runtime Server Creation Failed: '. $th->getMessage()); + return false; } finally { $orchestrationPool->put($orchestration); - return $build; } }; @@ -387,8 +389,9 @@ App::post('/v1/runtimes') // TODO: Check if runtime already exists.. - // TODO: Move orchestration pool to a utopia resource + // TODO: Move orchestration pool and swoole table to a utopia resource global $orchestrationPool; + global $activeFunctions; $orchestration = $orchestrationPool->get(); $build = []; @@ -400,7 +403,6 @@ App::post('/v1/runtimes') try { Console::info('Building runtime with ID : ' . $runtimeId); - /** * Temporary file paths in the executor */ @@ -409,20 +411,13 @@ App::post('/v1/runtimes') $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; /** - * Move code files from source to a temporary location on the executor + * Copy code files from source to a temporary location on the executor */ $device = new Local($destination); - - if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if(!$device->move($source, $tmpSource)) { - throw new Exception('Failed to move source code to temporary location.', 500); - }; - } else { - $buffer = $device->read($source); - if(!$device->write($tmpSource, $buffer)) { - throw new Exception('Failed to write source code to temporary location.', 500); - }; - } + $buffer = $device->read($source); + if(!$device->write($tmpSource, $buffer)) { + throw new Exception('Failed to write source code to temporary location.', 500); + }; /** * Create a temporary folder to store builds @@ -530,16 +525,16 @@ App::post('/v1/runtimes') } /** - * Upload built code to expected build directory + * Move built code to expected build directory */ - $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $outputPath = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if (!$device->move($tmpBuild, $path)) { + if (!$device->move($tmpBuild, $outputPath)) { throw new Exception('Failed to move built code to storage', 500); } } else { - if (!$device->upload($tmpBuild, $path)) { + if (!$device->upload($tmpBuild, $outputPath)) { throw new Exception('Failed to upload built code upload to storage', 500); } } @@ -550,7 +545,7 @@ App::post('/v1/runtimes') $buildEnd = \time(); $build = [ - 'outputPath' => $path, + 'outputPath' => $outputPath, 'status' => 'ready', 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), @@ -574,18 +569,22 @@ App::post('/v1/runtimes') ]; Console::error('Build failed: ' . $th->getMessage()); - } finally { if (!empty($id)) { $orchestration->remove($id, true); } - $orchestrationPool->put($orchestration); } - if ( $build['status'] === 'ready') { - $build = createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); - } else { - throw new Exception('Failed to build runtime: ' . $build['stderr'], 500); + if ( $build['status'] !== 'ready') { + return $response + ->setStatusCode(201) + ->json($build); + } + + $status = createRuntimeServer($runtimeId, $outputPath, $vars, $baseImage, $runtime); + + if (!$status) { + Console::error('Failed to create runtime server'); } $response diff --git a/app/workers/builds.php b/app/workers/builds.php index c68340398f..24f30984f6 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -85,13 +85,14 @@ class BuildsV1 extends Worker $buildId = $deployment->getAttribute('buildId', ''); $build = null; + $startTime = \time(); if (empty($buildId)) { $buildId = $dbForProject->getId(); $build = $dbForProject->createDocument('builds', new Document([ '$id' => $buildId, '$read' => [], '$write' => [], - 'startTime' => time(), + 'startTime' => $startTime, 'deploymentId' => $deploymentId, 'status' => 'processing', 'outputPath' => '', @@ -113,26 +114,37 @@ class BuildsV1 extends Worker $build->setAttribute('status', 'building'); $build = $dbForProject->updateDocument('builds', $buildId, $build); - $path = $deployment->getAttribute('path'); + $source = $deployment->getAttribute('path'); $vars = $function->getAttribute('vars', []); $baseImage = $runtime['image']; - $response = $this->executor->createRuntime( - projectId: $projectId, - functionId: $functionId, - deploymentId: $deploymentId, - source: $path, - vars: $vars, - runtime: $key, - baseImage: $baseImage - ); - - /** Update the build document */ - $build->setAttribute('endTime', $response['endTime']); - $build->setAttribute('duration', $response['duration']); - $build->setAttribute('status', $response['status']); - $build->setAttribute('outputPath', $response['outputPath']); - $build->setAttribute('stderr', $response['stderr']); - $build->setAttribute('stdout', $response['stdout']); + + try { + $response = $this->executor->createRuntime( + projectId: $projectId, + functionId: $functionId, + deploymentId: $deploymentId, + source: $source, + vars: $vars, + runtime: $key, + baseImage: $baseImage + ); + + /** Update the build document */ + $build->setAttribute('endTime', $response['endTime']); + $build->setAttribute('duration', $response['duration']); + $build->setAttribute('status', $response['status']); + $build->setAttribute('outputPath', $response['outputPath']); + $build->setAttribute('stderr', $response['stderr']); + $build->setAttribute('stdout', $response['stdout']); + } catch (\Throwable $th) { + $endtime = \time(); + Console::error($th->getMessage()); + $build->setAttribute('endTime', $endtime); + $build->setAttribute('duration', $endtime - $startTime); + $build->setAttribute('status', 'failed'); + $build->setAttribute('stderr', $th->getMessage()); + } + $build = $dbForProject->updateDocument('builds', $buildId, $build); /** Set auto deploy */ From bb9fa487f5b37b951d40bd2f7c05280e5aab18db Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 01:36:07 +0400 Subject: [PATCH 26/61] feat: refactor build function --- app/executor.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/executor.php b/app/executor.php index 274b89e1d7..3cf13b3227 100644 --- a/app/executor.php +++ b/app/executor.php @@ -243,7 +243,7 @@ function createRuntimeServer(string $runtimeId, string $destination, array $vars } }; -function execute(string $runtimeId, array $build, array $vars, string $data, string $baseImage, string $runtime, string $entrypoint, int $timeout): array +function execute(string $runtimeId, string $path, array $vars, string $data, string $baseImage, string $runtime, string $entrypoint, int $timeout): array { Console::info('Executing Runtime: ' . $runtimeId); @@ -254,7 +254,7 @@ function execute(string $runtimeId, array $build, array $vars, string $data, str /** Create a new runtime server if there's none running */ if (!$activeFunctions->exists($container)) { Console::info("Runtime server for $runtimeId not running. Creating new one..."); - createRuntimeServer($runtimeId, $build, $vars, $baseImage, $runtime); + createRuntimeServer($runtimeId, $path, $vars, $baseImage, $runtime); } $key = $activeFunctions->get('runtime-' . $runtimeId, 'key'); @@ -674,12 +674,8 @@ App::post('/v1/execution') ->action( function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, Response $response) { - $build = [ - 'outputPath' => $path, - ]; - // Send both data and vars from the caller - $execution = execute($runtimeId, $build, $vars, $data, $baseImage, $runtime, $entrypoint, $timeout); + $execution = execute($runtimeId, $path, $vars, $data, $baseImage, $runtime, $entrypoint, $timeout); $response ->setStatusCode(Response::STATUS_CODE_OK) From 43ef855e7dd868eebfeb9c7692365350003cd57e Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 04:00:16 +0400 Subject: [PATCH 27/61] feat: fix memory leak and separate error callback --- app/executor.php | 115 ++++++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 66 deletions(-) diff --git a/app/executor.php b/app/executor.php index 3cf13b3227..7d3fe170ba 100644 --- a/app/executor.php +++ b/app/executor.php @@ -136,7 +136,7 @@ try { call_user_func($logError, $error, "startupError"); } -function createRuntimeServer(string $runtimeId, string $destination, array $vars, string $baseImage, string $runtime): bool +function createRuntimeServer(string $runtimeId, string $buildOutputPath, array $vars, string $baseImage, string $runtime): bool { global $orchestrationPool; global $activeFunctions; @@ -156,37 +156,19 @@ function createRuntimeServer(string $runtimeId, string $destination, array $vars } /** - * Copy built code from build destination to a temporary directory + * Copy code files from source to a temporary location on the executor */ - $deploymentPath = $destination; - $deploymentPathTarget = "/tmp/$runtimeId/builds/code.tar.gz"; - $deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME); - + $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; $device = new Local(); - if (!\file_exists($deploymentPathTargetDir)) { - if (@\mkdir($deploymentPathTargetDir, 0777, true)) { - \chmod($deploymentPathTargetDir, 0777); - } else { - throw new Exception('Can\'t create directory ' . $deploymentPathTargetDir); - } - } - - if (!\file_exists($deploymentPathTarget)) { - if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { - if (!\copy($deploymentPath, $deploymentPathTarget)) { - throw new Exception('Can\'t create temporary code file ' . $deploymentPathTarget); - } - } else { - $buffer = $device->read($deploymentPath); - \file_put_contents($deploymentPathTarget, $buffer); - } + $buffer = $device->read($buildOutputPath); + if(!$device->write($tmpBuild, $buffer)) { + throw new Exception('Failed to write built code to temporary location.', 500); }; /** * Launch Runtime */ - // Generate random secret key $secret = \bin2hex(\random_bytes(16)); $vars = \array_merge($vars, [ 'INTERNAL_RUNTIME_KEY' => $secret @@ -214,7 +196,7 @@ function createRuntimeServer(string $runtimeId, string $destination, array $vars 'openruntimes-runtime' => $runtime ], hostname: $container, - mountFolder: $deploymentPathTargetDir, + mountFolder: \dirname($tmpBuild), ); if (empty($id)) { @@ -573,6 +555,7 @@ App::post('/v1/runtimes') if (!empty($id)) { $orchestration->remove($id, true); } + $orchestrationPool->put($orchestration); } if ( $build['status'] !== 'ready') { @@ -739,6 +722,46 @@ $http = new Server("0.0.0.0", 80); // } // }; +App::error(function ($error, $utopia, $request, $response) { + /** @var Exception $error */ + /** @var Utopia\App $utopia */ + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + + if ($error instanceof PDOException) { + throw $error; + } + + $route = $utopia->match($request); + // logError($error, "httpError", $route); + + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); + + $code = $error->getCode(); + $message = $error->getMessage(); + + $output = ((App::isDevelopment())) ? [ + 'message' => $error->getMessage(), + 'code' => $error->getCode(), + 'file' => $error->getFile(), + 'line' => $error->getLine(), + 'trace' => $error->getTrace(), + 'version' => $version, + ] : [ + 'message' => $message, + 'code' => $code, + 'version' => $version, + ]; + + $response + ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + ->addHeader('Expires', '0') + ->addHeader('Pragma', 'no-cache') + ->setStatusCode(500); + + $response->json($output); +}, ['error', 'utopia', 'request', 'response']); + $http->on('start', function ($http) { @Process::signal(SIGINT, function () use ($http) { // handleShutdown(); @@ -779,50 +802,10 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo return $swooleResponse->end('401: Authentication Error'); } - App::error(function ($error, $utopia, $request, $response) { - /** @var Exception $error */ - /** @var Utopia\App $utopia */ - /** @var Utopia\Swoole\Request $request */ - /** @var Appwrite\Utopia\Response $response */ - - if ($error instanceof PDOException) { - throw $error; - } - - $route = $utopia->match($request); - logError($error, "httpError", $route); - - $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); - - $code = $error->getCode(); - $message = $error->getMessage(); - - $output = ((App::isDevelopment())) ? [ - 'message' => $error->getMessage(), - 'code' => $error->getCode(), - 'file' => $error->getFile(), - 'line' => $error->getLine(), - 'trace' => $error->getTrace(), - 'version' => $version, - ] : [ - 'message' => $message, - 'code' => $code, - 'version' => $version, - ]; - - $response - ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') - ->addHeader('Expires', '0') - ->addHeader('Pragma', 'no-cache') - ->setStatusCode(500); - - $response->json($output); - }, ['error', 'utopia', 'request', 'response']); - try { $app->run($request, $response); } catch (Exception $e) { - logError($e, "serverError"); + // logError($e, "serverError"); $swooleResponse->end('500: Server Error'); } }); From b95434d0123e8d12b30846cf8fcbbd3befc0de14 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 04:12:01 +0400 Subject: [PATCH 28/61] feat: move create runtimr logic to controller --- app/executor.php | 177 +++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 98 deletions(-) diff --git a/app/executor.php b/app/executor.php index 7d3fe170ba..b8ce48383c 100644 --- a/app/executor.php +++ b/app/executor.php @@ -136,95 +136,6 @@ try { call_user_func($logError, $error, "startupError"); } -function createRuntimeServer(string $runtimeId, string $buildOutputPath, array $vars, string $baseImage, string $runtime): bool -{ - global $orchestrationPool; - global $activeFunctions; - - $orchestration = $orchestrationPool->get(); - - try { - $container = 'runtime-' . $runtimeId; - if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online - // If container is online then stop and remove it - try { - $orchestration->remove($container, true); - } catch (Exception $e) { - throw new Exception('Failed to remove container: ' . $e->getMessage()); - } - $activeFunctions->del($container); - } - - /** - * Copy code files from source to a temporary location on the executor - */ - $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; - $device = new Local(); - $buffer = $device->read($buildOutputPath); - if(!$device->write($tmpBuild, $buffer)) { - throw new Exception('Failed to write built code to temporary location.', 500); - }; - - /** - * Launch Runtime - */ - - $secret = \bin2hex(\random_bytes(16)); - $vars = \array_merge($vars, [ - 'INTERNAL_RUNTIME_KEY' => $secret - ]); - - if (!$activeFunctions->exists($container)) { - $executionStart = \microtime(true); - $executionTime = \time(); - - $vars = array_map(fn ($v) => strval($v), $vars); - - $orchestration - ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1')) - ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256')) - ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256')); - - $id = $orchestration->run( - image: $baseImage, - name: $container, - vars: $vars, - labels: [ - 'openruntimes-id' => $runtimeId, - 'openruntimes-type' => 'function', - 'openruntimes-created' => strval($executionTime), - 'openruntimes-runtime' => $runtime - ], - hostname: $container, - mountFolder: \dirname($tmpBuild), - ); - - if (empty($id)) { - throw new Exception('Failed to create container'); - } - - // Add to network - $orchestration->networkConnect($container, App::getEnv('_APP_EXECUTOR_RUNTIME_NETWORK', 'appwrite_runtimes')); - - $executionEnd = \microtime(true); - - $activeFunctions->set($container, [ - 'id' => $id, - 'name' => $container, - 'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', - 'key' => $secret, - ]); - } - Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); - return true; - } catch (\Throwable $th) { - Console::error('Runtime Server Creation Failed: '. $th->getMessage()); - return false; - } finally { - $orchestrationPool->put($orchestration); - } -}; - function execute(string $runtimeId, string $path, array $vars, string $data, string $baseImage, string $runtime, string $entrypoint, int $timeout): array { @@ -234,10 +145,10 @@ function execute(string $runtimeId, string $path, array $vars, string $data, str $container = 'runtime-' . $runtimeId; /** Create a new runtime server if there's none running */ - if (!$activeFunctions->exists($container)) { - Console::info("Runtime server for $runtimeId not running. Creating new one..."); - createRuntimeServer($runtimeId, $path, $vars, $baseImage, $runtime); - } + // if (!$activeFunctions->exists($container)) { + // Console::info("Runtime server for $runtimeId not running. Creating new one..."); + // createRuntimeServer($runtimeId, $path, $vars, $baseImage, $runtime); + // } $key = $activeFunctions->get('runtime-' . $runtimeId, 'key'); @@ -555,7 +466,6 @@ App::post('/v1/runtimes') if (!empty($id)) { $orchestration->remove($id, true); } - $orchestrationPool->put($orchestration); } if ( $build['status'] !== 'ready') { @@ -564,12 +474,83 @@ App::post('/v1/runtimes') ->json($build); } - $status = createRuntimeServer($runtimeId, $outputPath, $vars, $baseImage, $runtime); - - if (!$status) { - Console::error('Failed to create runtime server'); + /** Create runtime server */ + try { + $container = 'runtime-' . $runtimeId; + if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online + // If container is online then stop and remove it + try { + $orchestration->remove($container, true); + } catch (Exception $e) { + throw new Exception('Failed to remove container: ' . $e->getMessage()); + } + $activeFunctions->del($container); + } + + /** + * Copy code files from source to a temporary location on the executor + */ + $buffer = $device->read($outputPath); + if(!$device->write($tmpBuild, $buffer)) { + throw new Exception('Failed to write built code to temporary location.', 500); + }; + + /** + * Launch Runtime + */ + $secret = \bin2hex(\random_bytes(16)); + $vars = \array_merge($vars, [ + 'INTERNAL_RUNTIME_KEY' => $secret + ]); + + if (!$activeFunctions->exists($container)) { + $executionStart = \microtime(true); + $executionTime = \time(); + + $vars = array_map(fn ($v) => strval($v), $vars); + + $orchestration + ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1')) + ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256')) + ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256')); + + $id = $orchestration->run( + image: $baseImage, + name: $container, + vars: $vars, + labels: [ + 'openruntimes-id' => $runtimeId, + 'openruntimes-type' => 'function', + 'openruntimes-created' => strval($executionTime), + 'openruntimes-runtime' => $runtime + ], + hostname: $container, + mountFolder: \dirname($tmpBuild), + ); + + if (empty($id)) { + throw new Exception('Failed to create container'); + } + + // Add to network + $orchestration->networkConnect($container, App::getEnv('_APP_EXECUTOR_RUNTIME_NETWORK', 'appwrite_runtimes')); + + $executionEnd = \microtime(true); + + $activeFunctions->set($container, [ + 'id' => $id, + 'name' => $container, + 'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', + 'key' => $secret, + ]); + } + Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); + } catch (\Throwable $th) { + Console::error('Runtime Server Creation Failed: '. $th->getMessage()); } + $orchestrationPool->put($orchestration); + $response ->setStatusCode(201) ->json($build); From b42da9e996038edc3fdd7cbe0c213005bc7a191f Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 04:20:12 +0400 Subject: [PATCH 29/61] feat: move create runtimr logic to controller --- app/executor.php | 1 + app/workers/builds.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/executor.php b/app/executor.php index b8ce48383c..528da2eb09 100644 --- a/app/executor.php +++ b/app/executor.php @@ -330,6 +330,7 @@ App::post('/v1/runtimes') ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0)) ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); + $id = $orchestration->run( image: $baseImage, name: $container, diff --git a/app/workers/builds.php b/app/workers/builds.php index 24f30984f6..888eaa5675 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -138,11 +138,11 @@ class BuildsV1 extends Worker $build->setAttribute('stdout', $response['stdout']); } catch (\Throwable $th) { $endtime = \time(); - Console::error($th->getMessage()); $build->setAttribute('endTime', $endtime); $build->setAttribute('duration', $endtime - $startTime); $build->setAttribute('status', 'failed'); $build->setAttribute('stderr', $th->getMessage()); + Console::error($th->getMessage()); } $build = $dbForProject->updateDocument('builds', $buildId, $build); From 66b47973bcceef639f668503121f4b7e1c5476f9 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 20:37:56 +0400 Subject: [PATCH 30/61] feat: add start up logic --- app/executor.php | 325 ++++++++++++++++++++------------------ docker-compose.yml | 2 + src/Executor/Executor.php | 2 +- 3 files changed, 173 insertions(+), 156 deletions(-) diff --git a/app/executor.php b/app/executor.php index 528da2eb09..540045f67f 100644 --- a/app/executor.php +++ b/app/executor.php @@ -1,6 +1,7 @@ getAll(true, $allowList); // Warmup: make sure images are ready to run fast 🚀 Co\run(function () use ($runtimes, $orchestrationPool) { @@ -104,6 +119,7 @@ try { $activeFunctions = new Swoole\Table(1024); $activeFunctions->column('id', Swoole\Table::TYPE_STRING, 512); + $activeFunctions->column('created', Swoole\Table::TYPE_INT, 512); $activeFunctions->column('name', Swoole\Table::TYPE_STRING, 512); $activeFunctions->column('status', Swoole\Table::TYPE_STRING, 512); $activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096); @@ -136,137 +152,6 @@ try { call_user_func($logError, $error, "startupError"); } -function execute(string $runtimeId, string $path, array $vars, string $data, string $baseImage, string $runtime, string $entrypoint, int $timeout): array -{ - - Console::info('Executing Runtime: ' . $runtimeId); - - global $activeFunctions; - $container = 'runtime-' . $runtimeId; - - /** Create a new runtime server if there's none running */ - // if (!$activeFunctions->exists($container)) { - // Console::info("Runtime server for $runtimeId not running. Creating new one..."); - // createRuntimeServer($runtimeId, $path, $vars, $baseImage, $runtime); - // } - - $key = $activeFunctions->get('runtime-' . $runtimeId, 'key'); - - $stdout = ''; - $stderr = ''; - - $executionStart = \microtime(true); - - $statusCode = 0; - - $errNo = -1; - $attempts = 0; - $max = 5; - - $executorResponse = ''; - - // cURL request to runtime - do { - $attempts++; - $ch = \curl_init(); - - $body = \json_encode([ - 'path' => '/usr/code', - 'file' => $entrypoint, - 'env' => $vars, - 'payload' => $data, - 'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) - ]); - - \curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/"); - \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)); - \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - - \curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Content-Length: ' . \strlen($body), - 'x-internal-challenge: ' . $key, - 'host: null' - ]); - - $executorResponse = \curl_exec($ch); - - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - - $error = \curl_error($ch); - - $errNo = \curl_errno($ch); - - \curl_close($ch); - if ($errNo != CURLE_COULDNT_CONNECT && $errNo != 111) { - break; - } - - sleep(1); - } while ($attempts < $max); - - if ($attempts >= 5) { - $stderr = 'Failed to connect to executor runtime after 5 attempts.'; - $statusCode = 124; - } - - // If timeout error - if (in_array($errNo, [CURLE_OPERATION_TIMEDOUT, 110])) { - $statusCode = 124; - } - - // 110 is the Swoole error code for timeout, see: https://www.swoole.co.uk/docs/swoole-error-code - if ($errNo !== 0 && $errNo !== CURLE_COULDNT_CONNECT && $errNo !== CURLE_OPERATION_TIMEDOUT && $errNo !== 110) { - throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500); - } - - $executionData = []; - - if (!empty($executorResponse)) { - $executionData = json_decode($executorResponse, true); - } - - if (isset($executionData['code'])) { - $statusCode = $executionData['code']; - } - - if ($statusCode === 500) { - if (isset($executionData['message'])) { - $stderr = $executionData['message']; - } else { - $stderr = 'Internal Runtime error'; - } - } else if ($statusCode === 124) { - $stderr = 'Execution timed out.'; - } else if ($statusCode === 0) { - $stderr = 'Execution failed.'; - } else if ($statusCode >= 200 && $statusCode < 300) { - $stdout = $executorResponse; - } else { - $stderr = 'Execution failed.'; - } - - $executionEnd = \microtime(true); - $executionTime = ($executionEnd - $executionStart); - $functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed'; - - Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus); - - $execution = [ - 'status' => $functionStatus, - 'statusCode' => $statusCode, - 'stdout' => \utf8_encode(\mb_substr($stdout, -8000)), - 'stderr' => \utf8_encode(\mb_substr($stderr, -8000)), - 'time' => $executionTime, - ]; - - return $execution; -}; - // POST /v1/runtimes App::post('/v1/runtimes') @@ -275,6 +160,7 @@ App::post('/v1/runtimes') ->param('source', '', new Text(0), 'Path to source files.') ->param('destination', '', new Text(0), 'Destination folder to store build files into.') ->param('vars', '', new Assoc(), 'Environment Variables required for the build') + // refactor to `name` ->param('runtime', '', new Text(128), 'Runtime for the cloud function') ->param('baseImage', '', new Text(128), 'Base image name of the runtime') ->inject('response') @@ -455,6 +341,7 @@ App::post('/v1/runtimes') $buildStderr = $th->getMessage(); $build = [ 'status' => 'failed', + // Increase logs limit 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), 'startTime' => $buildStart, @@ -471,7 +358,7 @@ App::post('/v1/runtimes') if ( $build['status'] !== 'ready') { return $response - ->setStatusCode(201) + ->setStatusCode(500) ->json($build); } @@ -566,6 +453,7 @@ App::get('/v1/runtimes') // TODO : Get list of active runtimes from swoole table $runtimes = []; + // Response model for runtime list $response ->setStatusCode(200) ->json($runtimes); @@ -639,8 +527,133 @@ App::post('/v1/execution') ->action( function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, Response $response) { - // Send both data and vars from the caller - $execution = execute($runtimeId, $path, $vars, $data, $baseImage, $runtime, $entrypoint, $timeout); + global $activeFunctions; + + $container = 'runtime-' . $runtimeId; + + // TODO: Also check for container status + if (!$activeFunctions->exists($container)) { + throw new Exception('Runtime not found. Please create the runtime.', 404); + } + + $secret = $activeFunctions->get($container, 'key'); + if (empty($secret)) { + throw new Exception('Runtime secret not found. Please create the runtime.', 500); + } + + Console::info('Executing Runtime: ' . $runtimeId); + + $executionStart = \microtime(true); + $stdout = ''; + $stderr = ''; + $statusCode = 0; + $errNo = -1; + $executorResponse = ''; + + try { + $attempts = 0; + $max = 5; + // cURL request to runtime + do { + $attempts++; + $ch = \curl_init(); + + $body = \json_encode([ + 'path' => '/usr/code', + 'file' => $entrypoint, + 'env' => $vars, + 'payload' => $data, + 'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + ]); + + \curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/"); + \curl_setopt($ch, CURLOPT_POST, true); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)); + \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + + \curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . \strlen($body), + 'x-internal-challenge: ' . $secret, + 'host: null' + ]); + + $executorResponse = \curl_exec($ch); + + $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + + $error = \curl_error($ch); + + $errNo = \curl_errno($ch); + + \curl_close($ch); + if ($errNo != CURLE_COULDNT_CONNECT && $errNo != 111) { + break; + } + + sleep(1); + } while ($attempts < $max); + + if ($attempts >= 5) { + $stderr = 'Failed to connect to executor runtime after 5 attempts.'; + $statusCode = 124; + } + + // If timeout error + if (in_array($errNo, [CURLE_OPERATION_TIMEDOUT, 110])) { + $statusCode = 124; + } + + // 110 is the Swoole error code for timeout, see: https://www.swoole.co.uk/docs/swoole-error-code + if ($errNo !== 0 && $errNo !== CURLE_COULDNT_CONNECT && $errNo !== CURLE_OPERATION_TIMEDOUT && $errNo !== 110) { + throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500); + } + + $executionData = []; + + if (!empty($executorResponse)) { + $executionData = json_decode($executorResponse, true); + } + + if (isset($executionData['code'])) { + $statusCode = $executionData['code']; + } + + if ($statusCode === 500) { + if (isset($executionData['message'])) { + $stderr = $executionData['message']; + } else { + $stderr = 'Internal Runtime error'; + } + } else if ($statusCode === 124) { + $stderr = 'Execution timed out.'; + } else if ($statusCode === 0) { + $stderr = 'Execution failed.'; + } else if ($statusCode >= 200 && $statusCode < 300) { + $stdout = $executorResponse; + } else { + $stderr = 'Execution failed.'; + } + + $executionEnd = \microtime(true); + $executionTime = ($executionEnd - $executionStart); + $functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed'; + + Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus); + + $execution = [ + 'status' => $functionStatus, + 'statusCode' => $statusCode, + 'stdout' => \utf8_encode(\mb_substr($stdout, -8000)), + 'stderr' => \utf8_encode(\mb_substr($stderr, -8000)), + 'time' => $executionTime, + ]; + } catch (\Throwable $th) { + + } $response ->setStatusCode(Response::STATUS_CODE_OK) @@ -710,10 +723,6 @@ App::error(function ($error, $utopia, $request, $response) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - if ($error instanceof PDOException) { - throw $error; - } - $route = $utopia->match($request); // logError($error, "httpError", $route); @@ -744,6 +753,17 @@ App::error(function ($error, $utopia, $request, $response) { $response->json($output); }, ['error', 'utopia', 'request', 'response']); +App::init(function ($request, $response) { + $secretKey = $request->getHeader('x-appwrite-executor-key', ''); + if (empty($secretKey)) { + throw new Exception('Missing executor key', 401); + } + + if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) { + throw new Exception('Missing executor key', 401); + } +}, ['request', 'response']); + $http->on('start', function ($http) { @Process::signal(SIGINT, function () use ($http) { // handleShutdown(); @@ -771,24 +791,19 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $response = new Response($swooleResponse); $app = new App('UTC'); - // Check environment variable key - $secretKey = $request->getHeader('x-appwrite-executor-key', ''); - - if (empty($secretKey)) { - $swooleResponse->status(401); - return $swooleResponse->end('401: Authentication Error'); - } - - if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) { - $swooleResponse->status(401); - return $swooleResponse->end('401: Authentication Error'); - } - try { $app->run($request, $response); - } catch (Exception $e) { + } catch (\Throwable $th) { // logError($e, "serverError"); - $swooleResponse->end('500: Server Error'); + $swooleResponse->setStatusCode(500); + $output = [ + 'message' => 'Error: '. $th->getMessage(), + 'code' => 500, + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'trace' => $th->getTrace() + ]; + $swooleResponse->end(\json_encode($output)); } }); diff --git a/docker-compose.yml b/docker-compose.yml index 3c6ffb5a56..d209ea175d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -394,6 +394,8 @@ services: networks: appwrite: runtimes: + ports: + - 9509:8080 volumes: - /var/run/docker.sock:/var/run/docker.sock - appwrite-functions:/storage/functions:rw diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 05e36fd1fa..384a5c3102 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -59,7 +59,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { - throw new \Exception('Error creating build: ', $status); + throw new \Exception($response['body'], $status); } return $response['body']; From 3ea9c9a9ce889febfa3fdac735d452d80b578e05 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 21:18:48 +0400 Subject: [PATCH 31/61] feat: move orchestration pool to utopia resource --- app/executor.php | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/executor.php b/app/executor.php index 540045f67f..004350a499 100644 --- a/app/executor.php +++ b/app/executor.php @@ -10,7 +10,6 @@ use Swoole\Http\Server; use Swoole\Process; use Utopia\App; use Utopia\CLI\Console; -use Utopia\Config\Config; use Utopia\Logger\Log; use Utopia\Orchestration\Adapter\DockerCLI; use Utopia\Orchestration\Orchestration; @@ -33,6 +32,7 @@ use Utopia\Validator\Text; // - delete pending runtimes older than X minutes ? ENV_VARS // Implement other endpoints // Get list of supported runtimes on startup - Done +// Add size validators for the runtime IDs // Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); @@ -163,14 +163,12 @@ App::post('/v1/runtimes') // refactor to `name` ->param('runtime', '', new Text(128), 'Runtime for the cloud function') ->param('baseImage', '', new Text(128), 'Base image name of the runtime') + ->inject('orchestrationPool') + ->inject('activeFunctions') ->inject('response') - ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, Response $response) { + ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, $orchestrationPool, $activeFunctions, Response $response) { // TODO: Check if runtime already exists.. - - // TODO: Move orchestration pool and swoole table to a utopia resource - global $orchestrationPool; - global $activeFunctions; $orchestration = $orchestrationPool->get(); $build = []; @@ -448,8 +446,9 @@ App::post('/v1/runtimes') // GET /v1/runtimes App::get('/v1/runtimes') ->desc("Get the list of currently active runtimes") + ->inject('activeFunctions') ->inject('response') - ->action(function (Response $response) { + ->action(function ($activeFunctions, Response $response) { // TODO : Get list of active runtimes from swoole table $runtimes = []; @@ -463,8 +462,9 @@ App::get('/v1/runtimes') App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') + ->inject('activeFunctions') ->inject('response') - ->action(function (Response $response) { + ->action(function ($activeFunctions, Response $response) { // Get a runtime by its ID $runtime = []; @@ -479,8 +479,9 @@ App::delete('/v1/runtimes/:runtimeId') ->desc('Delete a runtime') ->param('runtimeId', '', new Text(128), 'Runtime unique ID.', false) ->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false) + ->inject('orchestrationPool') ->inject('response') - ->action(function (string $runtimeId, array $buildIds, Response $response) use ($orchestrationPool) { + ->action(function (string $runtimeId, array $buildIds, $orchestrationPool, Response $response) { Console::info('Deleting runtime: ' . $runtimeId); $orchestration = $orchestrationPool->get(); @@ -523,11 +524,10 @@ App::post('/v1/execution') ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file') ->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true) ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) + ->inject('activeFunctions') ->inject('response') ->action( - function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, Response $response) { - - global $activeFunctions; + function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, $activeFunctions, Response $response) { $container = 'runtime-' . $runtimeId; @@ -717,6 +717,11 @@ $http = new Server("0.0.0.0", 80); // } // }; +/** Set Resources */ +App::setResource('orchestrationPool', fn() => $orchestrationPool); +App::setResource('activeFunctions', fn() => $activeFunctions); + +/** Set callbacks */ App::error(function ($error, $utopia, $request, $response) { /** @var Exception $error */ /** @var Utopia\App $utopia */ From f87b66264524192e20772e6d767c801f68c925ea Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 21:52:44 +0400 Subject: [PATCH 32/61] feat: implemented other endpoints --- app/executor.php | 22 ++++++++++++++-------- docker-compose.yml | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/executor.php b/app/executor.php index 004350a499..8d28dd9231 100644 --- a/app/executor.php +++ b/app/executor.php @@ -30,7 +30,7 @@ use Utopia\Validator\Text; // - Remove orphans // Maintenance job // - delete pending runtimes older than X minutes ? ENV_VARS -// Implement other endpoints +// Implement other endpoints - Done // Get list of supported runtimes on startup - Done // Add size validators for the runtime IDs // @@ -445,14 +445,17 @@ App::post('/v1/runtimes') // GET /v1/runtimes App::get('/v1/runtimes') - ->desc("Get the list of currently active runtimes") + ->desc("List currently active runtimes") ->inject('activeFunctions') ->inject('response') ->action(function ($activeFunctions, Response $response) { - // TODO : Get list of active runtimes from swoole table $runtimes = []; - // Response model for runtime list + foreach($activeFunctions as $runtime) { + $runtimes[] = $runtime; + } + + // TODO: Response model for runtimes and runtimes list $response ->setStatusCode(200) ->json($runtimes); @@ -464,10 +467,13 @@ App::get('/v1/runtimes/:runtimeId') ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') ->inject('activeFunctions') ->inject('response') - ->action(function ($activeFunctions, Response $response) { - - // Get a runtime by its ID - $runtime = []; + ->action(function ($runtimeId, $activeFunctions, Response $response) { + + if(!$activeFunctions->exists($runtimeId)) { + throw new Exception('Runtime not found', 404); + } + + $runtime = $activeFunctions->get($runtimeId); $response ->setStatusCode(200) diff --git a/docker-compose.yml b/docker-compose.yml index d209ea175d..45b29524f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -395,7 +395,7 @@ services: appwrite: runtimes: ports: - - 9509:8080 + - 9509:80 volumes: - /var/run/docker.sock:/var/run/docker.sock - appwrite-functions:/storage/functions:rw From 7436d3b24f281d4ae04b84d534f557c3388218f8 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 14 Feb 2022 22:18:13 +0400 Subject: [PATCH 33/61] feat: handle shutdown --- app/executor.php | 94 +++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 61 deletions(-) diff --git a/app/executor.php b/app/executor.php index 8d28dd9231..103b5d62c7 100644 --- a/app/executor.php +++ b/app/executor.php @@ -23,16 +23,17 @@ use Utopia\Validator\Range as ValidatorRange; use Utopia\Validator\Text; -// EXECUTOR_ pattern // TODO -// Discuss and fix startup logic -// - Pull runtmies -// - Remove orphans +// Implement other endpoints - Done +// Handle shutdown - Done +// Get list of supported runtimes on startup - Done +// Pull runtimes on startup -- Done +// Remove orphans on startup // Maintenance job // - delete pending runtimes older than X minutes ? ENV_VARS -// Implement other endpoints - Done -// Get list of supported runtimes on startup - Done -// Add size validators for the runtime IDs +// -- // EXECUTOR_ pattern +// Add size validators for the runtime IDs +// Decide on logic for built and runtime containers // Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); @@ -519,7 +520,6 @@ App::delete('/v1/runtimes/:runtimeId') }); -// POST /v1/execution (get runtime as param, if 404 or 501/503, go and create a runtime first) App::post('/v1/execution') ->desc('Create an execution') ->param('runtimeId', '', new Text(1024), 'The runtimeID to execute') @@ -671,57 +671,29 @@ App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode $http = new Server("0.0.0.0", 80); -// function handleShutdown() -// { -// global $orchestrationPool; -// global $register; +function handleShutdown() +{ + global $orchestrationPool; + Console::info('Cleaning up containers before shutdown...'); -// try { -// Console::info('Cleaning up containers before shutdown...'); + $orchestration = $orchestrationPool->get(); + $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); + $orchestrationPool->put($orchestration); -// // Remove all containers. - -// /** @var Orchestration $orchestration */ -// $orchestration = $orchestrationPool->get(); - -// $functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']); - -// foreach ($functionsToRemove as $container) { -// go(fn () => $orchestration->remove($container->getId(), true)); - -// // Get a database instance -// $db = $register->get('dbPool')->get(); -// $cache = $register->get('redisPool')->get(); - -// $cache = new Cache(new RedisCache($cache)); -// $database = new Database(new MariaDB($db), $cache); -// $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); -// $database->setNamespace('_project_' . $container->getLabels()["appwrite-project"]); - -// // Get list of all processing executions -// $executions = $database->find('executions', [ -// new Query('deploymentId', Query::TYPE_EQUAL, [$container->getLabels()["appwrite-deployment"]]), -// new Query('status', Query::TYPE_EQUAL, ['waiting']) -// ]); - -// // Mark all processing executions as failed -// foreach ($executions as $execution) { -// $execution -// ->setAttribute('status', 'failed') -// ->setAttribute('statusCode', 1) -// ->setAttribute('stderr', 'Appwrite was shutdown during execution'); - -// $database->updateDocument('executions', $execution->getId(), $execution); -// } - -// Console::info('Removed container ' . $container->getName()); -// } -// } catch (\Throwable $error) { -// logError($error, 'shutdownError'); -// } finally { -// $orchestrationPool->put($orchestration); -// } -// }; + foreach ($functionsToRemove as $container) { + go(function () use ($orchestrationPool, $container) { + 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); + } + }); + } +}; /** Set Resources */ App::setResource('orchestrationPool', fn() => $orchestrationPool); @@ -777,22 +749,22 @@ App::init(function ($request, $response) { $http->on('start', function ($http) { @Process::signal(SIGINT, function () use ($http) { - // handleShutdown(); + handleShutdown(); $http->shutdown(); }); @Process::signal(SIGQUIT, function () use ($http) { - // handleShutdown(); + handleShutdown(); $http->shutdown(); }); @Process::signal(SIGKILL, function () use ($http) { - // handleShutdown(); + handleShutdown(); $http->shutdown(); }); @Process::signal(SIGTERM, function () use ($http) { - // handleShutdown(); + handleShutdown(); $http->shutdown(); }); }); From e6d1b542433c8e705b04a9dec42f7e625cd37863 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 01:26:31 +0400 Subject: [PATCH 34/61] feat: handle shutdown --- app/executor.php | 91 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/app/executor.php b/app/executor.php index 103b5d62c7..b5a83701b7 100644 --- a/app/executor.php +++ b/app/executor.php @@ -8,6 +8,8 @@ use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; use Swoole\Http\Server; use Swoole\Process; +use Swoole\Runtime; +use Swoole\Timer; use Utopia\App; use Utopia\CLI\Console; use Utopia\Logger\Log; @@ -28,15 +30,16 @@ use Utopia\Validator\Text; // Handle shutdown - Done // Get list of supported runtimes on startup - Done // Pull runtimes on startup -- Done + // Remove orphans on startup // Maintenance job // - delete pending runtimes older than X minutes ? ENV_VARS // -- // EXECUTOR_ pattern // Add size validators for the runtime IDs -// Decide on logic for built and runtime containers +// Decide on logic for build and runtime containers names ( runtime-ID and build-ID) // -Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); function logError(Throwable $error, string $action, Utopia\Route $route = null) { @@ -671,30 +674,6 @@ App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode $http = new Server("0.0.0.0", 80); -function handleShutdown() -{ - global $orchestrationPool; - Console::info('Cleaning up containers before shutdown...'); - - $orchestration = $orchestrationPool->get(); - $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); - $orchestrationPool->put($orchestration); - - foreach ($functionsToRemove as $container) { - go(function () use ($orchestrationPool, $container) { - 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); - } - }); - } -}; - /** Set Resources */ App::setResource('orchestrationPool', fn() => $orchestrationPool); App::setResource('activeFunctions', fn() => $activeFunctions); @@ -748,27 +727,79 @@ App::init(function ($request, $response) { }, ['request', 'response']); $http->on('start', function ($http) { + + /** + * Register handlers for shutdown + */ @Process::signal(SIGINT, function () use ($http) { - handleShutdown(); $http->shutdown(); }); @Process::signal(SIGQUIT, function () use ($http) { - handleShutdown(); $http->shutdown(); }); @Process::signal(SIGKILL, function () use ($http) { - handleShutdown(); $http->shutdown(); }); @Process::signal(SIGTERM, function () use ($http) { - handleShutdown(); $http->shutdown(); }); + + /** + * Run a maintenance worker every 5 minutes to remove unused containers + */ + // global $orchestrationPool; + // Timer::tick(5000, function () use ($orchestrationPool) { + // Console::info('Running maintenance task every 5 minutes ...'); + // $orchestration = $orchestrationPool->get(); + // $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); + // $orchestrationPool->put($orchestration); + + // foreach ($functionsToRemove as $container) { + // go(function () use ($orchestrationPool, $container) { + // 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); + // } + // }); + // } + // }); + }); + +$http->on("shutdown", function() { + global $orchestrationPool; + Console::info('Cleaning up containers before shutdown...'); + + $orchestration = $orchestrationPool->get(); + $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); + $orchestrationPool->put($orchestration); + + // This does not seem to be working since this is not a coroutine scope . + foreach ($functionsToRemove as $container) { + go(function () use ($orchestrationPool, $container) { + 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); + } + }); + } +}); + + $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { $request = new Request($swooleRequest); $response = new Response($swooleResponse); From f51b855b7e86d65d95e8ecab155f1c762ec5eb74 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 02:15:08 +0400 Subject: [PATCH 35/61] feat: move logic to server start --- app/executor.php | 152 ++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/app/executor.php b/app/executor.php index b5a83701b7..6bf88ce717 100644 --- a/app/executor.php +++ b/app/executor.php @@ -30,6 +30,8 @@ use Utopia\Validator\Text; // Handle shutdown - Done // Get list of supported runtimes on startup - Done // Pull runtimes on startup -- Done +// Move orchestration pool creation to server start +// Remove attempts logic in executor // Remove orphans on startup // Maintenance job @@ -41,6 +43,27 @@ use Utopia\Validator\Text; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +/** +* Create a Swoole table to store runtime information +*/ +$activeFunctions = new Swoole\Table(1024); +$activeFunctions->column('id', Swoole\Table::TYPE_STRING, 512); +$activeFunctions->column('created', Swoole\Table::TYPE_INT, 512); +$activeFunctions->column('name', Swoole\Table::TYPE_STRING, 512); +$activeFunctions->column('status', Swoole\Table::TYPE_STRING, 512); +$activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096); +$activeFunctions->create(); + +/** + * 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); + function logError(Throwable $error, string $action, Utopia\Route $route = null) { global $register; @@ -84,80 +107,6 @@ function logError(Throwable $error, string $action, Utopia\Route $route = null) Console::error('[Error] Line: ' . $error->getLine()); }; -$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; -}, 6); - -try { - - $runtimes = new Runtimes(); - $allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES')); - $runtimes = $runtimes->getAll(true, $allowList); - - // Warmup: make sure images are ready to run fast 🚀 - Co\run(function () use ($runtimes, $orchestrationPool) { - foreach ($runtimes as $runtime) { - go(function () use ($runtime, $orchestrationPool) { - try { - $orchestration = $orchestrationPool->get(); - - Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...'); - - $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']}!"); - } - } catch (\Throwable $th) { - } finally { - $orchestrationPool->put($orchestration); - } - }); - } - }); - - $activeFunctions = new Swoole\Table(1024); - $activeFunctions->column('id', Swoole\Table::TYPE_STRING, 512); - $activeFunctions->column('created', Swoole\Table::TYPE_INT, 512); - $activeFunctions->column('name', Swoole\Table::TYPE_STRING, 512); - $activeFunctions->column('status', Swoole\Table::TYPE_STRING, 512); - $activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096); - $activeFunctions->create(); - - Co\run(function () use ($orchestrationPool, $activeFunctions) { - try { - $orchestration = $orchestrationPool->get(); - $executionStart = \microtime(true); - $residueList = $orchestration->list(['label' => 'openruntimes-type=function']); - } catch (\Throwable $th) { - } finally { - $orchestrationPool->put($orchestration); - } - - - foreach ($residueList as $value) { - go(fn () => $activeFunctions->set($value->getName(), [ - 'id' => $value->getId(), - 'name' => $value->getName(), - 'status' => $value->getStatus(), - 'private-key' => '' - ])); - } - - $executionEnd = \microtime(true); - Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); - }); -} catch (\Throwable $error) { - call_user_func($logError, $error, "startupError"); -} - - -// POST /v1/runtimes App::post('/v1/runtimes') ->desc("Create a new runtime server") ->param('runtimeId', '', new Text(128), 'Unique runtime ID.') @@ -728,6 +677,59 @@ App::init(function ($request, $response) { $http->on('start', function ($http) { + /** + * Warmup: make sure images are ready to run fast 🚀 + */ + global $orchestrationPool; + $runtimes = new Runtimes(); + $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...'); + + $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']}!"); + } + } catch (\Throwable $th) { + } finally { + $orchestrationPool->put($orchestration); + } + }); + } + + /** + * Populate swoole table with active runtimes + */ + global $activeFunctions; + try { + $orchestration = $orchestrationPool->get(); + $executionStart = \microtime(true); + $residueList = $orchestration->list(['label' => 'openruntimes-type=function']); + } catch (\Throwable $th) { + } finally { + $orchestrationPool->put($orchestration); + } + + foreach ($residueList as $value) { + go(fn () => $activeFunctions->set($value->getName(), [ + 'id' => $value->getId(), + 'name' => $value->getName(), + 'status' => $value->getStatus(), + 'key' => '' + ])); + } + + $executionEnd = \microtime(true); + Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); + /** * Register handlers for shutdown */ @@ -752,7 +754,7 @@ $http->on('start', function ($http) { */ // global $orchestrationPool; // Timer::tick(5000, function () use ($orchestrationPool) { - // Console::info('Running maintenance task every 5 minutes ...'); + // Console::info('Running maintenance task every 5 seconds ...'); // $orchestration = $orchestrationPool->get(); // $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); // $orchestrationPool->put($orchestration); From ff5314e2024cfd4bc7d3273e4c4d709e68813da6 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 03:22:16 +0400 Subject: [PATCH 36/61] feat: add updated property to runtime --- app/executor.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/executor.php b/app/executor.php index 6bf88ce717..046a359774 100644 --- a/app/executor.php +++ b/app/executor.php @@ -30,9 +30,11 @@ use Utopia\Validator\Text; // Handle shutdown - Done // Get list of supported runtimes on startup - Done // Pull runtimes on startup -- Done -// Move orchestration pool creation to server start +// Move some logic to server start - Done +// Add updated property to swoole table - Done +// Clean up deployments older than X seconds // Remove attempts logic in executor - +// Fix delete endpoint // Remove orphans on startup // Maintenance job // - delete pending runtimes older than X minutes ? ENV_VARS @@ -48,10 +50,11 @@ Runtime::enableCoroutine(SWOOLE_HOOK_ALL); */ $activeFunctions = new Swoole\Table(1024); $activeFunctions->column('id', Swoole\Table::TYPE_STRING, 512); -$activeFunctions->column('created', Swoole\Table::TYPE_INT, 512); +$activeFunctions->column('created', Swoole\Table::TYPE_INT, 8); +$activeFunctions->column('updated', Swoole\Table::TYPE_INT, 8); $activeFunctions->column('name', Swoole\Table::TYPE_STRING, 512); $activeFunctions->column('status', Swoole\Table::TYPE_STRING, 512); -$activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096); +$activeFunctions->column('key', Swoole\Table::TYPE_STRING, 512); $activeFunctions->create(); /** @@ -379,6 +382,8 @@ App::post('/v1/runtimes') $activeFunctions->set($container, [ 'id' => $id, 'name' => $container, + 'created' => $executionTime, + 'updated' => $executionTime, 'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', 'key' => $secret, ]); @@ -494,7 +499,8 @@ App::post('/v1/execution') throw new Exception('Runtime not found. Please create the runtime.', 404); } - $secret = $activeFunctions->get($container, 'key'); + $runtime = $activeFunctions->get($container); + $secret = $runtime['key']; if (empty($secret)) { throw new Exception('Runtime secret not found. Please create the runtime.', 500); } @@ -609,8 +615,11 @@ App::post('/v1/execution') 'stderr' => \utf8_encode(\mb_substr($stderr, -8000)), 'time' => $executionTime, ]; - } catch (\Throwable $th) { + $runtime['updated'] = \time(); + $activeFunctions->set($container, $runtime); + } catch (\Throwable $th) { + Console::error('Runtime execution failed: ' . $th->getMessage()); } $response @@ -675,8 +684,8 @@ App::init(function ($request, $response) { } }, ['request', 'response']); -$http->on('start', function ($http) { +$http->on('start', function ($http) { /** * Warmup: make sure images are ready to run fast 🚀 */ From 0acd573c892faa468767e1e965967a5dad65cfe3 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 04:23:20 +0400 Subject: [PATCH 37/61] feat: maintenance task --- .env | 1 + app/executor.php | 120 ++++++++++++++++++++++++--------------------- docker-compose.yml | 1 + 3 files changed, 66 insertions(+), 56 deletions(-) diff --git a/.env b/.env index 4416061925..99ccde1e18 100644 --- a/.env +++ b/.env @@ -48,3 +48,4 @@ _APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_USAGE_STATS=enabled _APP_LOGGING_PROVIDER= _APP_LOGGING_CONFIG= +OPENRUNTIMES_INACTIVE_THRESHOLD=60 \ No newline at end of file diff --git a/app/executor.php b/app/executor.php index 046a359774..ad96a4c5dc 100644 --- a/app/executor.php +++ b/app/executor.php @@ -32,7 +32,8 @@ use Utopia\Validator\Text; // Pull runtimes on startup -- Done // Move some logic to server start - Done // Add updated property to swoole table - Done -// Clean up deployments older than X seconds +// Clean up deployments older than X seconds - Done + // Remove attempts logic in executor // Fix delete endpoint // Remove orphans on startup @@ -45,17 +46,20 @@ use Utopia\Validator\Text; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +/** Constants */ +const MAINTENANCE_INTERVAL = 1200; // 20 minutes + /** * Create a Swoole table to store runtime information */ -$activeFunctions = new Swoole\Table(1024); -$activeFunctions->column('id', Swoole\Table::TYPE_STRING, 512); -$activeFunctions->column('created', Swoole\Table::TYPE_INT, 8); -$activeFunctions->column('updated', Swoole\Table::TYPE_INT, 8); -$activeFunctions->column('name', Swoole\Table::TYPE_STRING, 512); -$activeFunctions->column('status', Swoole\Table::TYPE_STRING, 512); -$activeFunctions->column('key', Swoole\Table::TYPE_STRING, 512); -$activeFunctions->create(); +$activeRuntimes = new Swoole\Table(1024); +$activeRuntimes->column('id', Swoole\Table::TYPE_STRING, 512); +$activeRuntimes->column('created', Swoole\Table::TYPE_INT, 8); +$activeRuntimes->column('updated', Swoole\Table::TYPE_INT, 8); +$activeRuntimes->column('name', Swoole\Table::TYPE_STRING, 512); +$activeRuntimes->column('status', Swoole\Table::TYPE_STRING, 512); +$activeRuntimes->column('key', Swoole\Table::TYPE_STRING, 512); +$activeRuntimes->create(); /** * Create orchestration pool @@ -120,9 +124,9 @@ App::post('/v1/runtimes') ->param('runtime', '', new Text(128), 'Runtime for the cloud function') ->param('baseImage', '', new Text(128), 'Base image name of the runtime') ->inject('orchestrationPool') - ->inject('activeFunctions') + ->inject('activeRuntimes') ->inject('response') - ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, $orchestrationPool, $activeFunctions, Response $response) { + ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, $orchestrationPool, $activeRuntimes, Response $response) { // TODO: Check if runtime already exists.. $orchestration = $orchestrationPool->get(); @@ -319,14 +323,14 @@ App::post('/v1/runtimes') /** Create runtime server */ try { $container = 'runtime-' . $runtimeId; - if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online + if ($activeRuntimes->exists($container) && !(\substr($activeRuntimes->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online // If container is online then stop and remove it try { $orchestration->remove($container, true); } catch (Exception $e) { throw new Exception('Failed to remove container: ' . $e->getMessage()); } - $activeFunctions->del($container); + $activeRuntimes->del($container); } /** @@ -345,7 +349,7 @@ App::post('/v1/runtimes') 'INTERNAL_RUNTIME_KEY' => $secret ]); - if (!$activeFunctions->exists($container)) { + if (!$activeRuntimes->exists($container)) { $executionStart = \microtime(true); $executionTime = \time(); @@ -379,7 +383,7 @@ App::post('/v1/runtimes') $executionEnd = \microtime(true); - $activeFunctions->set($container, [ + $activeRuntimes->set($container, [ 'id' => $id, 'name' => $container, 'created' => $executionTime, @@ -404,12 +408,12 @@ App::post('/v1/runtimes') // GET /v1/runtimes App::get('/v1/runtimes') ->desc("List currently active runtimes") - ->inject('activeFunctions') + ->inject('activeRuntimes') ->inject('response') - ->action(function ($activeFunctions, Response $response) { + ->action(function ($activeRuntimes, Response $response) { $runtimes = []; - foreach($activeFunctions as $runtime) { + foreach($activeRuntimes as $runtime) { $runtimes[] = $runtime; } @@ -423,15 +427,15 @@ App::get('/v1/runtimes') App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') - ->inject('activeFunctions') + ->inject('activeRuntimes') ->inject('response') - ->action(function ($runtimeId, $activeFunctions, Response $response) { + ->action(function ($runtimeId, $activeRuntimes, Response $response) { - if(!$activeFunctions->exists($runtimeId)) { + if(!$activeRuntimes->exists($runtimeId)) { throw new Exception('Runtime not found', 404); } - $runtime = $activeFunctions->get($runtimeId); + $runtime = $activeRuntimes->get($runtimeId); $response ->setStatusCode(200) @@ -444,20 +448,23 @@ App::delete('/v1/runtimes/:runtimeId') ->param('runtimeId', '', new Text(128), 'Runtime unique ID.', false) ->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false) ->inject('orchestrationPool') + ->inject('activeRuntimes') ->inject('response') - ->action(function (string $runtimeId, array $buildIds, $orchestrationPool, Response $response) { + ->action(function (string $runtimeId, array $buildIds, $orchestrationPool, $activeRuntimes, Response $response) { Console::info('Deleting runtime: ' . $runtimeId); $orchestration = $orchestrationPool->get(); - // Remove the container of the deployment - $status = $orchestration->remove('runtime-' . $runtimeId , true); + $container = 'runtime-' . $runtimeId; + $status = $orchestration->remove($container, true); if ($status) { Console::success('Removed runtime container: ' . $runtimeId); } else { Console::error('Failed to remove runtime container: ' . $runtimeId); } + $activeRuntimes->del($container); + // Remove all the build containers with that same ID // TODO:: Delete build containers // foreach ($buildIds as $buildId) { @@ -487,19 +494,19 @@ App::post('/v1/execution') ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file') ->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true) ->param('baseImage', '', new Text(128), 'Base image name of the runtime', false) - ->inject('activeFunctions') + ->inject('activeRuntimes') ->inject('response') ->action( - function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, $activeFunctions, Response $response) { + function (string $runtimeId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, $activeRuntimes, Response $response) { $container = 'runtime-' . $runtimeId; // TODO: Also check for container status - if (!$activeFunctions->exists($container)) { + if (!$activeRuntimes->exists($container)) { throw new Exception('Runtime not found. Please create the runtime.', 404); } - $runtime = $activeFunctions->get($container); + $runtime = $activeRuntimes->get($container); $secret = $runtime['key']; if (empty($secret)) { throw new Exception('Runtime secret not found. Please create the runtime.', 500); @@ -617,7 +624,7 @@ App::post('/v1/execution') ]; $runtime['updated'] = \time(); - $activeFunctions->set($container, $runtime); + $activeRuntimes->set($container, $runtime); } catch (\Throwable $th) { Console::error('Runtime execution failed: ' . $th->getMessage()); } @@ -634,7 +641,7 @@ $http = new Server("0.0.0.0", 80); /** Set Resources */ App::setResource('orchestrationPool', fn() => $orchestrationPool); -App::setResource('activeFunctions', fn() => $activeFunctions); +App::setResource('activeRuntimes', fn() => $activeRuntimes); /** Set callbacks */ App::error(function ($error, $utopia, $request, $response) { @@ -717,7 +724,7 @@ $http->on('start', function ($http) { /** * Populate swoole table with active runtimes */ - global $activeFunctions; + global $activeRuntimes; try { $orchestration = $orchestrationPool->get(); $executionStart = \microtime(true); @@ -728,7 +735,7 @@ $http->on('start', function ($http) { } foreach ($residueList as $value) { - go(fn () => $activeFunctions->set($value->getName(), [ + go(fn () => $activeRuntimes->set($value->getName(), [ 'id' => $value->getId(), 'name' => $value->getName(), 'status' => $value->getStatus(), @@ -737,7 +744,7 @@ $http->on('start', function ($http) { } $executionEnd = \microtime(true); - Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); + Console::info(count($activeRuntimes) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); /** * Register handlers for shutdown @@ -759,29 +766,30 @@ $http->on('start', function ($http) { }); /** - * Run a maintenance worker every 5 minutes to remove unused containers + * Run a maintenance worker every MAINTENANCE_INTERVAL seconds to remove inactive runtimes */ - // global $orchestrationPool; - // Timer::tick(5000, function () use ($orchestrationPool) { - // Console::info('Running maintenance task every 5 seconds ...'); - // $orchestration = $orchestrationPool->get(); - // $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); - // $orchestrationPool->put($orchestration); - - // foreach ($functionsToRemove as $container) { - // go(function () use ($orchestrationPool, $container) { - // 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); - // } - // }); - // } - // }); + global $orchestrationPool; + global $activeRuntimes; + Timer::tick(MAINTENANCE_INTERVAL * 1000, function () use ($orchestrationPool, $activeRuntimes) { + Console::warning("Running maintenance task ..."); + foreach ($activeRuntimes as $runtime) { + $inactiveThreshold = \time() - App::getEnv('OPENRUNTIMES_INACTIVE_THRESHOLD', 60); + 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); + } + }); + } + } + }); }); diff --git a/docker-compose.yml b/docker-compose.yml index 45b29524f2..9cf45f3024 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -416,6 +416,7 @@ services: - _APP_LOGGING_CONFIG - DOCKERHUB_PULL_USERNAME - DOCKERHUB_PULL_PASSWORD + - OPENRUNTIMES_INACTIVE_THRESHOLD appwrite-worker-mails: entrypoint: worker-mails From a627d45ba7ace1c9c7ef2556efc8487037c92816 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 04:40:16 +0400 Subject: [PATCH 38/61] feat: maintenance task --- app/executor.php | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/app/executor.php b/app/executor.php index ad96a4c5dc..39eae51a61 100644 --- a/app/executor.php +++ b/app/executor.php @@ -33,13 +33,12 @@ use Utopia\Validator\Text; // Move some logic to server start - Done // Add updated property to swoole table - Done // Clean up deployments older than X seconds - Done +// Remove orphans on startup - done + // Remove attempts logic in executor +// Fix error handling and logging // Fix delete endpoint -// Remove orphans on startup -// Maintenance job -// - delete pending runtimes older than X minutes ? ENV_VARS -// -- // EXECUTOR_ pattern // Add size validators for the runtime IDs // Decide on logic for build and runtime containers names ( runtime-ID and build-ID) // @@ -693,10 +692,12 @@ App::init(function ($request, $response) { $http->on('start', function ($http) { - /** + global $orchestrationPool; + global $activeRuntimes; + + /** * Warmup: make sure images are ready to run fast 🚀 */ - global $orchestrationPool; $runtimes = new Runtimes(); $allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES')); $runtimes = $runtimes->getAll(true, $allowList); @@ -722,30 +723,31 @@ $http->on('start', function ($http) { } /** - * Populate swoole table with active runtimes + * Remove residual runtimes */ - global $activeRuntimes; + Console::info('Removing orphan runtimes...'); try { $orchestration = $orchestrationPool->get(); - $executionStart = \microtime(true); - $residueList = $orchestration->list(['label' => 'openruntimes-type=function']); + $orphans = $orchestration->list(['label' => 'openruntimes-type=function']); } catch (\Throwable $th) { } finally { $orchestrationPool->put($orchestration); } - foreach ($residueList as $value) { - go(fn () => $activeRuntimes->set($value->getName(), [ - 'id' => $value->getId(), - 'name' => $value->getName(), - 'status' => $value->getStatus(), - 'key' => '' - ])); + 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); + } + }); } - $executionEnd = \microtime(true); - Console::info(count($activeRuntimes) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); - /** * Register handlers for shutdown */ @@ -768,8 +770,7 @@ $http->on('start', function ($http) { /** * Run a maintenance worker every MAINTENANCE_INTERVAL seconds to remove inactive runtimes */ - global $orchestrationPool; - global $activeRuntimes; + Timer::tick(MAINTENANCE_INTERVAL * 1000, function () use ($orchestrationPool, $activeRuntimes) { Console::warning("Running maintenance task ..."); foreach ($activeRuntimes as $runtime) { From b80965edd832abc0cc1e59d320bed6096bb2c807 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 04:49:15 +0400 Subject: [PATCH 39/61] feat: remove multiple request logic from executor --- app/executor.php | 98 +++++++++++++++++++----------------------------- 1 file changed, 38 insertions(+), 60 deletions(-) diff --git a/app/executor.php b/app/executor.php index 39eae51a61..e7d945c6f3 100644 --- a/app/executor.php +++ b/app/executor.php @@ -34,9 +34,11 @@ use Utopia\Validator\Text; // Add updated property to swoole table - Done // Clean up deployments older than X seconds - Done // Remove orphans on startup - done +// Remove multiple request attempt to the runtime logic in executor - done -// Remove attempts logic in executor +// Shutdown callback isn't working as expected +// Incorporate Matej's changes in the build stage ( moving of the tar file will be performed by the runtime and not the build stage ) // Fix error handling and logging // Fix delete endpoint // Add size validators for the runtime IDs @@ -52,12 +54,12 @@ const MAINTENANCE_INTERVAL = 1200; // 20 minutes * Create a Swoole table to store runtime information */ $activeRuntimes = new Swoole\Table(1024); -$activeRuntimes->column('id', Swoole\Table::TYPE_STRING, 512); +$activeRuntimes->column('id', Swoole\Table::TYPE_STRING, 256); $activeRuntimes->column('created', Swoole\Table::TYPE_INT, 8); $activeRuntimes->column('updated', Swoole\Table::TYPE_INT, 8); -$activeRuntimes->column('name', Swoole\Table::TYPE_STRING, 512); -$activeRuntimes->column('status', Swoole\Table::TYPE_STRING, 512); -$activeRuntimes->column('key', Swoole\Table::TYPE_STRING, 512); +$activeRuntimes->column('name', Swoole\Table::TYPE_STRING, 128); +$activeRuntimes->column('status', Swoole\Table::TYPE_STRING, 128); +$activeRuntimes->column('key', Swoole\Table::TYPE_STRING, 256); $activeRuntimes->create(); /** @@ -404,7 +406,6 @@ App::post('/v1/runtimes') }); -// GET /v1/runtimes App::get('/v1/runtimes') ->desc("List currently active runtimes") ->inject('activeRuntimes') @@ -422,7 +423,6 @@ App::get('/v1/runtimes') ->json($runtimes); }); -// GET /v1/runtimes/:runtimeId App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') @@ -441,7 +441,6 @@ App::get('/v1/runtimes/:runtimeId') ->json($runtime); }); -// DELETE /v1/runtimes/:runtimeId App::delete('/v1/runtimes/:runtimeId') ->desc('Delete a runtime') ->param('runtimeId', '', new Text(128), 'Runtime unique ID.', false) @@ -521,57 +520,38 @@ App::post('/v1/execution') $executorResponse = ''; try { - $attempts = 0; - $max = 5; - // cURL request to runtime - do { - $attempts++; - $ch = \curl_init(); - - $body = \json_encode([ - 'path' => '/usr/code', - 'file' => $entrypoint, - 'env' => $vars, - 'payload' => $data, - 'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) - ]); - - \curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/"); - \curl_setopt($ch, CURLOPT_POST, true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)); - \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - - \curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Content-Length: ' . \strlen($body), - 'x-internal-challenge: ' . $secret, - 'host: null' - ]); - - $executorResponse = \curl_exec($ch); - - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - - $error = \curl_error($ch); - - $errNo = \curl_errno($ch); - - \curl_close($ch); - if ($errNo != CURLE_COULDNT_CONNECT && $errNo != 111) { - break; - } - - sleep(1); - } while ($attempts < $max); + $ch = \curl_init(); + $body = \json_encode([ + 'path' => '/usr/code', + 'file' => $entrypoint, + 'env' => $vars, + 'payload' => $data, + 'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + ]); + \curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/"); + \curl_setopt($ch, CURLOPT_POST, true); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + \curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)); + \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + + \curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . \strlen($body), + 'x-internal-challenge: ' . $secret, + 'host: null' + ]); + + $executorResponse = \curl_exec($ch); + + $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + + $error = \curl_error($ch); + + $errNo = \curl_errno($ch); + + \curl_close($ch); - if ($attempts >= 5) { - $stderr = 'Failed to connect to executor runtime after 5 attempts.'; - $statusCode = 124; - } - // If timeout error if (in_array($errNo, [CURLE_OPERATION_TIMEDOUT, 110])) { $statusCode = 124; @@ -647,7 +627,6 @@ App::error(function ($error, $utopia, $request, $response) { /** @var Exception $error */ /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ - /** @var Appwrite\Utopia\Response $response */ $route = $utopia->match($request); // logError($error, "httpError", $route); @@ -770,7 +749,6 @@ $http->on('start', function ($http) { /** * Run a maintenance worker every MAINTENANCE_INTERVAL seconds to remove inactive runtimes */ - Timer::tick(MAINTENANCE_INTERVAL * 1000, function () use ($orchestrationPool, $activeRuntimes) { Console::warning("Running maintenance task ..."); foreach ($activeRuntimes as $runtime) { From 19c9ee4dc6f202172a432445e639c5a582e364da Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 04:58:12 +0400 Subject: [PATCH 40/61] feat: Remove builds param from delete endpoint --- app/executor.php | 31 ++++++++++++++----------------- app/workers/deletes.php | 12 ++++-------- src/Executor/Executor.php | 6 ++---- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/app/executor.php b/app/executor.php index e7d945c6f3..c90623b620 100644 --- a/app/executor.php +++ b/app/executor.php @@ -3,7 +3,6 @@ require_once __DIR__ . '/../vendor/autoload.php'; use Appwrite\Runtimes\Runtimes; use Swoole\ConnectionPool; -use Swoole\Coroutine as Co; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; use Swoole\Http\Server; @@ -19,12 +18,10 @@ use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\Swoole\Request; use Utopia\Swoole\Response; -use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; use Utopia\Validator\Range as ValidatorRange; use Utopia\Validator\Text; - // TODO // Implement other endpoints - Done // Handle shutdown - Done @@ -43,7 +40,6 @@ use Utopia\Validator\Text; // Fix delete endpoint // Add size validators for the runtime IDs // Decide on logic for build and runtime containers names ( runtime-ID and build-ID) -// Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -444,25 +440,28 @@ App::get('/v1/runtimes/:runtimeId') App::delete('/v1/runtimes/:runtimeId') ->desc('Delete a runtime') ->param('runtimeId', '', new Text(128), 'Runtime unique ID.', false) - ->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false) ->inject('orchestrationPool') ->inject('activeRuntimes') ->inject('response') - ->action(function (string $runtimeId, array $buildIds, $orchestrationPool, $activeRuntimes, Response $response) { - - Console::info('Deleting runtime: ' . $runtimeId); - $orchestration = $orchestrationPool->get(); + ->action(function (string $runtimeId, $orchestrationPool, $activeRuntimes, Response $response) { $container = 'runtime-' . $runtimeId; - $status = $orchestration->remove($container, true); - if ($status) { - Console::success('Removed runtime container: ' . $runtimeId); - } else { + Console::info('Deleting runtime: ' . $container); + try { + $orchestration = $orchestrationPool->get(); + $status = $orchestration->remove($container, true); + if ($status) { + Console::success('Removed runtime container: ' . $runtimeId); + } else { + Console::error('Failed to remove runtime container: ' . $runtimeId); + } + $activeRuntimes->del($container); + } catch (\Throwable $th) { Console::error('Failed to remove runtime container: ' . $runtimeId); + } finally { + $orchestrationPool->put($orchestration); } - $activeRuntimes->del($container); - // Remove all the build containers with that same ID // TODO:: Delete build containers // foreach ($buildIds as $buildId) { @@ -474,8 +473,6 @@ App::delete('/v1/runtimes/:runtimeId') // } // } - $orchestrationPool->put($orchestration); - $response ->setStatusCode(Response::STATUS_CODE_OK) ->send(); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 1b3494f058..43c7f7bc0d 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -336,12 +336,10 @@ class DeletesV1 extends Worker */ Console::info("Deleting builds for function " . $functionId); $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); - $buildIds = []; foreach ($deploymentIds as $deploymentId) { $this->deleteByGroup('builds', [ new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]) - ], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId, &$buildIds) { - $buildIds[$deploymentId][] = $document->getId(); + ], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId) { if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) { Console::success('Deleted build files: ' . $document->getAttribute('outputPath', '')); } else { @@ -365,7 +363,7 @@ class DeletesV1 extends Worker $executor = new Executor(); foreach ($deploymentIds as $deploymentId) { try { - $executor->deleteRuntime($projectId, $functionId, $deploymentId, $buildIds[$deploymentId]); + $executor->deleteRuntime($projectId, $functionId, $deploymentId); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -398,12 +396,10 @@ class DeletesV1 extends Worker * Delete builds */ Console::info("Deleting builds for deployment " . $deploymentId); - $buildIds = []; $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); $this->deleteByGroup('builds', [ new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]) - ], $dbForProject, function (Document $document) use ($storageBuilds, &$buildIds) { - $buildIds[] = $document->getId(); + ], $dbForProject, function (Document $document) use ($storageBuilds) { if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) { Console::success('Deleted build files: ' . $document->getAttribute('outputPath', '')); } else { @@ -417,7 +413,7 @@ class DeletesV1 extends Worker Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId); try { $executor = new Executor(); - $executor->deleteRuntime($projectId, $functionId, $deploymentId, $buildIds); + $executor->deleteRuntime($projectId, $functionId, $deploymentId); } catch (Throwable $th) { Console::error($th->getMessage()); } diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 384a5c3102..2e91cd6aee 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -65,7 +65,7 @@ class Executor return $response['body']; } - public function deleteRuntime(string $projectId, string $functionId, string $deploymentId, array $buildIds) + public function deleteRuntime(string $projectId, string $functionId, string $deploymentId) { $runtimeId = "$projectId-$deploymentId"; $route = "/runtimes/$runtimeId"; @@ -75,9 +75,7 @@ class Executor 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') ]; - $params = [ - 'buildIds' => $buildIds, - ]; + $params = []; $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); From c0f7d90cd3999200a4d2dc261e60ef6bfcd3aeb3 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 05:14:43 +0400 Subject: [PATCH 41/61] feat: some cleanup --- app/executor.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/executor.php b/app/executor.php index c90623b620..3485539d8b 100644 --- a/app/executor.php +++ b/app/executor.php @@ -32,6 +32,7 @@ use Utopia\Validator\Text; // Clean up deployments older than X seconds - Done // Remove orphans on startup - done // Remove multiple request attempt to the runtime logic in executor - done +// Remove builds param from delete endpoint - done // Shutdown callback isn't working as expected @@ -421,6 +422,7 @@ App::get('/v1/runtimes') App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") + // Change the text validators to UID ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') ->inject('activeRuntimes') ->inject('response') @@ -482,8 +484,8 @@ App::delete('/v1/runtimes/:runtimeId') App::post('/v1/execution') ->desc('Create an execution') ->param('runtimeId', '', new Text(1024), 'The runtimeID to execute') - ->param('path', '', new Text(0), 'Path to built files.', false) - ->param('vars', '', new Assoc(), 'Environment Variables required for the build', false) + ->param('path', '', new Text(0), 'Path containing the built files.', false) + ->param('vars', '', new Assoc(), 'Environment variables required for the build', false) ->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true) ->param('runtime', '', new Text(128), 'Runtime for the cloud function', false) ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file') @@ -496,7 +498,6 @@ App::post('/v1/execution') $container = 'runtime-' . $runtimeId; - // TODO: Also check for container status if (!$activeRuntimes->exists($container)) { throw new Exception('Runtime not found. Please create the runtime.', 404); } @@ -599,6 +600,7 @@ App::post('/v1/execution') 'time' => $executionTime, ]; + /** Update swoole table */ $runtime['updated'] = \time(); $activeRuntimes->set($container, $runtime); } catch (\Throwable $th) { @@ -681,11 +683,8 @@ $http->on('start', function ($http) { go(function () use ($runtime, $orchestrationPool) { try { $orchestration = $orchestrationPool->get(); - Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...'); - $response = $orchestration->pull($runtime['image']); - if ($response) { Console::success("Successfully Warmed up {$runtime['name']} {$runtime['version']}!"); } else { From 166cd6e7485fe51ca1b405f987a46f86e8a4e107 Mon Sep 17 00:00:00 2001 From: Matej Baco Date: Tue, 15 Feb 2022 09:46:21 +0100 Subject: [PATCH 42/61] Renamed deploy to activate --- app/config/collections.php | 2 +- app/controllers/api/functions.php | 12 ++++++------ app/workers/builds.php | 2 +- src/Appwrite/Utopia/Response/Model/Deployment.php | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 7614cde4a7..3a811f6aaf 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1993,7 +1993,7 @@ $collections = [ 'filters' => [], ], [ - '$id' => 'deploy', + '$id' => 'activate', 'type' => Database::VAR_BOOLEAN, 'format' => '', 'size' => 0, diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b42e495fb7..0852a5f966 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -464,14 +464,14 @@ App::post('/v1/functions/:functionId/deployments') ->param('functionId', '', new UID(), 'Function ID.') ->param('entrypoint', '', new Text('1028'), 'Entrypoint File.') ->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false) - ->param('deploy', false, new Boolean(true), 'Automatically deploy the function when it is finished building.', false) + ->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.', false) ->inject('request') ->inject('response') ->inject('dbForProject') ->inject('usage') ->inject('user') ->inject('project') - ->action(function ($functionId, $entrypoint, $file, $deploy, $request, $response, $dbForProject, $usage, $user, $project) { + ->action(function ($functionId, $entrypoint, $file, $activate, $request, $response, $dbForProject, $usage, $user, $project) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForProject */ @@ -520,16 +520,16 @@ App::post('/v1/functions/:functionId/deployments') throw new Exception('Failed moving file', 500); } - if ((bool) $deploy) { + if ((bool) $activate) { // Remove deploy for all other deployments. $deployments = $dbForProject->find('deployments', [ - new Query('deploy', Query::TYPE_EQUAL, [true]), + new Query('activate', Query::TYPE_EQUAL, [true]), new Query('resourceId', Query::TYPE_EQUAL, [$functionId]), new Query('resourceType', Query::TYPE_EQUAL, ['functions']) ]); foreach ($deployments as $deployment) { - $deployment->setAttribute('deploy', false); + $deployment->setAttribute('activate', false); $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); } } @@ -546,7 +546,7 @@ App::post('/v1/functions/:functionId/deployments') 'path' => $path, 'size' => $size, 'search' => implode(' ', [$deploymentId, $entrypoint]), - 'deploy' => ($deploy === 'true'), + 'activate' => ((bool) $activate === true), ])); // Enqueue a message to start the build diff --git a/app/workers/builds.php b/app/workers/builds.php index 888eaa5675..41f3228d94 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -148,7 +148,7 @@ class BuildsV1 extends Worker $build = $dbForProject->updateDocument('builds', $buildId, $build); /** Set auto deploy */ - if ($deployment->getAttribute('deploy') === true) { + if ($deployment->getAttribute('activate') === true) { $function->setAttribute('deployment', $deployment->getId()); $function = $dbForProject->updateDocument('functions', $functionId, $function); } diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index 28a01ad6c0..b2d38f144a 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -52,9 +52,9 @@ class Deployment extends Model 'default' => '', 'example' => '5e5ea5c16897e', ]) - ->addRule('deploy', [ + ->addRule('activate', [ 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether the deployment should be automatically deployed.', + 'description' => 'Whether the deployment should be automatically activated.', 'default' => false, 'example' => true, ]) From 07f0eb46b288bff57c33c9296a006af49da099b1 Mon Sep 17 00:00:00 2001 From: Matej Baco Date: Tue, 15 Feb 2022 10:16:32 +0100 Subject: [PATCH 43/61] Improved deployment and build path structure --- app/controllers/api/functions.php | 25 ++++++++++++++----- .../Functions/FunctionsCustomClientTest.php | 20 +++++---------- .../Functions/FunctionsCustomServerTest.php | 19 +++++--------- .../Realtime/RealtimeCustomClientTest.php | 6 ++--- .../Webhooks/WebhooksCustomServerTest.php | 6 ++--- 5 files changed, 35 insertions(+), 41 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b42e495fb7..27fe597e1a 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -345,7 +345,7 @@ App::put('/v1/functions/:functionId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::patch('/v1/functions/:functionId/deployment') +App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Update Function Deployment') ->label('scope', 'functions.write') @@ -358,17 +358,17 @@ App::patch('/v1/functions/:functionId/deployment') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_FUNCTION) ->param('functionId', '', new UID(), 'Function ID.') - ->param('deployment', '', new UID(), 'Deployment ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') ->inject('dbForProject') ->inject('project') - ->action(function ($functionId, $deployment, $response, $dbForProject, $project) { + ->action(function ($functionId, $deploymentId, $response, $dbForProject, $project) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForProject */ /** @var Utopia\Database\Document $project */ $function = $dbForProject->getDocument('functions', $functionId); - $deployment = $dbForProject->getDocument('deployments', $deployment); + $deployment = $dbForProject->getDocument('deployments', $deploymentId); $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); if ($function->isEmpty()) { @@ -984,7 +984,7 @@ App::get('/v1/functions/:functionId/executions/:executionId') $response->dynamic($execution, Response::MODEL_EXECUTION); }); -App::post('/v1/builds/:buildId') +App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->groups(['api', 'functions']) ->desc('Retry Build') ->label('scope', 'functions.write') @@ -995,15 +995,28 @@ App::post('/v1/builds/:buildId') ->label('sdk.description', '/docs/references/functions/retry-build.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) + ->param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') ->param('buildId', '', new UID(), 'Build unique ID.') ->inject('response') ->inject('dbForProject') ->inject('project') - ->action(function ($buildId, $response, $dbForProject, $project) { + ->action(function ($functionId, $deploymentId, $buildId, $response, $dbForProject, $project) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForProject */ /** @var Utopia\Database\Document $project */ + $function = $dbForProject->getDocument('functions', $functionId); + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($function->isEmpty()) { + throw new Exception('Function not found', 404); + } + + if ($deployment->isEmpty()) { + throw new Exception('Deployment not found', 404); + } + $build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $buildId)); if ($build->isEmpty()) { diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 3255caf2a9..26e41542ff 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -89,15 +89,11 @@ class FunctionsCustomClientTest extends Scope // Wait for deployment to be built. sleep(10); - $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$function['body']['$id'].'/deployment', [ + $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$function['body']['$id'].'/deployments/'.$deploymentId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'deployment' => $deploymentId, - ]); - - // var_dump($function); + ], []); $this->assertEquals(200, $function['headers']['status-code']); @@ -176,13 +172,11 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(201, $deployment['headers']['status-code']); - $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployment', [ + $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployments/'.$deploymentId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apikey, - ], [ - 'deployment' => $deploymentId, - ]); + ], []); $this->assertEquals(200, $function['headers']['status-code']); @@ -360,13 +354,11 @@ class FunctionsCustomClientTest extends Scope // Wait for deployment to be built. sleep(10); - $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployment', [ + $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployments/'.$deploymentId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apikey, - ], [ - 'deployment' => $deploymentId, - ]); + ], []); $this->assertEquals(200, $function['headers']['status-code']); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 9bb2a296e9..deb1b5b205 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -310,12 +310,10 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$data['functionId'].'/deployment', array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$data['functionId'].'/deployments/'.$data['deploymentId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'deployment' => $data['deploymentId'], - ]); + ], $this->getHeaders()), []); $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); @@ -687,13 +685,10 @@ class FunctionsCustomServerTest extends Scope // Allow build step to run sleep(10); - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployment', array_merge([ + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployments/'.$deploymentId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'functionId' => $functionId, - 'deployment' => $deploymentId, - ]); + ], $this->getHeaders()), []); $this->assertEquals(200, $deployment['headers']['status-code']); @@ -779,12 +774,10 @@ class FunctionsCustomServerTest extends Scope // Allow build step to run sleep(10); - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployment', array_merge([ + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployments/'.$deploymentId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'deployment' => $deploymentId, - ]); + ], $this->getHeaders()), []); $this->assertEquals(200, $deployment['headers']['status-code']); diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 8f8e467ea3..a12272398f 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1002,13 +1002,11 @@ class RealtimeCustomClientTest extends Scope // Wait for deployment to be built. sleep(5); - $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployment', array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/deployments/'.$deploymentId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'deployment' => $deploymentId, - ]); + ]), []); $this->assertEquals($response['headers']['status-code'], 200); $this->assertNotEmpty($response['body']['$id']); diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index 94db8cc15a..fe3efa1c3c 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -434,12 +434,10 @@ class WebhooksCustomServerTest extends Scope /** * Test for SUCCESS */ - $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$data['functionId'].'/deployment', array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$data['functionId'].'/deployments/'.$data['deploymentId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'deployment' => $data['deploymentId'], - ]); + ], $this->getHeaders()), []); $this->assertEquals($response['headers']['status-code'], 200); $this->assertNotEmpty($response['body']['$id']); From aba9dc323db2e58419d132caa6d26df04579d402 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 19:42:45 +0400 Subject: [PATCH 44/61] feat: some cleanup --- app/executor.php | 2 +- src/Executor/Executor.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/executor.php b/app/executor.php index 3485539d8b..389fe150fa 100644 --- a/app/executor.php +++ b/app/executor.php @@ -769,7 +769,7 @@ $http->on('start', function ($http) { }); -$http->on("shutdown", function() { +$http->on('shutdown', function() { global $orchestrationPool; Console::info('Cleaning up containers before shutdown...'); diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 2e91cd6aee..1504319cbe 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -4,7 +4,6 @@ namespace Executor; use Exception; use Utopia\App; -use Utopia\Storage\Storage; class Executor { From 1d6d43baf8981ea824d8b50114063305b38dc0f4 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 19:59:26 +0400 Subject: [PATCH 45/61] feat: fix issues with delete endpoint --- app/executor.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/executor.php b/app/executor.php index 389fe150fa..8b9d3abc24 100644 --- a/app/executor.php +++ b/app/executor.php @@ -33,16 +33,15 @@ use Utopia\Validator\Text; // Remove orphans on startup - done // Remove multiple request attempt to the runtime logic in executor - done // Remove builds param from delete endpoint - done +// Shutdown callback isn't working as expected - done - -// Shutdown callback isn't working as expected // Incorporate Matej's changes in the build stage ( moving of the tar file will be performed by the runtime and not the build stage ) // Fix error handling and logging // Fix delete endpoint // Add size validators for the runtime IDs // Decide on logic for build and runtime containers names ( runtime-ID and build-ID) -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); /** Constants */ const MAINTENANCE_INTERVAL = 1200; // 20 minutes @@ -769,7 +768,7 @@ $http->on('start', function ($http) { }); -$http->on('shutdown', function() { +$http->on('beforeShutdown', function() { global $orchestrationPool; Console::info('Cleaning up containers before shutdown...'); @@ -777,7 +776,6 @@ $http->on('shutdown', function() { $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=function']); $orchestrationPool->put($orchestration); - // This does not seem to be working since this is not a coroutine scope . foreach ($functionsToRemove as $container) { go(function () use ($orchestrationPool, $container) { try { From 123c47fb9a2fc26819703347c5f923e2412f0b17 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 21:39:03 +0400 Subject: [PATCH 46/61] feat: handle build and execution errors --- app/controllers/api/functions.php | 50 ++++---- app/executor.php | 202 +++++++++++++----------------- app/workers/functions.php | 60 ++++----- src/Executor/Executor.php | 4 +- 4 files changed, 142 insertions(+), 174 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b42e495fb7..a54c4e49a3 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -864,32 +864,40 @@ App::post('/v1/functions/:functionId/executions') /** Execute function */ $executor = new Executor(); - $responseExecute = $executor->createExecution( - projectId: $project->getId(), - functionId: $function->getId(), - deploymentId: $deployment->getId(), - path: $build->getAttribute('outputPath', ''), - vars: $vars, - data: $data, - entrypoint: $deployment->getAttribute('entrypoint', ''), - runtime: $function->getAttribute('runtime', ''), - timeout: $function->getAttribute('timeout', 0), - baseImage: $runtime['image'] - ); + $executionResponse = []; + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + functionId: $function->getId(), + deploymentId: $deployment->getId(), + path: $build->getAttribute('outputPath', ''), + vars: $vars, + data: $data, + entrypoint: $deployment->getAttribute('entrypoint', ''), + runtime: $function->getAttribute('runtime', ''), + timeout: $function->getAttribute('timeout', 0), + baseImage: $runtime['image'] + ); + + /** Update execution status */ + $execution->setAttribute('status', $executionResponse['status']); + $execution->setAttribute('statusCode', $executionResponse['statusCode']); + $execution->setAttribute('stdout', $executionResponse['stdout']); + $execution->setAttribute('stderr', $executionResponse['stderr']); + $execution->setAttribute('time', $executionResponse['time']); + } catch (\Throwable $th) { + $execution->setAttribute('status', 'failed'); + $execution->setAttribute('statusCode', $th->getCode()); + $execution->setAttribute('stderr', $th->getMessage()); + Console::error($th->getMessage()); + } - /** Update execution status */ - $execution->setAttribute('status', $responseExecute['status']); - $execution->setAttribute('statusCode', $responseExecute['statusCode']); - $execution->setAttribute('stdout', $responseExecute['stdout']); - $execution->setAttribute('stderr', $responseExecute['stderr']); - $execution->setAttribute('time', $responseExecute['time']); Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution)); - - $responseExecute['response'] = ($responseExecute['status'] !== 'completed') ? $responseExecute['stderr'] : $responseExecute['stdout']; + $executionResponse['response'] = ($executionResponse['status'] !== 'completed') ? $executionResponse['stderr'] : $executionResponse['stdout']; $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic(new Document($responseExecute), Response::MODEL_SYNC_EXECUTION); + ->dynamic(new Document($executionResponse), Response::MODEL_SYNC_EXECUTION); }); App::get('/v1/functions/:functionId/executions') diff --git a/app/executor.php b/app/executor.php index 8b9d3abc24..643e2ae2c7 100644 --- a/app/executor.php +++ b/app/executor.php @@ -35,9 +35,10 @@ use Utopia\Validator\Text; // Remove builds param from delete endpoint - done // Shutdown callback isn't working as expected - done -// Incorporate Matej's changes in the build stage ( moving of the tar file will be performed by the runtime and not the build stage ) -// Fix error handling and logging +// Fix error handling +// Fix logging // Fix delete endpoint +// Incorporate Matej's changes in the build stage ( moving of the tar file will be performed by the runtime and not the build stage ) // Add size validators for the runtime IDs // Decide on logic for build and runtime containers names ( runtime-ID and build-ID) @@ -126,10 +127,14 @@ App::post('/v1/runtimes') ->action(function (string $runtimeId, string $source, string $destination, array $vars, string $runtime, string $baseImage, $orchestrationPool, $activeRuntimes, Response $response) { // TODO: Check if runtime already exists.. - $orchestration = $orchestrationPool->get(); + $container = 'runtime-' . $runtimeId; + + if ($activeRuntimes->exists($container)) { + throw new Exception('Runtime already exists.', 409); + } $build = []; - $id = ''; + $buildId = ''; $buildStdout = ''; $buildStderr = ''; $buildStart = \time(); @@ -150,7 +155,7 @@ App::post('/v1/runtimes') $device = new Local($destination); $buffer = $device->read($source); if(!$device->write($tmpSource, $buffer)) { - throw new Exception('Failed to write source code to temporary location.', 500); + throw new Exception('Failed to copy source code to temporary directory', 500); }; /** @@ -165,6 +170,7 @@ App::post('/v1/runtimes') /** * Create container */ + $orchestration = $orchestrationPool->get(); $container = 'build-' . $runtimeId; $vars = array_map(fn ($v) => strval($v), $vars); $orchestration @@ -172,7 +178,7 @@ App::post('/v1/runtimes') ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); - $id = $orchestration->run( + $buildId = $orchestration->run( image: $baseImage, name: $container, vars: $vars, @@ -195,7 +201,7 @@ App::post('/v1/runtimes') ] ); - if (empty($id)) { + if (empty($buildId)) { throw new Exception('Failed to create build container', 500); } @@ -217,7 +223,7 @@ App::post('/v1/runtimes') ); if (!$untarSuccess) { - throw new Exception('Failed to extract tar: ' . $untarStderr); + throw new Exception('Failed to extract tarfile ' . $untarStderr, 500); } /** @@ -232,7 +238,7 @@ App::post('/v1/runtimes') ); if (!$buildSuccess) { - throw new Exception('Failed to build dependencies: ' . $buildStderr); + throw new Exception('Failed to build dependencies: ' . $buildStderr, 500); } /** @@ -251,12 +257,12 @@ App::post('/v1/runtimes') ); if (!$compressSuccess) { - throw new Exception('Failed to compress built code: ' . $compressStderr); + throw new Exception('Failed to compress built code: ' . $compressStderr, 500); } // 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.'); + throw new Exception('Something went wrong during the build process'); } /** @@ -288,116 +294,90 @@ App::post('/v1/runtimes') 'endTime' => $buildEnd, 'duration' => $buildEnd - $buildStart, ]; - - Console::success('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds'); + Console::success('Build Stage completed in ' . ($buildEnd - $buildStart) . ' seconds'); + } catch (Throwable $th) { - $buildEnd = \time(); - $buildStderr = $th->getMessage(); - $build = [ - 'status' => 'failed', - // Increase logs limit - 'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)), - 'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)), - 'startTime' => $buildStart, - 'endTime' => $buildEnd, - 'duration' => $buildEnd - $buildStart, - ]; - Console::error('Build failed: ' . $th->getMessage()); + throw new Exception($th->getMessage(), 500); } finally { - if (!empty($id)) { - $orchestration->remove($id, true); + if (!empty($buildId)) { + $orchestration->remove($buildId, true); } - } - - if ( $build['status'] !== 'ready') { - return $response - ->setStatusCode(500) - ->json($build); + $orchestrationPool->put($orchestration); } /** Create runtime server */ try { - $container = 'runtime-' . $runtimeId; - if ($activeRuntimes->exists($container) && !(\substr($activeRuntimes->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online - // If container is online then stop and remove it - try { - $orchestration->remove($container, true); - } catch (Exception $e) { - throw new Exception('Failed to remove container: ' . $e->getMessage()); - } - $activeRuntimes->del($container); - } - + $orchestration = $orchestrationPool->get(); /** * Copy code files from source to a temporary location on the executor */ $buffer = $device->read($outputPath); if(!$device->write($tmpBuild, $buffer)) { - throw new Exception('Failed to write built code to temporary location.', 500); + throw new Exception('Failed to copy built code to temporary location.', 500); }; /** * Launch Runtime */ + $container = 'runtime-' . $runtimeId; $secret = \bin2hex(\random_bytes(16)); $vars = \array_merge($vars, [ 'INTERNAL_RUNTIME_KEY' => $secret ]); - if (!$activeRuntimes->exists($container)) { - $executionStart = \microtime(true); - $executionTime = \time(); - - $vars = array_map(fn ($v) => strval($v), $vars); - - $orchestration - ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1')) - ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256')) - ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256')); - - $id = $orchestration->run( - image: $baseImage, - name: $container, - vars: $vars, - labels: [ - 'openruntimes-id' => $runtimeId, - 'openruntimes-type' => 'function', - 'openruntimes-created' => strval($executionTime), - 'openruntimes-runtime' => $runtime - ], - hostname: $container, - mountFolder: \dirname($tmpBuild), - ); - - if (empty($id)) { - throw new Exception('Failed to create container'); - } - - // Add to network - $orchestration->networkConnect($container, App::getEnv('_APP_EXECUTOR_RUNTIME_NETWORK', 'appwrite_runtimes')); - - $executionEnd = \microtime(true); - - $activeRuntimes->set($container, [ - 'id' => $id, - 'name' => $container, - 'created' => $executionTime, - 'updated' => $executionTime, - 'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', - 'key' => $secret, - ]); + $executionStart = \microtime(true); + $executionTime = \time(); + + $vars = array_map(fn ($v) => strval($v), $vars); + + $orchestration + ->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1')) + ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256')) + ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256')); + + $id = $orchestration->run( + image: $baseImage, + name: $container, + vars: $vars, + labels: [ + 'openruntimes-id' => $runtimeId, + 'openruntimes-type' => 'function', + 'openruntimes-created' => strval($executionTime), + 'openruntimes-runtime' => $runtime + ], + hostname: $container, + mountFolder: \dirname($tmpBuild), + ); + + if (empty($id)) { + throw new Exception('Failed to create runtime', 500); } + + $orchestration->networkConnect($container, App::getEnv('_APP_EXECUTOR_RUNTIME_NETWORK', 'openruntimes')); + + $executionEnd = \microtime(true); + + $activeRuntimes->set($container, [ + 'id' => $id, + 'name' => $container, + 'created' => $executionTime, + 'updated' => $executionTime, + 'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', + 'key' => $secret, + ]); + Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); } catch (\Throwable $th) { Console::error('Runtime Server Creation Failed: '. $th->getMessage()); + throw new Exception($th->getMessage(), 500); + } finally { + $orchestrationPool->put($orchestration); } - $orchestrationPool->put($orchestration); - $response - ->setStatusCode(201) + ->setStatusCode(Response::STATUS_CODE_CREATED) ->json($build); }); @@ -413,7 +393,6 @@ App::get('/v1/runtimes') $runtimes[] = $runtime; } - // TODO: Response model for runtimes and runtimes list $response ->setStatusCode(200) ->json($runtimes); @@ -421,7 +400,6 @@ App::get('/v1/runtimes') App::get('/v1/runtimes/:runtimeId') ->desc("Get a runtime by its ID") - // Change the text validators to UID ->param('runtimeId', '', new Text(128), 'Runtime unique ID.') ->inject('activeRuntimes') ->inject('response') @@ -447,18 +425,19 @@ App::delete('/v1/runtimes/:runtimeId') ->action(function (string $runtimeId, $orchestrationPool, $activeRuntimes, Response $response) { $container = 'runtime-' . $runtimeId; + + if(!$activeRuntimes->exists($container)) { + throw new Exception('Runtime not found', 404); + } + Console::info('Deleting runtime: ' . $container); + try { $orchestration = $orchestrationPool->get(); - $status = $orchestration->remove($container, true); - if ($status) { - Console::success('Removed runtime container: ' . $runtimeId); - } else { - Console::error('Failed to remove runtime container: ' . $runtimeId); - } + $orchestration->remove($container, true); $activeRuntimes->del($container); + Console::success('Removed runtime container: ' . $container); } catch (\Throwable $th) { - Console::error('Failed to remove runtime container: ' . $runtimeId); } finally { $orchestrationPool->put($orchestration); } @@ -621,40 +600,27 @@ App::setResource('orchestrationPool', fn() => $orchestrationPool); App::setResource('activeRuntimes', fn() => $activeRuntimes); /** Set callbacks */ -App::error(function ($error, $utopia, $request, $response) { - /** @var Exception $error */ - /** @var Utopia\App $utopia */ - /** @var Utopia\Swoole\Request $request */ - - $route = $utopia->match($request); +App::error(function ($error, $response) { + // $route = $utopia->match($request); // logError($error, "httpError", $route); - $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); - - $code = $error->getCode(); - $message = $error->getMessage(); - - $output = ((App::isDevelopment())) ? [ + $output = [ 'message' => $error->getMessage(), 'code' => $error->getCode(), 'file' => $error->getFile(), 'line' => $error->getLine(), 'trace' => $error->getTrace(), - 'version' => $version, - ] : [ - 'message' => $message, - 'code' => $code, - 'version' => $version, + 'version' => App::getEnv('_APP_VERSION', 'UNKNOWN'), ]; $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Expires', '0') ->addHeader('Pragma', 'no-cache') - ->setStatusCode(500); + ->setStatusCode($error->getCode()); $response->json($output); -}, ['error', 'utopia', 'request', 'response']); +}, ['error', 'response']); App::init(function ($request, $response) { $secretKey = $request->getHeader('x-appwrite-executor-key', ''); diff --git a/app/workers/functions.php b/app/workers/functions.php index dc5b661f04..c98dae63f0 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -14,28 +14,12 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\Orchestration\Adapter\DockerAPI; -use Utopia\Orchestration\Orchestration; require_once __DIR__.'/../init.php'; -Runtime::enableCoroutine(0); - Console::title('Functions V1 Worker'); Console::success(APP_NAME . ' functions worker v1 has started'); -$runtimes = Config::getParam('runtimes'); - -$dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null); -$dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null); -$dockerEmail = App::getEnv('DOCKERHUB_PULL_EMAIL', null); -$orchestration = new Orchestration(new DockerAPI($dockerUser, $dockerPass, $dockerEmail)); - -$warmupEnd = \microtime(true); -$warmupTime = $warmupEnd - $warmupStart; - -Console::success('Finished warmup in ' . $warmupTime . ' seconds'); - class FunctionsV1 extends Worker { /** @@ -303,25 +287,33 @@ class FunctionsV1 extends Worker $vars = \array_merge($function->getAttribute('vars', []), $vars); /** Execute function */ - $executionResponse = $this->executor->createExecution( - projectId: $projectId, - functionId: $functionId, - deploymentId: $deploymentId, - path: $build->getAttribute('outputPath', ''), - vars: $vars, - entrypoint: $deployment->getAttribute('entrypoint', ''), - data: $vars['APPWRITE_FUNCTION_DATA'], - runtime: $function->getAttribute('runtime', ''), - timeout: $function->getAttribute('timeout', 0), - baseImage: $runtime['image'] - ); + try { + $executionResponse = $this->executor->createExecution( + projectId: $projectId, + functionId: $functionId, + deploymentId: $deploymentId, + path: $build->getAttribute('outputPath', ''), + vars: $vars, + entrypoint: $deployment->getAttribute('entrypoint', ''), + data: $vars['APPWRITE_FUNCTION_DATA'], + runtime: $function->getAttribute('runtime', ''), + timeout: $function->getAttribute('timeout', 0), + baseImage: $runtime['image'] + ); + + /** Update execution status */ + $execution->setAttribute('status', $executionResponse['status']); + $execution->setAttribute('statusCode', $executionResponse['statusCode']); + $execution->setAttribute('stdout', $executionResponse['stdout']); + $execution->setAttribute('stderr', $executionResponse['stderr']); + $execution->setAttribute('time', $executionResponse['time']); + } catch (\Throwable $th) { + $execution->setAttribute('status', 'failed'); + $execution->setAttribute('statusCode', $th->getCode()); + $execution->setAttribute('stderr', $th->getMessage()); + Console::error($th->getMessage()); + } - /** Update execution status */ - $execution->setAttribute('status', $executionResponse['status']); - $execution->setAttribute('statusCode', $executionResponse['statusCode']); - $execution->setAttribute('stdout', $executionResponse['stdout']); - $execution->setAttribute('stderr', $executionResponse['stderr']); - $execution->setAttribute('time', $executionResponse['time']); $execution = Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution)); /** Trigger Webhook */ diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 1504319cbe..29d5e1acf7 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -54,6 +54,7 @@ class Executor 'baseImage' => $baseImage ]; + var_dump($params); $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); $status = $response['headers']['status-code']; @@ -115,7 +116,8 @@ class Executor 'timeout' => $timeout, 'baseImage' => $baseImage, ]; - + + var_dump($params); $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); $status = $response['headers']['status-code']; From 2674186f3b008fb50514933e042ffabfed77a5d6 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 21:51:52 +0400 Subject: [PATCH 47/61] feat: remove var_dump() --- src/Executor/Executor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 29d5e1acf7..f6c988c8f0 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -116,8 +116,7 @@ class Executor 'timeout' => $timeout, 'baseImage' => $baseImage, ]; - - var_dump($params); + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); $status = $response['headers']['status-code']; From cd4436fe6d6d17a92a5c51b0bdd1056db1a95e98 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 15 Feb 2022 21:53:17 +0400 Subject: [PATCH 48/61] feat: remove var_dump() --- app/executor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/executor.php b/app/executor.php index 643e2ae2c7..fcb4f4ee66 100644 --- a/app/executor.php +++ b/app/executor.php @@ -34,8 +34,8 @@ use Utopia\Validator\Text; // Remove multiple request attempt to the runtime logic in executor - done // Remove builds param from delete endpoint - done // Shutdown callback isn't working as expected - done +// Fix error handling - done -// Fix error handling // Fix logging // Fix delete endpoint // Incorporate Matej's changes in the build stage ( moving of the tar file will be performed by the runtime and not the build stage ) @@ -437,7 +437,6 @@ App::delete('/v1/runtimes/:runtimeId') $orchestration->remove($container, true); $activeRuntimes->del($container); Console::success('Removed runtime container: ' . $container); - } catch (\Throwable $th) { } finally { $orchestrationPool->put($orchestration); } From 20b1e3c04e32f6ce86a2b764b94414d0566bcc3a Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Wed, 16 Feb 2022 00:21:19 +0400 Subject: [PATCH 49/61] feat: remove var_dump --- src/Executor/Executor.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index f6c988c8f0..1504319cbe 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -54,7 +54,6 @@ class Executor 'baseImage' => $baseImage ]; - var_dump($params); $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30); $status = $response['headers']['status-code']; From 896e63bdd8cdc329460db2eca146ad2d60fcea29 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Wed, 16 Feb 2022 03:39:09 +0400 Subject: [PATCH 50/61] feat: fix some bugs --- app/executor.php | 27 +++++++++++++++++++++++---- docker-compose.yml | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/executor.php b/app/executor.php index fcb4f4ee66..a6cfe88259 100644 --- a/app/executor.php +++ b/app/executor.php @@ -405,11 +405,13 @@ App::get('/v1/runtimes/:runtimeId') ->inject('response') ->action(function ($runtimeId, $activeRuntimes, Response $response) { - if(!$activeRuntimes->exists($runtimeId)) { + $container = 'runtime-' . $runtimeId; + + if(!$activeRuntimes->exists($container)) { throw new Exception('Runtime not found', 404); } - $runtime = $activeRuntimes->get($runtimeId); + $runtime = $activeRuntimes->get($container); $response ->setStatusCode(200) @@ -603,20 +605,37 @@ App::error(function ($error, $response) { // $route = $utopia->match($request); // logError($error, "httpError", $route); + 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 + case 409: // Error allowed publicly + case 412: // Error allowed publicly + 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 + } + $output = [ 'message' => $error->getMessage(), 'code' => $error->getCode(), 'file' => $error->getFile(), 'line' => $error->getLine(), 'trace' => $error->getTrace(), - 'version' => App::getEnv('_APP_VERSION', 'UNKNOWN'), + 'version' => App::getEnv('OPENRUNTIMES_VERSION', 'UNKNOWN'), ]; $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Expires', '0') ->addHeader('Pragma', 'no-cache') - ->setStatusCode($error->getCode()); + ->setStatusCode($code); $response->json($output); }, ['error', 'response']); diff --git a/docker-compose.yml b/docker-compose.yml index 9cf45f3024..fbd06318a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -395,7 +395,7 @@ services: appwrite: runtimes: ports: - - 9509:80 + - 9519:80 volumes: - /var/run/docker.sock:/var/run/docker.sock - appwrite-functions:/storage/functions:rw From ee8a1e10ff6a89d3c9cfe93cb80d81086c9d82a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 16 Feb 2022 09:17:00 +0000 Subject: [PATCH 51/61] Review changes --- app/views/console/functions/function.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/console/functions/function.phtml b/app/views/console/functions/function.phtml index 929adaedf0..15e0ce6071 100644 --- a/app/views/console/functions/function.phtml +++ b/app/views/console/functions/function.phtml @@ -685,7 +685,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
(Max file size allowed: )
- +