1
0
Fork 0
mirror of synced 2024-09-20 03:17:30 +12:00

Merge branch '1.6.x' of github.com:appwrite/appwrite into update-sdks-1.6

This commit is contained in:
Christy Jacob 2024-08-20 14:53:08 +04:00
commit a6748be70f
36 changed files with 552 additions and 60 deletions

4
.env
View file

@ -69,8 +69,8 @@ _APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000
_APP_FUNCTIONS_TIMEOUT=900
_APP_FUNCTIONS_BUILD_TIMEOUT=900
_APP_FUNCTIONS_CPUS=1
_APP_FUNCTIONS_MEMORY=1024
_APP_FUNCTIONS_CPUS=8
_APP_FUNCTIONS_MEMORY=8192
_APP_FUNCTIONS_INACTIVE_THRESHOLD=600
_APP_FUNCTIONS_MAINTENANCE_INTERVAL=600
_APP_FUNCTIONS_RUNTIMES_NETWORK=runtimes

View file

@ -28,6 +28,9 @@ RUN \
apk add boost boost-dev; \
fi
RUN apk add git openssh-client
RUN git config --global user.email "christyjacob4@gmail.com"
WORKDIR /usr/src/code
COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor

View file

@ -3055,6 +3055,17 @@ $projectCollections = array_merge([
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('specification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_FUNCTION_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('scopes'),
'type' => Database::VAR_STRING,

View file

@ -0,0 +1,51 @@
<?php
use Appwrite\Functions\Specification;
return [
Specification::S_05VCPU_512MB => [
'slug' => Specification::S_05VCPU_512MB,
'memory' => 512,
'cpus' => 0.5
],
Specification::S_1VCPU_512MB => [
'slug' => Specification::S_1VCPU_512MB,
'memory' => 512,
'cpus' => 1
],
Specification::S_1VCPU_1GB => [
'slug' => Specification::S_1VCPU_1GB,
'memory' => 1024,
'cpus' => 1
],
Specification::S_2VCPU_2GB => [
'slug' => Specification::S_2VCPU_2GB,
'memory' => 2048,
'cpus' => 2
],
Specification::S_2VCPU_4GB => [
'slug' => Specification::S_2VCPU_4GB,
'memory' => 4096,
'cpus' => 2
],
Specification::S_4VCPU_4GB => [
'slug' => Specification::S_4VCPU_4GB,
'memory' => 4096,
'cpus' => 4
],
Specification::S_4VCPU_8GB => [
'slug' => Specification::S_4VCPU_8GB,
'memory' => 8192,
'cpus' => 4
],
Specification::S_8VCPU_4GB => [
'slug' => Specification::S_8VCPU_4GB,
'memory' => 4096,
'cpus' => 8
],
Specification::S_8VCPU_8GB => [
'slug' => Specification::S_8VCPU_8GB,
'memory' => 8192,
'cpus' => 8
]
];

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -177,12 +177,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
default => throw new Exception(Exception::USER_INVALID_TOKEN)
});
$sendAlert = (match ($verifiedToken->getAttribute('type')) {
Auth::TOKEN_TYPE_MAGIC_URL,
Auth::TOKEN_TYPE_EMAIL => false,
default => true
});
$session = new Document(array_merge(
[
'$id' => ID::unique(),
@ -210,7 +204,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->purgeCachedDocument('users', $user->getId());
Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId()));
$dbForProject->purgeCachedDocument('users', $user->getId());
@ -229,12 +222,22 @@ $createSession = function (string $userId, string $secret, Request $request, Res
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
}
if (($project->getAttribute('auths', [])['sessionAlerts'] ?? false) && $sendAlert) {
if ($dbForProject->count('sessions', [
Query::equal('userId', [$user->getId()]),
]) !== 1) {
sendSessionAlert($locale, $user, $project, $session, $queueForMails);
}
$isAllowedTokenType = match ($verifiedToken->getAttribute('type')) {
Auth::TOKEN_TYPE_MAGIC_URL,
Auth::TOKEN_TYPE_EMAIL => false,
default => true
};
$hasUserEmail = $user->getAttribute('email', false) !== false;
$isSessionAlertsEnabled = $project->getAttribute('auths', [])['sessionAlerts'] ?? false;
$isNotFirstSession = $dbForProject->count('sessions', [
Query::equal('userId', [$user->getId()]),
]) !== 1;
if ($isAllowedTokenType && $hasUserEmail && $isSessionAlertsEnabled && $isNotFirstSession) {
sendSessionAlert($locale, $user, $project, $session, $queueForMails);
}
$queueForEvents

View file

@ -11,6 +11,7 @@ use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\Validator\Headers;
use Appwrite\Functions\Validator\RuntimeSpecification;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\Task\Validator\Cron;
@ -166,6 +167,12 @@ App::post('/v1/functions')
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true)
->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true)
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true)
->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
$plan,
Config::getParam('runtime-specifications', []),
App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT),
App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('request')
->inject('response')
->inject('dbForProject')
@ -175,7 +182,7 @@ App::post('/v1/functions')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
@ -236,6 +243,7 @@ App::post('/v1/functions')
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification
]));
$schedule = Authorization::skip(
@ -474,6 +482,42 @@ App::get('/v1/functions/runtimes')
]), Response::MODEL_RUNTIME_LIST);
});
App::get('/v1/functions/specifications')
->groups(['api', 'functions'])
->desc('List available function runtime specifications')
->label('scope', 'functions.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'listSpecifications')
->label('sdk.description', '/docs/references/functions/list-specifications.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SPECIFICATION_LIST)
->inject('response')
->inject('plan')
->action(function (Response $response, array $plan) {
$allRuntimeSpecs = Config::getParam('runtime-specifications', []);
$runtimeSpecs = [];
foreach ($allRuntimeSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
// Only add specs that are within the limits set by environment variables
if ($spec['cpus'] <= System::getEnv('_APP_FUNCTIONS_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_FUNCTIONS_MEMORY', 512)) {
$runtimeSpecs[] = $spec;
}
}
$response->dynamic(new Document([
'specifications' => $runtimeSpecs,
'total' => count($runtimeSpecs)
]), Response::MODEL_SPECIFICATION_LIST);
});
App::get('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Get function')
@ -732,6 +776,12 @@ App::put('/v1/functions/:functionId')
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
$plan,
Config::getParam('runtime-specifications', []),
App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT),
App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('request')
->inject('response')
->inject('dbForProject')
@ -740,9 +790,8 @@ App::put('/v1/functions/:functionId')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -840,6 +889,21 @@ App::put('/v1/functions/:functionId')
$live = false;
}
$spec = Config::getParam('runtime-specifications')[$specification] ?? [];
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deployment'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'execute' => $execute,
'name' => $name,
@ -861,6 +925,7 @@ App::put('/v1/functions/:functionId')
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'search' => implode(' ', [$functionId, $name, $runtime]),
])));
@ -1637,7 +1702,7 @@ App::post('/v1/functions/:functionId/executions')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_MULTIPART)
->label('sdk.response.model', Response::MODEL_EXECUTION)
->label('sdk.request.type', Response::CONTENT_TYPE_MULTIPART)
->label('sdk.request.type', Response::CONTENT_TYPE_JSON)
->param('functionId', '', new UID(), 'Function ID.')
->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true)
->param('async', false, new Boolean(), 'Execute code in the background. Default value is false.', true)
@ -1695,6 +1760,7 @@ App::post('/v1/functions/:functionId/executions')
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
@ -1906,6 +1972,8 @@ App::post('/v1/functions/:functionId/executions')
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
]);
@ -1930,6 +1998,8 @@ App::post('/v1/functions/:functionId/executions')
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
@ -1968,8 +2038,8 @@ App::post('/v1/functions/:functionId/executions')
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(512 * $execution->getAttribute('duration', 0)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(512 * $execution->getAttribute('duration', 0)))
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
;
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));

