diff --git a/.env b/.env index ea7f385c9..7e9a5858f 100644 --- a/.env +++ b/.env @@ -68,14 +68,6 @@ _APP_STORAGE_PREVIEW_LIMIT=20000000 _APP_FUNCTIONS_SIZE_LIMIT=30000000 _APP_FUNCTIONS_TIMEOUT=900 _APP_FUNCTIONS_BUILD_TIMEOUT=900 -_APP_FUNCTIONS_CONTAINERS=10 -_APP_FUNCTIONS_CPUS=0 -_APP_FUNCTIONS_MEMORY=0 -_APP_FUNCTIONS_MEMORY_SWAP=0 -_APP_FUNCTIONS_INACTIVE_THRESHOLD=60 -OPEN_RUNTIMES_NETWORK=appwrite_runtimes -_APP_EXECUTOR_SECRET=your-secret-key -_APP_EXECUTOR_HOST=http://appwrite-executor/v1 _APP_MAINTENANCE_INTERVAL=86400 _APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 @@ -86,6 +78,18 @@ _APP_USAGE_DATABASE_INTERVAL=15 _APP_USAGE_STATS=enabled _APP_LOGGING_PROVIDER= _APP_LOGGING_CONFIG= -DOCKERHUB_PULL_USERNAME= -DOCKERHUB_PULL_PASSWORD= -DOCKERHUB_PULL_EMAIL= \ No newline at end of file +_APP_EXECUTOR_SECRET=your-secret-key +_APP_EXECUTOR_HOST=http://exc1/v1 +_APP_FUNCTIONS_RUNTIMES= + +OPR_EXECUTOR_CONNECTION_STORAGE=file://localhost +OPR_EXECUTOR_INACTIVE_TRESHOLD=600 +OPR_EXECUTOR_NETWORK=openruntimes-runtimes +OPR_EXECUTOR_DOCKER_HUB_USERNAME= +OPR_EXECUTOR_DOCKER_HUB_PASSWORD= + +# OPR_PROXY_ALGORITHM=round-robin +# OPR_PROXY_EXECUTORS=exc1 +# OPR_PROXY_HEALTHCHECK=enabled +# OPR_PROXY_HEALTHCHECK_INTERVAL=5000 +# OPR_PROXY_EXECUTOR_SECRET=your-secret-key \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3910bc61a..6604d260e 100755 --- a/Dockerfile +++ b/Dockerfile @@ -246,13 +246,8 @@ ENV _APP_SERVER=swoole \ _APP_SMS_FROM= \ _APP_FUNCTIONS_SIZE_LIMIT=30000000 \ _APP_FUNCTIONS_TIMEOUT=900 \ - _APP_FUNCTIONS_CONTAINERS=10 \ - _APP_FUNCTIONS_CPUS=1 \ - _APP_FUNCTIONS_MEMORY=128 \ - _APP_FUNCTIONS_MEMORY_SWAP=128 \ _APP_EXECUTOR_SECRET=a-random-secret \ _APP_EXECUTOR_HOST=http://appwrite-executor/v1 \ - _APP_EXECUTOR_RUNTIME_NETWORK=appwrite_runtimes \ _APP_SETUP=self-hosted \ _APP_VERSION=$VERSION \ _APP_USAGE_STATS=enabled \ @@ -349,7 +344,6 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/install && \ chmod +x /usr/local/bin/migrate && \ chmod +x /usr/local/bin/realtime && \ - chmod +x /usr/local/bin/executor && \ chmod +x /usr/local/bin/schedule && \ chmod +x /usr/local/bin/sdks && \ chmod +x /usr/local/bin/specs && \ diff --git a/app/config/variables.php b/app/config/variables.php index f52983119..dbe1a9748 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -701,7 +701,7 @@ return [ ], [ 'name' => '_APP_FUNCTIONS_CONTAINERS', - 'description' => 'The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10.', 'introduction' => '0.7.0', 'default' => '10', 'required' => false, @@ -710,7 +710,7 @@ return [ ], [ 'name' => '_APP_FUNCTIONS_CPUS', - 'description' => 'The maximum number of CPU core a single cloud function is allowed to use. Please note that setting a value higher than available cores will result in a function error, which might result in an error. The default value is empty. When it\'s empty, CPU limit will be disabled.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The maximum number of CPU core a single cloud function is allowed to use. Please note that setting a value higher than available cores will result in a function error, which might result in an error. The default value is empty. When it\'s empty, CPU limit will be disabled.', 'introduction' => '0.7.0', 'default' => '0', 'required' => false, @@ -719,7 +719,7 @@ return [ ], [ 'name' => '_APP_FUNCTIONS_MEMORY', - 'description' => 'The maximum amount of memory a single cloud function is allowed to use in megabytes. The default value is empty. When it\'s empty, memory limit will be disabled.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The maximum amount of memory a single cloud function is allowed to use in megabytes. The default value is empty. When it\'s empty, memory limit will be disabled.', 'introduction' => '0.7.0', 'default' => '0', 'required' => false, @@ -728,7 +728,7 @@ return [ ], [ 'name' => '_APP_FUNCTIONS_MEMORY_SWAP', - 'description' => 'The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it\'s empty, swap memory limit will be disabled.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it\'s empty, swap memory limit will be disabled.', 'introduction' => '0.7.0', 'default' => '0', 'required' => false, @@ -782,7 +782,7 @@ return [ ], [ 'name' => '_APP_FUNCTIONS_INACTIVE_THRESHOLD', - 'description' => 'The minimum time a function can be inactive before it\'s container is shutdown and put to sleep. The default value is 60 seconds', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The minimum time a function can be inactive before it\'s container is shutdown and put to sleep. The default value is 60 seconds', 'introduction' => '0.13.0', 'default' => '60', 'required' => false, @@ -791,7 +791,7 @@ return [ ], [ 'name' => 'DOCKERHUB_PULL_USERNAME', - 'description' => 'The username for hub.docker.com. This variable is used to pull images from hub.docker.com.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The username for hub.docker.com. This variable is used to pull images from hub.docker.com.', 'introduction' => '0.10.0', 'default' => '', 'required' => false, @@ -800,7 +800,7 @@ return [ ], [ 'name' => 'DOCKERHUB_PULL_PASSWORD', - 'description' => 'The password for hub.docker.com. This variable is used to pull images from hub.docker.com.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The password for hub.docker.com. This variable is used to pull images from hub.docker.com.', 'introduction' => '0.10.0', 'default' => '', 'required' => false, @@ -809,7 +809,7 @@ return [ ], [ 'name' => 'DOCKERHUB_PULL_EMAIL', - 'description' => 'The email for hub.docker.com. This variable is used to pull images from hub.docker.com.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The email for hub.docker.com. This variable is used to pull images from hub.docker.com.', 'introduction' => '0.10.0', 'default' => '', 'required' => false, @@ -818,7 +818,7 @@ return [ ], [ 'name' => 'OPEN_RUNTIMES_NETWORK', - 'description' => 'The docker network used for communication between the executor and runtimes. Change this if you have altered the default network names.', + 'description' => 'Deprecated since 1.1.0. Use Open Runtimes Executor instead. The docker network used for communication between the executor and runtimes. Change this if you have altered the default network names.', 'introduction' => '0.13.0', 'default' => 'appwrite_runtimes', 'required' => false, diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 188c5e453..9b91df5d8 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1148,13 +1148,12 @@ App::post('/v1/functions/:functionId/executions') $executionResponse = $executor->createExecution( projectId: $project->getId(), deploymentId: $deployment->getId(), - path: $build->getAttribute('outputPath', ''), - vars: $vars, - data: $data, - entrypoint: $deployment->getAttribute('entrypoint', ''), - runtime: $function->getAttribute('runtime', ''), + payload: $data, + variables: $vars, timeout: $function->getAttribute('timeout', 0), - baseImage: $runtime['image'] + image: $runtime['image'], + source: $build->getAttribute('outputPath', ''), + entrypoint: $deployment->getAttribute('entrypoint', ''), ); /** Update execution status */ diff --git a/app/executor.php b/app/executor.php deleted file mode 100644 index fba8c4c41..000000000 --- a/app/executor.php +++ /dev/null @@ -1,802 +0,0 @@ -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, 128); -$activeRuntimes->column('status', Swoole\Table::TYPE_STRING, 128); -$activeRuntimes->column('key', Swoole\Table::TYPE_STRING, 256); -$activeRuntimes->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); - - -/** - * Create logger instance - */ -$providerName = App::getEnv('_APP_LOGGING_PROVIDER', ''); -$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', ''); -$logger = null; - -if (!empty($providerName) && !empty($providerConfig) && Logger::hasProvider($providerName)) { - $classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName); - $adapter = new $classname($providerConfig); - $logger = new Logger($adapter); -} - -function logError(Throwable $error, string $action, Utopia\Route $route = null) -{ - global $logger; - - if ($logger) { - $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); - - $log = new Log(); - $log->setNamespace("executor"); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - - if ($route) { - $log->addTag('method', $route->getMethod()); - $log->addTag('url', $route->getPath()); - } - - $log->addTag('code', $error->getCode()); - $log->addTag('verboseType', get_class($error)); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('detailedTrace', $error->getTrace()); - - $log->setAction($action); - - $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - $responseCode = $logger->addLog($log); - Console::info('Executor log pushed with status code: ' . $responseCode); - } - - 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()); -} - -function getStorageDevice($root): Device -{ - switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) { - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - case Storage::DEVICE_S3: - $s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); - $s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', ''); - $s3Region = App::getEnv('_APP_STORAGE_S3_REGION', ''); - $s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', ''); - $s3Acl = 'private'; - return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); - case Storage::DEVICE_DO_SPACES: - $doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); - $doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); - $doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); - $doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); - $doSpacesAcl = 'private'; - return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); - case Storage::DEVICE_BACKBLAZE: - $backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); - $backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); - $backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); - $backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); - $backblazeAcl = 'private'; - return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); - case Storage::DEVICE_LINODE: - $linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); - $linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', ''); - $linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', ''); - $linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); - $linodeAcl = 'private'; - return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); - case Storage::DEVICE_WASABI: - $wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); - $wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', ''); - $wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', ''); - $wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); - $wasabiAcl = 'private'; - return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); - } -} - -App::post('/v1/runtimes') - ->desc("Create a new runtime server") - ->param('runtimeId', '', new Text(64), 'Unique runtime ID.') - ->param('source', '', new Text(0), 'Path to source files.') - ->param('destination', '', new Text(0), 'Destination folder to store build files into.', true) - ->param('vars', [], new Assoc(), 'Environment Variables required for the build.') - ->param('commands', [], new ArrayList(new Text(1024), 100), 'Commands required to build the container. Maximum of 100 commands are allowed, each 1024 characters long.') - ->param('runtime', '', new Text(128), 'Runtime for the cloud function.') - ->param('baseImage', '', new Text(128), 'Base image name of the runtime.') - ->param('entrypoint', '', new Text(256), 'Entrypoint of the code file.', true) - ->param('remove', false, new Boolean(), 'Remove a runtime after execution.') - ->param('workdir', '', new Text(256), 'Working directory.', true) - ->inject('orchestrationPool') - ->inject('activeRuntimes') - ->inject('response') - ->action(function (string $runtimeId, string $source, string $destination, array $vars, array $commands, string $runtime, string $baseImage, string $entrypoint, bool $remove, string $workdir, $orchestrationPool, $activeRuntimes, Response $response) { - if ($activeRuntimes->exists($runtimeId)) { - if ($activeRuntimes->get($runtimeId)['status'] == 'pending') { - throw new \Exception('A runtime with the same ID is already being created. Attempt a execution soon.', 500); - } - - throw new Exception('Runtime already exists.', 409); - } - - $container = []; - $containerId = ''; - $stdout = ''; - $stderr = ''; - $startTime = DateTime::now(); - $startTimeUnix = (new \DateTime($startTime))->getTimestamp(); - $endTimeUnix = 0; - $orchestration = $orchestrationPool->get(); - - $secret = \bin2hex(\random_bytes(16)); - - if (!$remove) { - $activeRuntimes->set($runtimeId, [ - 'id' => $containerId, - 'name' => $runtimeId, - 'created' => $startTimeUnix, - 'updated' => $endTimeUnix, - 'status' => 'pending', - 'key' => $secret, - ]); - } - - try { - Console::info('Building container : ' . $runtimeId); - - /** - * Temporary file paths in the executor - */ - $tmpSource = "/tmp/$runtimeId/src/code.tar.gz"; - $tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz"; - - /** - * Copy code files from source to a temporary location on the executor - */ - $sourceDevice = getStorageDevice("/"); - $localDevice = new Local(); - $buffer = $sourceDevice->read($source); - if (!$localDevice->write($tmpSource, $buffer)) { - throw new Exception('Failed to copy source code to temporary directory', 500); - }; - - /** - * Create the mount folder - */ - if (!\file_exists(\dirname($tmpBuild))) { - if (!@\mkdir(\dirname($tmpBuild), 0755, true)) { - throw new Exception("Failed to create temporary directory", 500); - } - } - - /** - * Create container - */ - $vars = \array_merge($vars, [ - 'INTERNAL_RUNTIME_KEY' => $secret, - 'INTERNAL_RUNTIME_ENTRYPOINT' => $entrypoint, - ]); - $vars = array_map(fn ($v) => strval($v), $vars); - $orchestration - ->setCpus((int) App::getEnv('_APP_FUNCTIONS_CPUS', 0)) - ->setMemory((int) App::getEnv('_APP_FUNCTIONS_MEMORY', 0)) - ->setSwap((int) App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 0)); - - /** Keep the container alive if we have commands to be executed */ - $entrypoint = !empty($commands) ? [ - 'tail', - '-f', - '/dev/null' - ] : []; - - $containerId = $orchestration->run( - image: $baseImage, - name: $runtimeId, - hostname: $runtimeId, - vars: $vars, - command: $entrypoint, - labels: [ - 'openruntimes-id' => $runtimeId, - 'openruntimes-type' => 'runtime', - 'openruntimes-created' => strval($startTimeUnix), - 'openruntimes-runtime' => $runtime, - ], - workdir: $workdir, - volumes: [ - \dirname($tmpSource) . ':/tmp:rw', - \dirname($tmpBuild) . ':/usr/code:rw' - ] - ); - - if (empty($containerId)) { - throw new Exception('Failed to create build container', 500); - } - - $orchestration->networkConnect($runtimeId, App::getEnv('OPEN_RUNTIMES_NETWORK', 'appwrite_runtimes')); - - /** - * Execute any commands if they were provided - */ - if (!empty($commands)) { - $status = $orchestration->execute( - name: $runtimeId, - command: $commands, - stdout: $stdout, - stderr: $stderr, - timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900) - ); - - if (!$status) { - throw new Exception('Failed to build dependenices ' . $stderr, 500); - } - } - - /** - * Move built code to expected build directory - */ - if (!empty($destination)) { - // Check if the build was successful by checking if file exists - if (!\file_exists($tmpBuild)) { - throw new Exception('Something went wrong during the build process', 500); - } - - $destinationDevice = getStorageDevice($destination); - $outputPath = $destinationDevice->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - - $buffer = $localDevice->read($tmpBuild); - if (!$destinationDevice->write($outputPath, $buffer, $localDevice->getFileMimeType($tmpBuild))) { - throw new Exception('Failed to move built code to storage', 500); - }; - - $container['outputPath'] = $outputPath; - } - - if (empty($stdout)) { - $stdout = 'Build Successful!'; - } - - $endTime = DateTime::now(); - $endTimeUnix = (new \DateTime($endTime))->getTimestamp(); - $duration = $endTimeUnix - $startTimeUnix; - - $container = array_merge($container, [ - 'status' => 'ready', - 'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB - 'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB - 'startTime' => $startTime, - 'endTime' => $endTime, - 'duration' => $duration, - ]); - - - if (!$remove) { - $activeRuntimes->set($runtimeId, [ - 'id' => $containerId, - 'name' => $runtimeId, - 'created' => $startTimeUnix, - 'updated' => $endTimeUnix, - 'status' => 'Up ' . \round($duration, 2) . 's', - 'key' => $secret, - ]); - } - - Console::success('Build Stage completed in ' . ($duration) . ' seconds'); - } catch (Throwable $th) { - Console::error('Build failed: ' . $th->getMessage() . $stdout); - - throw new Exception($th->getMessage() . $stdout, 500); - } finally { - // Container cleanup - if ($remove) { - if (!empty($containerId)) { - // If container properly created - $orchestration->remove($containerId, true); - $activeRuntimes->del($runtimeId); - } else { - // If whole creation failed, but container might have been initialized - try { - // Try to remove with contaier name instead of ID - $orchestration->remove($runtimeId, true); - $activeRuntimes->del($runtimeId); - } catch (Throwable $th) { - // If fails, means initialization also failed. - // Contianer is not there, no need to remove - } - } - } - - // Release orchestration back to pool, we are done with it - $orchestrationPool->put($orchestration); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($container); - }); - - -App::get('/v1/runtimes') - ->desc("List currently active runtimes") - ->inject('activeRuntimes') - ->inject('response') - ->action(function ($activeRuntimes, Response $response) { - $runtimes = []; - - foreach ($activeRuntimes as $runtime) { - $runtimes[] = $runtime; - } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->json($runtimes); - }); - -App::get('/v1/runtimes/:runtimeId') - ->desc("Get a runtime by its ID") - ->param('runtimeId', '', new Text(64), 'Runtime unique ID.') - ->inject('activeRuntimes') - ->inject('response') - ->action(function ($runtimeId, $activeRuntimes, Response $response) { - - if (!$activeRuntimes->exists($runtimeId)) { - throw new Exception('Runtime not found', 404); - } - - $runtime = $activeRuntimes->get($runtimeId); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->json($runtime); - }); - -App::delete('/v1/runtimes/:runtimeId') - ->desc('Delete a runtime') - ->param('runtimeId', '', new Text(64), 'Runtime unique ID.', false) - ->inject('orchestrationPool') - ->inject('activeRuntimes') - ->inject('response') - ->action(function (string $runtimeId, $orchestrationPool, $activeRuntimes, Response $response) { - - if (!$activeRuntimes->exists($runtimeId)) { - throw new Exception('Runtime not found', 404); - } - - Console::info('Deleting runtime: ' . $runtimeId); - - try { - $orchestration = $orchestrationPool->get(); - $orchestration->remove($runtimeId, true); - $activeRuntimes->del($runtimeId); - Console::success('Removed runtime container: ' . $runtimeId); - } finally { - $orchestrationPool->put($orchestration); - } - - // Remove all the build containers with that same ID - // TODO:: Delete build containers - // foreach ($buildIds as $buildId) { - // try { - // Console::info('Deleting build container : ' . $buildId); - // $status = $orchestration->remove('build-' . $buildId, true); - // } catch (Throwable $th) { - // Console::error($th->getMessage()); - // } - // } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->send(); - }); - - -App::post('/v1/execution') - ->desc('Create an execution') - ->param('runtimeId', '', new Text(64), 'The runtimeID to execute.') - ->param('vars', [], new Assoc(), 'Environment variables required for the build.') - ->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true) - ->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.') - ->inject('activeRuntimes') - ->inject('response') - ->action( - function (string $runtimeId, array $vars, string $data, $timeout, $activeRuntimes, Response $response) { - if (!$activeRuntimes->exists($runtimeId)) { - throw new Exception('Runtime not found. Please create the runtime.', 404); - } - - for ($i = 0; $i < 5; $i++) { - if ($activeRuntimes->get($runtimeId)['status'] === 'pending') { - Console::info('Waiting for runtime to be ready...'); - sleep(1); - } else { - break; - } - - if ($i === 4) { - throw new Exception('Runtime failed to launch in allocated time.', 500); - } - } - - $runtime = $activeRuntimes->get($runtimeId); - $secret = $runtime['key']; - if (empty($secret)) { - throw new Exception('Runtime secret not found. Please re-create the runtime.', 500); - } - - Console::info('Executing Runtime: ' . $runtimeId); - - $execution = []; - $executionStart = \microtime(true); - $stdout = ''; - $stderr = ''; - $res = ''; - $statusCode = 0; - $errNo = -1; - $executorResponse = ''; - - $timeout ??= (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900); - - $ch = \curl_init(); - $body = \json_encode([ - 'variables' => $vars, - 'payload' => $data, - 'timeout' => $timeout - ]); - \curl_setopt($ch, CURLOPT_URL, "http://" . $runtimeId . ":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); - \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); - $executorResponse = json_decode($executorResponse, true); - - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - - $error = \curl_error($ch); - - $errNo = \curl_errno($ch); - - \curl_close($ch); - - switch (true) { - /** No Error. */ - case $errNo === 0: - break; - /** Runtime not ready for requests yet. 111 is the swoole error code for Connection Refused - see https://openswoole.com/docs/swoole-error-code */ - case $errNo === 111: - throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 406); - /** Any other CURL error */ - default: - throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500); - } - - switch (true) { - case $statusCode >= 500: - $stderr = ($executorResponse ?? [])['stderr'] ?? 'Internal Runtime error.'; - $stdout = ($executorResponse ?? [])['stdout'] ?? 'Internal Runtime error.'; - break; - case $statusCode >= 100: - $stdout = $executorResponse['stdout']; - $res = $executorResponse['response']; - if (is_array($res)) { - $res = json_encode($res, JSON_UNESCAPED_UNICODE); - } - break; - default: - $stderr = ($executorResponse ?? [])['stderr'] ?? 'Execution failed.'; - $stdout = ($executorResponse ?? [])['stdout'] ?? ''; - break; - } - - $executionEnd = \microtime(true); - $executionTime = ($executionEnd - $executionStart); - $functionStatus = ($statusCode >= 500) ? 'failed' : 'completed'; - - Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus); - - $execution = [ - 'status' => $functionStatus, - 'statusCode' => $statusCode, - 'response' => \mb_strcut($res, 0, 1000000), // Limit to 1MB - 'stdout' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB - 'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB - 'duration' => $executionTime, - ]; - - /** Update swoole table */ - $runtime['updated'] = \time(); - $activeRuntimes->set($runtimeId, $runtime); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->json($execution); - } - ); - -App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode - -$http = new Server("0.0.0.0", 80); - -/** Set Resources */ -App::setResource('orchestrationPool', fn() => $orchestrationPool); -App::setResource('activeRuntimes', fn() => $activeRuntimes); - -/** Set callbacks */ -App::error() - ->inject('utopia') - ->inject('error') - ->inject('request') - ->inject('response') - ->action(function (App $utopia, throwable $error, Request $request, Response $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 406: // Error allowed publicly - case 409: // Error allowed publicly - case 412: // Error allowed publicly - case 425: // 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') - ]; - - $response - ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') - ->addHeader('Expires', '0') - ->addHeader('Pragma', 'no-cache') - ->setStatusCode($code); - - $response->json($output); - }); - -App::init() - ->inject('request') - ->action(function (Request $request) { - $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); - } - }); - - -$http->on('start', function ($http) { - global $orchestrationPool; - global $activeRuntimes; - - /** - * Warmup: make sure images are ready to run fast 🚀 - */ - $runtimes = new Runtimes('v2'); - $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); - } - }); - } - - /** - * Remove residual runtimes - */ - Console::info('Removing orphan runtimes...'); - try { - $orchestration = $orchestrationPool->get(); - $orphans = $orchestration->list(['label' => 'openruntimes-type=runtime']); - } finally { - $orchestrationPool->put($orchestration); - } - - 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); - } - }); - } - - /** - * Register handlers for shutdown - */ - @Process::signal(SIGINT, function () use ($http) { - $http->shutdown(); - }); - - @Process::signal(SIGQUIT, function () use ($http) { - $http->shutdown(); - }); - - @Process::signal(SIGKILL, function () use ($http) { - $http->shutdown(); - }); - - @Process::signal(SIGTERM, function () use ($http) { - $http->shutdown(); - }); - - /** - * 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) { - $inactiveThreshold = \time() - App::getEnv('_APP_FUNCTIONS_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); - } - }); - } - } - }); -}); - - -$http->on('beforeShutdown', function () { - global $orchestrationPool; - Console::info('Cleaning up containers before shutdown...'); - - $orchestration = $orchestrationPool->get(); - $functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=runtime']); - $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('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { - $request = new Request($swooleRequest); - $response = new Response($swooleResponse); - $app = new App('UTC'); - - try { - $app->run($request, $response); - } catch (\Throwable $th) { - logError($th, "serverError"); - $swooleResponse->setStatusCode(500); - $output = [ - 'message' => 'Error: ' . $th->getMessage(), - 'code' => 500, - 'file' => $th->getFile(), - 'line' => $th->getLine(), - 'trace' => $th->getTrace() - ]; - $swooleResponse->end(\json_encode($output)); - } -}); - -$http->start(); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 1d7fdcd04..6bdbaf54d 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -397,7 +397,6 @@ services: depends_on: - redis - mariadb - - appwrite-executor environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 @@ -417,64 +416,6 @@ services: - DOCKERHUB_PULL_USERNAME - DOCKERHUB_PULL_PASSWORD - appwrite-executor: - image: /: - entrypoint: executor - <<: *x-logging - container_name: appwrite-executor - restart: unless-stopped - stop_signal: SIGINT - networks: - appwrite: - runtimes: - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - appwrite-functions:/storage/functions:rw - - appwrite-builds:/storage/builds:rw - - /tmp:/tmp:rw - depends_on: - - redis - - mariadb - - appwrite - environment: - - _APP_ENV - - _APP_VERSION - - _APP_FUNCTIONS_TIMEOUT - - _APP_FUNCTIONS_BUILD_TIMEOUT - - _APP_FUNCTIONS_CONTAINERS - - _APP_FUNCTIONS_RUNTIMES - - _APP_FUNCTIONS_CPUS - - _APP_FUNCTIONS_MEMORY - - _APP_FUNCTIONS_MEMORY_SWAP - - _APP_FUNCTIONS_INACTIVE_THRESHOLD - - _APP_EXECUTOR_SECRET - - OPEN_RUNTIMES_NETWORK - - _APP_LOGGING_PROVIDER - - _APP_LOGGING_CONFIG - - _APP_STORAGE_DEVICE - - _APP_STORAGE_S3_ACCESS_KEY - - _APP_STORAGE_S3_SECRET - - _APP_STORAGE_S3_REGION - - _APP_STORAGE_S3_BUCKET - - _APP_STORAGE_DO_SPACES_ACCESS_KEY - - _APP_STORAGE_DO_SPACES_SECRET - - _APP_STORAGE_DO_SPACES_REGION - - _APP_STORAGE_DO_SPACES_BUCKET - - _APP_STORAGE_BACKBLAZE_ACCESS_KEY - - _APP_STORAGE_BACKBLAZE_SECRET - - _APP_STORAGE_BACKBLAZE_REGION - - _APP_STORAGE_BACKBLAZE_BUCKET - - _APP_STORAGE_LINODE_ACCESS_KEY - - _APP_STORAGE_LINODE_SECRET - - _APP_STORAGE_LINODE_REGION - - _APP_STORAGE_LINODE_BUCKET - - _APP_STORAGE_WASABI_ACCESS_KEY - - _APP_STORAGE_WASABI_SECRET - - _APP_STORAGE_WASABI_REGION - - _APP_STORAGE_WASABI_BUCKET - - DOCKERHUB_PULL_USERNAME - - DOCKERHUB_PULL_PASSWORD - appwrite-worker-mails: image: /: entrypoint: worker-mails @@ -709,4 +650,3 @@ volumes: appwrite-builds: appwrite-influxdb: appwrite-config: - appwrite-executor: diff --git a/app/workers/builds.php b/app/workers/builds.php index 235ad0da7..06c3de396 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -150,20 +150,17 @@ class BuildsV1 extends Worker return $carry; }, []); - $baseImage = $runtime['image']; - try { $response = $this->executor->createRuntime( projectId: $project->getId(), deploymentId: $deployment->getId(), - entrypoint: $deployment->getAttribute('entrypoint'), source: $source, - destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}", - vars: $vars, - runtime: $key, - baseImage: $baseImage, - workdir: '/usr/code', + image: $runtime['image'], remove: true, + entrypoint: $deployment->getAttribute('entrypoint'), + workdir: '/usr/code', + destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}", + variables: $vars, commands: [ 'sh', '-c', 'tar -zxf /tmp/code.tar.gz -C /usr/code && \ @@ -171,13 +168,16 @@ class BuildsV1 extends Worker ] ); + $endTime = new \DateTime(); + $endTime->setTimestamp($response['endTimeUnix']); + /** Update the build document */ - $build->setAttribute('endTime', $response['endTime']); - $build->setAttribute('duration', $response['duration']); + $build->setAttribute('endTime', DateTime::format($endTime)); + $build->setAttribute('duration', \intval($response['duration'])); $build->setAttribute('status', $response['status']); $build->setAttribute('outputPath', $response['outputPath']); $build->setAttribute('stderr', $response['stderr']); - $build->setAttribute('stdout', $response['response']); + $build->setAttribute('stdout', $response['stdout']); Console::success("Build id: $buildId created"); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 9ef593dbb..0134fad66 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -470,16 +470,17 @@ class DeletesV1 extends Worker /** * Request executor to delete all deployment containers + * TODO: Re-enable. Disabled for now because of proxy. Container killed after inactivity automatically. */ - Console::info("Requesting executor to delete all deployment containers for function " . $functionId); - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - foreach ($deploymentIds as $deploymentId) { - try { - $executor->deleteRuntime($projectId, $deploymentId); - } catch (Throwable $th) { - Console::error($th->getMessage()); - } - } + // Console::info("Requesting executor to delete all deployment containers for function " . $functionId); + // $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + // foreach ($deploymentIds as $deploymentId) { + // try { + // $executor->deleteRuntime($projectId, $deploymentId); + // } catch (Throwable $th) { + // Console::error($th->getMessage()); + // } + // } } /** @@ -520,15 +521,16 @@ class DeletesV1 extends Worker }); /** - * Request executor to delete the deployment container + * Request executor to delete the deployment container. + * TODO: Re-enable. Disabled for now because of proxy. Container killed after inactivity automatically. */ - Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId); - try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - $executor->deleteRuntime($projectId, $deploymentId); - } catch (Throwable $th) { - Console::error($th->getMessage()); - } + // Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId); + // try { + // $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + // $executor->deleteRuntime($projectId, $deploymentId); + // } catch (Throwable $th) { + // Console::error($th->getMessage()); + // } } diff --git a/app/workers/functions.php b/app/workers/functions.php index 1ba4b6575..6d1b3f41f 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -289,13 +289,12 @@ class FunctionsV1 extends Worker $executionResponse = $this->executor->createExecution( projectId: $project->getId(), deploymentId: $deploymentId, - path: $build->getAttribute('outputPath', ''), - vars: $vars, - entrypoint: $deployment->getAttribute('entrypoint', ''), - data: $vars['APPWRITE_FUNCTION_DATA'] ?? '', - runtime: $function->getAttribute('runtime', ''), + payload: $vars['APPWRITE_FUNCTION_DATA'] ?? '', + variables: $vars, timeout: $function->getAttribute('timeout', 0), - baseImage: $runtime['image'] + image: $runtime['image'], + source: $build->getAttribute('outputPath', ''), + entrypoint: $deployment->getAttribute('entrypoint', ''), ); /** Update execution status */ diff --git a/bin/executor b/bin/executor deleted file mode 100644 index f08bd68e4..000000000 --- a/bin/executor +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -php -e /usr/src/code/app/executor.php -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/composer.lock b/composer.lock index 52153a52d..de2a363a8 100644 --- a/composer.lock +++ b/composer.lock @@ -115,15 +115,15 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.11.0", + "version": "0.11.1", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "547fc026e11c0946846a8ac690898f5bf53be101" + "reference": "9d74a477ba3333cbcfac565c46fcf19606b7b603" }, "require": { "php": ">=8.0", - "utopia-php/system": "0.4.*" + "utopia-php/system": "0.6.*" }, "require-dev": { "phpunit/phpunit": "^9.3", @@ -154,7 +154,7 @@ "php", "runtimes" ], - "time": "2022-08-15T14:03:36+00:00" + "time": "2022-11-07T16:45:52+00:00" }, { "name": "chillerlan/php-qrcode", @@ -803,6 +803,72 @@ }, "time": "2020-12-26T17:45:17+00:00" }, + { + "name": "laravel/pint", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "1d276e4c803397a26cc337df908f55c2a4e90d86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/1d276e4c803397a26cc337df908f55c2a4e90d86", + "reference": "1d276e4c803397a26cc337df908f55c2a4e90d86", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.11.0", + "illuminate/view": "^9.27", + "laravel-zero/framework": "^9.1.3", + "mockery/mockery": "^1.5.0", + "nunomaduro/larastan": "^2.2", + "nunomaduro/termwind": "^1.14.0", + "pestphp/pest": "^1.22.1" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2022-09-13T15:07:15+00:00" + }, { "name": "matomo/device-detector", "version": "6.0.0", @@ -2411,23 +2477,25 @@ }, { "name": "utopia-php/system", - "version": "0.4.0", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "67c92c66ce8f0cc925a00bca89f7a188bf9183c0" + "reference": "289c4327713deadc9c748b5317d248133a02f245" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/67c92c66ce8f0cc925a00bca89f7a188bf9183c0", - "reference": "67c92c66ce8f0cc925a00bca89f7a188bf9183c0", + "url": "https://api.github.com/repos/utopia-php/system/zipball/289c4327713deadc9c748b5317d248133a02f245", + "reference": "289c4327713deadc9c748b5317d248133a02f245", "shasum": "" }, "require": { + "laravel/pint": "1.2.*", "php": ">=7.4" }, "require-dev": { "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.6", "vimeo/psalm": "4.0.1" }, "type": "library", @@ -2460,9 +2528,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.4.0" + "source": "https://github.com/utopia-php/system/tree/0.6.0" }, - "time": "2021-02-04T14:14:49+00:00" + "time": "2022-11-07T13:51:59+00:00" }, { "name": "utopia-php/websocket", @@ -5107,14 +5175,7 @@ "time": "2022-09-28T08:42:51+00:00" } ], - "aliases": [ - { - "package": "utopia-php/database", - "version": "0.28.0.0", - "alias": "0.26.99", - "alias_normalized": "0.26.99.0" - } - ], + "aliases": [], "minimum-stability": "stable", "stability-flags": { "utopia-php/pools": 20 diff --git a/docker-compose.yml b/docker-compose.yml index 16626f7bc..14c53b312 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,7 +76,7 @@ services: - appwrite-cache:/storage/cache:rw - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw - - appwrite-functions:/storage/functions:rw + - openruntimes-functions:/storage/functions:rw - ./phpunit.xml:/usr/src/code/phpunit.xml - ./tests:/usr/src/code/tests - ./app:/usr/src/code/app @@ -160,10 +160,6 @@ services: - _APP_FUNCTIONS_SIZE_LIMIT - _APP_FUNCTIONS_TIMEOUT - _APP_FUNCTIONS_BUILD_TIMEOUT - - _APP_FUNCTIONS_CONTAINERS - - _APP_FUNCTIONS_CPUS - - _APP_FUNCTIONS_MEMORY - - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_RUNTIMES - _APP_EXECUTOR_SECRET - _APP_EXECUTOR_HOST @@ -303,8 +299,8 @@ services: volumes: - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - - appwrite-functions:/storage/functions:rw - - appwrite-builds:/storage/builds:rw + - openruntimes-functions:/storage/functions:rw + - openruntimes-builds:/storage/builds:rw - appwrite-certificates:/storage/certificates:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src @@ -466,7 +462,6 @@ services: depends_on: - redis - mariadb - - appwrite-executor environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 @@ -490,67 +485,6 @@ services: - DOCKERHUB_PULL_USERNAME - DOCKERHUB_PULL_PASSWORD - appwrite-executor: - container_name: appwrite-executor - <<: *x-logging - entrypoint: executor - stop_signal: SIGINT - image: appwrite-dev - networks: - appwrite: - runtimes: - ports: - - 9519:80 - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./app:/usr/src/code/app - - ./src:/usr/src/code/src - - appwrite-functions:/storage/functions:rw - - appwrite-builds:/storage/builds:rw - - /tmp:/tmp:rw - depends_on: - - redis - - mariadb - - appwrite - environment: - - _APP_ENV - - _APP_VERSION - - _APP_FUNCTIONS_TIMEOUT - - _APP_FUNCTIONS_BUILD_TIMEOUT - - _APP_FUNCTIONS_CONTAINERS - - _APP_FUNCTIONS_RUNTIMES - - _APP_FUNCTIONS_CPUS - - _APP_FUNCTIONS_MEMORY - - _APP_FUNCTIONS_MEMORY_SWAP - - _APP_FUNCTIONS_INACTIVE_THRESHOLD - - _APP_EXECUTOR_SECRET - - OPEN_RUNTIMES_NETWORK - - _APP_LOGGING_PROVIDER - - _APP_LOGGING_CONFIG - - _APP_STORAGE_DEVICE - - _APP_STORAGE_S3_ACCESS_KEY - - _APP_STORAGE_S3_SECRET - - _APP_STORAGE_S3_REGION - - _APP_STORAGE_S3_BUCKET - - _APP_STORAGE_DO_SPACES_ACCESS_KEY - - _APP_STORAGE_DO_SPACES_SECRET - - _APP_STORAGE_DO_SPACES_REGION - - _APP_STORAGE_DO_SPACES_BUCKET - - _APP_STORAGE_BACKBLAZE_ACCESS_KEY - - _APP_STORAGE_BACKBLAZE_SECRET - - _APP_STORAGE_BACKBLAZE_REGION - - _APP_STORAGE_BACKBLAZE_BUCKET - - _APP_STORAGE_LINODE_ACCESS_KEY - - _APP_STORAGE_LINODE_SECRET - - _APP_STORAGE_LINODE_REGION - - _APP_STORAGE_LINODE_BUCKET - - _APP_STORAGE_WASABI_ACCESS_KEY - - _APP_STORAGE_WASABI_SECRET - - _APP_STORAGE_WASABI_REGION - - _APP_STORAGE_WASABI_BUCKET - - DOCKERHUB_PULL_USERNAME - - DOCKERHUB_PULL_PASSWORD - appwrite-worker-mails: entrypoint: worker-mails <<: *x-logging @@ -751,6 +685,54 @@ services: - _APP_REDIS_PASS - _APP_CONNECTIONS_QUEUE + openruntimes-executor: + container_name: openruntimes-executor + hostname: exc1 + <<: *x-logging + stop_signal: SIGINT + image: openruntimes/executor:0.1.0 + networks: + - appwrite # Remove this when using OPR Proxy + # - openruntimes-executors + - openruntimes-runtimes + ports: + - 9900:80 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - openruntimes-builds:/storage/builds:rw + - openruntimes-functions:/storage/functions:rw + - /tmp:/tmp:rw + environment: + - OPR_EXECUTOR_CONNECTION_STORAGE + - OPR_EXECUTOR_INACTIVE_TRESHOLD + - OPR_EXECUTOR_NETWORK + - OPR_EXECUTOR_DOCKER_HUB_USERNAME + - OPR_EXECUTOR_DOCKER_HUB_PASSWORD + - OPR_EXECUTOR_ENV=$_APP_ENV + - OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES + - OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET # If using OPR proxy, use OPR_PROXY_EXECUTOR_SECRET instead + - OPR_EXECUTOR_LOGGING_PROVIDER=$_APP_LOGGING_PROVIDER + - OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG + + # openruntimes-proxy: + # container_name: openruntimes-proxy + # hostname: proxy1 + # <<: *x-logging + # image: openruntimes/proxy:0.3.0 + # networks: + # - appwrite + # - openruntimes-executors + # environment: + # - OPR_PROXY_ALGORITHM + # - OPR_PROXY_EXECUTORS + # - OPR_PROXY_HEALTHCHECK + # - OPR_PROXY_HEALTHCHECK_INTERVAL + # - OPR_PROXY_EXECUTOR_SECRET + # - OPR_PROXY_ENV=$_APP_ENV + # - OPR_PROXY_SECRET=$_APP_EXECUTOR_SECRET + # - OPR_PROXY_LOGGING_PROVIDER=$_APP_LOGGING_PROVIDER + # - OPR_PROXY_LOGGING_CONFIG=$_APP_LOGGING_CONFIG + mariadb: image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p container_name: mariadb @@ -914,7 +896,10 @@ services: networks: gateway: appwrite: - runtimes: + openruntimes-runtimes: + name: openruntimes-runtimes + openruntimes-executors: + name: openruntimes-executors volumes: appwrite-mariadb: @@ -922,9 +907,8 @@ volumes: appwrite-cache: appwrite-uploads: appwrite-certificates: - appwrite-functions: - appwrite-builds: appwrite-influxdb: appwrite-config: - appwrite-executor: + openruntimes-functions: + openruntimes-builds: # appwrite-chronograf: diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 416d47ab8..ff7935055 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -26,6 +26,10 @@ class Executor 'content-type' => '', ]; + protected int $cpus = 2; + + protected int $memory = 512; + public function __construct(string $endpoint) { if (!filter_var($endpoint, FILTER_VALIDATE_URL)) { @@ -42,45 +46,43 @@ class Executor * @param string $deploymentId * @param string $projectId * @param string $source - * @param string $runtime - * @param string $baseImage + * @param string $image * @param bool $remove * @param string $entrypoint * @param string $workdir - * @param string $destinaction - * @param string $network - * @param array $vars + * @param string $destination + * @param array $variables * @param array $commands */ public function createRuntime( string $deploymentId, string $projectId, string $source, - string $runtime, - string $baseImage, + string $image, bool $remove = false, string $entrypoint = '', string $workdir = '', string $destination = '', - array $vars = [], + array $variables = [], array $commands = [] ) { $route = "/runtimes"; $headers = [ 'content-type' => 'application/json', - 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + 'authorization' => 'Bearer ' . App::getEnv('_APP_EXECUTOR_SECRET', '') ]; $params = [ 'runtimeId' => "$projectId-$deploymentId", 'source' => $source, 'destination' => $destination, - 'runtime' => $runtime, - 'baseImage' => $baseImage, + 'image' => $image, 'entrypoint' => $entrypoint, 'workdir' => $workdir, - 'vars' => $vars, + 'variables' => $variables, 'remove' => $remove, - 'commands' => $commands + 'commands' => $commands, + 'cpus' => $this->cpus, + 'memory' => $this->memory, ]; $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); @@ -96,25 +98,51 @@ class Executor } /** - * Delete Runtime - * - * Deletes a runtime and cleans up any containers remaining. + * Create an execution * * @param string $projectId * @param string $deploymentId + * @param string $payload + * @param array $variables + * @param int $timeout + * @param string $image + * @param string $source + * @param string $entrypoint + * + * @return array */ - public function deleteRuntime(string $projectId, string $deploymentId) - { + public function createExecution( + string $projectId, + string $deploymentId, + string $payload, + array $variables, + int $timeout, + string $image, + string $source, + string $entrypoint, + ) { $runtimeId = "$projectId-$deploymentId"; - $route = "/runtimes/$runtimeId"; + $route = '/runtimes/' . $runtimeId . '/execution'; $headers = [ 'content-type' => 'application/json', - 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + 'authorization' => 'Bearer ' . App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'runtimeId' => $runtimeId, + 'variables' => $variables, + 'payload' => $payload, + 'timeout' => $timeout, + + 'image' => $image, + 'source' => $source, + 'entrypoint' => $entrypoint, + 'cpus' => $this->cpus, + 'memory' => $this->memory, ]; - $params = []; + $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); - $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $timeout); $status = $response['headers']['status-code']; if ($status >= 400) { @@ -124,93 +152,6 @@ class Executor return $response['body']; } - /** - * Create an execution - * - * @param string $projectId - * @param string $deploymentId - * @param string $path - * @param array $vars - * @param string $entrypoint - * @param string $data - * @param string runtime - * @param string $baseImage - * @param int $timeout - * - * @return array - */ - public function createExecution( - string $projectId, - string $deploymentId, - string $path, - array $vars, - string $entrypoint, - string $data, - string $runtime, - string $baseImage, - $timeout - ) { - $route = "/execution"; - $headers = [ - 'content-type' => 'application/json', - 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') - ]; - $params = [ - 'runtimeId' => "$projectId-$deploymentId", - 'vars' => $vars, - 'data' => $data, - 'timeout' => $timeout, - ]; - - /* Add 2 seconds as a buffer to the actual timeout value since there can be a slight variance*/ - $requestTimeout = $timeout + 2; - - $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout); - $status = $response['headers']['status-code']; - - for ($attempts = 0; $attempts < 10; $attempts++) { - try { - switch (true) { - case $status < 400: - return $response['body']; - case $status === 404: - $response = $this->createRuntime( - deploymentId: $deploymentId, - projectId: $projectId, - source: $path, - runtime: $runtime, - baseImage: $baseImage, - vars: $vars, - entrypoint: $entrypoint, - commands: [] - ); - $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout); - $status = $response['headers']['status-code']; - - if ($status < 400) { - return $response['body']; - } - break; - case $status === 406: - $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout); - $status = $response['headers']['status-code']; - - if ($status < 400) { - return $response['body']; - } - break; - default: - throw new \Exception($response['body']['message'], $status); - } - } catch (\Exception $e) { - throw new \Exception($e->getMessage(), $e->getCode()); - } - sleep(2); - } - - throw new Exception($response['body']['message'], 503); - } - /** * Call *