Skip to content

FOUR-15875 Optimize Script Tasks Execution with Nayra Engine #6942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions ProcessMaker/Console/Commands/BuildScriptExecutors.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Events\BuildScriptExecutor;
use ProcessMaker\Exception\InvalidDockerImageException;
use ProcessMaker\Facades\Docker;
use ProcessMaker\Models\ScriptExecutor;
use UnexpectedValueException;

class BuildScriptExecutors extends Command
{
Expand Down Expand Up @@ -157,6 +159,36 @@ public function buildExecutor()
$command = Docker::command() .
" build --build-arg SDK_DIR=./sdk -t {$image} -f {$packagePath}/Dockerfile.custom {$packagePath}";

$this->execCommand($command);

$isNayra = $scriptExecutor->language === 'nayra';
if ($isNayra) {
$instanceName = config('app.instance');
$this->info('Stop existing nayra container');
$this->execCommand(Docker::command() . " stop {$instanceName}_nayra 2>&1 || true");
$this->execCommand(Docker::command() . " rm {$instanceName}_nayra 2>&1 || true");
$this->info('Bring up the nayra container');
$this->execCommand(Docker::command() . ' run -d --name ' . $instanceName . '_nayra ' . $image);
$this->info('Get IP address of the nayra container');
$ip = '';
for ($i = 0; $i < 10; $i++) {
$ip = exec(Docker::command() . " inspect --format '{{ .NetworkSettings.IPAddress }}' {$instanceName}_nayra");
if ($ip) {
$this->info('Nayra container IP: ' . $ip);
Cache::forever('nayra_ips', [$ip]);
$this->sendEvent(0, 'done');
break;
}
sleep(1);
}
if (!$ip) {
throw new UnexpectedValueException('Could not get IP address of the nayra container');
}
}
}

