Skip to content

Commit 503049d

Browse files
committed
add supervisor config command
1 parent 9ec67ae commit 503049d

7 files changed

Lines changed: 352 additions & 0 deletions

File tree

src/Messenger/Kernel/CommandBusDependencies.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ enum CommandBusDependencies: string {
99
case Logger = self::class.'::Logger';
1010
case EventDispatcher = self::class.'::EventDispatcher';
1111
case Serializer = self::class.'::Serializer';
12+
case Worker = self::class.'::Worker';
13+
case SupervisorConfigDir = self::class.'::SupervisorConfigDir';
1214
}

src/Messenger/Kernel/MessengerServiceFactory.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Psr\Log\LoggerInterface;
1414
use Psr\Log\NullLogger;
1515
use Symfony\Component\Cache\Adapter\ArrayAdapter;
16+
use Symfony\Component\Console\Application;
1617
use Symfony\Component\EventDispatcher\EventDispatcher;
1718
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
1819
use Symfony\Component\Messenger\Command\StopWorkersCommand;
@@ -31,6 +32,8 @@
3132
use WonderNetwork\SlimKernel\Messenger\QueryBus;
3233
use WonderNetwork\SlimKernel\ServiceFactory;
3334
use WonderNetwork\SlimKernel\ServicesBuilder;
35+
use WonderNetwork\SlimKernel\Supervisor\GenerateSupervisorConfigCommand;
36+
use WonderNetwork\SlimKernel\Supervisor\SupervisorConfiguration;
3437
use function DI\autowire;
3538
use function DI\create;
3639
use function DI\factory;
@@ -41,10 +44,12 @@
4144
public function __construct(
4245
private string $commandPath = '/src/Application/Command/**/*Handler.php',
4346
private string $queryPath = '/src/Application/Query/**/*Handler.php',
47+
private string $supervisorConfigDir = 'app/supervisor',
4448
private null|Closure|Reference|DefinitionHelper|TransportLocatorBuilder $transports = null,
4549
private null|Closure|Reference|DefinitionHelper|EventDispatcher $eventDispatcher = null,
4650
private null|Closure|Reference|DefinitionHelper|LoggerInterface $logger = null,
4751
private null|Closure|Reference|DefinitionHelper|CacheItemPoolInterface $cachePool = null,
52+
private null|Closure|Reference|DefinitionHelper|SupervisorConfiguration $programs = null,
4853
) {
4954
}
5055

