1
0
Fork 0
mirror of synced 2024-06-27 02:31:04 +12:00

Merge branch 'feat-functions-refactor' of github.com:appwrite/appwrite into feat-cleanup-collections

This commit is contained in:
Christy Jacob 2022-01-31 16:01:46 +04:00
commit 9dcab06015
24 changed files with 498 additions and 353 deletions

3
.env
View file

@ -39,7 +39,8 @@ _APP_FUNCTIONS_CONTAINERS=10
_APP_FUNCTIONS_CPUS=4
_APP_FUNCTIONS_MEMORY=2000
_APP_FUNCTIONS_MEMORY_SWAP=2000
_APP_EXECUTOR_SECRET=a-randomly-generated-key
_APP_EXECUTOR_RUNTIME_NETWORK=appwrite_runtimes
_APP_EXECUTOR_SECRET=a-random-secret
_APP_MAINTENANCE_INTERVAL=86400
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400

View file

@ -3,6 +3,7 @@
- Introduced new execution model for functions
- Improved functions execution times
- Improved functions execution times
- Create a new builds worker to handle building of deployments
- **[ Breaking ]** **Tags** have been renamed to **Deployments**
- Rename `tagId` to `deplyomentId` in collections
- Rename tags to deployments in the docs

View file

@ -174,6 +174,7 @@ ENV _APP_SERVER=swoole \
_APP_FUNCTIONS_MEMORY=128 \
_APP_FUNCTIONS_MEMORY_SWAP=128 \
_APP_EXECUTOR_SECRET=a-random-secret \
_APP_EXECUTOR_RUNTIME_NETWORK=appwrite_runtimes \
_APP_SETUP=self-hosted \
_APP_VERSION=$VERSION \
_APP_USAGE_STATS=enabled \
@ -269,6 +270,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-database && \
chmod +x /usr/local/bin/worker-deletes && \
chmod +x /usr/local/bin/worker-functions && \
chmod +x /usr/local/bin/worker-builds && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-webhooks && \
chmod +x /usr/local/bin/executor

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -498,6 +498,15 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_EXECUTOR_RUNTIME_NETWORK',
'description' => '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,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_FUNCTIONS_ENVS',
'description' => 'Deprecated with 0.8.0, use \'_APP_FUNCTIONS_RUNTIMES\' instead!',

View file

@ -3,6 +3,7 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Event\Event;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;
@ -367,7 +368,7 @@ App::patch('/v1/functions/:functionId/deployment')
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deployment);
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
@ -433,11 +434,8 @@ App::delete('/v1/functions/:functionId')
// Request executor to delete deployment containers
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/cleanup/function");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'functionId' => $functionId
]));
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/$functionId");
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, 900);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
@ -581,45 +579,18 @@ App::post('/v1/functions/:functionId/deployments')
'deploy' => ($deploy === 'true'),
]));
// Enqueue a message to start the build
Resque::enqueue(Event::BUILDS_QUEUE_NAME, Event::BUILDS_CLASS_NAME, [
'projectId' => $project->getId(),
'functionId' => $function->getId(),
'deploymentId' => $deploymentId,
'type' => BUILD_TYPE_DEPLOYMENT
]);
$usage
->setParam('storage', $deployment->getAttribute('size', 0))
;
// Send start build reqeust to executor using /v1/deployment
$function = $dbForProject->getDocument('functions', $functionId);
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/deployment");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'functionId' => $function->getId(),
'deploymentId' => $deployment->getId(),
'userId' => $user->getId(),
]));
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, 900);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-appwrite-project: '.$project->getId(),
'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '')
]);
$executorResponse = \curl_exec($ch);
$error = \curl_error($ch);
if (!empty($error)) {
throw new Exception('Executor Communication Error: ' . $error, 500);
}
// Check status code
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (200 !== $statusCode) {
throw new Exception('Executor error: ' . $executorResponse, $statusCode);
}
\curl_close($ch);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
@ -767,11 +738,8 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
// Request executor to delete deployment containers
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/cleanup/deployment");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'deploymentId' => $deploymentId
]));
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/deployments/$deploymentId");
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, 900);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
@ -925,13 +893,12 @@ App::post('/v1/functions/:functionId/executions')
// Directly execute function.
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/execute");
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$function->getId()}/executions");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'trigger' => 'http',
'projectId' => $project->getId(),
'executionId' => $execution->getId(),
'functionId' => $function->getId(),
'data' => $data,
'webhooks' => $project->getAttribute('webhooks', []),
'userId' => $user->getId(),
@ -956,8 +923,8 @@ App::post('/v1/functions/:functionId/executions')
\curl_close($ch);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic(new Document(json_decode($responseExecute, true)), Response::MODEL_SYNC_EXECUTION);
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic(new Document(json_decode($responseExecute, true)), Response::MODEL_SYNC_EXECUTION);
});
App::get('/v1/functions/:functionId/executions')
@ -1072,9 +1039,7 @@ App::post('/v1/builds/:buildId')
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
$build = Authorization::skip(function () use ($dbForProject, $buildId) {
return $dbForProject->getDocument('builds', $buildId);
});
$build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $buildId));
if ($build->isEmpty()) {
throw new Exception('Build not found', 404);
@ -1084,34 +1049,12 @@ App::post('/v1/builds/:buildId')
throw new Exception('Build not failed', 400);
}
// Retry build
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/build/{$buildId}");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, 900);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-appwrite-project: '.$project->getId(),
'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '')
// Enqueue a message to start the build
Resque::enqueue(Event::BUILDS_QUEUE_NAME, Event::BUILDS_CLASS_NAME, [
'projectId' => $project->getId(),
'buildId' => $buildId,
'type' => BUILD_TYPE_RETRY
]);
$executorResponse = \curl_exec($ch);
$error = \curl_error($ch);
if (!empty($error)) {
throw new Exception('Executor Communication Error: ' . $error, 500);
}
// Check status code
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (200 !== $statusCode) {
throw new Exception('Executor error: ' . $executorResponse, $statusCode);
}
\curl_close($ch);
$response->noContent();
});

