From da8b4bb9f9aca3dc4d58bd70eea0cc35055af8bb Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 13 Sep 2021 11:50:45 +0100 Subject: [PATCH] Add build step and more function cleanup --- app/config/collections.php | 18 + app/controllers/api/functions.php | 72 ++- app/executor.php | 487 +++++++++++++++--- app/workers/functions.php | 21 - composer.json | 1 - composer.lock | 5 +- docker-compose.yml | 4 +- .../Functions/FunctionsCustomClientTest.php | 2 +- .../Functions/FunctionsCustomServerTest.php | 3 + tests/resources/functions/php-fn.tar.gz | Bin 358 -> 25009 bytes tests/resources/functions/php-fn/index.php | 1 + .../functions/packages/php-fn/code.tar.gz | Bin 0 -> 104 bytes 12 files changed, 508 insertions(+), 106 deletions(-) create mode 100644 tests/resources/functions/tests/resources/functions/packages/php-fn/code.tar.gz diff --git a/app/config/collections.php b/app/config/collections.php index 4c6f81b154..2e17cee5d1 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1622,6 +1622,24 @@ $collections = [ 'default' => '', 'required' => false, 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Build Status', + 'key' => 'status', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Build Path', + 'key' => 'builtPath', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, ] ], ], diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 271cfb6370..d1a3d497df 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -403,15 +403,47 @@ App::delete('/v1/functions/:functionId') ->label('sdk.response.model', Response::MODEL_NONE) ->param('functionId', '', new UID(), 'Function unique ID.') ->inject('response') + ->inject('project') ->inject('projectDB') ->inject('deletes') - ->action(function ($functionId, $response, $projectDB, $deletes) { + ->action(function ($functionId, $response, $project, $projectDB, $deletes) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $deletes */ $function = $projectDB->getDocument($functionId); + // Request executor to delete tag containers + $ch = \curl_init(); + \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/cleanup/function"); + \curl_setopt($ch, CURLOPT_POST, true); + \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'functionId' => $functionId + ])); + \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: '.$project->getId(), + ]); + + $executorResponse = \curl_exec($ch); + + $error = \curl_error($ch); + + if (!empty($error)) { + throw new Exception('Curl error: ' . $error, 500); + } + + // Check status code + $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (200 !== $statusCode) { + throw new Exception('Executor error: ' . $executorResponse, $statusCode); + } + + \curl_close($ch); + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { throw new Exception('Function not found', 404); } @@ -506,7 +538,9 @@ App::post('/v1/functions/:functionId/tags') 'dateCreated' => time(), 'entrypoint' => $entrypoint, 'path' => $path, - 'size' => $size + 'size' => $size, + 'status' => 'pending', + 'builtPath' => '' ]); if (false === $tag) { @@ -620,9 +654,10 @@ App::delete('/v1/functions/:functionId/tags/:tagId') ->param('functionId', '', new UID(), 'Function unique ID.') ->param('tagId', '', new UID(), 'Tag unique ID.') ->inject('response') + ->inject('project') ->inject('projectDB') ->inject('usage') - ->action(function ($functionId, $tagId, $response, $projectDB, $usage) { + ->action(function ($functionId, $tagId, $response, $project, $projectDB, $usage) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $usage */ @@ -643,6 +678,37 @@ App::delete('/v1/functions/:functionId/tags/:tagId') throw new Exception('Tag not found', 404); } + // Request executor to delete tag containers + $ch = \curl_init(); + \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/cleanup/tag"); + \curl_setopt($ch, CURLOPT_POST, true); + \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'tagId' => $tagId + ])); + \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: '.$project->getId(), + ]); + + $executorResponse = \curl_exec($ch); + + $error = \curl_error($ch); + + if (!empty($error)) { + throw new Exception('Curl error: ' . $error, 500); + } + + // Check status code + $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (200 !== $statusCode) { + throw new Exception('Executor error: ' . $executorResponse, $statusCode); + } + + \curl_close($ch); + $device = Storage::getDevice('functions'); if ($device->delete($tag->getAttribute('path', ''))) { diff --git a/app/executor.php b/app/executor.php index 594a7c23c7..0e26d40e75 100644 --- a/app/executor.php +++ b/app/executor.php @@ -26,6 +26,12 @@ use Utopia\Validator\ArrayList; use Utopia\Validator\JSON; use Utopia\Validator\Text; use Cron\CronExpression; +use Utopia\Storage\Device\Local; +use Utopia\Storage\Storage; +use Utopia\Storage\Validator\FileExt; +use Utopia\Storage\Validator\FileSize; +use Utopia\Storage\Validator\Upload; +use Swoole\Coroutine as Co; require_once __DIR__ . '/workers.php'; @@ -39,12 +45,12 @@ $runtimes = Config::getParam('runtimes'); Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_CURL); // Warmup: make sure images are ready to run fast 🚀 -Co\run(function() use ($runtimes, $orchestration) { - foreach($runtimes as $runtime) { - go(function() use ($runtime, $orchestration) { - Console::info('Warming up '.$runtime['name'].' '.$runtime['version'].' environment...'); - - $response = $orchestration->pull($runtime['image']); +Co\run(function () use ($runtimes, $orchestration) { + foreach ($runtimes as $runtime) { + go(function () use ($runtime, $orchestration) { + 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']}!"); @@ -70,7 +76,7 @@ foreach ($response as $value) { $executionEnd = \microtime(true); -Console::info(count($activeFunctions).' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); +Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); App::post('/v1/execute') // Define Route ->inject('request') @@ -87,19 +93,127 @@ App::post('/v1/execute') // Define Route ->inject('response') ->action( function ($trigger, $projectId, $executionId, $functionId, $event, $eventData, $data, $webhooks, $userId, $jwt, $request, $response) { + global $register; + + $db = $register->get('dbPool')->get(); + $cache = $register->get('redisPool')->get(); + + // Create new Database Instance + $database = new Database(); + $database->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); + $database->setNamespace('app_' . $projectId); + $database->setMocks(Config::getParam('collections', [])); + try { - $data = execute($trigger, $projectId, $executionId, $functionId, $event, $eventData, $data, $webhooks, $userId, $jwt); - return $response->json($data); + $data = execute($trigger, $projectId, $executionId, $functionId, $database, $event, $eventData, $data, $webhooks, $userId, $jwt); + $response->json($data); } catch (Exception $e) { - return $response + $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Expires', '0') ->addHeader('Pragma', 'no-cache') ->json(['error' => $e->getMessage()]); + } finally { + $register->get('dbPool')->put($db); + $register->get('redisPool')->put($cache); } } ); + +// Cleanup Endpoints used internally by appwrite when a function or tag gets deleted to also clean up their containers +App::post('/v1/cleanup/function') + ->param('functionId', '', new UID()) + ->inject('response') + ->inject('projectDB') + ->inject('projectID') + ->action(function ($functionId, $response, $projectDB, $projectID) { + /** @var string $functionId */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var string $projectID */ + + global $orchestration; + + try { + Authorization::disable(); + $function = $projectDB->getDocument($functionId); + Authorization::reset(); + + if (\is_null($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + Authorization::disable(); + $results = $projectDB->getCollection([ + 'limit' => 999, + 'offset' => 0, + 'orderType' => 'ASC', + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_TAGS, + 'functionId='.$functionId, + ], + ]); + Authorization::reset(); + + // If amount is 0 then we simply return true + if (count($results) === 0) { + return $response->json(['success' => true]); + } + + // Delete the containers of all tags + foreach ($results as $tag) { + try { + $orchestration->remove('appwrite-function-'.$tag['$id'], true); + Console::info('Removed container for tag ' . $tag['$id']); + } catch (Exception $e) { + // Do nothing, we don't care that much if it fails + } + } + + return $response->json(['success' => true]); + } catch (Exception $e) { + Console::error($e->getMessage()); + return $response->json(['error' => $e->getMessage()]); + } + }); + +App::post('/v1/cleanup/tag') + ->param('tagId', '', new UID(), 'Tag unique ID.') + ->inject('response') + ->inject('projectDB') + ->inject('projectID') + ->action(function ($tagId, $response, $projectDB, $projectID) { + /** @var string $tagId */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var string $projectID */ + + global $orchestration; + + try { + Authorization::disable(); + $tag = $projectDB->getDocument($tagId); + Authorization::reset(); + + if (\is_null($tag->getId()) || Database::SYSTEM_COLLECTION_TAGS != $tag->getCollection()) { + throw new Exception('Tag not found', 404); + } + + try { + $orchestration->remove('appwrite-function-'.$tag['$id'], true); + Console::info('Removed container for tag ' . $tag['$id']); + } catch (Exception $e) { + // Do nothing, we don't care that much if it fails + } + } catch (Exception $e) { + Console::error($e->getMessage()); + return $response->json(['error' => $e->getMessage()]); + } + + return $response->json(['success' => true]); + }); + App::post('/v1/tag') ->param('functionId', '', new UID(), 'Function unique ID.') ->param('tagId', '', new UID(), 'Tag unique ID.') @@ -132,8 +246,14 @@ App::post('/v1/tag') ])); Authorization::reset(); - // Deploy Runtime Server - createRuntimeServer($functionId, $projectID, $tag); + // Build Code + go(function() use ($projectDB, $projectID, $function, $tagId, $functionId) { + // Build Code + $tag = runBuildStage($tagId, $function, $projectID, $projectDB); + + // Deploy Runtime Server + createRuntimeServer($functionId, $projectID, $tag, $projectDB); + }); if ($next) { // Init first schedule ResqueScheduler::enqueueAt($next, 'v1-functions', 'FunctionsV1', [ @@ -165,27 +285,210 @@ App::get('/v1/healthz') } ); -function createRuntimeServer(string $functionId, string $projectId, Document $tag) { +function runBuildStage(string $tagID, Document $function, string $projectID, Database $database): Document +{ + global $runtimes; + global $orchestration; + + // Update Tag Status + Authorization::disable(); + $tag = $database->getDocument($tagID); + $tag = $database->updateDocument(array_merge($tag->getArrayCopy(), [ + 'status' => 'building' + ])); + Authorization::reset(); + + // Check if runtime is active + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) + ? $runtimes[$function->getAttribute('runtime', '')] + : null; + + if ($tag->getAttribute('functionId') !== $function->getId()) { + throw new Exception('Tag not found', 404); + } + + if (\is_null($runtime)) { + throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); + } + + // Grab Tag Files + $tagPath = $tag->getAttribute('path', ''); + $tagPathTarget = '/tmp/project-' . $projectID . '/' . $tag->getId() . '/code.tar.gz'; + $tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME); + $container = 'build-stage-' . $tag->getId(); + + if (!\is_readable($tagPath)) { + throw new Exception('Code is not readable: ' . $tag->getAttribute('path', '')); + } + + if (!\file_exists($tagPathTargetDir)) { + if (!\mkdir($tagPathTargetDir, 0755, true)) { + throw new Exception('Can\'t create directory ' . $tagPathTargetDir); + } + } + + if (!\file_exists($tagPathTarget)) { + if (!\copy($tagPath, $tagPathTarget)) { + throw new Exception('Can\'t create temporary code file ' . $tagPathTarget); + } + } + + $vars = \array_merge($function->getAttribute('vars', []), [ + 'APPWRITE_FUNCTION_ID' => $function->getId(), + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + 'APPWRITE_FUNCTION_TAG' => $tag->getId(), + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], + 'APPWRITE_FUNCTION_PROJECT_ID' => $projectID, + ]); + + $buildStart = \microtime(true); + $buildTime = \time(); + + $orchestration->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1')); + $orchestration->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256')); + $orchestration->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256')); + + foreach ($vars as &$value) { + $value = strval($value); + } + + $id = $orchestration->run( + image: $runtime['base'], + name: $container, + vars: $vars, + workdir: '/usr/code', + labels: [ + 'appwrite-type' => 'function', + 'appwrite-created' => strval($buildTime), + 'appwrite-runtime' => $function->getAttribute('runtime', ''), + ], + command: [ + 'tail', + '-f', + '/dev/null' + ], + hostname: $container, + mountFolder: $tagPathTargetDir, + volumes: [ + '/tmp/project-' . $projectID . '/' . $tag->getId() . '/builtCode'. ':/usr/builtCode:rw' + ] + ); + + $untarStdout = ''; + $untarStderr = ''; + + $untarSuccess = $orchestration->execute( + name: $container, + command: [ + 'sh', + '-c', + 'mkdir /usr/code -p && cp /tmp/code.tar.gz /usr/code.tar.gz && cd /usr && tar -zxf /usr/code.tar.gz -C /usr/code && rm /usr/code.tar.gz' + ], + stdout: $untarStdout, + stderr: $untarStderr, + timeout: 60 + ); + + if (!$untarSuccess) { + throw new Exception('Failed to extract tar: ' . $untarStderr); + } + + // Build Code / Install Dependencies + $buildStdout = ''; + $buildStderr = ''; + + $buildSuccess = $orchestration->execute( + name: $container, + command: $runtime['buildCommand'], + stdout: $buildStdout, + stderr: $buildStderr, + timeout: 60 + ); + + if (!$buildSuccess) { + throw new Exception('Failed to build dependencies: ' . $buildStderr); + } + + // Repackage Code and Save. + $compressStdout = ''; + $compressStderr = ''; + + $builtCodePath = '/tmp/project-' . $projectID . '/' . $tag->getId() . '/builtCode/code.tar.gz'; + + $compressSuccess = $orchestration->execute( + name: $container, + command: [ + 'sh', + '-c', + 'tar -czvf /usr/builtCode/code.tar.gz /usr/code' + ], + stdout: $compressStdout, + stderr: $compressStderr, + timeout: 60 + ); + + if (!$compressSuccess) { + 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.'); + } + + // Upload new code + $device = Storage::getDevice('functions'); + + $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), 0755, true)) { + throw new Exception('Can\'t create directory: ' . \dirname($path)); + } + } + + if (!\rename($builtCodePath, $path)) { + throw new Exception('Failed moving file', 500); + } + + // Update tag with built code attribute + Authorization::disable(); + $tag = $database->updateDocument(array_merge($tag->getArrayCopy(), [ + 'builtPath' => $path, + 'status' => 'ready' + ])); + Authorization::enable(); + + $buildEnd = \microtime(true); + + Console::info('Tag Built in ' . ($buildEnd - $buildStart) . ' seconds'); + + return $tag; +} + +function createRuntimeServer(string $functionId, string $projectId, Document $tag, Database $database) +{ global $register; global $orchestration; global $runtimes; global $activeFunctions; - $db = $register->get('db'); - $cache = $register->get('cache'); - - // Create new Database Instance - $database = new Database(); - $database->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $database->setNamespace('app_' . $projectId); - $database->setMocks(Config::getParam('collections', [])); - // Grab Tag Document Authorization::disable(); $function = $database->getDocument($functionId); - $tag = $database->getDocument($function->getAttribute('tag', '')); Authorization::reset(); + // Check if function isn't already created + $functions = $orchestration->list(['label' => 'appwrite-type=function', 'name' => 'appwrite-function-' . $tag->getId()]); + + if (\count($functions) > 0) { + return; + } + // Check if runtime is active $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] @@ -211,7 +514,7 @@ function createRuntimeServer(string $functionId, string $projectId, Document $ta $container = 'appwrite-function-' . $tag->getId(); - if (isset($activeFunctions[$container]) && !(\substr($activeFunctions[$container]->getStatus(), 0, 2) === 'Up')) { // Remove conatiner if not online + if (isset($activeFunctions[$container]) && !(\substr($activeFunctions[$container]->getStatus(), 0, 2) === 'Up')) { // Remove container if not online // If container is online then stop and remove it try { $orchestration->remove($container); @@ -222,8 +525,14 @@ function createRuntimeServer(string $functionId, string $projectId, Document $ta unset($activeFunctions[$container]); } + // Check if tag is built yet. + if ($tag->getAttribute('status') !== 'ready') { + throw new Exception('Tag is not built yet', 500); + } + // Grab Tag Files - $tagPath = $tag->getAttribute('path', ''); + $tagPath = $tag->getAttribute('builtPath', ''); + $tagPathTarget = '/tmp/project-' . $projectId . '/' . $tag->getId() . '/code.tar.gz'; $tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME); $container = 'appwrite-function-' . $tag->getId(); @@ -318,22 +627,13 @@ function createRuntimeServer(string $functionId, string $projectId, Document $ta } }; -function execute(string $trigger, string $projectId, string $executionId, string $functionId, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): array +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 { + Console::info('Executing function: ' . $functionId); + global $activeFunctions; global $runtimes; - global $register; - - $db = $register->get('db'); - $cache = $register->get('cache'); - - // Create new Database Instance - $database = new Database(); - $database->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $database->setNamespace('app_' . $projectId); - $database->setMocks(Config::getParam('collections', [])); - // Grab Tag Document Authorization::disable(); $function = $database->getDocument($functionId); @@ -396,10 +696,41 @@ function execute(string $trigger, string $projectId, string $executionId, string $container = 'appwrite-function-' . $tag->getId(); - if (!isset($activeFunctions[$container])) { // Create contianer if not ready - createRuntimeServer($functionId, $projectId, $tag); - } else { - Console::info('Container is ready to run'); + // Check if code is built + + try { + if ($tag->getAttribute('status') !== 'ready') { + runBuildStage($tag->getId(), $function, $projectId, $database); + sleep(1); + } + } catch (Exception $e) { + Console::error('Something went wrong building the code. ' . $e->getMessage()); + $execution = $database->updateDocument(array_merge($execution->getArrayCopy(), [ + 'tagId' => $tag->getId(), + 'status' => 'failed', + 'exitCode' => 1, + 'stderr' => \utf8_encode(\mb_substr($e->getMessage(), -4000)), // log last 4000 chars output + 'time' => 0 + ])); + } + + try { + if (!isset($activeFunctions[$container])) { // Create contianer if not ready + createRuntimeServer($functionId, $projectId, $tag, $database); + } else if ($activeFunctions[$container]->getStatus() === 'Down') { + sleep(1); + } else { + Console::info('Container is ready to run'); + } + } catch (Exception $e) { + Console::error('Something went wrong building the runtime server. ' . $e->getMessage()); + $execution = $database->updateDocument(array_merge($execution->getArrayCopy(), [ + 'tagId' => $tag->getId(), + 'status' => 'failed', + 'exitCode' => 1, + 'stderr' => \utf8_encode(\mb_substr($e->getMessage(), -4000)), // log last 4000 chars output + 'time' => 0 + ])); } $stdout = ''; @@ -419,7 +750,7 @@ function execute(string $trigger, string $projectId, string $executionId, string do { $attempts++; $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, "http://".$container.":3000/"); + \curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/"); \curl_setopt($ch, CURLOPT_POST, true); \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'path' => '/usr/code', @@ -432,11 +763,11 @@ function execute(string $trigger, string $projectId, string $executionId, string \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_CONNECTTIMEOUT, 10); - + $executorResponse = \curl_exec($ch); $error = \curl_error($ch); - + $errNo = \curl_errno($ch); \curl_close($ch); @@ -456,7 +787,7 @@ function execute(string $trigger, string $projectId, string $executionId, string if ($errNo == CURLE_OPERATION_TIMEDOUT) { $exitCode = 124; } - + if ($errNo !== 0 && $errNo != CURLE_COULDNT_CONNECT && $errNo != CURLE_OPERATION_TIMEDOUT) { throw new Exception('Curl error: ' . $error, 500); } @@ -487,8 +818,8 @@ function execute(string $trigger, string $projectId, string $executionId, string 'tagId' => $tag->getId(), 'status' => $functionStatus, 'exitCode' => $exitCode, - 'stdout' => \mb_substr($stdout, -4000), // log last 4000 chars output - 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output + 'stdout' => \utf8_encode(\mb_substr($stdout, -4000)), // log last 4000 chars output + 'stderr' => \utf8_encode(\mb_substr($stderr, -4000)), // log last 4000 chars output 'time' => $executionTime ])); @@ -578,12 +909,14 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $projectId = $request->getHeader('x-appwrite-project', ''); - App::setResource('projectDB', function($db, $cache) use ($projectId) { + Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId)); + + App::setResource('projectDB', function ($db, $cache) use ($projectId) { $projectDB = new Database(); $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $projectDB->setNamespace('app_'.$projectId); + $projectDB->setNamespace('app_' . $projectId); $projectDB->setMocks(Config::getParam('collections', [])); - + return $projectDB; }, ['db', 'cache']); @@ -592,31 +925,31 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - + if ($error instanceof PDOException) { throw $error; } - + $route = $utopia->match($request); - - Console::error('[Error] Timestamp: '.date('c', time())); - - if($route) { - Console::error('[Error] Method: '.$route->getMethod()); + + Console::error('[Error] Timestamp: ' . date('c', time())); + + if ($route) { + Console::error('[Error] Method: ' . $route->getMethod()); } - - Console::error('[Error] Type: '.get_class($error)); - Console::error('[Error] Message: '.$error->getMessage()); - Console::error('[Error] File: '.$error->getFile()); - Console::error('[Error] Line: '.$error->getLine()); - + + Console::error('[Error] Type: ' . get_class($error)); + Console::error('[Error] Message: ' . $error->getMessage()); + Console::error('[Error] File: ' . $error->getFile()); + Console::error('[Error] Line: ' . $error->getLine()); + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); $code = $error->getCode(); $message = $error->getMessage(); - + //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised - + $output = ((App::isDevelopment())) ? [ 'message' => $error->getMessage(), 'code' => $error->getCode(), @@ -629,19 +962,20 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo 'code' => $code, 'version' => $version, ]; - + $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Expires', '0') ->addHeader('Pragma', 'no-cache') - ->setStatusCode($code) - ; - - $response->dynamic(new Document($output), - $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR); + ->setStatusCode($code); + + $response->dynamic( + new Document($output), + $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR + ); }, ['error', 'utopia', 'request', 'response']); - App::setResource('projectID', function() use ($projectId) { + App::setResource('projectID', function () use ($projectId) { return $projectId; }); @@ -655,7 +989,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $http->start(); -function handleShutdown() { +function handleShutdown() +{ Console::info('Cleaning up containers before shutdown...'); // Remove all containers. @@ -666,9 +1001,9 @@ function handleShutdown() { foreach ($functionsToRemove as $container) { try { $orchestration->remove($container->getId(), true); - Console::info('Removed container '.$container->getName()); + Console::info('Removed container ' . $container->getName()); } catch (Exception $e) { - Console::error('Failed to remove container: '.$container->getName()); + Console::error('Failed to remove container: ' . $container->getName()); } } -} \ No newline at end of file +} diff --git a/app/workers/functions.php b/app/workers/functions.php index ada5498b10..1db663ea74 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -34,27 +34,6 @@ $dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null); $dockerEmail = App::getEnv('DOCKERHUB_PULL_EMAIL', null); $orchestration = new Orchestration(new DockerAPI($dockerUser, $dockerPass, $dockerEmail)); -/** - * Warmup Docker Images - */ -$warmupStart = \microtime(true); - -Co\run(function () use ($runtimes, $orchestration) { // Warmup: make sure images are ready to run fast 🚀 - foreach ($runtimes as $runtime) { - go(function () use ($runtime, $orchestration) { - 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::error("Failed to Warmup {$runtime['name']} {$runtime['version']}!"); - } - }); - } -}); - $warmupEnd = \microtime(true); $warmupTime = $warmupEnd - $warmupStart; diff --git a/composer.json b/composer.json index 228dbdb876..06858795df 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,6 @@ "utopia-php/orchestration": "dev-exp1", "resque/php-resque": "1.3.6", "matomo/device-detector": "4.2.3", - "utopia-php/orchestration": "0.2.1", "dragonmantank/cron-expression": "3.1.0", "influxdb/influxdb-php": "1.15.2", "phpmailer/phpmailer": "6.5.0", diff --git a/composer.lock b/composer.lock index 2cf7fa61e2..e8b27e7f3d 100644 --- a/composer.lock +++ b/composer.lock @@ -119,7 +119,7 @@ "source": { "type": "git", "url": "https://github.com/PineappleIOnic/php-runtimes.git", - "reference": "d633a896c4e5c20fd166f4b5461a869b0db2616e" + "reference": "147e76f4a72bc925d9d1613494417d583bd5de34" }, "require": { "php": ">=8.0", @@ -155,7 +155,7 @@ "php", "runtimes" ], - "time": "2021-09-02T09:13:23+00:00" + "time": "2021-09-06T14:46:02+00:00" }, { "name": "chillerlan/php-qrcode", @@ -4920,6 +4920,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { diff --git a/docker-compose.yml b/docker-compose.yml index 23ff2ad44b..ac7eeea38d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -368,7 +368,7 @@ services: entrypoint: - php - -e - - app/executor.php + - /usr/src/code/app/executor.php - -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php stop_signal: SIGINT ports: @@ -376,7 +376,7 @@ services: build: context: . args: - - DEBUG=false + - DEBUG=true - TESTING=true - VERSION=dev networks: diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 0725f4e207..41400ddb92 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -186,7 +186,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(201, $execution['headers']['status-code']); $executionId = $execution['body']['$id'] ?? ''; - + sleep(10); $executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions/'.$executionId, [ diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index fdb3b1d8aa..a3b680dabf 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -492,6 +492,9 @@ class FunctionsCustomServerTest extends Scope ]); $this->assertEquals(200, $tag['headers']['status-code']); + + // Allow build step to run + sleep(20); $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([ 'content-type' => 'application/json', diff --git a/tests/resources/functions/php-fn.tar.gz b/tests/resources/functions/php-fn.tar.gz index 525d5182e8bca7f0b6d465948a15a7868eb5bae5..da781b5302341e03671cec911d03ca501d435a4b 100644 GIT binary patch literal 25009 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 literal 358 zcmV-s0h#_EiwFRN(lB8F1MQSsYlAQx#eMcuRRUMFFHjO%clqi&_gn8InhmRHy)A zCoK*r?Qf0a@-%x)$X3qfl;@~xMN${2jX*LGrEsZ0b1|#sv z1ERWPqQ9mQ9&Qi4g$BlQxp6EP`FEau>sk-CZ_NkbS_c(wq6heW`@3&9rh#l@f!I(7 zCmrS*CL60TowOqm=>Y z&kfgTz&!ftV0I(}%v&Lc`D?GtbmaYp@fHhXiQ0{3{MYQi|0=zD_3DDY0jXG2U;qpN E0K2QEi2wiq diff --git a/tests/resources/functions/php-fn/index.php b/tests/resources/functions/php-fn/index.php index fce4d1c38d..ddbb0cddcd 100644 --- a/tests/resources/functions/php-fn/index.php +++ b/tests/resources/functions/php-fn/index.php @@ -13,5 +13,6 @@ return function ($request, $response) { 'APPWRITE_FUNCTION_DATA' => $request->env['APPWRITE_FUNCTION_DATA'], 'APPWRITE_FUNCTION_USER_ID' => $request->env['APPWRITE_FUNCTION_USER_ID'], 'APPWRITE_FUNCTION_JWT' => $request->env['APPWRITE_FUNCTION_JWT'], + 'APPWRITE_FUNCTION_PROJECT_ID' => $request->env['APPWRITE_FUNCTION_PROJECT_ID'], ]); }; diff --git a/tests/resources/functions/tests/resources/functions/packages/php-fn/code.tar.gz b/tests/resources/functions/tests/resources/functions/packages/php-fn/code.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a58d1389354d511a7c08c752398c3f2bf1751ba5 GIT binary patch literal 104 zcmb2|=3oE;Cg!*2Hu5$*2(TQ`eyY8}^5y*X$3+zqU&#A-G$?nZO|s22-*MeK&-_hQ zw)?wtm3JbIkIyPSbAR3UHG#MOu6JE?|EckGq0c6|;k)Z?!3Kf|n+)dIUn{E_G#D5F Dr@krW literal 0 HcmV?d00001