Skip to content

Commit

Permalink
Implement blocking http php server for master
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Oct 22, 2024
1 parent 67f1c0d commit e9d99ca
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 172 deletions.
3 changes: 3 additions & 0 deletions docs/UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Upgrade a major version

## Upgrade from 7.x to 8.x


## Upgrade from 6.x to 7.x

### Step 1: Update deploy.php
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ parameters:
ignoreErrors:
- "#^Constant DEPLOYER_VERSION not found\\.$#"
- "#^Constant DEPLOYER_BIN not found\\.$#"
- "#^Constant MASTER_ENDPOINT not found\\.$#"
- "#CpanelPhp#"
- "#AMQPMessage#"

Expand Down
2 changes: 0 additions & 2 deletions src/Command/MainCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ protected function execute(Input $input, Output $output): int

if (!$plan) {
$this->checkUpdates();
$this->deployer->server->start();

if (!empty($skippedTasks)) {
foreach ($skippedTasks as $taskName) {
$output->writeln("<fg=yellow;options=bold>skip</> $taskName");
Expand Down
3 changes: 2 additions & 1 deletion src/Command/WorkerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!$output->isDecorated() && !defined('NO_ANSI')) {
define('NO_ANSI', 'true');
}
$this->deployer->config->set('master_url', 'http://localhost:' . $input->getOption('port'));

define('MASTER_ENDPOINT', 'http://localhost:' . $input->getOption('port'));

$task = $this->deployer->tasks->get($input->getOption('task'));
$host = $this->deployer->hosts->get($input->getOption('host'));
Expand Down
13 changes: 9 additions & 4 deletions src/Configuration/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace Deployer\Configuration;

use Deployer\Deployer;
use Deployer\Exception\ConfigurationException;
use Deployer\Utility\Httpie;

Expand Down Expand Up @@ -204,11 +205,13 @@ public function offsetUnset($offset): void

public function load(): void
{
if (!$this->has('master_url')) {
if (!Deployer::isWorker()) {
return;
}

$values = Httpie::get($this->get('master_url') . '/load')
$values = Httpie::get(MASTER_ENDPOINT . '/load')
->setopt(CURLOPT_CONNECTTIMEOUT, 0)
->setopt(CURLOPT_TIMEOUT, 0)
->jsonBody([
'host' => $this->get('alias'),
])
Expand All @@ -218,11 +221,13 @@ public function load(): void

public function save(): void
{
if (!$this->has('master_url')) {
if (!Deployer::isWorker()) {
return;
}

Httpie::get($this->get('master_url') . '/save')
Httpie::get(MASTER_ENDPOINT . '/save')
->setopt(CURLOPT_CONNECTTIMEOUT, 0)
->setopt(CURLOPT_TIMEOUT, 0)
->jsonBody([
'host' => $this->get('alias'),
'config' => $this->persist(),
Expand Down
29 changes: 10 additions & 19 deletions src/Deployer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
use Deployer\Configuration\Configuration;
use Deployer\Executor\Master;
use Deployer\Executor\Messenger;
use Deployer\Executor\Server;
use Deployer\Host\Host;
use Deployer\Host\HostCollection;
use Deployer\Host\Localhost;
Expand All @@ -37,7 +36,6 @@
use Deployer\Logger\Handler\NullHandler;
use Deployer\Logger\Logger;
use Deployer\Selector\Selector;
use Deployer\Task;
use Deployer\Task\ScriptManager;
use Deployer\Task\TaskCollection;
use Deployer\Utility\Httpie;
Expand Down Expand Up @@ -66,7 +64,6 @@
* @property ProcessRunner $processRunner
* @property Task\ScriptManager $scriptManager
* @property Selector $selector
* @property Server $server
* @property Master $master
* @property Messenger $messenger
* @property Messenger $logger
Expand All @@ -79,9 +76,8 @@ class Deployer extends Container
{
/**
* Global instance of deployer. It's can be accessed only after constructor call.
* @var Deployer
*/
private static $instance;
private static Deployer $instance;

public function __construct(Application $console)
{
Expand Down Expand Up @@ -163,17 +159,11 @@ public function __construct(Application $console)
$this['messenger'] = function ($c) {
return new Messenger($c['input'], $c['output'], $c['logger']);
};
$this['server'] = function ($c) {
return new Server(
$c['output'],
$this,
);
};
$this['master'] = function ($c) {
return new Master(
$c['hosts'],
$c['input'],
$c['output'],
$c['server'],
$c['messenger'],
);
};
Expand Down Expand Up @@ -351,21 +341,22 @@ public static function printException(OutputInterface $output, Throwable $except

public static function isWorker(): bool
{
return Deployer::get()->config->has('master_url');
return defined('MASTER_ENDPOINT');
}

/**
* @param mixed ...$arguments
* @return array|bool|string
* @throws \Exception
*/
public static function proxyCallToMaster(Host $host, string $func, ...$arguments)
public static function masterCall(Host $host, string $func, ...$arguments)
{
// As request to master will stop master permanently,
// wait a little bit in order for periodic timer of
// master gather worker outputs and print it to user.
usleep(100000); // Sleep 100ms.
return Httpie::get(get('master_url') . '/proxy')
// As request to master will stop master permanently, wait a little bit
// in order for ticker gather worker outputs and print it to user.
usleep(100_000); // Sleep 100ms.

return Httpie::get(MASTER_ENDPOINT . '/proxy')
->setopt(CURLOPT_CONNECTTIMEOUT, 0) // no timeout
->setopt(CURLOPT_TIMEOUT, 0) // no timeout
->jsonBody([
'host' => $host->getAlias(),
Expand Down
101 changes: 59 additions & 42 deletions src/Executor/Master.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
use Deployer\Component\Ssh\Client;
use Deployer\Component\Ssh\IOArguments;
use Deployer\Deployer;
use Deployer\Exception\Exception;
use Deployer\Host\Host;
use Deployer\Host\HostCollection;
use Deployer\Host\Localhost;
use Deployer\Selector\Selector;
use Deployer\Task\Context;
use Deployer\Task\Task;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand All @@ -32,40 +35,21 @@ function spinner(string $message = ''): string

class Master
{
/**
* @var InputInterface
*/
private $input;

/**
* @var OutputInterface
*/
private $output;

/**
* @var Server
*/
private $server;

/**
* @var Messenger
*/
private $messenger;

/**
* @var false|string
*/
private $phpBin;
private HostCollection $hosts;
private InputInterface $input;
private OutputInterface $output;
private Messenger $messenger;
private string|false $phpBin;

public function __construct(
HostCollection $hosts,
InputInterface $input,
OutputInterface $output,
Server $server,
Messenger $messenger,
) {
$this->hosts = $hosts;
$this->input = $input;
$this->output = $output;
$this->server = $server;
$this->messenger = $messenger;
$this->phpBin = (new PhpExecutableFinder())->find();
}
Expand Down Expand Up @@ -182,43 +166,76 @@ private function runTask(Task $task, array $hosts): int
return 0;
}

$callback = function (string $output) {
$output = preg_replace('/\n$/', '', $output);
if (strlen($output) !== 0) {
$this->output->writeln($output);
}
};
$server = new Server('127.0.0.1', 0, $this->output);

/** @var Process[] $processes */
$processes = [];

$this->server->loop->futureTick(function () use (&$processes, $hosts, $task) {
$server->afterRun(function (int $port) use (&$processes, $hosts, $task) {
foreach ($hosts as $host) {
$processes[] = $this->createProcess($host, $task);
$processes[] = $this->createProcess($host, $task, $port);
}

foreach ($processes as $process) {
$process->start();
}
});

$this->server->loop->addPeriodicTimer(0.03, function ($timer) use (&$processes, $callback) {
$this->gatherOutput($processes, $callback);
$echoCallback = function (string $output) {
$output = preg_replace('/\n$/', '', $output);
if (strlen($output) !== 0) {
$this->output->writeln($output);
}
};

$server->ticker(function () use (&$processes, $server, $echoCallback) {
$this->gatherOutput($processes, $echoCallback);
if ($this->output->isDecorated() && !getenv('CI')) {
$this->output->write(spinner());
}
if ($this->allFinished($processes)) {
$this->server->loop->stop();
$this->server->loop->cancelTimer($timer);
$server->stop();
}
});

$server->router(function (string $path, array $payload) {
switch ($path) {
case '/load':
['host' => $host] = $payload;

$host = $this->hosts->get($host);
$config = $host->config()->persist();

return new Response(200, $config);

case '/save':
['host' => $host, 'config' => $config] = $payload;

$host = $this->hosts->get($host);
$host->config()->update($config);

return new Response(200, true);

case '/proxy':
['host' => $host, 'func' => $func, 'arguments' => $arguments] = $payload;

Context::push(new Context($this->hosts->get($host)));
$answer = call_user_func($func, ...$arguments);
Context::pop();

return new Response(200, $answer);

default:
return new Response(404, null);
}
});

$this->server->loop->run();
$server->run();

if ($this->output->isDecorated() && !getenv('CI')) {
$this->output->write(" \r"); // clear spinner
}
$this->gatherOutput($processes, $callback);
$this->gatherOutput($processes, $echoCallback);

if ($this->cumulativeExitCode($processes) !== 0) {
$this->messenger->endTask($task, true);
Expand All @@ -227,11 +244,11 @@ private function runTask(Task $task, array $hosts): int
return $this->cumulativeExitCode($processes);
}

protected function createProcess(Host $host, Task $task): Process
protected function createProcess(Host $host, Task $task, int $port): Process
{
$command = [
$this->phpBin, DEPLOYER_BIN,
'worker', '--port', $this->server->getPort(),
'worker', '--port', $port,
'--task', $task,
'--host', $host->getAlias(),
];
Expand Down
33 changes: 33 additions & 0 deletions src/Executor/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/* (c) Anton Medvedev <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Deployer\Executor;

class Response
{
private int $status;
private mixed $body;

public function __construct(int $status, mixed $body)
{
$this->status = $status;
$this->body = $body;
}

public function getStatus(): int
{
return $this->status;
}

public function getBody(): mixed
{
return $this->body;
}
}
Loading

0 comments on commit e9d99ca

Please sign in to comment.