View file

@ -25,7 +25,6 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Orchestration\Adapter\DockerAPI;
use Utopia\Orchestration\Adapter\DockerCLI;
use Utopia\Orchestration\Orchestration;
use Utopia\Registry\Registry;
@ -300,7 +299,7 @@ function createRuntimeServer(string $functionId, string $projectId, string $depl
}
// Add to network
$orchestration->networkConnect($container, 'appwrite_runtimes');
$orchestration->networkConnect($container, App::getEnv('_APP_EXECUTOR_RUNTIME_NETWORK', 'appwrite_runtimes'));
$executionEnd = \microtime(true);
@ -311,12 +310,13 @@ function createRuntimeServer(string $functionId, string $projectId, string $depl
'key' => $secret,
]);
Console::info('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds');
Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds');
} else {
Console::info('Runtime server is ready to run');
Console::success('Runtime server is ready to run');
}
} catch (\Throwable $th) {
$orchestrationPool->put($orchestration);
Console::error($th->getMessage());
$orchestrationPool->put($orchestration ?? null);
throw $th;
}
$orchestrationPool->put($orchestration);
@ -363,7 +363,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
}
if ($build->getAttribute('status') == 'building') {
if ($build->getAttribute('status') === 'building') {
$execution
->setAttribute('status', 'failed')
@ -427,7 +427,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
$database->updateDocument('deployments', $deployment->getId(), $deployment);
runBuildStage($buildId, $projectId);
runBuildStage($buildId, $deployment->getId(), $projectId);
}
} catch (Exception $e) {
$execution
@ -591,7 +591,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
$executionTime = ($executionEnd - $executionStart);
$functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed';
Console::info('Function executed in ' . ($executionEnd - $executionStart) . ' seconds, status: ' . $functionStatus);
Console::success('Function executed in ' . ($executionEnd - $executionStart) . ' seconds, status: ' . $functionStatus);
$execution->setAttribute('deploymentId', $deployment->getId())
->setAttribute('status', $functionStatus)
@ -624,7 +624,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
roles: $target['roles']
);
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
$statsd = $register->get('statsd');
$usage = new Stats($statsd);
@ -649,7 +649,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
];
};
function runBuildStage(string $buildId, string $projectID): Document
function runBuildStage(string $buildId, string $deploymentId, string $projectID): Document
{
global $runtimes;
global $orchestrationPool;
@ -671,6 +671,7 @@ function runBuildStage(string $buildId, string $projectID): Document
// Check if build has already been run
$build = $database->getDocument('builds', $buildId);
$deployment = $database->getDocument('deployments', $deploymentId);
// Start tracking time
@ -682,10 +683,11 @@ function runBuildStage(string $buildId, string $projectID): Document
return $build;
}
// Update Tag Status
// Update deployment Status
$build->setAttribute('status', 'building');
$database->updateDocument('builds', $build->getId(), $build);
$deployment->setAttribute('status', 'building');
$database->updateDocument('builds', $buildId, $build);
$database->updateDocument('deployments', $deploymentId, $deployment);
// Check if runtime is active
$runtime = $runtimes[$build->getAttribute('runtime', '')] ?? null;
@ -694,16 +696,14 @@ function runBuildStage(string $buildId, string $projectID): Document
throw new Exception('Runtime "' . $build->getAttribute('runtime', '') . '" is not supported');
}
// Grab Tag Files
// Grab Deployment Files
$deploymentPath = $build->getAttribute('source', '');
$sourceType = $build->getAttribute('sourceType', '');
$device = Storage::getDevice('builds');
$deploymentPathTarget = '/tmp/project-' . $projectID . '/' . $build->getId() . '/code.tar.gz';
$deploymentPathTarget = '/tmp/project-' . $projectID . '/' . $buildId . '/code.tar.gz';
$deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME);
$container = 'build-stage-' . $build->getId();
$container = 'build-stage-' . $buildId;
// Perform various checks
if (!\file_exists($deploymentPathTargetDir)) {
@ -755,13 +755,13 @@ function runBuildStage(string $buildId, string $projectID): Document
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256));
$vars = array_map(fn ($v) => strval($v), $vars);
$path = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode';
$path = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode';
if (!\file_exists($path)) {
if (@\mkdir($path, 0777, true)) {
\chmod($path, 0777);
} else {
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode');
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $buildId . '/builtCode');
}
}
@ -776,7 +776,7 @@ function runBuildStage(string $buildId, string $projectID): Document
'appwrite-created' => strval($buildStart),
'appwrite-runtime' => $build->getAttribute('runtime', ''),
'appwrite-project' => $projectID,
'appwrite-build' => $build->getId(),
'appwrite-build' => $buildId,
],
command: [
'tail',
@ -786,7 +786,7 @@ function runBuildStage(string $buildId, string $projectID): Document
hostname: $container,
mountFolder: $deploymentPathTargetDir,
volumes: [
'/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode' . ':/usr/builtCode:rw'
'/tmp/project-' . $projectID . '/' . $buildId . '/builtCode' . ':/usr/builtCode:rw'
]
);
@ -878,7 +878,7 @@ function runBuildStage(string $buildId, string $projectID): Document
}
}
if ($buildStdout == '') {
if ($buildStdout === '') {
$buildStdout = 'Build Successful!';
}
@ -908,6 +908,9 @@ function runBuildStage(string $buildId, string $projectID): Document
$build = $database->updateDocument('builds', $buildId, $build);
$deployment->setAttribute('status', 'failed');
$database->updateDocument('deployments', $deploymentId, $deployment);
// also remove the container if it exists
if (isset($id)) {
$orchestration->remove($id, true);
@ -927,40 +930,32 @@ function runBuildStage(string $buildId, string $projectID): Document
return $build;
}
App::post('/v1/execute') // Define Route
App::post('/v1/functions/:functionId/executions')
->desc('Execute a function')
->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)
->param('trigger', '', new Text(1024), 'What triggered this execution, can be http / schedule / event')
->param('projectId', '', new Text(1024), 'The ProjectID this execution belongs to')
->param('executionId', '', new Text(1024), 'An optional execution ID, If not specified a new execution document is created.', true)
->param('functionId', '', new Text(1024), 'The FunctionID to execute')
->param('event', '', new Text(1024), 'The event that triggered this execution', true)
->param('eventData', '', new Text(0), 'Extra Data for the event', true)
->param('data', '', new Text(1024), 'Data to be forwarded to the function, this is user specified.', true)
->param('webhooks', [], new ArrayList(new JSON()), 'Any webhooks that need to be triggered after this execution', true)
->param('userId', '', new Text(1024), 'The UserID of the user who triggered the execution if it was called from a client SDK', true)
->param('jwt', '', new Text(1024), 'A JWT of the user who triggered the execution if it was called from a client SDK', true)
->inject('response')
->inject('dbForProject')
->action(
function (string $trigger, string $projectId, string $executionId, string $functionId, string $event, string $eventData, string $data, array $webhooks, string $userId, string $jwt, Response $response, Database $dbForProject) {
try {
$data = execute($trigger, $projectId, $executionId, $functionId, $dbForProject, $event, $eventData, $data, $webhooks, $userId, $jwt);
$response->json($data);
} catch (Exception $e) {
logError($e, 'executeEndpoint');
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->json(['error' => $e->getMessage()]);
}
$data = execute($trigger, $projectId, $executionId, $functionId, $dbForProject, $event, $eventData, $data, $webhooks, $userId, $jwt);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->json($data);
}
);
// Cleanup Endpoints used internally by appwrite when a function or deployment gets deleted to also clean up their containers
App::post('/v1/cleanup/function')
->param('functionId', '', new UID())
App::delete('/v1/functions/:functionId')
->desc('Delete a function')
->param('functionId', '', new UID(), 'The FunctionID to delete')
->inject('response')
->inject('dbForProject')
->action(
@ -968,7 +963,7 @@ App::post('/v1/cleanup/function')
try {
/** @var Orchestration $orchestration */
$orchestration = $orchestrationPool->get();
// Get function document
$function = $dbForProject->getDocument('functions', $functionId);
@ -981,42 +976,74 @@ App::post('/v1/cleanup/function')
// If amount is 0 then we simply return true
if (count($results) === 0) {
return $response->json(['success' => true]);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->send();
}
// Delete the containers of all deployments
foreach ($results as $deployment) {
try {
// Remove any ongoing builds
if ($deployment->getAttribute('buildId')) {
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
// Remove any ongoing builds
if ($deployment->getAttribute('buildId')) {
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->getAttribute('status') === 'building') {
// Remove the build
$orchestration->remove('build-stage-' . $deployment->getAttribute('buildId'), true);
Console::info('Removed build for deployment ' . $deployment['$id']);
}
if ($build->getAttribute('status') === 'building') {
// Remove the build
$orchestration->remove('build-stage-' . $deployment->getAttribute('buildId'), true);
Console::info('Removed build for deployment ' . $deployment['$id']);
}
$orchestration->remove('appwrite-function-' . $deployment['$id'], true);
Console::info('Removed container for deployment ' . $deployment['$id']);
} catch (Exception $e) {
// Do nothing, we don't care that much if it fails
}
$orchestration->remove('appwrite-function-' . $deployment['$id'], true);
Console::info('Removed container for deployment ' . $deployment['$id']);
}
return $response->json(['success' => true]);
} catch (Exception $e) {
logError($e, "cleanupFunction");
$response
->setStatusCode(Response::STATUS_CODE_OK)
->send();
} catch (Throwable $th) {
$orchestrationPool->put($orchestration);
throw $th;
} finally {
$orchestrationPool->put($orchestration);
return $response->json(['error' => $e->getMessage()]);
}
$orchestrationPool->put($orchestration);
}
);
App::post('/v1/cleanup/deployment')
App::post('/v1/functions/:functionId/deployments/:deploymentId/runtime')
->desc('Create a new runtime server for a deployment')
->param('functionId', '', new UID(), 'Function unique ID.')
->param('deploymentId', '', new UID(), 'Deployment unique ID.')
->inject('response')
->inject('dbForProject')
->inject('projectId')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, string $projectID) use ($runtimes) {
// Get function document
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
// Get deployment document
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found', 404);
}
$runtime = $runtimes[$function->getAttribute('runtime')] ?? null;
if (\is_null($runtime)) {
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" not found.', 404);
}
createRuntimeServer($functionId, $projectID, $deploymentId, $dbForProject);
$response
->setStatusCode(201)
->send();
});
App::delete('/v1/deployments/:deploymentId')
->desc('Delete a deployment')
->param('deploymentId', '', new UID(), 'Deployment unique ID.')
->inject('response')
->inject('dbForProject')
@ -1024,7 +1051,7 @@ App::post('/v1/cleanup/deployment')
try {
/** @var Orchestration $orchestration */
$orchestration = $orchestrationPool->get();
// Get deployment document
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@ -1033,204 +1060,106 @@ App::post('/v1/cleanup/deployment')
throw new Exception('Deployment not found', 404);
}
try {
// Remove any ongoing builds
if ($deployment->getAttribute('buildId')) {
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
// Remove any ongoing builds
if ($deployment->getAttribute('buildId')) {
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->getAttribute('status') == 'building') {
// Remove the build
$orchestration->remove('build-stage-' . $deployment->getAttribute('buildId'), true);
Console::info('Removed build for deployment ' . $deployment['$id']);
}
if ($build->getAttribute('status') === 'building') {
// Remove the build
$orchestration->remove('build-stage-' . $deployment->getAttribute('buildId'), true);
Console::info('Removed build for deployment ' . $deployment['$id']);
}
// Remove the container of the deployment
$orchestration->remove('appwrite-function-' . $deployment['$id'], true);
Console::info('Removed container for deployment ' . $deployment['$id']);
} catch (Exception $e) {
// Do nothing, we don't care that much if it fails
}
} catch (Exception $e) {
logError($e, "cleanupFunction");
$orchestrationPool->put($orchestration);
return $response->json(['error' => $e->getMessage()]);
// Remove the container of the deployment
$orchestration->remove('appwrite-function-' . $deployment['$id'], true);
Console::info('Removed container for deployment ' . $deployment['$id']);
} catch (Throwable $th) {
$orchestrationPool->put($orchestration);
throw $th;
}
$orchestrationPool->put($orchestration);
return $response->json(['success' => true]);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->send();
});
App::post('/v1/deployment')
->param('functionId', '', new UID(), 'Function unique ID.')
->param('deploymentId', '', new UID(), 'Deployment unique ID.')
->param('userId', '', new UID(), 'User unique ID.', true)
App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->desc("Create a new build")
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('deploymentId', '', new UID(), 'Deployment unique ID.', false)
->param('buildId', '', new UID(), 'Build unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('projectID')
->inject('register')
->action(function (string $functionId, string $deploymentId, string $userId, Response $response, Database $dbForProject, string $projectID, Registry $register) use ($runtimes) {
// Get function document
->inject('projectId')
->action(function (string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, string $projectId) {
$function = $dbForProject->getDocument('functions', $functionId);
// Get deployment document
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
// Check if both documents exist
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found', 404);
}
$runtime = $runtimes[$function->getAttribute('runtime')] ?? null;
if (\is_null($runtime)) {
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
$build = $dbForProject->getDocument('builds', $buildId);
if ($build->isEmpty()) {
throw new Exception('Build not found', 404);
}
// Create a new build entry
$buildId = $dbForProject->getId();
if ($build->getAttribute('status') === 'building') {
throw new Exception('Build is already running', 409);
}
if ($deployment->getAttribute('buildId')) {
$buildId = $deployment->getAttribute('buildId');
} else {
try {
$dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$read' => (!empty($userId)) ? ['user:' . $userId] : [],
'$write' => ['role:all'],
'startTime' => time(),
'status' => 'processing',
'deploymentId' => $deploymentId,
'runtime' => $function->getAttribute('runtime'),
'outputPath' => '',
'source' => $deployment->getAttribute('path'),
'sourceType' => Storage::DEVICE_LOCAL,
'stdout' => '',
'stderr' => '',
'endTime' => 0,
'duration' => 0
]));
// Check if build is already finished
if ($build->getAttribute('status') === 'ready') {
throw new Exception('Build is already finished', 409);
}
$deployment->setAttribute('buildId', $buildId);
go(function() use ($functionId, $deploymentId, $buildId, $projectId, $dbForProject, $function, $deployment) {
Console::info('Starting build for deployment ' . $deployment['$id']);
runBuildStage($buildId, $deploymentId, $projectId);
$dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} catch (\Throwable $th) {
var_dump($deployment->getArrayCopy());
throw $th;
// Update the schedule
$schedule = $function->getAttribute('schedule', '');
$cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null;
$next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0;
// Grab build
$build = $dbForProject->getDocument('builds', $buildId);
// If the build failed, it won't be possible to deploy
if ($build->getAttribute('status') !== 'ready') {
throw new Exception('Build failed', 500);
}
}
// Build Code
go(function () use ($projectID, $deploymentId, $buildId, $functionId, $function, $register) {
if ($deployment->getAttribute('deploy') === true) {
// Update the function document setting the deployment as the active one
$function
->setAttribute('deployment', $deployment->getId())
->setAttribute('scheduleNext', (int)$next);
$function = $dbForProject->updateDocument('functions', $functionId, $function);
}
// Deploy Runtime Server
try {
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$cache = new Cache(new RedisCache($redis));
$dbForProject = new Database(new MariaDB($db), $cache);
$dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForProject->setNamespace('_project_' . $projectID);
// Build Code
runBuildStage($buildId, $projectID);
// Update the schedule
$schedule = $function->getAttribute('schedule', '');
$cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null;
$next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0;
// Grab deployment
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
// Grab build
$build = $dbForProject->getDocument('builds', $buildId);
// If the build failed, it won't be possible to deploy
if ($build->getAttribute('status') !== 'ready') {
return;
}
if ($deployment->getAttribute('automaticDeploy') === true) {
// Update the function document setting the deployment as the active one
$function
->setAttribute('deployment', $deployment->getId())
->setAttribute('scheduleNext', (int)$next);
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
}
// Deploy Runtime Server
createRuntimeServer($functionId, $projectID, $deploymentId, $dbForProject);
Console::info("[ INFO ] Creating runtime server");
createRuntimeServer($functionId, $projectId, $deploymentId, $dbForProject);
} catch (\Throwable $th) {
} finally {
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($redis);
Console::error($th->getMessage());
$deployment->setAttribute('status', 'failed');
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
throw $th;
}
});
if (false === $function) {
throw new Exception('Failed saving function to DB', 500);
}
$response->dynamic($function, Response::MODEL_FUNCTION);
});
App::get('/v1/')
->inject('response')
->action(
function (Response $response) {
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->json(['status' => 'online']);
}
);
// Build Endpoints
App::post('/v1/build/:buildId') // Start a Build
->param('buildId', '', new UID(), 'Build unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('projectID')
->action(function (string $buildId, Response $response, Database $dbForProject, string $projectID) {
try {
// Get build document
$build = $dbForProject->getDocument('builds', $buildId);
// Check if build exists
if ($build->isEmpty()) {
throw new Exception('Build not found', 404);
}
// Check if build is already running
if ($build->getAttribute('status') === 'running') {
throw new Exception('Build is already running', 409);
}
// Check if build is already finished
if ($build->getAttribute('status') === 'finished') {
throw new Exception('Build is already finished', 409);
}
go(function () use ($buildId, $dbForProject, $projectID) {
// Build Code
runBuildStage($buildId, $projectID, $dbForProject);
});
// return success
return $response->json(['success' => true]);
} catch (Exception $e) {
logError($e, "buildEndpoint");
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->json(['error' => $e->getMessage()]);
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->send();
});
App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
@ -1395,7 +1324,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
);
}, ['error', 'utopia', 'request', 'response']);
App::setResource('projectID', function () use ($projectId) {
App::setResource('projectId', function () use ($projectId) {
return $projectId;
});

View file

@ -92,6 +92,9 @@ const DATABASE_TYPE_CREATE_ATTRIBUTE = 'createAttribute';
const DATABASE_TYPE_CREATE_INDEX = 'createIndex';
const DATABASE_TYPE_DELETE_ATTRIBUTE = 'deleteAttribute';
const DATABASE_TYPE_DELETE_INDEX = 'deleteIndex';
// Build Worker Types
const BUILD_TYPE_DEPLOYMENT = 'deployment';
const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';

View file

@ -191,6 +191,7 @@ services:
- _APP_FUNCTIONS_MEMORY
- _APP_FUNCTIONS_MEMORY_SWAP
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_RUNTIME_NETWORK
- _APP_USAGE_STATS
- _APP_STATSD_HOST
- _APP_STATSD_PORT
@ -221,6 +222,31 @@ services:
- _APP_DB_USER
- _APP_DB_PASS
appwrite-worker-builds:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-builds
container_name: appwrite-worker-builds
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
appwrite-worker-audits:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-audits

161
app/workers/builds.php Normal file
View file

@ -0,0 +1,161 @@
<?php
use Appwrite\Resque\Worker;
use Cron\CronExpression;
use Utopia\Database\Validator\Authorization;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Storage\Storage;
use Utopia\Database\Document;
use Utopia\Config\Config;
require_once __DIR__.'/../init.php';
// Disable Auth since we already validate it in the API
Authorization::disable();
Console::title('Builds V1 Worker');
Console::success(APP_NAME.' build worker v1 has started');
// TODO: Executor should return appropriate response codes.
class BuildsV1 extends Worker
{
public function getName(): string
{
return "builds";
}
public function init(): void {}
public function run(): void
{
$type = $this->args['type'] ?? '';
$projectId = $this->args['projectId'] ?? '';
switch ($type) {
case BUILD_TYPE_DEPLOYMENT:
$functionId = $this->args['functionId'] ?? '';
$deploymentId = $this->args['deploymentId'] ?? '';
Console::info("[ INFO ] Creating build for deployment: $deploymentId");
$this->buildDeployment($projectId, $functionId, $deploymentId);
break;
case BUILD_TYPE_RETRY:
$buildId = $this->args['buildId'] ?? '';
$functionId = $this->args['functionId'] ?? '';
$deploymentId = $this->args['deploymentId'] ?? '';
Console::info("[ INFO ] Retrying build for id: $buildId");
$this->createBuild($projectId, $functionId, $deploymentId, $buildId);
break;
default:
throw new \Exception('Invalid build type');
break;
}
}
protected function createBuild(string $projectId, string $functionId, string $deploymentId, string $buildId)
{
// TODO: What is a reasonable time to wait for a build to complete?
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/$functionId/deployments/$deploymentId/builds/$buildId");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, 900);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-appwrite-project: '.$projectId,
'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '')
]);
$response = \curl_exec($ch);
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
if (!empty($error)) {
throw new \Exception($error);
}
\curl_close($ch);
if ($responseStatus >= 400) {
throw new \Exception("Build failed with status code: $responseStatus");
}
}
protected function buildDeployment(string $projectId, string $functionId, string $deploymentId)
{
$dbForProject = $this->getProjectDB($projectId);
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
// Get deployment document
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found', 404);
}
$runtimes = Config::getParam('runtimes', []);
$key = $function->getAttribute('runtime');
$runtime = isset($runtimes[$key]) ? $runtimes[$key] : null;
if (\is_null($runtime)) {
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
$buildId = $deployment->getAttribute('buildId', '');
// If build ID is empty, create a new build
if (empty($buildId)) {
try {
$buildId = $dbForProject->getId();
// TODO : There is no way to associate a build with a deployment. So we need to add a deploymentId attribute to the build document
$dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$read' => [],
'$write' => [],
'dateCreated' => time(),
'status' => 'processing',
'runtime' => $function->getAttribute('runtime'),
'outputPath' => '',
'source' => $deployment->getAttribute('path'),
'sourceType' => Storage::DEVICE_LOCAL,
'stdout' => '',
'stderr' => '',
'time' => 0,
'vars' => [
'ENTRYPOINT_NAME' => $deployment->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,
]
]));
} catch (\Throwable $th) {
$deployment->setAttribute('buildId', '');
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
Console::error($th->getMessage());
throw $th;
}
}
// Build the Code
try {
$deployment->setAttribute('buildId', $buildId);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
$this->createBuild($projectId, $functionId, $deploymentId, $buildId);
} catch (\Throwable $th) {
Console::error($th->getMessage());
throw $th;
}
Console::success("[ SUCCESS ] Build id: $buildId started");
}
public function shutdown(): void {}
}

