Skip to content

Commit 48d0d30

Browse files
authored
ENGCOM-5150: [Backport] Implement Better Error Handling and Fix Waits on Null PIDs in Parallel SCD Execution #22610
2 parents d4707f3 + e9f1af2 commit 48d0d30

File tree

1 file changed

+88
-14
lines changed

1 file changed

+88
-14
lines changed

app/code/Magento/Deploy/Process/Queue.php

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
declare(strict_types=1);
7+
68
namespace Magento\Deploy\Process;
79

810
use Magento\Deploy\Package\Package;
911
use Magento\Deploy\Service\DeployPackage;
1012
use Magento\Framework\App\ResourceConnection;
11-
use Psr\Log\LoggerInterface;
1213
use Magento\Framework\App\State as AppState;
1314
use Magento\Framework\Locale\ResolverInterface as LocaleResolver;
15+
use Psr\Log\LoggerInterface;
1416

1517
/**
1618
* Deployment Queue
@@ -125,6 +127,8 @@ public function __construct(
125127
}
126128

127129
/**
130+
* Adds deployment package.
131+
*
128132
* @param Package $package
129133
* @param Package[] $dependencies
130134
* @return bool true on success
@@ -140,6 +144,8 @@ public function add(Package $package, array $dependencies = [])
140144
}
141145

142146
/**
147+
* Returns packages array.
148+
*
143149
* @return Package[]
144150
*/
145151
public function getPackages()
@@ -159,9 +165,11 @@ public function process()
159165
$packages = $this->packages;
160166
while (count($packages) && $this->checkTimeout()) {
161167
foreach ($packages as $name => $packageJob) {
168+
// Unsets each member of $packages array (passed by reference) as each is executed
162169
$this->assertAndExecute($name, $packages, $packageJob);
163170
}
164171
$this->logger->info('.');
172+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
165173
sleep(3);
166174
foreach ($this->inProgress as $name => $package) {
167175
if ($this->isDeployed($package)) {
@@ -182,8 +190,6 @@ public function process()
182190
* @param array $packages
183191
* @param array $packageJob
184192
* @return void
185-
*
186-
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
187193
*/
188194
private function assertAndExecute($name, array & $packages, array $packageJob)
189195
{
@@ -207,13 +213,23 @@ private function assertAndExecute($name, array & $packages, array $packageJob)
207213
}
208214
}
209215
}
216+
$this->executePackage($package, $name, $packages, $dependenciesNotFinished);
217+
}
210218

219+
/**
220+
* Executes deployment package.
221+
*
222+
* @param Package $package
223+
* @param string $name
224+
* @param array $packages
225+
* @param bool $dependenciesNotFinished
226+
* @return void
227+
*/
228+
private function executePackage(Package $package, string $name, array &$packages, bool $dependenciesNotFinished)
229+
{
211230
if (!$dependenciesNotFinished
212231
&& !$this->isDeployed($package)
213-
&& (
214-
$this->maxProcesses < 2
215-
|| (count($this->inProgress) < $this->maxProcesses)
216-
)
232+
&& ($this->maxProcesses < 2 || (count($this->inProgress) < $this->maxProcesses))
217233
) {
218234
unset($packages[$name]);
219235
$this->execute($package);
@@ -234,6 +250,7 @@ private function awaitForAllProcesses()
234250
}
235251
}
236252
$this->logger->info('.');
253+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
237254
sleep(5);
238255
}
239256
if ($this->isCanBeParalleled()) {
@@ -243,6 +260,8 @@ private function awaitForAllProcesses()
243260
}
244261

245262
/**
263+
* Checks if can be parallel.
264+
*
246265
* @return bool
247266
*/
248267
private function isCanBeParalleled()
@@ -251,9 +270,12 @@ private function isCanBeParalleled()
251270
}
252271

