1
0
Fork 0
mirror of synced 2024-06-02 10:54:44 +12:00

Merge pull request #2853 from appwrite/feat-realtime-build-status

feat: realtime on deployments
This commit is contained in:
Torsten Dittmann 2022-02-28 17:11:01 +01:00 committed by GitHub
commit 9c444c32a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 68 additions and 13 deletions

View file

@ -160,13 +160,14 @@ App::post('/v1/runtimes')
$stderr = '';
$startTime = \time();
$endTime = 0;
$orchestration = $orchestrationPool->get();
try {
Console::info('Building container : ' . $runtimeId);
/**
* Temporary file paths in the executor
*/
$tmpSource = "/tmp/$runtimeId/src/code.tar.gz";
$tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz";
@ -192,7 +193,6 @@ App::post('/v1/runtimes')
/**
* Create container
*/
$orchestration = $orchestrationPool->get();
$secret = \bin2hex(\random_bytes(16));
$vars = \array_merge($vars, [
'INTERNAL_RUNTIME_KEY' => $secret,

View file

@ -179,7 +179,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
</div>
<div class="pull-start">
<button data-ls-ui-trigger="deploy-deployment">Create Deployment</button>
<button data-ls-ui-trigger="create-deployment">Create Deployment</button>
</div>
<div class="pull-end paging">
@ -631,7 +631,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<button type="submit" style="vertical-align: top;">Execute Now</button>
</form>
</div>
<div data-ui-modal class="modal close box sticky-footer" data-button-hide="on" data-open-event="deploy-deployment">
<div data-ui-modal class="modal close box sticky-footer" data-button-hide="on" data-open-event="create-deployment">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1 class="margin-bottom-xl">Create a New Deployment</h1>

View file

@ -1,5 +1,6 @@
<?php
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Cron\CronExpression;
use Executor\Executor;
@ -58,6 +59,8 @@ class BuildsV1 extends Worker
protected function buildDeployment(string $projectId, string $functionId, string $deploymentId)
{
$dbForProject = $this->getProjectDB($projectId);
$dbForConsole = $this->getConsoleDB();
$project = $dbForConsole->getDocument('projects', $projectId);
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -107,6 +110,16 @@ class BuildsV1 extends Worker
$build->setAttribute('status', 'building');
$build = $dbForProject->updateDocument('builds', $buildId, $build);
/** Send realtime event */
$target = Realtime::fromPayload('functions.deployments.update', $build, $project);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
event: 'functions.deployments.update',
channels: $target['channels'],
roles: $target['roles']
);
$source = $deployment->getAttribute('path');
$vars = $function->getAttribute('vars', []);
$baseImage = $runtime['image'];
@ -162,6 +175,18 @@ class BuildsV1 extends Worker
Console::error($th->getMessage());
} finally {
$build = $dbForProject->updateDocument('builds', $buildId, $build);
/**
* Send realtime Event
*/
$target = Realtime::fromPayload('functions.deployments.update', $build, $project);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
event: 'functions.deployments.update',
channels: $target['channels'],
roles: $target['roles']
);
}
}

View file