View file

@ -133,6 +133,38 @@ App::get('/v1/project/usage')
];
}, $dbForProject->find('functions'));
$executionsMbSecondsBreakdown = array_map(function ($function) use ($dbForProject) {
$id = $function->getId();
$name = $function->getAttribute('name');
$metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS);
$value = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
return [
'resourceId' => $id,
'name' => $name,
'value' => $value['value'] ?? 0,
];
}, $dbForProject->find('functions'));
$buildsMbSecondsBreakdown = array_map(function ($function) use ($dbForProject) {
$id = $function->getId();
$name = $function->getAttribute('name');
$metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS);
$value = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
return [
'resourceId' => $id,
'name' => $name,
'value' => $value['value'] ?? 0,
];
}, $dbForProject->find('functions'));
$bucketsBreakdown = array_map(function ($bucket) use ($dbForProject) {
$id = $bucket->getId();
$name = $bucket->getAttribute('name');
@ -236,6 +268,8 @@ App::get('/v1/project/usage')
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
'bucketsBreakdown' => $bucketsBreakdown,
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
'deploymentsStorageBreakdown' => $deploymentsStorageBreakdown,
]), Response::MODEL_USAGE_PROJECT);
});

View file

@ -133,6 +133,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
@ -266,6 +267,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
]);
@ -290,6 +293,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
@ -329,8 +334,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(512 * $execution->getAttribute('duration', 0)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(512 * $execution->getAttribute('duration', 0)))
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;