View file

@ -224,13 +224,12 @@ class FunctionsV1 extends Worker
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): void
{
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/execute");
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/functions/{$function->getId()}/executions");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'trigger' => $trigger,
'projectId' => $projectId,
'executionId' => $executionId,
'functionId' => $function->getId(),
'event' => $event,
'eventData' => $eventData,
'data' => $data,

10
bin/worker-builds Normal file
View file

@ -0,0 +1,10 @@
#!/bin/sh
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
INTERVAL=0.1 QUEUE='v1-builds' APP_INCLUDE='/usr/src/code/app/workers/builds.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php

View file

@ -50,7 +50,7 @@
"utopia-php/cache": "0.4.*",
"utopia-php/cli": "0.11.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.13.*",
"utopia-php/database": "0.14.*",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",

33
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cba39f50398d5ae2b121db34c9e4c529",
"content-hash": "1a5d84f96eb76e59f7ad0ff7bcd4a8d8",
"packages": [
{
"name": "adhocore/jwt",
@ -2135,16 +2135,16 @@
},
{
"name": "utopia-php/database",
"version": "0.13.2",
"version": "0.14.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "bf92279b707b3a10ee5ec5df5c065023b2221357"
"reference": "2f2527bb080cf578fba327ea2ec637064561d403"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/bf92279b707b3a10ee5ec5df5c065023b2221357",
"reference": "bf92279b707b3a10ee5ec5df5c065023b2221357",
"url": "https://api.github.com/repos/utopia-php/database/zipball/2f2527bb080cf578fba327ea2ec637064561d403",
"reference": "2f2527bb080cf578fba327ea2ec637064561d403",
"shasum": ""
},
"require": {
@ -2192,9 +2192,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.13.2"
"source": "https://github.com/utopia-php/database/tree/0.14.0"
},
"time": "2022-01-04T10:51:22+00:00"
"time": "2022-01-21T16:34:34+00:00"
},
{
"name": "utopia-php/domains",
@ -3126,23 +3126,23 @@
},
{
"name": "composer/pcre",
"version": "1.0.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "3d322d715c43a1ac36c7fe215fa59336265500f2"
"reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2",
"reference": "3d322d715c43a1ac36c7fe215fa59336265500f2",
"url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560",
"reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1",
"phpstan/phpstan": "^1.3",
"phpstan/phpstan-strict-rules": "^1.1",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
@ -3177,7 +3177,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/1.0.0"
"source": "https://github.com/composer/pcre/tree/1.0.1"
},
"funding": [
{
@ -3193,7 +3193,7 @@
"type": "tidelift"
}
],
"time": "2021-12-06T15:17:27+00:00"
"time": "2022-01-21T20:24:37+00:00"
},
{
"name": "composer/semver",
@ -3697,9 +3697,6 @@
"require": {
"php": "^7.1 || ^8.0"
},
"replace": {
"myclabs/deep-copy": "self.version"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
@ -6665,5 +6662,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.2.0"
}

View file

@ -285,6 +285,34 @@ services:
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
entrypoint: worker-builds
container_name: appwrite-worker-builds
build:
context: .
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
appwrite-worker-certificates:
entrypoint: worker-certificates
@ -350,7 +378,11 @@ services:
appwrite-executor:
container_name: appwrite-executor
entrypoint: executor
entrypoint:
- php
- -e
- /usr/src/code/app/executor.php
- -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
stop_signal: SIGINT
build:
context: .
@ -391,6 +423,7 @@ services:
- _APP_FUNCTIONS_MEMORY
- _APP_FUNCTIONS_MEMORY_SWAP
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_RUNTIME_NETWORK
- _APP_USAGE_STATS
- _APP_STATSD_HOST
- _APP_STATSD_PORT

View file

@ -30,6 +30,9 @@ class Event
const CERTIFICATES_QUEUE_NAME = 'v1-certificates';
const CERTIFICATES_CLASS_NAME = 'CertificatesV1';
const BUILDS_QUEUE_NAME = 'v1-builds';
const BUILDS_CLASS_NAME = 'BuildsV1';
/**
* @var string

View file

@ -45,7 +45,7 @@ abstract class Worker
* @throws \Exception|\Throwable
*/
public function init() {
throw new Exception("Please implement getName method in worker");
throw new Exception("Please implement init method in worker");
}
/**
@ -56,7 +56,7 @@ abstract class Worker
* @throws \Exception|\Throwable
*/
public function run() {
throw new Exception("Please implement getName method in worker");
throw new Exception("Please implement run method in worker");
}
/**
@ -67,7 +67,7 @@ abstract class Worker
* @throws \Exception|\Throwable
*/
public function shutdown() {
throw new Exception("Please implement getName method in worker");
throw new Exception("Please implement shutdown method in worker");
}
const MAX_ATTEMPTS = 10;

View file

@ -242,7 +242,7 @@ class FunctionsCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'async' => 1,
'async' => true,
]);
$this->assertEquals(401, $execution['headers']['status-code']);

View file

@ -266,6 +266,34 @@ services:
- _APP_REDIS_PORT
- _APP_SMTP_HOST
- _APP_SMTP_PORT
appwrite-worker-builds:
entrypoint: worker-builds
container_name: appwrite-worker-builds
build:
context: .
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
appwrite-schedule:
entrypoint: schedule