253272
/**
273+
* Executes the process.
274+
*
254275
* @param Package $package
255276
* @return bool true on success for main process and exit for child process
256277
* @SuppressWarnings(PHPMD.ExitExpression)
278+
* @throws \RuntimeException
257279
*/
258280
private function execute(Package $package)
259281
{
@@ -281,6 +303,7 @@ function () use ($package) {
281303
);
282304

283305
if ($this->isCanBeParalleled()) {
306+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
284307
$pid = pcntl_fork();
285308
if ($pid === -1) {
286309
throw new \RuntimeException('Unable to fork a new process');
@@ -295,6 +318,7 @@ function () use ($package) {
295318
// process child process
296319
$this->inProgress = [];
297320
$this->deployPackageService->deploy($package, $this->options, true);
321+
// phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage
298322
exit(0);
299323
} else {
300324
$this->deployPackageService->deploy($package, $this->options);
@@ -303,19 +327,50 @@ function () use ($package) {
303327
}
304328

305329
/**
330+
* Checks if package is deployed.
331+
*
306332
* @param Package $package
307333
* @return bool
308334
*/
309335
private function isDeployed(Package $package)
310336
{
311337
if ($this->isCanBeParalleled()) {
312338
if ($package->getState() === null) {
313-
$pid = pcntl_waitpid($this->getPid($package), $status, WNOHANG);
314-
if ($pid === $this->getPid($package)) {
339+
$pid = $this->getPid($package);
340+
341+
// When $pid comes back as null the child process for this package has not yet started; prevents both
342+
// hanging until timeout expires (which was behaviour in 2.2.x) and the type error from strict_types
343+
if ($pid === null) {
344+
return false;
345+
}
346+
347+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
348+
$result = pcntl_waitpid($pid, $status, WNOHANG);
349+
if ($result === $pid) {
315350
$package->setState(Package::STATE_COMPLETED);
351+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
352+
$exitStatus = pcntl_wexitstatus($status);
353+
354+
$this->logger->info(
355+
"Exited: " . $package->getPath() . "(status: $exitStatus)",
356+
[
357+
'process' => $package->getPath(),
358+
'status' => $exitStatus,
359+
]
360+
);
316361

317362
unset($this->inProgress[$package->getPath()]);
363+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
318364
return pcntl_wexitstatus($status) === 0;
365+
} elseif ($result === -1) {
366+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
367+
$errno = pcntl_errno();
368+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
369+
$strerror = pcntl_strerror($errno);
370+
371+
throw new \RuntimeException(
372+
"Error encountered checking child process status (PID: $pid): $strerror (errno: $errno)"
373+
);
319374
}
320375
return false;
321376
}
@@ -324,17 +379,19 @@ private function isDeployed(Package $package)
324379
}
325380

326381
/**
382+
* Returns process ID or null if not found.
383+
*
327384
* @param Package $package
328385
* @return int|null
329386
*/
330387
private function getPid(Package $package)
331388
{
332-
return isset($this->processIds[$package->getPath()])
333-
? $this->processIds[$package->getPath()]
334-
: null;
389+
return $this->processIds[$package->getPath()] ?? null;
335390
}
336391

337392
/**
393+
* Checks timeout.
394+
*
338395
* @return bool
339396
*/
340397
private function checkTimeout()
@@ -347,14 +404,31 @@ private function checkTimeout()
347404
*
348405
* Protect against zombie process
349406
*
407+
* @throws \RuntimeException
408+
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
350409
* @return void
351410
*/
352411
public function __destruct()
353412
{
354413
foreach ($this->inProgress as $package) {
355-
if (pcntl_waitpid($this->getPid($package), $status) === -1) {
414+
$pid = $this->getPid($package);
415+
$this->logger->info(
416+
"Reaping child process: {$package->getPath()} (PID: $pid)",
417+
[
418+
'process' => $package->getPath(),
419+
'pid' => $pid,
420+
]
421+
);
422+
423+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
424+
if (pcntl_waitpid($pid, $status) === -1) {
425+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
426+
$errno = pcntl_errno();
427+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
428+
$strerror = pcntl_strerror($errno);
429+
356430
throw new \RuntimeException(
357-
'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status
431+
"Error encountered waiting for child process (PID: $pid): $strerror (errno: $errno)"
358432
);
359433
}
360434
}

0 commit comments

Comments
 (0)