| Current Path : /var/www/www.indacotrentino.com/www/app/code/Amasty/ImportExportCore/Parallelization/ |
| Current File : /var/www/www.indacotrentino.com/www/app/code/Amasty/ImportExportCore/Parallelization/JobManager.php |
<?php
declare(strict_types=1);
/**
* @author Amasty Team
* @copyright Copyright (c) Amasty (https://www.amasty.com)
* @package Import-Export Core for Magento 2 (System)
*/
namespace Amasty\ImportExportCore\Parallelization;
use Magento\Framework\App\ResourceConnection;
/**
* phpcs:ignoreFile
* @codeCoverageIgnore Can't be covered by unit/integration tests due to pcntl_fork() usage
*/
class JobManager
{
public const JOB_STATUS_DONE = 0;
public const JOB_STATUS_FAILED = 1;
public const JOB_STATUS_PROCESSING = 2;
public const DEFAULT_JOBS_LIMIT = 4;
/**
* @var array
*/
private $allPids = [];
/**
* @var array
*/
private $childrenSockets = [];
/**
* @var resource|null
*/
private $parentSocket;
/**
* @var array
*/
private $jobsInProgress = [];
/**
* @var ResourceConnection
*/
private $resourceConnection;
/**
* @var int
*/
private $maxJobs;
/**
* @var callable
*/
private $jobDoneCallback;
public function __construct(
ResourceConnection $resourceConnection,
callable $jobDoneCallback = null,
$maxJobs = self::DEFAULT_JOBS_LIMIT
) {
$this->resourceConnection = $resourceConnection;
$this->maxJobs = $maxJobs;
$this->jobDoneCallback = $jobDoneCallback;
}
/**
* Returns true is job manager functionality is available
*
* @return bool
*/
public static function isAvailable(): bool
{
return function_exists('pcntl_fork');
}
/**
* Forks the current process and returns PID of child process
*
* @return int
*/
public function fork(): int
{
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
$pid = \pcntl_fork();
if ($pid == -1) {
throw new \RuntimeException('Could not fork a child process.');
} elseif ($pid) {
$this->allPids[] = $pid;
$this->jobsInProgress[$pid] = $pid;
$this->resourceConnection->closeConnection();
$this->childrenSockets[$pid] = $sockets[0];
fclose($sockets[1]);
} else {
$this->parentSocket = $sockets[1];
fclose($sockets[0]);
}
return $pid;
}
/**
* Returns job status using PID
*
* @param int $pid
* @param bool $waitForTermination
* @return int
*/
public function getJobStatus(int $pid, bool $waitForTermination = false): int
{
switch (pcntl_waitpid($pid, $status, $waitForTermination ? 0 : WNOHANG)) {
case $pid:
$this->getChildReport($pid);
return pcntl_wexitstatus($status) === 0 ? self::JOB_STATUS_DONE : self::JOB_STATUS_FAILED;
case 0:
return self::JOB_STATUS_PROCESSING;
default:
return self::JOB_STATUS_FAILED;
}
}
/**
* Waiting for free slot when jobs count exceeds max jobs limit
*
* @return int
*/
public function waitForFreeSlot(): int
{
while (count($this->jobsInProgress) >= $this->maxJobs) {
foreach ($this->jobsInProgress as $pid) {
switch ($this->getJobStatus($pid)) {
case self::JOB_STATUS_DONE:
unset($this->jobsInProgress[$pid]);
return $pid;
case self::JOB_STATUS_FAILED:
throw new \RuntimeException('One of workers processes had failed.');
default:
continue 2;
}
}
sleep(1);
}
return 0;
}
public function waitForJobs(array $pids): \Generator
{
foreach ($pids as $pid) {
if (pcntl_waitpid($pid, $status) === -1) {
throw new \RuntimeException(
'Error while waiting for worker process; Status: ' . $status
);
}
$this->getChildReport($pid);
unset($this->jobsInProgress[$pid]);
yield $pid;
}
}
public function waitForJobCompletion(): \Generator
{
return $this->waitForJobs($this->jobsInProgress);
}
public function waitForAllJobs(): array
{
$result = [];
foreach ($this->waitForJobCompletion() as $pid) {
$result[] = $pid;
}
return $result;
}
/**
* Protect against Zombie children
*/
public function __destruct()
{
$this->waitForJobs($this->allPids);
}
public function reportToParent(string $message): JobManager
{
if ($this->parentSocket) {
fwrite($this->parentSocket, $message);
}
return $this;
}
protected function getChildReport(int $pid): ?string
{
if (empty($this->childrenSockets[$pid])) {
return null;
}
$report = fgets($this->childrenSockets[$pid]);
if (is_callable($this->jobDoneCallback)) {
call_user_func($this->jobDoneCallback, $report);
}
return $report ?: null;
}
}