args['projectId']; $functionId = $this->args['functionId']; $functionTag = $this->args['functionTag']; $executionId = $this->args['executionId']; $functionTrigger = $this->args['functionTrigger']; $projectDB = new Database(); $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); $projectDB->setNamespace('app_'.$projectId); $projectDB->setMocks(Config::getParam('collections', [])); Authorization::disable(); $function = $projectDB->getDocument($functionId); Authorization::reset(); if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { throw new Exception('Function not found', 404); } Authorization::disable(); $tag = $projectDB->getDocument($functionTag); Authorization::reset(); if($tag->getAttribute('functionId') !== $function->getId()) { throw new Exception('Tag not found', 404); } Authorization::disable(); $execution = $projectDB->getDocument($executionId); if (empty($execution->getId()) || Database::SYSTEM_COLLECTION_EXECUTIONS != $execution->getCollection()) { $execution = $projectDB->createDocument([ '$collection' => Database::SYSTEM_COLLECTION_EXECUTIONS, '$permissions' => [ 'read' => [], 'write' => [], ], 'dateCreated' => time(), 'functionId' => $function->getId(), 'status' => 'processing', // waiting / processing / completed / failed 'exitCode' => 0, 'stdout' => '', 'stderr' => '', 'time' => 0, ]); if (false === $execution) { throw new Exception('Failed saving execution to DB', 500); } } Authorization::reset(); $environment = (isset($environments[$function->getAttribute('env', '')])) ? $environments[$function->getAttribute('env', '')] : null; if(\is_null($environment)) { throw new Exception('Environment "'.$function->getAttribute('env', '').' is not supported'); } $vars = array_merge($function->getAttribute('vars', []), [ 'APPWRITE_FUNCTION_ID' => $functionId, 'APPWRITE_FUNCTION_TAG' => $functionTag, 'APPWRITE_FUNCTION_TRIGGER' => $functionTrigger, 'APPWRITE_FUNCTION_ENV_NAME' => $environment['name'], 'APPWRITE_FUNCTION_ENV_VERSION' => $environment['version'], ]); array_walk($vars, function (&$value, $key) { $value = "\t\t\t--env {$key}={$value} \\"; }); /* * 1. Get Original Task * 2. Check for updates * If has updates skip task and don't reschedule * If status not equal to play skip task * 3. Check next run date, update task and add new job at the given date * 4. Execute task (set optional timeout) * 5. Update task response to log * On success reset error count * On failure add error count * If error count bigger than allowed change status to pause */ /** * 1. Get event args - DONE * 2. Unpackage code in the isolated container - DONE * 3. Execute in container with timeout * + messure execution time - DONE * + pass env vars - DONE * + pass one-time api key * 4. Update execution status * 5. Update execution stdout & stderr * 6. Trigger audit log * 7. Trigger usage log */ $stdout = ''; $stderr = ''; $timeout = 15; $start = \microtime(true); //TODO aviod scheduled execution if delay is bigger than X offest /** * 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 */ $tagPath = $tag->getAttribute('codePath', ''); $tagDir = \pathinfo($tag->getAttribute('codePath', ''), PATHINFO_DIRNAME); $tagFile = \pathinfo($tag->getAttribute('codePath', ''), PATHINFO_BASENAME); $tagPathTarget = '/tmp/project-'.$projectId.'/'.$tag->getId().'/code.tar.gz'; $tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME); if(!\is_readable($tagPath)) { throw new Exception('Code is not readable: '.$tag->getAttribute('codePath', '')); } if (!\file_exists($tagPathTargetDir)) { if (!\mkdir($tagPathTargetDir, 0755, true)) { throw new Exception('Can\'t create directory '.$tagPathTargetDir); } } if (!\file_exists($tagPathTarget)) { if(!\copy($tagPath, $tagPathTarget)) { throw new Exception('Can\'t create temporary code file '.$tagPathTarget); } } $exitCode = Console::execute("docker run \ --cpus=1 \ --memory=50m \ --memory-swap=50m \ --rm \ --name=appwrite-function-{$functionId} \ --volume {$tagPathTargetDir}:/tmp:rw \ --workdir /usr/local/src \ ".implode("\n", $vars)." {$environment['image']} \ sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz && {$tag->getAttribute('command', '')}'" , null, $stdout, $stderr, $timeout); $end = \microtime(true); Authorization::disable(); $execution = $projectDB->updateDocument(array_merge($execution->getArrayCopy(), [ 'status' => ($exitCode === 0) ? 'completed' : 'failed', 'exitCode' => $exitCode, 'stdout' => mb_substr($stdout, -2000), // log last 2000 chars output 'stderr' => mb_substr($stderr, -2000), // log last 2000 chars output 'time' => ($end - $start), ])); Authorization::reset(); if (false === $function) { throw new Exception('Failed saving execution to DB', 500); } /** * Get Usage Stats * -> Network (docker stats --no-stream --format="{{.NetIO}}" appwrite) * -> CPU Time * -> Invoctions (+1) * Report to usage worker */ var_dump('stdout', $stdout); var_dump('stderr', $stderr); Console::info("Code executed in " . ($end - $start) . " seconds with exit code {$exitCode}"); // Double-check Cleanup } public function tearDown() { } }