View file

@ -33,6 +33,7 @@ use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Specification;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
use Appwrite\GraphQL\Schema;
use Appwrite\Hooks\Hooks;
@ -147,6 +148,9 @@ const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
const APP_HOSTNAME_INTERNAL = 'appwrite';
const APP_FUNCTION_SPECIFICATION_DEFAULT = Specification::S_05VCPU_512MB;
const APP_FUNCTION_CPUS_DEFAULT = 0.5;
const APP_FUNCTION_MEMORY_DEFAULT = 512;
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
@ -307,6 +311,7 @@ Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
Config::load('runtime-specifications', __DIR__ . '/config/runtimes/specifications.php');
Config::load('function-templates', __DIR__ . '/config/function-templates.php');
/**

View file

@ -83,6 +83,7 @@ services:
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
- ~/.ssh:/root/.ssh
depends_on:
- mariadb
- redis

View file

@ -0,0 +1 @@
List allowed function specifications for this instance.

View file

@ -0,0 +1,16 @@
<?php
namespace Appwrite\Functions;
class Specification
{
public const S_05VCPU_512MB = 's-0.5vcpu-512mb';
public const S_1VCPU_512MB = 's-1vcpu-512mb';
public const S_1VCPU_1GB = 's-1vcpu-1gb';
public const S_2VCPU_2GB = 's-2vcpu-2gb';
public const S_2VCPU_4GB = 's-2vcpu-4gb';
public const S_4VCPU_4GB = 's-4vcpu-4gb';
public const S_4VCPU_8GB = 's-4vcpu-8gb';
public const S_8VCPU_4GB = 's-8vcpu-4gb';
public const S_8VCPU_8GB = 's-8vcpu-8gb';
}

View file

@ -0,0 +1,112 @@
<?php
namespace Appwrite\Functions\Validator;
use Utopia\Validator;
class RuntimeSpecification extends Validator
{
private array $plan;
private array $specifications;
private float $maxCpus;
private int $maxMemory;
public function __construct(array $plan, array $specifications, float $maxCpus, int $maxMemory)
{
$this->plan = $plan;
$this->specifications = $specifications;
$this->maxCpus = $maxCpus;
$this->maxMemory = $maxMemory;
}
/**
* Get Allowed Specifications.
*
* Get allowed specifications taking into account the limits set by the environment variables and the plan.
*
* @return array
*/
public function getAllowedSpecifications(): array
{
$allowedSpecifications = [];
foreach ($this->specifications as $size => $values) {
if ($values['cpus'] <= $this->maxCpus && $values['memory'] <= $this->maxMemory) {
if (!empty($this->plan) && array_key_exists('runtimeSpecifications', $this->plan)) {
if (!\in_array($size, $this->plan['runtimeSpecifications'])) {
continue;
}
}
$allowedSpecifications[] = $size;
}
}
return $allowedSpecifications;
}
/**
* Get Description.
*
* Returns validator description.
*
* @return string
*/
public function getDescription(): string
{
return 'Specification must be one of: ' . implode(', ', $this->getAllowedSpecifications());
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (empty($value)) {
return false;
}
if (!\is_string($value)) {
return false;
}
if (!\in_array($value, $this->getAllowedSpecifications())) {
return false;
}
return true;
}
/**
* Is array.
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type.
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -177,7 +177,7 @@ class V21 extends Migration
$document->setAttribute('scopes', []);
// Add size attribute
$document->setAttribute('specification', APP_FUNCTION_BASE_SPECIFICATION);
$document->setAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT);
}
return $document;

View file

@ -142,6 +142,7 @@ class Builds extends Action
}
$version = $function->getAttribute('version', 'v2');
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specifications', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$key = $function->getAttribute('runtime');
$runtime = $runtimes[$key] ?? null;
@ -475,6 +476,9 @@ class Builds extends Action
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$cpus = $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT;
$memory = max($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, 1024); // We have a minimum of 1024MB here because some runtimes can't compile with less memory than this.
$jwtExpiry = (int)System::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([
@ -496,6 +500,8 @@ class Builds extends Action
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $cpus,
'APPWRITE_FUNCTION_MEMORY' => $memory,
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
]);
@ -511,7 +517,7 @@ class Builds extends Action
}
Co::join([
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $function, $runtime, $vars, $command, &$err) {
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $function, $runtime, $vars, $command, $cpus, $memory, &$err) {
try {
$version = $function->getAttribute('version', 'v2');
$command = $version === 'v2' ? 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh' : 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . \trim(\escapeshellarg($command), "\'") . '"';
@ -522,6 +528,8 @@ class Builds extends Action
source: $source,
image: $runtime['image'],
version: $version,
cpus: $cpus,
memory: $memory,
remove: true,
entrypoint: $deployment->getAttribute('entrypoint'),
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
@ -686,11 +694,11 @@ class Builds extends Action
->addMetric(METRIC_BUILDS, 1) // per project
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(512 * $build->getAttribute('duration', 0)))
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), 1) // per function
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(512 * $build->getAttribute('duration', 0)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger();
}

View file

@ -315,6 +315,7 @@ class Functions extends Action
$user ??= new Document();
$functionId = $function->getId();
$deploymentId = $function->getAttribute('deployment', '');
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$log->addTag('deploymentId', $deploymentId);
@ -467,6 +468,8 @@ class Functions extends Action
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT),
'APPWRITE_FUNCTION_MEMORY' => ($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT),
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
]);
@ -491,6 +494,8 @@ class Functions extends Action
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
);
@ -529,8 +534,8 @@ class Functions extends Action
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000))// per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000))
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(512 * $execution->getAttribute('duration', 0)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(512 * $execution->getAttribute('duration', 0)))
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->trigger()
;
}

View file

@ -434,7 +434,7 @@ class Swagger2 extends Format
}
}
if ($allowed) {
if ($allowed && $validator->getType() === 'string') {
$node['enum'] = $validator->getList();
$node['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $method, $name);
$node['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $method, $name);

View file

@ -84,6 +84,7 @@ use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Specification;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team;
@ -256,6 +257,8 @@ class Response extends SwooleResponse
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
public const MODEL_HEADERS = 'headers';
public const MODEL_SPECIFICATION = 'specification';
public const MODEL_SPECIFICATION_LIST = 'specificationList';
public const MODEL_TEMPLATE_FUNCTION = 'templateFunction';
public const MODEL_TEMPLATE_FUNCTION_LIST = 'templateFunctionList';
public const MODEL_TEMPLATE_RUNTIME = 'templateRuntime';
@ -379,6 +382,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION))
->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT))
// Entities
->setModel(new Database())
@ -461,6 +465,7 @@ class Response extends SwooleResponse
->setModel(new UsageFunction())
->setModel(new UsageProject())
->setModel(new Headers())
->setModel(new Specification())
->setModel(new Rule())
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())

View file

@ -152,6 +152,12 @@ class Func extends Model
'default' => false,
'example' => false,
])
->addRule('specification', [
'type' => self::TYPE_STRING,
'description' => 'Machine specification for builds and executions.',
'default' => APP_FUNCTION_SPECIFICATION_DEFAULT,
'example' => APP_FUNCTION_SPECIFICATION_DEFAULT,
])
;
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Specification extends Model
{
public function __construct()
{
$this->addRule('memory', [
'type' => self::TYPE_INTEGER,
'description' => 'Memory size in MB.',
'default' => 0,
'example' => 512
]);
$this->addRule('cpus', [
'type' => self::TYPE_FLOAT,
'description' => 'Number of CPUs.',
'default' => 0,
'example' => 1
]);
$this->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is size enabled.',
'default' => false,
'example' => true
]);
$this->addRule('slug', [
'type' => self::TYPE_STRING,
'description' => 'Size slug.',
'default' => '',
'example' => APP_FUNCTION_SPECIFICATION_DEFAULT
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Specification';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SPECIFICATION;
}
}

View file

@ -119,7 +119,6 @@ class UsageFunction extends Model
'example' => [],
'array' => true
])
->addRule('executionsTime', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of function executions compute time per period.',

View file

@ -132,7 +132,6 @@ class UsageFunctions extends Model
'example' => [],
'array' => true
])
->addRule('executionsTime', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of functions execution compute time per period.',

View file

@ -25,10 +25,6 @@ class Executor
protected array $headers;
protected int $cpus;
protected int $memory;
public function __construct(string $endpoint)
{
if (!filter_var($endpoint, FILTER_VALIDATE_URL)) {
@ -36,8 +32,6 @@ class Executor
}
$this->endpoint = $endpoint;
$this->cpus = \intval(System::getEnv('_APP_FUNCTIONS_CPUS', '1'));
$this->memory = \intval(System::getEnv('_APP_FUNCTIONS_MEMORY', '512'));
$this->headers = [
'content-type' => 'application/json',
'authorization' => 'Bearer ' . System::getEnv('_APP_EXECUTOR_SECRET', ''),
@ -66,6 +60,8 @@ class Executor
string $source,
string $image,
string $version,
float $cpus,
int $memory,
bool $remove = false,
string $entrypoint = '',
string $destination = '',
@ -90,8 +86,8 @@ class Executor
'variables' => $variables,
'remove' => $remove,
'command' => $command,
'cpus' => $this->cpus,
'memory' => $this->memory,
'cpus' => $cpus,
'memory' => $memory,
'version' => $version,
'timeout' => $timeout,
];
@ -185,6 +181,8 @@ class Executor
string $path,
string $method,
array $headers,
float $cpus,
int $memory,
string $runtimeEntrypoint = null,
bool $logging,
int $requestTimeout = null
@ -211,8 +209,8 @@ class Executor
'image' => $image,
'source' => $source,
'entrypoint' => $entrypoint,
'cpus' => $this->cpus,
'memory' => $this->memory,
'cpus' => $cpus,
'memory' => $memory,
'version' => $version,
'runtimeEntrypoint' => $runtimeEntrypoint,
'logging' => $logging,

View file

@ -2,6 +2,7 @@
namespace Tests\E2E\General;
use Appwrite\Functions\Specification;
use Appwrite\Tests\Retry;
use CURLFile;
use DateTime;
@ -617,6 +618,7 @@ class UsageTest extends Scope
],
'schedule' => '0 0 1 1 *',
'timeout' => 10,
'specification' => Specification::S_8VCPU_8GB
]
);

View file

@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Functions;
use Appwrite\Functions\Specification;
use Appwrite\Tests\Retry;
use CURLFile;
use Tests\E2E\Client;
@ -1311,6 +1312,99 @@ class FunctionsCustomServerTest extends Scope
return $data;
}
/**
* @depends testGetExecution
*/
#[Retry(count: 2)]
public function testUpdateSpecs($data): array
{
/**
* Test for SUCCESS
*/
$response1 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'specification' => Specification::S_1VCPU_1GB,
]);
$this->assertEquals(200, $response1['headers']['status-code']);
$this->assertNotEmpty($response1['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_1GB, $response1['body']['specification']);
// Test Execution
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']);
$this->assertEquals(1024, $output['APPWRITE_FUNCTION_MEMORY']);
$response2 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'specification' => Specification::S_1VCPU_512MB,
]);
$this->assertEquals(200, $response2['headers']['status-code']);
$this->assertNotEmpty($response2['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_512MB, $response2['body']['specification']);
// Test Execution
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']);
$this->assertEquals(512, $output['APPWRITE_FUNCTION_MEMORY']);
/**
* Test for FAILURE
*/
$response3 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'specification' => 's-2vcpu-512mb', // Invalid specification
]);
$this->assertEquals(400, $response3['headers']['status-code']);
$this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $response3['body']['message']);
return $data;
}
/**
* @depends testGetExecution
*/
@ -2064,8 +2158,9 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertNotEmpty($execution['body']['responseBody']);
$this->assertGreaterThan(0, $execution['body']['duration']);
$this->assertNotEmpty($execution['body']['responseBody']);
$this->assertStringContainsString("total", $execution['body']['responseBody']);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
@ -2074,23 +2169,22 @@ class FunctionsCustomServerTest extends Scope
'async' => true
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty(201, $execution['body']['$id']);
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
\sleep(10);
$execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $execution['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'async' => true
]);
], $this->getHeaders()), []);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals(200, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertNotEmpty($execution['body']['responseBody']);
$this->assertGreaterThan(0, $execution['body']['duration']);
$this->assertNotEmpty($execution['body']['logs']);
$this->assertStringContainsString("total", $execution['body']['logs']);
// Cleanup : Delete function
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [

View file

@ -9,6 +9,8 @@ return function ($context) {
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '',
'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '',
'UNICODE_TEST' => "êä"
]);
};

View file

@ -12,5 +12,7 @@ return function ($context) {
->setProject(getenv('APPWRITE_FUNCTION_PROJECT_ID'))
->setKey($context->req->headers['x-appwrite-key']);
$users = new Users($client);
return $context->res->json($users->list());
$response = $users->list();
$context->log($response);
return $context->res->json($response);
};

View file

@ -12,6 +12,8 @@ return function ($context) {
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'UNICODE_TEST' => "êä",
'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: ''
'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: '',
'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '',
'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '',
], \intval($statusCode));
};