1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00
appwrite/app/executor.php

1419 lines
52 KiB
PHP
Raw Normal View History

2021-08-24 21:32:27 +12:00
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Utopia\Database\Document;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
2021-08-24 21:32:27 +12:00
use Appwrite\Event\Event;
use Appwrite\Utopia\Response\Model\Execution;
2021-10-13 00:54:50 +13:00
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Stats\Stats;
2021-08-24 21:32:27 +12:00
use Utopia\App;
use Utopia\Swoole\Request;
use Appwrite\Utopia\Response;
use Utopia\CLI\Console;
use Swoole\Process;
2021-08-24 21:32:27 +12:00
use Swoole\Http\Server;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Utopia\Orchestration\Orchestration;
2021-10-13 00:54:50 +13:00
use Utopia\Database\Adapter\MariaDB;
use Utopia\Cache\Adapter\Redis as RedisCache;
2021-08-24 21:32:27 +12:00
use Utopia\Config\Config;
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 Swoole\Coroutine as Co;
2021-10-13 00:54:50 +13:00
use Utopia\Cache\Cache;
use Utopia\Database\Query;
2021-11-22 22:27:08 +13:00
use Utopia\Orchestration\Adapter\DockerCLI;
use Utopia\Validator\Boolean;
2022-01-19 22:33:48 +13:00
use Utopia\Logger\Log;
2021-08-27 21:21:28 +12:00
2021-10-13 00:54:50 +13:00
require_once __DIR__ . '/init.php';
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
global $register;
$logError = function(Throwable $error, string $action, Utopia\Route $route = null) use ($register) {
$logger = $register->get('logger');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if($logger) {
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$log = new Log();
$log->setNamespace("executor");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
if($route) {
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
}
2022-01-19 05:55:53 +13:00
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
2022-01-19 05:55:53 +13:00
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->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);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $error->getMessage());
Console::error('[Error] File: ' . $error->getFile());
Console::error('[Error] Line: ' . $error->getLine());
};
2022-01-17 23:18:31 +13:00
2022-01-19 05:55:53 +13:00
try {
$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 DockerCLI($dockerUser, $dockerPass));
2022-01-17 23:09:29 +13:00
2022-01-19 05:55:53 +13:00
$runtimes = Config::getParam('runtimes');
2022-01-17 23:09:29 +13:00
2022-01-19 05:55:53 +13:00
Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// 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...');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$response = $orchestration->pull($runtime['image']);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if ($response) {
Console::success("Successfully Warmed up {$runtime['name']} {$runtime['version']}!");
} else {
Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!");
}
});
2021-08-24 21:32:27 +12:00
}
2022-01-19 05:55:53 +13:00
});
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$activeFunctions = new Swoole\Table(1024);
$activeFunctions->column('id', Swoole\Table::TYPE_STRING, 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();
2022-01-19 05:55:53 +13:00
Co\run(function () use ($orchestration, $activeFunctions) {
$executionStart = \microtime(true);
2022-01-19 05:55:53 +13:00
$residueList = $orchestration->list(['label' => 'appwrite-type=function']);
2022-01-19 05:55:53 +13:00
foreach ($residueList as $value) {
$activeFunctions->set($value->getName(), [
'id' => $value->getId(),
'name' => $value->getName(),
'status' => $value->getStatus(),
'private-key' => ''
]);
}
2022-01-19 05:55:53 +13:00
$executionEnd = \microtime(true);
2022-01-19 05:55:53 +13:00
Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds');
});
} catch (\Throwable $error) {
call_user_func($logError, $error, "startupError");
}
2022-01-19 05:55:53 +13:00
$createRuntimeServer = function(string $functionId, string $projectId, string $tagId, Database $database) use($logError): void
{
global $orchestration;
global $runtimes;
global $activeFunctions;
2022-01-19 05:55:53 +13:00
// Grab Function Document
$function = Authorization::skip(function () use ($database, $functionId) {
return $database->getDocument('functions', $functionId);
});
2022-01-19 05:55:53 +13:00
$tag = Authorization::skip(function () use ($database, $tagId) {
return $database->getDocument('tags', $tagId);
});
2022-01-19 05:55:53 +13:00
if ($tag->getAttribute('buildId') === null) {
throw new Exception('Tag has no buildId');
}
2022-01-19 05:55:53 +13:00
// Grab Build Document
$build = Authorization::skip(function () use ($database, $tag) {
return $database->getDocument('builds', $tag->getAttribute('buildId'));
});
2022-01-19 05:55:53 +13:00
// Check if function isn't already created
$functions = $orchestration->list(['label' => 'appwrite-type=function', 'name' => 'appwrite-function-' . $tag->getId()]);
2022-01-19 05:55:53 +13:00
if (\count($functions) > 0) {
return;
}
2022-01-19 05:55:53 +13:00
// Generate random secret key
$secret = \bin2hex(\random_bytes(16));
2022-01-19 05:55:53 +13:00
// Check if runtime is active
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')]))
? $runtimes[$function->getAttribute('runtime', '')]
: null;
2022-01-19 05:55:53 +13:00
if ($tag->getAttribute('functionId') !== $function->getId()) {
throw new Exception('Tag not found', 404);
}
2022-01-19 05:55:53 +13:00
if (\is_null($runtime)) {
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
2022-01-19 05:55:53 +13:00
// Process environment variables
$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,
'INTERNAL_RUNTIME_KEY' => $secret
]);
2022-01-19 05:55:53 +13:00
$vars = \array_merge($vars, $build->getAttribute('envVars', [])); // for gettng endpoint.
2022-01-19 05:55:53 +13:00
$container = 'appwrite-function-' . $tag->getId();
2022-01-19 05:55:53 +13:00
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) {
call_user_func($logError, $error, "createRuntimeServer");
}
2021-08-24 21:32:27 +12:00
}
2022-01-19 05:55:53 +13:00
$activeFunctions->del($container);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Check if tag hasn't failed
if ($build->getAttribute('status') == 'failed') {
throw new Exception('Tag build failed, please check your logs.', 500);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Check if tag is built yet.
if ($build->getAttribute('status') !== 'ready') {
throw new Exception('Tag is not built yet', 500);
}
2022-01-19 05:55:53 +13:00
// Grab Tag Files
$tagPath = $build->getAttribute('outputPath', '');
$tagPathTarget = '/tmp/project-' . $projectId . '/' . $build->getId() . '/builtCode/code.tar.gz';
$tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME);
$container = 'appwrite-function-' . $tag->getId();
$device = Storage::getDevice('builds');
if (!\file_exists($tagPathTargetDir)) {
if (!\mkdir($tagPathTargetDir, 0777, true)) {
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
}
}
if (!\file_exists($tagPathTarget)) {
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
if (!\copy($tagPath, $tagPathTarget)) {
throw new Exception('Can\'t create temporary code file ' . $tagPathTarget);
}
} else {
2022-01-19 05:55:53 +13:00
$buffer = $device->read($tagPath);
\file_put_contents($tagPathTarget, $buffer);
}
};
/**
* 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
$executionStart = \microtime(true);
$executionTime = \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 $key => $value) {
$vars[$key] = strval($value);
}
// Launch runtime server
$id = $orchestration->run(
image: $runtime['image'],
name: $container,
vars: $vars,
labels: [
'appwrite-type' => 'function',
'appwrite-created' => strval($executionTime),
'appwrite-runtime' => $function->getAttribute('runtime', ''),
'appwrite-project' => $projectId,
'appwrite-tag' => $tag->getId(),
],
hostname: $container,
mountFolder: $tagPathTargetDir,
);
if (empty($id)) {
throw new Exception('Failed to create container');
}
// Add to network
$orchestration->networkConnect($container, 'appwrite_runtimes');
$executionEnd = \microtime(true);
$activeFunctions->set($container, [
'id' => $id,
'name' => $container,
'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's',
'key' => $secret,
]);
Console::info('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds');
} else {
Console::info('Runtime server is ready to run');
}
};
$execute = function(string $trigger, string $projectId, string $executionId, string $functionId, Database $database, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = '') use($logError, $createRuntimeServer): array
{
Console::info('Executing function: ' . $functionId);
global $activeFunctions;
global $runtimes;
global $register;
// Grab Tag Document
$function = Authorization::skip(function () use ($database, $functionId) {
return $database->getDocument('functions', $functionId);
});
$tag = Authorization::skip(function () use ($database, $function) {
return $database->getDocument('tags', $function->getAttribute('tag', ''));
});
// Grab Build Document
$build = Authorization::skip(function () use ($database, $tag) {
return $database->getDocument('builds', $tag->getAttribute('buildId', ''));
});
if ($tag->getAttribute('functionId') !== $function->getId()) {
throw new Exception('Tag not found', 404);
}
// Grab execution document if exists
// It it doesn't exist, create a new one.
$execution = Authorization::skip(function () use ($database, $executionId, $userId, $function, $tag, $trigger, $functionId) {
return (!empty($executionId)) ? $database->getDocument('executions', $executionId) : $database->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => (!$userId == '') ? ['user:' . $userId] : [],
'$write' => [],
'dateCreated' => time(),
'functionId' => $function->getId(),
'tagId' => $tag->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', 'Tag is still being built.')
->setAttribute('time', 0);
Authorization::skip(function () use ($database, $execution) {
return $database->updateDocument('executions', $execution->getId(), $execution);
});
throw new Exception('Execution Failed. Reason: Tag is still being built.');
}
// Check if runtime is active
$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');
}
// Process environment variables
$vars = \array_merge($function->getAttribute('vars', []), [
'APPWRITE_FUNCTION_ID' => $function->getId(),
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'APPWRITE_FUNCTION_TAG' => $tag->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($vars, $build->getAttribute('envVars', []));
$container = 'appwrite-function-' . $tag->getId();
try {
if ($build->getAttribute('status') !== 'ready') {
// Create a new build entry
$buildId = $database->getId();
Authorization::skip(function () use ($buildId, $database, $tag, $userId, $runtime, $function, $projectId) {
$database->createDocument('builds', new Document([
'$id' => $buildId,
2022-01-19 05:55:53 +13:00
'$read' => (!$userId == '') ? ['user:' . $userId] : [],
'$write' => [],
'dateCreated' => time(),
'status' => 'processing',
'outputPath' => '',
2022-01-19 05:55:53 +13:00
'runtime' => $function->getAttribute('runtime', ''),
'source' => $tag->getAttribute('path'),
'sourceType' => Storage::DEVICE_LOCAL,
'stdout' => '',
'stderr' => '',
'buildTime' => 0,
'envVars' => [
'ENTRYPOINT_NAME' => $tag->getAttribute('entrypoint'),
'APPWRITE_FUNCTION_ID' => $function->getId(),
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
2022-01-19 05:55:53 +13:00
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
]
]));
$tag->setAttribute('buildId', $buildId);
2022-01-19 05:55:53 +13:00
$database->updateDocument('tags', $tag->getId(), $tag);
});
2022-01-19 05:55:53 +13:00
runBuildStage($buildId, $projectId, $database);
sleep(1);
}
} 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);
2022-01-19 05:55:53 +13:00
Authorization::skip(function () use ($database, $execution) {
return $database->updateDocument('executions', $execution->getId(), $execution);
});
2022-01-19 05:55:53 +13:00
throw new Error('Something went wrong building the code. ' . $e->getMessage());
}
2022-01-19 05:55:53 +13:00
try {
if (!$activeFunctions->exists($container)) { // Create contianer if not ready
$createRuntimeServer($functionId, $projectId, $tag->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);
2022-01-19 05:55:53 +13:00
$execution = Authorization::skip(function () use ($database, $execution) {
return $database->updateDocument('executions', $execution->getId(), $execution);
});
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
try {
throw new Exception('Something went wrong building the runtime server. ' . $e->getMessage());
} catch (\Exception $error) {
call_user_func($logError, $error, "execution");
2021-08-24 21:32:27 +12:00
}
2022-01-19 05:55:53 +13:00
return [
'status' => 'failed',
'response' => \utf8_encode(\mb_substr($e->getMessage(), -4000)), // log last 4000 chars output
'time' => 0
];
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$internalFunction = $activeFunctions->get('appwrite-function-' . $tag->getId());
$key = $internalFunction['key'];
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Process environment variables
$vars = \array_merge($function->getAttribute('vars', []), [
'APPWRITE_FUNCTION_ID' => $function->getId(),
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'APPWRITE_FUNCTION_TAG' => $tag->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
]);
2022-01-19 05:55:53 +13:00
$vars = \array_merge($vars, $build->getAttribute('envVars', []));
2022-01-19 05:55:53 +13:00
$stdout = '';
$stderr = '';
2022-01-19 05:55:53 +13:00
$executionStart = \microtime(true);
2022-01-19 05:55:53 +13:00
$statusCode = 0;
2022-01-19 05:55:53 +13:00
$errNo = -1;
$attempts = 0;
$max = 5;
2022-01-19 05:55:53 +13:00
$executorResponse = '';
2022-01-19 05:55:53 +13:00
// cURL request to runtime
do {
$attempts++;
$ch = \curl_init();
2022-01-19 05:55:53 +13:00
$body = \json_encode([
'path' => '/usr/code',
'file' => $build->getAttribute('envVars', [])['ENTRYPOINT_NAME'],
'env' => $vars,
'payload' => $data,
'timeout' => $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))
]);
2022-01-19 05:55:53 +13:00
\curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
2022-01-19 05:55:53 +13:00
\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);
2022-01-19 05:55:53 +13:00
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
'x-internal-challenge: ' . $key,
'host: null'
]);
2022-01-19 05:55:53 +13:00
$executorResponse = \curl_exec($ch);
2022-01-19 05:55:53 +13:00
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
2022-01-19 05:55:53 +13:00
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch);
if ($errNo != CURLE_COULDNT_CONNECT && $errNo != 111) {
break;
}
2022-01-19 05:55:53 +13:00
sleep(1);
} while ($attempts < $max);
2022-01-19 05:55:53 +13:00
if ($attempts >= 5) {
$stderr = 'Failed to connect to executor runtime after 5 attempts.';
$statusCode = 124;
}
2022-01-19 05:55:53 +13:00
// If timeout error
if ($errNo == CURLE_OPERATION_TIMEDOUT || $errNo == 110) {
$statusCode = 124;
}
2021-11-30 22:39:50 +13:00
2022-01-19 05:55:53 +13:00
// 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);
}
2022-01-19 05:55:53 +13:00
$executionData = [];
2022-01-19 05:55:53 +13:00
if (!empty($executorResponse)) {
$executionData = json_decode($executorResponse, true);
}
2022-01-19 05:55:53 +13:00
if (isset($executionData['code'])) {
$statusCode = $executionData['code'];
}
if ($statusCode === 500) {
if (isset($executionData['message'])) {
$stderr = $executionData['message'];
} else {
$stderr = 'Internal Runtime error';
}
2022-01-19 05:55:53 +13:00
} 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.';
}
2022-01-19 05:55:53 +13:00
$executionEnd = \microtime(true);
$executionTime = ($executionEnd - $executionStart);
$functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed';
2022-01-19 05:55:53 +13:00
Console::info('Function executed in ' . ($executionEnd - $executionStart) . ' seconds, status: ' . $functionStatus);
2022-01-19 05:55:53 +13:00
$execution->setAttribute('tagId', $tag->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);
2022-01-19 05:55:53 +13:00
$execution = Authorization::skip(function () use ($database, $execution) {
return $database->updateDocument('executions', $execution->getId(), $execution);
});
2022-01-19 05:55:53 +13:00
$executionModel = new Execution();
$executionUpdate = new Event('v1-webhooks', 'WebhooksV1');
2022-01-19 05:55:53 +13:00
$executionUpdate
->setParam('projectId', $projectId)
->setParam('userId', $userId)
->setParam('webhooks', $webhooks)
->setParam('event', 'functions.executions.update')
->setParam('eventData', $execution->getArrayCopy(array_keys($executionModel->getRules())));
2022-01-19 05:55:53 +13:00
$executionUpdate->trigger();
2021-11-22 22:27:08 +13:00
2022-01-19 05:55:53 +13:00
$target = Realtime::fromPayload('functions.executions.update', $execution);
2022-01-19 05:55:53 +13:00
Realtime::send(
projectId: $projectId,
payload: $execution->getArrayCopy(),
event: 'functions.executions.update',
channels: $target['channels'],
roles: $target['roles']
);
2022-01-19 05:55:53 +13:00
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$statsd = $register->get('statsd');
2022-01-19 05:55:53 +13:00
$usage = new Stats($statsd);
2022-01-19 05:55:53 +13:00
$usage
->setParam('projectId', $projectId)
->setParam('functionId', $function->getId())
->setParam('functionExecution', 1)
->setParam('functionStatus', $functionStatus)
->setParam('functionExecutionTime', $executionTime * 1000) // ms
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();
2022-01-19 05:55:53 +13:00
$usage->submit();
}
2022-01-19 05:55:53 +13:00
return [
'status' => $functionStatus,
'response' => ($functionStatus !== 'completed') ? $stderr : $stdout,
'time' => $executionTime
];
};
2022-01-19 05:55:53 +13:00
App::post('/v1/execute') // Define Route
->desc('Execute a function')
->inject('request')
->param('trigger', '', new Text(1024))
->param('projectId', '', new Text(1024))
->param('executionId', '', new Text(1024), '', true)
->param('functionId', '', new Text(1024))
->param('event', '', new Text(1024), '', true)
->param('eventData', '', new Text(10240), '', true)
->param('data', '', new Text(1024), '', true)
->param('webhooks', [], new ArrayList(new JSON()), '', true)
->param('userId', '', new Text(1024), '', true)
->param('jwt', '', new Text(1024), '', true)
->inject('response')
->inject('dbForProject')
->action(
2022-01-20 23:31:04 +13:00
function ($trigger, $projectId, $executionId, $functionId, $event, $eventData, $data, $webhooks, $userId, $jwt, $request, $response, $dbForProject) use ($execute, $logError) {
2022-01-19 05:55:53 +13:00
try {
$data = $execute($trigger, $projectId, $executionId, $functionId, $dbForProject, $event, $eventData, $data, $webhooks, $userId, $jwt);
$response->json($data);
} catch (Exception $e) {
2022-01-20 23:31:04 +13:00
call_user_func($logError, $e, "executeEndpoint");
2022-01-19 05:55:53 +13:00
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->json(['error' => $e->getMessage()]);
}
}
2022-01-19 05:55:53 +13:00
);
2022-01-19 05:55:53 +13:00
// 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('dbForProject')
->inject('projectID')
->action(function ($functionId, $response, $dbForProject, $projectID) use($logError) {
/** @var string $functionId */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var string $projectID */
2022-01-19 05:55:53 +13:00
global $orchestration;
2022-01-19 05:55:53 +13:00
try {
// Get function document
$function = Authorization::skip(function () use ($dbForProject, $functionId) {
return $dbForProject->getDocument('functions', $functionId);
});
2022-01-19 05:55:53 +13:00
// Check if function exists
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
2022-01-19 05:55:53 +13:00
$results = Authorization::skip(function () use ($dbForProject, $functionId) {
return $dbForProject->find('tags', [new Query('functionId', Query::TYPE_EQUAL, [$functionId])], 999);
});
// If amount is 0 then we simply return true
if (count($results) === 0) {
return $response->json(['success' => true]);
}
2022-01-19 05:55:53 +13:00
// Delete the containers of all tags
foreach ($results as $tag) {
try {
// Remove any ongoing builds
if ($tag->getAttribute('buildId')) {
$build = Authorization::skip(function () use ($dbForProject, $tag) {
return $dbForProject->getDocument('builds', $tag->getAttribute('buildId'));
});
2021-11-15 15:18:53 +13:00
2022-01-19 05:55:53 +13:00
if ($build->getAttribute('status') == 'building') {
// Remove the build
$orchestration->remove('build-stage-' . $tag->getAttribute('buildId'), true);
Console::info('Removed build for tag ' . $tag['$id']);
}
}
2022-01-19 05:55:53 +13:00
$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
}
}
2022-01-19 05:55:53 +13:00
return $response->json(['success' => true]);
} catch (Exception $e) {
call_user_func($logError, $e, "cleanupFunction");
2022-01-19 05:55:53 +13:00
return $response->json(['error' => $e->getMessage()]);
}
});
2022-01-19 05:55:53 +13:00
App::post('/v1/cleanup/tag')
->param('tagId', '', new UID(), 'Tag unique ID.')
->inject('response')
->inject('dbForProject')
->inject('projectID')
->action(function ($tagId, $response, $dbForProject, $projectID) use($logError) {
/** @var string $tagId */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Database $dbForProject */
/** @var string $projectID */
2022-01-19 05:55:53 +13:00
global $orchestration;
2021-10-01 03:36:09 +13:00
2022-01-19 05:55:53 +13:00
try {
// Get tag document
$tag = Authorization::skip(function () use ($dbForProject, $tagId) {
return $dbForProject->getDocument('tags', $tagId);
});
2022-01-19 05:55:53 +13:00
// Check if tag exists
if ($tag->isEmpty()) {
throw new Exception('Tag not found', 404);
}
2022-01-19 05:55:53 +13:00
try {
// Remove any ongoing builds
if ($tag->getAttribute('buildId')) {
$build = Authorization::skip(function () use ($dbForProject, $tag) {
return $dbForProject->getDocument('builds', $tag->getAttribute('buildId'));
});
if ($build->getAttribute('status') == 'building') {
// Remove the build
$orchestration->remove('build-stage-' . $tag->getAttribute('buildId'), true);
Console::info('Removed build for tag ' . $tag['$id']);
}
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Remove the container of the tag
$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) {
call_user_func($logError, $e, "cleanupFunction");
return $response->json(['error' => $e->getMessage()]);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
return $response->json(['success' => true]);
});
2022-01-19 05:55:53 +13:00
App::post('/v1/tag')
->param('functionId', '', new UID(), 'Function unique ID.')
->param('tagId', '', new UID(), 'Tag unique ID.')
->param('userId', '', new UID(), 'User unique ID.', true)
->param('autoDeploy', false, new Boolean(), '', true)
->inject('response')
->inject('dbForProject')
->inject('projectID')
->action(function ($functionId, $tagId, $userId, $autoDeploy, $response, $dbForProject, $projectID) use ($createRuntimeServer) {
global $runtimes;
2022-01-19 05:55:53 +13:00
// Get function document
$function = Authorization::skip(function () use ($functionId, $dbForProject) {
return $dbForProject->getDocument('functions', $functionId);
});
2022-01-19 05:55:53 +13:00
// Get tag document
$tag = Authorization::skip(function () use ($tagId, $dbForProject) {
return $dbForProject->getDocument('tags', $tagId);
});
2022-01-19 05:55:53 +13:00
// Check if both documents exist
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if ($tag->isEmpty()) {
throw new Exception('Tag not found', 404);
}
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
$runtime = (isset($runtimes[$function->getAttribute('runtime')]))
? $runtimes[$function->getAttribute('runtime')]
: null;
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Create a new build entry
$buildId = $dbForProject->getId();
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if ($tag->getAttribute('buildId')) {
$buildId = $tag->getAttribute('buildId');
} else {
Authorization::skip(function () use ($buildId, $dbForProject, $tag, $userId, $function, $projectID, $runtime) {
$dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$read' => (!empty($userId)) ? ['user:' . $userId] : [],
'$write' => [],
'dateCreated' => time(),
'status' => 'processing',
'runtime' => $function->getAttribute('runtime'),
'outputPath' => '',
'source' => $tag->getAttribute('path'),
'sourceType' => Storage::DEVICE_LOCAL,
'stdout' => '',
'stderr' => '',
'buildTime' => 0,
'envVars' => [
'ENTRYPOINT_NAME' => $tag->getAttribute('entrypoint'),
'APPWRITE_FUNCTION_ID' => $function->getId(),
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
'APPWRITE_FUNCTION_PROJECT_ID' => $projectID,
]
]));
2022-01-19 05:55:53 +13:00
$tag->setAttribute('buildId', $buildId);
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
$dbForProject->updateDocument('tags', $tag->getId(), $tag);
});
2021-08-27 21:21:28 +12:00
}
2022-01-19 05:55:53 +13:00
// Build Code
go(function () use ($dbForProject, $projectID, $tagId, $buildId, $functionId, $function, $createRuntimeServer) {
// Build Code
runBuildStage($buildId, $projectID, $dbForProject);
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
// Update the schedule
$schedule = $function->getAttribute('schedule', '');
$cron = (empty($function->getAttribute('tag')) && !empty($schedule)) ? new CronExpression($schedule) : null;
$next = (empty($function->getAttribute('tag')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0;
2021-10-01 03:36:09 +13:00
2022-01-19 05:55:53 +13:00
// Grab tag
$tag = Authorization::skip(function () use ($dbForProject, $tagId, $next, $buildId) {
return $dbForProject->getDocument('tags', $tagId);
});
2022-01-19 05:55:53 +13:00
// Grab build
$build = Authorization::skip(function () use ($dbForProject, $buildId) {
return $dbForProject->getDocument('builds', $buildId);
});
2022-01-19 05:55:53 +13:00
// If the build failed, it won't be possible to deploy
if ($build->getAttribute('status') !== 'ready') {
return;
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if ($tag->getAttribute('automaticDeploy') === true) {
// Update the function document setting the tag as the active one
$function = Authorization::skip(function () use ($function, $dbForProject, $tag, $next) {
return $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'tag' => $tag->getId(),
'scheduleNext' => (int)$next,
])));
});
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Deploy Runtime Server
$createRuntimeServer($functionId, $projectID, $tagId, $dbForProject);
});
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if (false === $function) {
throw new Exception('Failed saving function to DB', 500);
2021-08-24 21:32:27 +12:00
}
2022-01-19 05:55:53 +13:00
$response->dynamic($function, Response::MODEL_FUNCTION);
});
2021-10-13 00:54:50 +13:00
2022-01-19 05:55:53 +13:00
App::get('/v1/')
->inject('request')
->inject('response')
->action(function ($request, $response) {
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->json(['status' => 'online']);
2021-08-24 21:32:27 +12:00
}
2022-01-19 05:55:53 +13:00
);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Build Endpoints
App::post('/v1/build/:buildId') // Start a Build
->param('buildId', '', new UID(), 'Build unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('projectID')
2022-01-20 23:31:04 +13:00
->action(function ($buildId, $response, $dbForProject, $projectID) use ($logError) {
2022-01-19 05:55:53 +13:00
/** @var string $buildId */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var string $projectID */
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
try {
// Get build document
$build = Authorization::skip(function () use ($buildId, $dbForProject) {
return $dbForProject->getDocument('builds', $buildId);
});
2021-11-22 22:27:08 +13:00
2022-01-19 05:55:53 +13:00
// Check if build exists
if ($build->isEmpty()) {
throw new Exception('Build not found', 404);
}
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
// Check if build is already running
if ($build->getAttribute('status') === 'running') {
throw new Exception('Build is already running', 409);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Check if build is already finished
if ($build->getAttribute('status') === 'finished') {
throw new Exception('Build is already finished', 409);
}
2022-01-19 05:55:53 +13:00
go(function () use ($buildId, $dbForProject, $projectID) {
// Build Code
runBuildStage($buildId, $projectID, $dbForProject);
});
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
// return success
return $response->json(['success' => true]);
} catch (Exception $e) {
2022-01-20 23:31:04 +13:00
call_user_func($logError, $e, "buildEndpoint");
2022-01-19 05:55:53 +13:00
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->json(['error' => $e->getMessage()]);
}
});
2022-01-19 05:55:53 +13:00
function runBuildStage(string $buildId, string $projectID, Database $database): Document
{
2021-08-27 21:21:28 +12:00
global $runtimes;
2022-01-19 05:55:53 +13:00
global $orchestration;
2022-01-19 05:55:53 +13:00
$buildStdout = '';
$buildStderr = '';
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
// Check if build has already been run
$build = Authorization::skip(function () use ($buildId, $database) {
return $database->getDocument('builds', $buildId);
});
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
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;
}
2022-01-19 05:55:53 +13:00
// Update Tag Status
$build->setAttribute('status', 'building');
2022-01-19 05:55:53 +13:00
Authorization::skip(function () use ($build, $database) {
$database->updateDocument('builds', $build->getId(), $build);
});
2022-01-19 05:55:53 +13:00
// Check if runtime is active
$runtime = (isset($runtimes[$build->getAttribute('runtime', '')]))
? $runtimes[$build->getAttribute('runtime', '')]
: null;
2022-01-19 05:55:53 +13:00
if (\is_null($runtime)) {
throw new Exception('Runtime "' . $build->getAttribute('runtime', '') . '" is not supported');
}
2022-01-19 05:55:53 +13:00
// Grab Tag Files
$tagPath = $build->getAttribute('source', '');
$sourceType = $build->getAttribute('sourceType', '');
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
$device = Storage::getDevice('builds');
2022-01-19 05:55:53 +13:00
$tagPathTarget = '/tmp/project-' . $projectID . '/' . $build->getId() . '/code.tar.gz';
$tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME);
2022-01-19 05:55:53 +13:00
$container = 'build-stage-' . $build->getId();
2022-01-19 05:55:53 +13:00
// Perform various checks
if (!\file_exists($tagPathTargetDir)) {
if (!\mkdir($tagPathTargetDir, 0777, true)) {
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
}
}
2022-01-19 05:55:53 +13:00
if (!\file_exists($tagPathTarget)) {
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
if (!\copy($tagPath, $tagPathTarget)) {
throw new Exception('Can\'t create temporary code file ' . $tagPathTarget);
}
} else {
$buffer = $device->read($tagPath);
\file_put_contents($tagPathTarget, $buffer);
}
}
2022-01-19 05:55:53 +13:00
if (!$device->exists($tagPath)) {
throw new Exception('Code is not readable: ' . $build->getAttribute('source', ''));
}
2022-01-19 05:55:53 +13:00
$vars = $build->getAttribute('envVars', []);
2022-01-19 05:55:53 +13:00
// Start tracking time
$buildStart = \microtime(true);
$buildTime = \time();
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$orchestration->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0));
$orchestration->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256));
$orchestration->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256));
2022-01-19 05:55:53 +13:00
foreach ($vars as &$value) {
$value = strval($value);
}
2022-01-19 05:55:53 +13:00
if (!\file_exists('/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode')) {
if (!\mkdir('/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode', 0777, true)) {
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode');
}
};
2022-01-19 05:55:53 +13:00
// Launch build container
$id = $orchestration->run(
image: $runtime['base'],
name: $container,
vars: $vars,
workdir: '/usr/code',
labels: [
'appwrite-type' => 'function',
'appwrite-created' => strval($buildTime),
'appwrite-runtime' => $build->getAttribute('runtime', ''),
'appwrite-project' => $projectID,
'appwrite-build' => $build->getId(),
],
command: [
'tail',
'-f',
'/dev/null'
],
hostname: $container,
mountFolder: $tagPathTargetDir,
volumes: [
'/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode' . ':/usr/builtCode:rw'
]
);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if (empty($id)) {
throw new Exception('Failed to start build container');
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Extract user code into build container
$untarStdout = '';
$untarStderr = '';
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$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
);
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
if (!$untarSuccess) {
throw new Exception('Failed to extract tar: ' . $untarStderr);
}
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
// Build Code / Install Dependencies
$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);
}
2021-09-21 03:52:12 +12:00
2022-01-19 05:55:53 +13:00
// Repackage Code and Save.
$compressStdout = '';
$compressStderr = '';
2021-09-21 03:52:12 +12:00
2022-01-19 05:55:53 +13:00
$builtCodePath = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode/code.tar.gz';
2021-09-01 21:48:56 +12:00
2022-01-19 05:55:53 +13:00
$compressSuccess = $orchestration->execute(
name: $container,
command: [
'tar', '-C', '/usr/code', '-czvf', '/usr/builtCode/code.tar.gz', './'
],
stdout: $compressStdout,
stderr: $compressStderr,
timeout: 60
);
2022-01-19 05:55:53 +13:00
if (!$compressSuccess) {
throw new Exception('Failed to compress built code: ' . $compressStderr);
}
2021-09-21 03:52:12 +12:00
2022-01-19 05:55:53 +13:00
// Remove Container
$orchestration->remove($id, true);
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
// 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.');
}
2021-10-01 03:36:09 +13:00
2022-01-19 05:55:53 +13:00
// Upload new code
$device = Storage::getDevice('builds');
2022-01-19 05:55:53 +13:00
$path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists
if (!@\mkdir(\dirname($path), 0777, true)) {
throw new Exception('Can\'t create directory: ' . \dirname($path));
}
2021-08-27 22:55:22 +12:00
}
2022-01-19 05:55:53 +13:00
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);
}
} else {
if (!$device->upload($builtCodePath, $path)) {
throw new Exception('Failed to upload built code upload to storage', 500);
}
}
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
if ($buildStdout == '') {
$buildStdout = 'Build Successful!';
}
2022-01-19 05:55:53 +13:00
$build->setAttribute('outputPath', $path)
->setAttribute('status', 'ready')
->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096)))
->setAttribute('stderr', \utf8_encode(\mb_substr($buildStderr, -4096)))
->setAttribute('buildTime', $buildTime);
2022-01-19 05:55:53 +13:00
// Update build with built code attribute
$build = Authorization::skip(function () use ($build, $buildId, $database) {
return $database->updateDocument('builds', $buildId, $build);
});
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
$buildEnd = \microtime(true);
2022-01-19 05:55:53 +13:00
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)));
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
$build = Authorization::skip(function () use ($build, $buildId, $database) {
return $database->updateDocument('builds', $buildId, $build);
});
2021-08-27 21:21:28 +12:00
2022-01-19 05:55:53 +13:00
// also remove the container if it exists
if ($id) {
$orchestration->remove($id, true);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
throw new Exception('Build failed: ' . $e->getMessage());
}
2022-01-19 05:55:53 +13:00
return $build;
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
2022-01-19 05:55:53 +13:00
$http = new Server("0.0.0.0", 8080);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$handleShutdown = function() use($logError)
{
try {
Console::info('Cleaning up containers before shutdown...');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Remove all containers.
global $orchestration;
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
global $register;
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
foreach ($functionsToRemove as $container) {
$orchestration->remove($container->getId(), true);
2021-10-13 00:54:50 +13:00
2022-01-19 05:55:53 +13:00
// Get a database instance
$db = $register->get('dbPool')->get();
$cache = $register->get('redisPool')->get();
2021-10-13 00:54:50 +13:00
2022-01-19 05:55:53 +13:00
$cache = new Cache(new RedisCache($cache));
2021-10-13 00:54:50 +13:00
2022-01-19 05:55:53 +13:00
$database = new Database(new MariaDB($db), $cache);
$database->setNamespace('project_'.$container->getLabels()["appwrite-project"].'_internal');
2021-10-13 00:54:50 +13:00
2022-01-19 05:55:53 +13:00
// Get list of all processing executions
$executions = Authorization::skip(function () use ($database, $container) {
return $database->find('executions', [
new Query('tagId', Query::TYPE_EQUAL, [$container->getLabels()["appwrite-tag"]]),
new Query('status', Query::TYPE_EQUAL, ['waiting'])
]);
});
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Mark all processing executions as failed
foreach ($executions as $execution) {
$execution->setAttribute('status', 'failed')
->setAttribute('statusCode', 1)
->setAttribute('stderr', 'Appwrite was shutdown during execution');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
Authorization::skip(function () use ($database, $execution) {
$database->updateDocument('executions', $execution->getId(), $execution);
});
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
Console::info('Removed container ' . $container->getName());
}
} catch(\Throwable $error) {
call_user_func($logError, $error, "shutdownError");
}
};
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$http->on('start', function ($http) use($handleShutdown) {
Process::signal(SIGINT, function () use ($http, $handleShutdown) {
$handleShutdown();
$http->shutdown();
});
2022-01-19 05:55:53 +13:00
Process::signal(SIGQUIT, function () use ($http, $handleShutdown) {
$handleShutdown();
$http->shutdown();
});
2022-01-19 05:55:53 +13:00
Process::signal(SIGKILL, function () use ($http, $handleShutdown) {
$handleShutdown();
$http->shutdown();
});
2022-01-19 05:55:53 +13:00
Process::signal(SIGTERM, function () use ($http, $handleShutdown) {
$handleShutdown();
$http->shutdown();
});
});
2022-01-19 05:55:53 +13:00
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use($logError) {
2021-08-24 21:32:27 +12:00
global $register;
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
$app = new App('UTC');
$db = $register->get('dbPool')->get();
2021-08-24 21:32:27 +12:00
App::setResource('db', function () use (&$db) {
return $db;
2021-08-24 21:32:27 +12:00
});
$redis = $register->get('redisPool')->get();
2021-08-24 21:32:27 +12:00
App::setResource('cache', function () use (&$redis) {
return $redis;
});
$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', '');
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');
}
2022-01-06 22:45:56 +13:00
App::setResource('dbForProject', function ($db, $cache) use ($projectId) {
2021-10-13 00:54:50 +13:00
$cache = new Cache(new RedisCache($cache));
2021-10-13 00:54:50 +13:00
$database = new Database(new MariaDB($db), $cache);
2022-01-06 22:45:56 +13:00
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace('_project_'.$projectId);
2021-10-13 00:54:50 +13:00
return $database;
}, ['db', 'cache']);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
App::error(function ($error, $utopia, $request, $response) use ($logError) {
2021-08-24 21:32:27 +12:00
/** @var Exception $error */
/** @var Utopia\App $utopia */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
2021-08-24 21:32:27 +12:00
if ($error instanceof PDOException) {
throw $error;
}
2021-08-24 21:32:27 +12:00
$route = $utopia->match($request);
2022-01-19 05:55:53 +13:00
call_user_func($logError, $error, "httpError", $route);
2021-08-24 21:32:27 +12:00
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$code = $error->getCode();
$message = $error->getMessage();
2021-08-24 21:32:27 +12:00
$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,
];
2021-08-24 21:32:27 +12:00
$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
);
2021-08-24 21:32:27 +12:00
}, ['error', 'utopia', 'request', 'response']);
App::setResource('projectID', function () use ($projectId) {
2021-08-24 21:32:27 +12:00
return $projectId;
});
try {
$app->run($request, $response);
} catch (Exception $e) {
2022-01-20 23:31:04 +13:00
call_user_func($logError, $e, "serverError");
2021-08-24 21:32:27 +12:00
$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);
2021-08-24 21:32:27 +12:00
}
});
$http->start();