@@ -126,5 +131,29 @@ public function __invoke(ServicesBuilder $builder): iterable {
126131
yield StopWorkersCommand::class => autowire()->constructor(
127132
restartSignalCachePool: get(CommandBusDependencies::CachePool->value),
128133
);
134+
135+
yield CommandBusDependencies::SupervisorConfigDir->value => $this->supervisorConfigDir;
136+
yield SupervisorConfiguration::class => $this->programs ?? SupervisorConfiguration::empty();
137+
yield GenerateSupervisorConfigCommand::class => autowire()->constructor(
138+
configDir: get(CommandBusDependencies::SupervisorConfigDir->value),
139+
);
140+
141+
yield CommandBusDependencies::Worker->value => function (ContainerInterface $container) {
142+
$app = new Application('worker');
143+
/** @var EventDispatcher $eventDispatcher */
144+
$eventDispatcher = $container->get(CommandBusDependencies::EventDispatcher->value);
145+
146+
$app->setDispatcher($eventDispatcher);
147+
$app->addCommands(
148+
[
149+
$container->get(StopWorkersCommand::class),
150+
$container->get(ConsumeMessagesCommand::class),
151+
$container->get(GenerateSupervisorConfigCommand::class),
152+
],
153+
);
154+
$app->setAutoExit(false);
155+
156+
return $app;
157+
};
129158
}
130159
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WonderNetwork\SlimKernel\Supervisor;
6+
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputArgument;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Input\InputOption;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Symfony\Component\Console\Style\SymfonyStyle;
13+
use WonderNetwork\SlimKernel\Cli\InitializeInputParamsTrait;
14+
15+
final class GenerateSupervisorConfigCommand extends Command {
16+
use InitializeInputParamsTrait;
17+
18+
public function __construct(
19+
private readonly string $configDir,
20+
private readonly SupervisorConfiguration $configuration,
21+
) {
22+
parent::__construct('supervisor:generate-config');
23+
}
24+
25+
protected function configure(): void {
26+
$this->addArgument(
27+
name: 'current-directory',
28+
mode: InputArgument::REQUIRED,
29+
description: 'The absolute directory to the script',
30+
);
31+
32+
$this->addOption(
33+
name: 'stdio',
34+
mode: InputOption::VALUE_NONE,
35+
description: "Log to stdout/stderr instead of files",
36+
);
37+
38+
$this->addOption(
39+
name: 'purge',
40+
mode: InputOption::VALUE_NONE | InputOption::VALUE_NEGATABLE,
41+
description: 'Remove all files before generating new ones',
42+
);
43+
44+
$this->addOption(
45+
name: 'logfile',
46+
mode: InputOption::VALUE_REQUIRED,
47+
default: '/var/log/supervisor/{processName}.{suffix}.log',
48+
);
49+
50+
$this->addOption(
51+
name: 'logfile-maxbytes',
52+
mode: InputOption::VALUE_REQUIRED,
53+
default: "50MB",
54+
);
55+
56+
$this->addOption(
57+
name: 'config-dir',
58+
mode: InputOption::VALUE_REQUIRED,
59+
default: $this->configDir,
60+
);
61+
}
62+
63+
protected function execute(InputInterface $input, OutputInterface $output): int {
64+
$io = new SymfonyStyle($input, $output);
65+
66+
$currentDirectory = $this->params->arguments->string('current-directory');
67+
$stdio = $this->params->options->bool('stdio');
68+
$noPurge = $this->params->options->bool('no-purge');
69+
$logfile = $this->params->options->string('logfile');
70+
$maxBytes = $this->params->options->string('logfile-maxbytes');
71+
$configDir = $this->params->options->string('config-dir');
72+
73+
if ("" === $configDir) {
74+
$io->error("Config directory can't be empty");
75+
76+
return self::FAILURE;
77+
}
78+
79+
if (false === $noPurge) {
80+
foreach (glob(sprintf('%s/*.conf', $configDir)) ?: [] as $preExistingConfig) {
81+
unlink($preExistingConfig);
82+
}
83+
}
84+
85+
if ($stdio) {
86+
$maxBytes = "0";
87+
$logfile = '/dev/std{suffix}';
88+
}
89+
90+
foreach ($this->configuration->programs as $program) {
91+
$concurrency = $program->concurrency;
92+
$processName = $program->name;
93+
94+
if ($concurrency > 1) {
95+
$processName = '%(program_name)s_%(process_num)02d';
96+
}
97+
98+
$errorLog = strtr($logfile, ['{processName}' => $processName, '{suffix}' => 'err']);
99+
$standardLog = strtr($logfile, ['{processName}' => $processName, '{suffix}' => 'out']);
100+
101+
$fullCommand = sprintf('%s/%s', rtrim($currentDirectory, '/'), $program->command);
102+
$supervisorConfigPath = sprintf('%s/%s.conf', $configDir, $program->name);
103+
104+
$supervisorConfig = <<<EOF
105+
[program:$program->name]
106+
command=$fullCommand
107+
process_name=$processName
108+
numprocs=$concurrency
109+
user=www-data
110+
autostart=true
111+
autorestart=true
112+
stderr_logfile=$errorLog
113+
stderr_logfile_maxbytes=$maxBytes
114+
stdout_logfile=$standardLog
115+
stdout_logfile_maxbytes=$maxBytes
116+
EOF;
117+
118+
file_put_contents($supervisorConfigPath, $supervisorConfig);
119+
$io->writeln(
120+
sprintf(
121+
'Writing <comment>%s</comment> configuration file to <info>%s</info>',
122+
$program->name,
123+
$supervisorConfigPath,
124+
),
125+
);
126+
}
127+
128+
return self::SUCCESS;
129+
}
130+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WonderNetwork\SlimKernel\Supervisor;
6+
7+
final readonly class SupervisorConfiguration {
8+
public static function empty(): self {
9+
return new self([]);
10+
}
11+
12+
public static function start(): self {
13+
return new self([]);
14+
}
15+
16+
/**
17+
* @param list<SupervisorProgram> $programs
18+
*/
19+
public function __construct(public array $programs) {
20+
}
21+
22+
public function withPrograms(SupervisorProgram ...$programs): self {
23+
return new self([...$this->programs, ...array_values($programs)]);
24+
}
25+
26+
public function withSimpleCommand(string $name, string $command): self {
27+
return new self([...$this->programs, SupervisorProgram::single($name, $command)]);
28+
}
29+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WonderNetwork\SlimKernel\Supervisor;
6+
7+
final readonly class SupervisorProgram {
8+
public static function single(string $name, string $command): self {
9+
return new self(
10+
name: $name,
11+
command: $command,
12+
concurrency: 1,
13+
);
14+
}
15+
16+
/**
17+
* @param positive-int $concurrency
18+
*/
19+
public function __construct(
20+
public string $name,
21+
public string $command,
22+
public int $concurrency,
23+
) {
24+
}
25+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.conf
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WonderNetwork\SlimKernel\Supervisor;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Symfony\Component\Console\Input\ArgvInput;
9+
use Symfony\Component\Console\Output\BufferedOutput;
10+
11+
final class GenerateSupervisorConfigCommandTest extends TestCase {
12+
public function testGeneratesConfig(): void {
13+
$programs = SupervisorConfiguration::start()
14+
->withSimpleCommand('worker', 'bin/worker async')
15+
->withPrograms(
16+
new SupervisorProgram(
17+
name: 'jobs',
18+
command: 'bin/worker jobs',
19+
concurrency: 3,
20+
),
21+
);
22+
$configDir = __DIR__.'/../Resources/Supervisor';
23+
$sut = new GenerateSupervisorConfigCommand(
24+
configDir: $configDir,
25+
configuration: $programs,
26+
);
27+
28+
$output = new BufferedOutput();
29+
$argv = [(string) $sut->getName(), '--config-dir', $configDir, '/var/app/current'];
30+
$sut->run(new ArgvInput($argv), $output);
31+
32+
self::assertSame(
33+
<<<EOF
34+
Writing worker configuration file to $configDir/worker.conf
35+
Writing jobs configuration file to $configDir/jobs.conf
36+
EOF,
37+
trim($output->fetch()),
38+
);
39+
40+
$files = glob($configDir.'/*.conf') ?: [];
41+
sort($files);
42+
43+
self::assertSame(["$configDir/jobs.conf", "$configDir/worker.conf"], $files);
44+
self::assertSame(
45+
<<<EOF
46+
[program:jobs]
47+
command=/var/app/current/bin/worker jobs
48+
process_name=%(program_name)s_%(process_num)02d
49+
numprocs=3
50+
user=www-data
51+
autostart=true
52+
autorestart=true
53+
stderr_logfile=/var/log/supervisor/%(program_name)s_%(process_num)02d.err.log
54+
stderr_logfile_maxbytes=50MB
55+
stdout_logfile=/var/log/supervisor/%(program_name)s_%(process_num)02d.out.log
56+
stdout_logfile_maxbytes=50MB
57+
[program:worker]
58+
command=/var/app/current/bin/worker async
59+
process_name=worker
60+
numprocs=1
61+
user=www-data
62+
autostart=true
63+
autorestart=true
64+
stderr_logfile=/var/log/supervisor/worker.err.log
65+
stderr_logfile_maxbytes=50MB
66+
stdout_logfile=/var/log/supervisor/worker.out.log
67+
stdout_logfile_maxbytes=50MB
68+
EOF,
69+
implode(
70+
"\n",
71+
array_map(
72+
static fn (string $filename) => file_get_contents($filename),
73+
$files,
74+
),
75+
),
76+
);
77+
}
78+
79+
public function testGeneratesConfigWithStdio(): void {
80+
$programs = SupervisorConfiguration::start()
81+
->withSimpleCommand('worker', 'bin/worker async')
82+
->withPrograms(
83+
new SupervisorProgram(
84+
name: 'jobs',
85+
command: 'bin/worker jobs',
86+
concurrency: 3,
87+
),
88+
);
89+
$configDir = __DIR__.'/../Resources/Supervisor';
90+
$sut = new GenerateSupervisorConfigCommand(
91+
configDir: $configDir,
92+
configuration: $programs,
93+
);
94+
95+
$output = new BufferedOutput();
96+
$argv = [(string) $sut->getName(), '--stdio', '--config-dir', $configDir, '/var/app/current'];
97+
$sut->run(new ArgvInput($argv), $output);
98+
99+
$files = glob($configDir.'/*.conf') ?: [];
100+
sort($files);
101+
102+
self::assertSame(
103+
<<<EOF
104+
[program:jobs]
105+
command=/var/app/current/bin/worker jobs
106+
process_name=%(program_name)s_%(process_num)02d
107+
numprocs=3
108+
user=www-data
109+
autostart=true
110+
autorestart=true
111+
stderr_logfile=/dev/stderr
112+
stderr_logfile_maxbytes=0
113+
stdout_logfile=/dev/stdout
114+
stdout_logfile_maxbytes=0
115+
[program:worker]
116+
command=/var/app/current/bin/worker async
117+
process_name=worker
118+
numprocs=1
119+
user=www-data
120+
autostart=true
121+
autorestart=true
122+
stderr_logfile=/dev/stderr
123+
stderr_logfile_maxbytes=0
124+
stdout_logfile=/dev/stdout
125+
stdout_logfile_maxbytes=0
126+
EOF,
127+
implode(
128+
"\n",
129+
array_map(
130+
static fn (string $filename) => file_get_contents($filename),
131+
$files,
132+
),
133+
),
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)