@ -328,6 +328,13 @@ class FunctionsV1 extends Worker
/** Trigger realtime event */
$target = Realtime::fromPayload('functions.executions.update', $execution);
Realtime::send(
projectId: 'console',
payload: $execution->getArrayCopy(),
event: 'functions.executions.update',
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $projectId,
payload: $execution->getArrayCopy(),

View file

@ -3541,7 +3541,8 @@ handler2.inline=(el,{expression},{cleanup:cleanup2})=>{let root=closestRoot(el);
root._x_refs={};root._x_refs[expression]=el;cleanup2(()=>delete root._x_refs[expression]);};directive("ref",handler2);directive("if",(el,{expression},{effect:effect3,cleanup:cleanup2})=>{let evaluate2=evaluateLater(el,expression);let show=()=>{if(el._x_currentIfEl)
return el._x_currentIfEl;let clone2=el.content.cloneNode(true).firstElementChild;addScopeToNode(clone2,{},el);mutateDom(()=>{el.after(clone2);initTree(clone2);});el._x_currentIfEl=clone2;el._x_undoIf=()=>{clone2.remove();delete el._x_currentIfEl;};return clone2;};let hide=()=>{if(!el._x_undoIf)
return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{switch(response.event){case'stats.connections':for(let project in response.payload){current[project]=response.payload[project]??0;}
break;case'database.attributes.create':case'database.attributes.update':case'database.attributes.delete':document.dispatchEvent(new CustomEvent('database.createAttribute'));break;case'database.indexes.create':case'database.indexes.update':case'database.indexes.delete':document.dispatchEvent(new CustomEvent('database.createIndex'));break;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
break;case'database.attributes.create':case'database.attributes.update':case'database.attributes.delete':document.dispatchEvent(new CustomEvent('database.createAttribute'));break;case'database.indexes.create':case'database.indexes.update':case'database.indexes.delete':document.dispatchEvent(new CustomEvent('database.createIndex'));break;case'functions.deployments.create':case'functions.deployments.update':case'functions.deployments.delete':document.dispatchEvent(new CustomEvent('functions.createDeployment'));break;case'functions.executions.create':case'functions.executions.update':case'functions.executions.delete':console.log("Received execution event")
document.dispatchEvent(new CustomEvent('functions.createExecution'));break;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
history=history[project];history.push({percentage:0,value:current[project]});if(history.length>=bars){history.shift();}
const highest=history.reduce((prev,curr)=>{return(curr.value>prev)?curr.value:prev;},0);history=history.map(({percentage,value})=>{createdHistory=true;percentage=value===0?0:((Math.round((value/highest)*10)/10)*100);if(percentage>100)percentage=100;else if(percentage==0&&value!=0)percentage=5;return{percentage:percentage,value:value};})
newHistory[project]=history;}

View file

@ -494,7 +494,8 @@ handler2.inline=(el,{expression},{cleanup:cleanup2})=>{let root=closestRoot(el);
root._x_refs={};root._x_refs[expression]=el;cleanup2(()=>delete root._x_refs[expression]);};directive("ref",handler2);directive("if",(el,{expression},{effect:effect3,cleanup:cleanup2})=>{let evaluate2=evaluateLater(el,expression);let show=()=>{if(el._x_currentIfEl)
return el._x_currentIfEl;let clone2=el.content.cloneNode(true).firstElementChild;addScopeToNode(clone2,{},el);mutateDom(()=>{el.after(clone2);initTree(clone2);});el._x_currentIfEl=clone2;el._x_undoIf=()=>{clone2.remove();delete el._x_currentIfEl;};return clone2;};let hide=()=>{if(!el._x_undoIf)
return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{switch(response.event){case'stats.connections':for(let project in response.payload){current[project]=response.payload[project]??0;}
break;case'database.attributes.create':case'database.attributes.update':case'database.attributes.delete':document.dispatchEvent(new CustomEvent('database.createAttribute'));break;case'database.indexes.create':case'database.indexes.update':case'database.indexes.delete':document.dispatchEvent(new CustomEvent('database.createIndex'));break;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
break;case'database.attributes.create':case'database.attributes.update':case'database.attributes.delete':document.dispatchEvent(new CustomEvent('database.createAttribute'));break;case'database.indexes.create':case'database.indexes.update':case'database.indexes.delete':document.dispatchEvent(new CustomEvent('database.createIndex'));break;case'functions.deployments.create':case'functions.deployments.update':case'functions.deployments.delete':document.dispatchEvent(new CustomEvent('functions.createDeployment'));break;case'functions.executions.create':case'functions.executions.update':case'functions.executions.delete':console.log("Received execution event")
document.dispatchEvent(new CustomEvent('functions.createExecution'));break;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
history=history[project];history.push({percentage:0,value:current[project]});if(history.length>=bars){history.shift();}
const highest=history.reduce((prev,curr)=>{return(curr.value>prev)?curr.value:prev;},0);history=history.map(({percentage,value})=>{createdHistory=true;percentage=value===0?0:((Math.round((value/highest)*10)/10)*100);if(percentage>100)percentage=100;else if(percentage==0&&value!=0)percentage=5;return{percentage:percentage,value:value};})
newHistory[project]=history;}

View file

@ -76,6 +76,20 @@ window.addEventListener("load", async () => {
document.dispatchEvent(new CustomEvent('database.createIndex'));
break;
case 'functions.deployments.create':
case 'functions.deployments.update':
case 'functions.deployments.delete':
document.dispatchEvent(new CustomEvent('functions.createDeployment'));
break;
case 'functions.executions.create':
case 'functions.executions.update':
case 'functions.executions.delete':
document.dispatchEvent(new CustomEvent('functions.createExecution'));
break;
}
});

View file

@ -307,12 +307,17 @@ class Realtime extends Adapter
break;
case strpos($event, 'functions.executions.') === 0:
if (!empty($payload->getRead())) {
$channels[] = 'console';
$channels[] = 'executions';
$channels[] = 'executions.' . $payload->getId();
$channels[] = 'functions.' . $payload->getAttribute('functionId');
$roles = $payload->getRead();
}
break;
case strpos($event, 'functions.deployments.') === 0:
$channels[] = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
break;
}
return [

View file

@ -205,7 +205,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertEquals($function['body']['name'], 'Test');
/**
* Test for FAILURE
*/
@ -281,7 +281,7 @@ class FunctionsCustomServerTest extends Scope
$folder = 'php';
$code = realpath(__DIR__ . '/../../../resources/functions'). "/$folder/code.tar.gz";
$this->packageCode($folder);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
@ -298,7 +298,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
// Wait for deployment to build.
sleep(15);
sleep(30);
return array_merge($data, ['deploymentId' => $deploymentId]);
}
@ -999,7 +999,7 @@ class FunctionsCustomServerTest extends Scope
$folder = 'python';
$code = realpath(__DIR__ . '/../../../resources/functions'). "/$folder/code.tar.gz";
$this->packageCode($folder);
$entrypoint = 'main.py';
$timeout = 2;
@ -1050,7 +1050,7 @@ class FunctionsCustomServerTest extends Scope
$executionId = $execution['body']['$id'] ?? '';
sleep(10);
sleep(30);
$executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions/'.$executionId, array_merge([
'content-type' => 'application/json',

View file

@ -1052,7 +1052,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(4, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains('executions', $response['data']['channels']);
$this->assertContains('executions.' . $execution['body']['$id'], $response['data']['channels']);
$this->assertContains('functions.' . $execution['body']['functionId'], $response['data']['channels']);
@ -1064,7 +1065,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $responseUpdate['type']);
$this->assertNotEmpty($responseUpdate['data']);
$this->assertArrayHasKey('timestamp', $responseUpdate['data']);
$this->assertCount(3, $responseUpdate['data']['channels']);
$this->assertCount(4, $responseUpdate['data']['channels']);
$this->assertContains('console', $responseUpdate['data']['channels']);
$this->assertContains('executions', $responseUpdate['data']['channels']);
$this->assertContains('executions.' . $execution['body']['$id'], $responseUpdate['data']['channels']);
$this->assertContains('functions.' . $execution['body']['functionId'], $responseUpdate['data']['channels']);