ADD Deployment csomagoló queue worker és scheduler újraindítás a teendők közé

A 2026-08-17-i eset általánosítása: a d2d/e2e Docker Swarmban ugyanazt a kódot több
service futtatja közös volume-ról, és a queue:work hosszan futó folyamat a betöltött
osztályokat a memóriájában tartja. A fájlmásolás után a worker RÉGI kóddal dolgozta fel
a jobot - a felület jónak látszott, a háttérfeladat némán a régi viselkedést hozta.

- config: 'runtime' minta (app/, config/, database/, routes/, bootstrap/, composer.lock),
  mert a restart nem csak a jobok változásakor kell; targetenként a service nevek és a
  Swarm manager elérése
- a felületen figyelmeztetés, a teendőkben a konkrét ssh + docker service update parancs
- a restart szándékosan a cache ürítés UTÁN: a frissen induló worker különben a régi,
  cache-elt konfigot töltené be
- a d2d schedulernél jelezzük, hogy 0 replikán fut (a restart üresjárat lehet), a t2t-nél
  pedig azt, hogy queue service még nincs, de a névkonvenció alapján t2t_queue lesz

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
E98Developer 2026-08-17 06:41:19 +02:00
parent 4bb89ae6a1
commit 2965f231ce
5 changed files with 167 additions and 4 deletions

View File