private function execCommand(string $command)
{
if ($this->userId) {
$this->runProc(
$command,
Expand Down
145 changes: 145 additions & 0 deletions ProcessMaker/Models/ScriptDockerNayraTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace ProcessMaker\Models;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Exception\ScriptException;
use ProcessMaker\Facades\Docker;

/**
* Execute a docker container copying files to interchange information.
*/
trait ScriptDockerNayraTrait
{

/**
* Execute the script task using Nayra Docker.
*
* @return string|bool
*/
public function handleNayraDocker(string $code, array $data, array $config, $timeout, array $environmentVariables)
{
$envVariables = [];
foreach ($environmentVariables as $line) {
list($key, $value) = explode('=', $line, 2);
$envVariables[$key] = $value;
}
$params = [
'name' => uniqid('script_', true),
'script' => $code,
'data' => $data,
'config' => $config,
'envVariables' => $envVariables,
'timeout' => $timeout,
];
$body = json_encode($params);
$servers = Cache::get('nayra_ips');
if (!$servers) {
$url = config('app.nayra_rest_api_host') . '/run_script';
} else {
$index = array_rand($servers);
$url = 'http://' . $servers[$index] . ':8080/run_script';
$this->ensureNayraServerIsRunning('http://' . $servers[$index] . ':8080');
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($body),
]);
$result = curl_exec($ch);
curl_close($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpStatus !== 200) {
Log::error('Error executing script with Nayra Docker', [
'url' => $url,
'httpStatus' => $httpStatus,
'result' => $result,
]);
throw new ScriptException($result);
}
return $result;
}

/**
* Ensure that the Nayra server is running.
*
* @param string $url URL of the Nayra server
* @return void
* @throws ScriptException If cannot connect to Nayra Service
*/
private function ensureNayraServerIsRunning(string $url)
{
$header = @get_headers($url);
if (!$header) {
$this->bringUpNayra($url);
}
}

/**
* Bring up Nayra and check the provided URL.
*
* @param string $url The URL to check
* @return void
*/
private function bringUpNayra(string $url)
{
$docker = Docker::command();
$instanceName = config('app.instance');
$image = $this->scriptExecutor->dockerImageName();
exec($docker . " stop {$instanceName}_nayra 2>&1 || true");
exec($docker . " rm {$instanceName}_nayra 2>&1 || true");
exec($docker . ' run -d --name ' . $instanceName . '_nayra ' . $image . ' &', $output, $status);
if ($status) {
Log::error('Error starting Nayra Docker', [
'output' => $output,
'status' => $status,
]);
throw new ScriptException('Error starting Nayra Docker');
}
$this->waitContainerNetwork($docker, $instanceName);
$this->nayraServiceIsRunning($url);
}

/**
* Waits for the container network to be ready.
*
* @param Docker $docker The Docker instance.
* @param string $instanceName The name of the container instance.
* @return string The IP of the container.
*/
private function waitContainerNetwork($docker, $instanceName): string
{
$ip = '';
for ($i = 0; $i < 30; $i++) {
$ip = exec($docker . " inspect --format '{{ .NetworkSettings.IPAddress }}' {$instanceName}_nayra");
if ($ip) {
Cache::forever('nayra_ips', [$ip]);
return $ip;
}
sleep(1);
}
throw new ScriptException('Could not get address of the nayra container');
}

/**
* Checks if the Nayra service is running.
*
* @param string $url The URL of the Nayra service.
* @return bool Returns true if the Nayra service is running, false otherwise.
*/
private function nayraServiceIsRunning($url): bool
{
for ($i = 0; $i < 30; $i++) {
$status = @get_headers($url);
if ($status) {
return true;
}
sleep(1);
}
throw new ScriptException('Could not connect to the nayra container');
}
}
24 changes: 21 additions & 3 deletions ProcessMaker/ScriptRunners/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace ProcessMaker\ScriptRunners;

use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use ProcessMaker\GenerateAccessToken;
use ProcessMaker\Models\EnvironmentVariable;
use ProcessMaker\Models\ScriptDockerBindingFilesTrait;
use ProcessMaker\Models\ScriptDockerCopyingFilesTrait;
use ProcessMaker\Models\ScriptDockerNayraTrait;
use ProcessMaker\Models\ScriptExecutor;
use ProcessMaker\Models\User;
use RuntimeException;
Expand All @@ -15,6 +18,9 @@ abstract class Base
{
use ScriptDockerCopyingFilesTrait;
use ScriptDockerBindingFilesTrait;
use ScriptDockerNayraTrait;

const NAYRA_LANG = 'php-nayra';

private $tokenId = '';

Expand All @@ -38,7 +44,7 @@ abstract public function config($code, array $dockerConfig);
/**
* Set the script executor
*
* @var \ProcessMaker\Models\User
* @var \ProcessMaker\Models\ScriptExecutor
*/
private $scriptExecutor;

Expand Down Expand Up @@ -70,13 +76,25 @@ public function run($code, array $data, array $config, $timeout, ?User $user)
// Create tokens for the SDK if a user is set
$token = null;
if ($user) {
$token = new GenerateAccessToken($user);
$environmentVariables[] = 'API_TOKEN=' . $token->getToken();
$expires = Carbon::now()->addWeek();
$accessToken = Cache::remember('script-runner-' . $user->id, $expires, function () use ($user) {
$user->removeOldRunScriptTokens();
$token = new GenerateAccessToken($user);
return $token->getToken();
});
$environmentVariables[] = 'API_TOKEN=' . $accessToken;
$environmentVariables[] = 'API_HOST=' . config('app.docker_host_url') . '/api/1.0';
$environmentVariables[] = 'APP_URL=' . config('app.docker_host_url');
$environmentVariables[] = 'API_SSL_VERIFY=' . (config('app.api_ssl_verify') ? '1' : '0');
}

// Nayra Executor
$isNayra = $this->scriptExecutor->language === self::NAYRA_LANG;
if ($isNayra) {
$response = $this->handleNayraDocker($code, $data, $config, $timeout, $environmentVariables);
return json_decode($response, true);
}

if ($environmentVariables) {
$parameters = '-e ' . implode(' -e ', $environmentVariables);
} else {
Expand Down
11 changes: 11 additions & 0 deletions config/script-runners.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,15 @@
| https://github.com/ProcessMaker/docker-executor-node
|
*/
'php-nayra' => [
'name' => 'PHP (µService)',
'runner' => 'PhpRunner',
'mime_type' => 'application/x-php',
'options' => ['invokerPackage' => 'ProcessMaker\\Client'],
'init_dockerfile' => [
],
'package_path' => base_path('/docker-services/nayra'),
'package_version' => '1.0.0',
'sdk' => '',
],
];
2 changes: 2 additions & 0 deletions docker-services/nayra/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM processmaker4dev/nayra-engine:next
# The tag :next is temporal until the official release of the engine
9 changes: 5 additions & 4 deletions resources/js/processes/scripts/components/ScriptEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,9 @@
<div class="text-light bg-danger">
{{ preview.error.exception }}
</div>
<div class="text-light text-monospace small">
{{ preview.error.message }}
</div>
<pre class="text-light text-monospace small">{{
preview.error.message
}}</pre>
</div>
</div>
</b-collapse>
Expand Down Expand Up @@ -552,7 +552,7 @@ export default {
return this.packageAi && this.newCode !== "" && !this.changesApplied && !this.isDiffEditor;
},
language() {
return this.scriptExecutor.language;
return this.scriptExecutor.language === 'php-nayra' ? 'php' : this.scriptExecutor.language;
},
autosaveApiCall() {
return () => {
Expand Down Expand Up @@ -887,6 +887,7 @@ export default {
if (this.script.code === "[]") {
switch (this.script.language) {
case "php":
case "php-nayra":
this.code = Vue.filter("php")(this.boilerPlateTemplate);
break;
case "lua":
Expand Down
45 changes: 45 additions & 0 deletions upgrades/2024_06_12_150527_create_nayra_script_executor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Illuminate\Support\Facades\Log;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\ScriptExecutor;
use ProcessMaker\ScriptRunners\Base;
use ProcessMaker\Upgrades\UpgradeMigration as Upgrade;

class CreateNayraScriptExecutor extends Upgrade
{
/**
* Run the upgrade migration.
*
* @return void
*/
public function up()
{
$exists = ScriptExecutor::where('language', Base::NAYRA_LANG)->exists();
if (!$exists) {
$scriptExecutor = new ScriptExecutor();
$scriptExecutor->language = Base::NAYRA_LANG;
$scriptExecutor->title = 'Nayra (µService)';
$scriptExecutor->save();
}
}

/**
* Reverse the upgrade migration.
*
* @return void
*/
public function down()
{
try {
$existsScriptsUsingNayra = Script::where('language', Base::NAYRA_LANG)->exists();
if (!$existsScriptsUsingNayra) {
ScriptExecutor::where('language', Base::NAYRA_LANG)->delete();
} else {
Log::error('There are scripts using Nayra, so the Nayra script executor cannot be deleted.');
}
} catch (Exception $e) {
Log::error('Cannot delete Nayra script executor: ' . $e->getMessage());
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing "down" method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing method was added

Loading