1
0
Fork 0
mirror of synced 2024-07-04 14:10:33 +12:00

Merge pull request #5230 from appwrite/feat-executor-v3-sync

Feat executor v3 sync
This commit is contained in:
Matej Bačo 2023-03-15 09:22:14 +01:00 committed by GitHub
commit fd499265ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 470 additions and 38 deletions

2
.env
View file

@ -47,6 +47,8 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_HAMSTER_RECIPIENTS=
_APP_HAMSTER_INTERVAL=86400
_APP_SMS_PROVIDER=sms://username:password@mock
_APP_SMS_FROM=+123456789
_APP_STORAGE_LIMIT=30000000

View file

@ -4,7 +4,6 @@
- Increase Traefik TCP + file limits [#4673](https://github.com/appwrite/appwrite/pull/4673)
- Fix invited account verified status [#4776](https://github.com/appwrite/appwrite/pull/4776)
- Get default region from environment on project create [#4780](https://github.com/appwrite/appwrite/pull/4780)
- Store build output file size [#4844](https://github.com/appwrite/appwrite/pull/4844)
- Fix max mimetype size [#4814](https://github.com/appwrite/appwrite/pull/4814)
# Version 1.1.2

View file

@ -165,6 +165,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/specs && \
chmod +x /usr/local/bin/ssl && \
chmod +x /usr/local/bin/hamster && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/vars && \
chmod +x /usr/local/bin/worker-audits && \

View file

@ -2672,6 +2672,17 @@ $collections = [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('endTime'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('duration'),
'type' => Database::VAR_INTEGER,
@ -2684,7 +2695,18 @@ $collections = [
'filters' => [],
],
[
'$id' => 'deploymentInternalId',
'$id' => ID::custom('size'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@ -2738,17 +2760,6 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('size'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stderr'),
'type' => Database::VAR_STRING,

View file

@ -88,6 +88,11 @@ return [
'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.',
'code' => 500,
],
Exception::GENERAL_CODES_DISABLED => [
'name' => Exception::GENERAL_CODES_DISABLED,
'description' => 'Invitation codes are disabled on this server. Please contact the server administrator.',
'code' => 500,
],
/** User Errors */
Exception::USER_COUNT_EXCEEDED => [
@ -125,8 +130,8 @@ return [
'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.',
'code' => 401,
],
Exception::USER_CODE_INVALID => [
'name' => Exception::USER_CODE_INVALID,
Exception::USER_INVALID_CODE => [
'name' => Exception::USER_INVALID_CODE,
'description' => 'The specified code is not valid. Contact your administrator for more information.',
'code' => 401,
],

View file

@ -82,8 +82,12 @@ App::post('/v1/account/invite')
$whitelistCodes = (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_CODES', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_CODES', null)) : [];
if (empty($whitelistCodes)) {
throw new Exception(Exception::GENERAL_CODES_DISABLED);
}
if (!empty($whitelistCodes) && !\in_array($code, $whitelistCodes)) {
throw new Exception(Exception::USER_CODE_INVALID);
throw new Exception(Exception::USER_INVALID_CODE);
}
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@ -170,7 +174,7 @@ App::post('/v1/account')
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
$whitelistIPs = $project->getAttribute('authWhitelistIPs');
if (!empty($whitelistEmails) && !\in_array($email, $whitelistEmails)) {
if (!empty($whitelistEmails) && !\in_array($email, $whitelistEmails) && !\in_array(strtoupper($email), $whitelistEmails)) {
throw new Exception(Exception::USER_EMAIL_NOT_WHITELISTED);
}

View file

@ -101,13 +101,14 @@ class BuildsV1 extends Worker
'deploymentId' => $deployment->getId(),
'status' => 'processing',
'path' => '',
'size' => 0,
'runtime' => $function->getAttribute('runtime'),
'source' => $deployment->getAttribute('path'),
'sourceType' => $device,
'stdout' => '',
'stderr' => '',
'duration' => 0
'endTime' => null,
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
@ -189,8 +190,11 @@ class BuildsV1 extends Worker
]
);
$endTime = DateTime::now();
/** Update the build document */
$build->setAttribute('startTime', DateTime::format((new \DateTime())->setTimestamp($response['startTime'])));
$build->setAttribute('endTime', $endTime);
$build->setAttribute('duration', \intval(\ceil($response['duration'])));
$build->setAttribute('status', 'ready');
$build->setAttribute('path', $response['path']);
@ -226,7 +230,7 @@ class BuildsV1 extends Worker
} catch (\Throwable $th) {
$endTime = DateTime::now();
$interval = (new \DateTime($endTime))->diff(new \DateTime($startTime));
$build->setAttribute('endTime', $endTime);
$build->setAttribute('duration', $interval->format('%s') + 0);
$build->setAttribute('status', 'failed');
$build->setAttribute('stderr', $th->getMessage());
@ -262,7 +266,6 @@ class BuildsV1 extends Worker
->setParam('builds.{scope}.compute', 1)
->setParam('buildStatus', $build->getAttribute('status', ''))
->setParam('buildTime', $build->getAttribute('duration'))
->setParam('buildSize', $build->getAttribute('size'))
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();

3
bin/hamster Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php hamster $@

View file

@ -73,7 +73,8 @@
"phpmailer/phpmailer": "6.6.0",
"chillerlan/php-qrcode": "4.3.3",
"adhocore/jwt": "1.1.2",
"slickdeals/statsd": "3.1.0"
"slickdeals/statsd": "3.1.0",
"league/csv": "^9.0.0"
},
"repositories": [
{

89
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": "e8e64d67c559b26264b6a89ebab1ab72",
"content-hash": "d388807afe22fb473b5136a75cb3d7e7",
"packages": [
{
"name": "adhocore/jwt",
@ -870,6 +870,93 @@
},
"time": "2022-11-29T16:25:20+00:00"
},
{
"name": "league/csv",
"version": "9.9.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "b4418ede47fbd88facc34e40a16c8ce9153b961b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/b4418ede47fbd88facc34e40a16c8ce9153b961b",
"reference": "b4418ede47fbd88facc34e40a16c8ce9153b961b",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^8.1.2"
},
"require-dev": {
"doctrine/collections": "^2.1.2",
"ext-dom": "*",
"ext-xdebug": "*",
"friendsofphp/php-cs-fixer": "^v3.14.3",
"phpbench/phpbench": "^1.2.8",
"phpstan/phpstan": "^1.10.4",
"phpstan/phpstan-deprecation-rules": "^1.1.2",
"phpstan/phpstan-phpunit": "^1.3.10",
"phpstan/phpstan-strict-rules": "^1.5.0",
"phpunit/phpunit": "^10.0.14"
},
"suggest": {
"ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"League\\Csv\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "CSV data manipulation made easy in PHP",
"homepage": "https://csv.thephpleague.com",
"keywords": [
"convert",
"csv",
"export",
"filter",
"import",
"read",
"transform",
"write"
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"time": "2023-03-11T15:57:12+00:00"
},
{
"name": "matomo/device-detector",
"version": "6.0.0",

View file

@ -59,7 +59,7 @@ services:
DEBUG: false
TESTING: true
VERSION: dev
VITE_CONSOLE_MODE: self-hosted
VITE_CONSOLE_MODE: cloud
ports:
- 9501:80
networks:
@ -145,6 +145,7 @@ services:
- _APP_SMTP_SECURE
- _APP_SMTP_USERNAME
- _APP_SMTP_PASSWORD
- _APP_HAMSTER_RECIPIENTS
- _APP_USAGE_STATS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
@ -554,6 +555,39 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-hamster:
entrypoint: hamster
<<: *x-logging
container_name: appwrite-hamster
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_CONNECTIONS_MAX
- _APP_POOL_CLIENTS
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_CONNECTIONS_DB_CONSOLE
- _APP_CONNECTIONS_DB_PROJECT
- _APP_CONNECTIONS_CACHE
- _APP_HAMSTER_RECIPIENTS
- _APP_HAMSTER_INTERVAL
appwrite-maintenance:
entrypoint: maintenance
<<: *x-logging

View file

@ -50,6 +50,7 @@ class Exception extends \Exception
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
public const GENERAL_SERVER_ERROR = 'general_server_error';
public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported';
public const GENERAL_CODES_DISABLED = 'general_codes_disabled';
/** Users */
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
@ -60,7 +61,7 @@ class Exception extends \Exception
public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
public const USER_CODE_INVALID = 'user_code_invalid';
public const USER_INVALID_CODE = 'user_invalid_code';
public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials';
public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
@ -186,6 +187,7 @@ class Exception extends \Exception
public const PLATFORM_NOT_FOUND = 'platform_not_found';
protected $type = '';
protected $errors = [];
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)
{

View file

@ -46,17 +46,6 @@ class V17 extends Migration
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
switch ($id) {
case 'files':
try {
/**
* Update 'mimeType' attribute size (127->255)
*/
$this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
break;
case 'builds':
try {
/**
@ -68,6 +57,18 @@ class V17 extends Migration
Console::warning("'size' from {$id}: {$th->getMessage()}");
}
break;
case 'files':
try {
/**
* Update 'mimeType' attribute size (127->255)
*/
$this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
try {
/**
* Delete 'endTime' attribute (use startTime+duration if needed)

View file

@ -12,6 +12,7 @@ use Appwrite\Platform\Tasks\PatchCreateMissingSchedules;
use Appwrite\Platform\Tasks\SDKs;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\Platform\Tasks\SSL;
use Appwrite\Platform\Tasks\Hamster;
use Appwrite\Platform\Tasks\Usage;
use Appwrite\Platform\Tasks\Vars;
use Appwrite\Platform\Tasks\Version;
@ -27,6 +28,7 @@ class Tasks extends Service
->addAction(Usage::getName(), new Usage())
->addAction(Vars::getName(), new Vars())
->addAction(SSL::getName(), new SSL())
->addAction(Hamster::getName(), new Hamster())
->addAction(Doctor::getName(), new Doctor())
->addAction(Install::getName(), new Install())
->addAction(Maintenance::getName(), new Maintenance())

View file

@ -0,0 +1,271 @@
<?php
namespace Appwrite\Platform\Tasks;
use Exception;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use League\Csv\Writer;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Database\Document;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
class Hamster extends Action
{
private array $columns = [
'Project ID',
'Project Name',
'Functions',
'Deployments',
'Members',
'Domains',
'Files',
'Buckets',
'Databases',
'Documents',
'Collections',
'Storage',
'Requests',
'Bandwidth',
'Users',
'Sessions',
'Executions'
];
private array $usageStats = [
'Files' => 'files.$all.count.total',
'Buckets' => 'buckets.$all.count.total',
'Databases' => 'databases.$all.count.total',
'Documents' => 'documents.$all.count.total',
'Collections' => 'collections.$all.count.total',
'Storage' => 'project.$all.storage.size',
'Requests' => 'project.$all.network.requests',
'Bandwidth' => 'project.$all.network.bandwidth',
'Users' => 'users.$all.count.total',
'Sessions' => 'sessions.$all.requests.create',
'Executions' => 'executions.$all.compute.total',
];
protected string $directory = '/usr/local';
protected string $path;
protected string $date;
public static function getName(): string
{
return 'hamster';
}
public function __construct()
{
$this
->desc('Get stats for projects')
->inject('register')
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->callback(function (Registry $register, Group $pools, Cache $cache, Database $dbForConsole) {
$this->action($register, $pools, $cache, $dbForConsole);
});
}
private function getStats(Database $dbForConsole, Database $dbForProject, Document $project): array
{
$stats = [];
/** Get Project ID */
$stats['Project ID'] = $project->getId();
/** Get Project Name */
$stats['Project Name'] = $project->getAttribute('name');
/** Get Total Functions */
$stats['Functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT);
/** Get Total Deployments */
$stats['Deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT);
/** Get Total Members */
$teamInternalId = $project->getAttribute('teamInternalId', null);
if ($teamInternalId) {
$stats['Members'] = $dbForConsole->count('memberships', [
Query::equal('teamInternalId', [$teamInternalId])
], APP_LIMIT_COUNT);
} else {
$stats['Members'] = 0;
}
/** Get Domains */
$stats['Domains'] = $dbForProject->count('domains', [], APP_LIMIT_COUNT);
/** Get Usage stats */
$range = '90d';
$periods = [
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$metrics = array_values($this->usageStats);
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$stats[$metric] = array_reverse($stats[$metric]);
// Calculate aggregate of each metric
$stats[$metric] = array_sum(array_column($stats[$metric], 'value'));
}
});
return $stats;
}
public function action(Registry $register, Group $pools, Cache $cache, Database $dbForConsole): void
{
Console::title('Cloud Hamster V1');
Console::success(APP_NAME . ' cloud hamster process v1 has started');
$interval = (int) App::getEnv('_APP_HAMSTER_INTERVAL', '30'); // 30 seconds (by default)
Console::loop(function () use ($register, $pools, $cache, $dbForConsole, $interval) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Getting Cloud Usage Stats every {$interval} seconds");
$loopStart = microtime(true);
/* Initialise new Utopia app */
$app = new App('UTC');
$console = $app->getResource('console');
/** CSV stuff */
$this->date = date('Y-m-d');
$this->path = "{$this->directory}/stats_{$this->date}.csv";
$csv = Writer::createFromPath($this->path, 'w');
$csv->insertOne($this->columns);
/** Database connections */
$totalProjects = $dbForConsole->count('projects') + 1;
Console::success("Found a total of: {$totalProjects} projects");
$projects = [$console];
$count = 0;
$limit = 30;
$sum = 30;
$offset = 0;
while (!empty($projects)) {
foreach ($projects as $project) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
continue;
}
Console::info("Getting stats for {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
$statsPerProject = $this->getStats($dbForConsole, $dbForProject, $project);
$csv->insertOne(array_values($statsPerProject));
} catch (\Throwable $th) {
throw $th;
Console::error('Failed to update project ("' . $project->getId() . '") version with error: ' . $th->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [
Query::limit($limit),
Query::offset($offset),
]);
$offset = $offset + $limit;
$count = $count + $sum;
Console::log('Iterated through ' . $count . '/' . $totalProjects . ' projects...');
}
$this->sendEmail($register);
$pools
->get('console')
->reclaim();
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Cloud Stats took {$loopTook} seconds");
}, $interval);
}
private function sendEmail(Registry $register)
{
/** @var \PHPMailer\PHPMailer\PHPMailer $mail */
$mail = $register->get('smtp');
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
try {
/** Addresses */
$mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), 'Appwrite Cloud Hamster');
$recipients = explode(',', App::getEnv('_APP_HAMSTER_RECIPIENTS', ''));
foreach ($recipients as $recipient) {
$mail->addAddress($recipient);
}
/** Attachments */
$mail->addAttachment($this->path);
/** Content */
$mail->Subject = "Cloud Report for {$this->date}";
$mail->Body = "Please find the daily cloud report atttached";
$mail->send();
Console::success('Email has been sent!');
} catch (Exception $e) {
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
}
}
}

View file

@ -51,6 +51,12 @@ class Build extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('endTime', [
'type' => self::TYPE_DATETIME,
'description' => 'The time the build was finished in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('duration', [
'type' => self::TYPE_INTEGER,
'description' => 'The build duration in seconds.',

View file

@ -38,7 +38,7 @@ class AccountConsoleClientTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 401);
$this->assertEquals($response['body']['type'], Exception::USER_CODE_INVALID);
$this->assertEquals($response['body']['type'], Exception::USER_INVALID_CODE);
$response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([
'origin' => 'http://localhost',
@ -52,7 +52,7 @@ class AccountConsoleClientTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 401);
$this->assertEquals($response['body']['type'], Exception::USER_CODE_INVALID);
$this->assertEquals($response['body']['type'], Exception::USER_INVALID_CODE);
/**
* Test for SUCCESS