@ -135,6 +135,30 @@ private function warnings(array $copied, array $deleted, int $excludedCount, ?ar
];
}
if ($this->matching($copied, 'runtime') !== []) {
$jobs = count($this->matching($copied, 'job'));
$consoles = count($this->matching($copied, 'console'));
$detail = [];
if ($jobs > 0) {
$detail[] = $jobs.' job';
}
if ($consoles > 0) {
$detail[] = $consoles.' konzol parancs';
}
$warnings[] = [
'level' => 'warning',
'title' => 'Hosszan futó konténerek érintve'.($detail === [] ? '' : ' ('.implode(', ', $detail).')'),
'body' => 'A queue worker és a scheduler külön konténerben fut, és a betöltött osztályokat a memóriájában '
.'tartja - a fájlok felmásolása után is a RÉGI kóddal dolgozik, amíg újra nem indul. Ez némán rossz '
.'viselkedést okoz (a job "elkészül", de nem azt csinálja, amit az új kód). A konkrét restart parancsok '
.'a csomag _TEENDOK.md fájljában lesznek.',
];
}
if ($deleted !== []) {
$warnings[] = [
'level' => 'warning',

View File

@ -298,6 +298,9 @@ private function writeInstructions(string $path, array $manifest, array $target)
.' — mappa-összeolvasztás, NEM a teljes app mappa cseréje. A `_` kezdetű fájlokat ne másold fel.',
];
/** @var array<int, string> $restartSteps */
$restartSteps = [];
if ($manifest['deleted'] !== []) {
$steps[] = sprintf(
'Töröld a `_TORLENDO.txt`-ben felsorolt %d fájlt a célgépen. Nagyobb mennyiségnél a mellékelt '
@ -336,9 +339,31 @@ private function writeInstructions(string $path, array $manifest, array $target)
if (str_contains($warning['title'], 'Frontend')) {
$steps[] = 'A fordított assetek NINCSENEK a csomagban (`.gitignore`) — `npm run build` után kézzel másold fel a `public/build` tartalmát.';
}
// A restart lépéseket külön gyűjtjük: azoknak a cache ürítés UTÁN kell jönniük,
// különben a frissen induló worker még a régi, cache-elt konfigot tölti be.
if (str_contains($warning['title'], 'Hosszan futó')) {
$commands = $this->targets->restartCommands($target['key']);
if ($commands !== []) {
$restartSteps[] = 'Utolsó lépésként indítsd újra a hosszan futó konténereket. A queue worker és a '
.'scheduler **a memóriájában tartja a betöltött kódot**, ezért a fájlmásolás önmagában NEM elég — '
."enélkül a háttérfeladatok némán a régi viselkedést hozzák:\n\n"
." ```bash\n ".implode("\n ", $commands)."\n ```";
}
if ($target['services_note']) {
$restartSteps[] = $target['services_note'];
}
}
}
$steps[] = 'Ürítsd a cache-t: `php artisan config:clear && php artisan view:clear && php artisan route:clear`';
foreach ($restartSteps as $restartStep) {
$steps[] = $restartStep;
}
$steps[] = 'Ellenőrizd a kirakott verziót: `curl https://'.$target['domain'].'/deploy-version.json`'
.' — a commitnak `'.substr((string) $manifest['to'], 0, 7).'` kell lennie.';

View File

@ -11,18 +11,25 @@
class DeploymentTargets
{
/**
* @return array<string, array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}>
* @return array<string, array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string, services:array<string, string>, services_note:?string}>
*/
public function all(): array
{
/** @var array<string, array<string, string>> $targets */
/** @var array<string, array<string, mixed>> $targets */
$targets = (array) config('deployment.targets', []);
$resolved = [];
foreach ($targets as $key => $target) {
$resolved[$key] = array_merge(
['label' => $key, 'domain' => '', 'remote_path' => '', 'expected_branch' => null],
[
'label' => $key,
'domain' => '',
'remote_path' => '',
'expected_branch' => null,
'services' => [],
'services_note' => null,
],
$target,
['key' => $key],
);
@ -32,7 +39,34 @@ public function all(): array
}
/**
* @return array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null
* A célkörnyezet hosszan futó service-einek újraindító parancsai.
*
* @return array<int, string>
*/
public function restartCommands(?string $key): array
{
$target = $this->find($key);
if (! $target || $target['services'] === []) {
return [];
}
$user = (string) config('deployment.swarm.user', 'root');
$host = (string) config('deployment.swarm.host');
return array_values(array_map(
fn (string $service): string => sprintf(
'ssh %s@%s "docker service update --force %s"',
$user,
$host,
$service,
),
$target['services'],
));
}
/**
* @return array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string, services:array<string, string>, services_note:?string}|null
*/
public function find(?string $key): ?array
{

View File

@ -98,6 +98,41 @@
'package.json',
'package-lock.json',
],
/*
| A queue worker és a scheduler hosszan futó PHP folyamat: a már betöltött
| osztályokat a memóriájában tartja, ezért a fájlok felmásolása után is a RÉGI
| kóddal dolgozik, amíg újra nem indul. Ez némán rossz viselkedést okoz (2026-08-17,
| árlista végrehajtás: a job "elkészült" státuszt adott anélkül, hogy bármit írt volna),
| ezért minden háttérben is futó kód változása restartot igényel - nem csak a jobok.
*/
'runtime' => [
'app/*',
'config/*',
'database/*',
'routes/*',
'bootstrap/*',
'composer.lock',
],
/* Csak a figyelmeztetés szövegének pontosításához, a restart ettől függetlenül kell. */
'job' => ['app/Jobs/*'],
'console' => ['app/Console/*'],
],
/*
|--------------------------------------------------------------------------
| Docker Swarm
|--------------------------------------------------------------------------
|
| A restart parancsokat a Swarm leader node-on keresztül kell kiadni
| (ld. docs/cicd-context.md).
|
*/
'swarm' => [
'host' => env('DEPLOYMENT_SWARM_HOST', '46.29.140.234'),
'user' => env('DEPLOYMENT_SWARM_USER', 'root'),
],
/*
@ -121,12 +156,23 @@
'domain' => 'd2d.emegrendeles.hu',
'remote_path' => '/delirest/d2d.emegrendeles.hu/app/',
'expected_branch' => 'test',
// Ugyanabból az imageből, közös volume-ról több service is futtatja a kódot.
// A web konténer (d2d-environment_app) kérésenként újratölti, ezért az nem kell ide.
'services' => [
'queue' => 'd2d-environment_queue',
'scheduler' => 'd2d-environment_scheduler',
],
'services_note' => 'A d2d scheduler jelenleg 0 replikán fut — ha leskálázva marad, a restartja üresjárat.',
],
'e2e' => [
'label' => 'e2e — t2t éles',
'domain' => 'e2e.emegrendeles.hu',
'remote_path' => '/delirest/test.t2t.emegrendeles.hu/app/',
'expected_branch' => 'main',
'services' => [
'scheduler' => 't2t_scheduler',
],
'services_note' => 'A t2t-n még nincs queue worker service; ha létrejön, a névkonvenció alapján t2t_queue lesz, és azt is újra kell majd indítani.',
],
],

View File

@ -265,6 +265,40 @@ function deploymentCleanup(string $path): void
deploymentCleanup($repository['path']);
});
test('a teendők kérik a queue worker és a scheduler újraindítását, a cache ürítés után', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();
[$first, , $third] = $repository['commits'];
$package = app(DeploymentPackageBuilder::class)->build('d2d', 'test', $first, $third);
$teendok = File::get($package['path'].'/_TEENDOK.md');
expect($teendok)->toContain('docker service update --force d2d-environment_queue')
->and($teendok)->toContain('docker service update --force d2d-environment_scheduler')
// A 0 replikás scheduler miatt tudni kell, hogy a restart üresjárat lehet.
->and($teendok)->toContain('0 replikán fut');
// A restartnak a cache ürítés UTÁN kell jönnie: a frissen induló worker különben
// a régi, cache-elt konfigot töltené be.
expect(strpos($teendok, 'docker service update'))->toBeGreaterThan(strpos($teendok, 'config:clear'));
deploymentCleanup($repository['path']);
});
test('a queue worker nélküli környezetnél is kiderül, mit kell majd újraindítani', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();
[$first, , $third] = $repository['commits'];
$package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third);
$teendok = File::get($package['path'].'/_TEENDOK.md');
expect($teendok)->toContain('docker service update --force t2t_scheduler')
->and($teendok)->toContain('t2t_queue');
deploymentCleanup($repository['path']);
});
test('a teendők között szerepel a seeder futtatása a konkrét osztálynévvel', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();