Skip to content

Commit 1d9e07b

Browse files
authored
Merge pull request #4577 from magento-thunder/MC-18477
Fixed issues: - MC-18477: Deadlocks when consumers run from cron
2 parents e50fe6f + a842b18 commit 1d9e07b

File tree

8 files changed

+198
-422
lines changed

8 files changed

+198
-422
lines changed

app/code/Magento/MessageQueue/Console/StartConsumerCommand.php

+38-21
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Symfony\Component\Console\Input\InputOption;
1212
use Symfony\Component\Console\Output\OutputInterface;
1313
use Magento\Framework\MessageQueue\ConsumerFactory;
14-
use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager;
14+
use Magento\Framework\Lock\LockManagerInterface;
1515

1616
/**
1717
* Command for starting MessageQueue consumers.
@@ -22,6 +22,7 @@ class StartConsumerCommand extends Command
2222
const OPTION_NUMBER_OF_MESSAGES = 'max-messages';
2323
const OPTION_BATCH_SIZE = 'batch-size';
2424
const OPTION_AREACODE = 'area-code';
25+
const OPTION_SINGLE_THREAD = 'single-thread';
2526
const PID_FILE_PATH = 'pid-file-path';
2627
const COMMAND_QUEUE_CONSUMERS_START = 'queue:consumers:start';
2728

@@ -36,9 +37,9 @@ class StartConsumerCommand extends Command
3637
private $appState;
3738

3839
/**
39-
* @var PidConsumerManager
40+
* @var LockManagerInterface
4041
*/
41-
private $pidConsumerManager;
42+
private $lockManager;
4243

4344
/**
4445
* StartConsumerCommand constructor.
@@ -47,54 +48,60 @@ class StartConsumerCommand extends Command
4748
* @param \Magento\Framework\App\State $appState
4849
* @param ConsumerFactory $consumerFactory
4950
* @param string $name
50-
* @param PidConsumerManager $pidConsumerManager
51+
* @param LockManagerInterface $lockManager
5152
*/
5253
public function __construct(
5354
\Magento\Framework\App\State $appState,
5455
ConsumerFactory $consumerFactory,
5556
$name = null,
56-
PidConsumerManager $pidConsumerManager = null
57+
LockManagerInterface $lockManager = null
5758
) {
5859
$this->appState = $appState;
5960
$this->consumerFactory = $consumerFactory;
60-
$this->pidConsumerManager = $pidConsumerManager ?: \Magento\Framework\App\ObjectManager::getInstance()
61-
->get(PidConsumerManager::class);
61+
$this->lockManager = $lockManager ?: \Magento\Framework\App\ObjectManager::getInstance()
62+
->get(LockManagerInterface::class);
6263
parent::__construct($name);
6364
}
6465

6566
/**
66-
* {@inheritdoc}
67+
* @inheritdoc
6768
*/
6869
protected function execute(InputInterface $input, OutputInterface $output)
6970
{
7071
$consumerName = $input->getArgument(self::ARGUMENT_CONSUMER);
7172
$numberOfMessages = $input->getOption(self::OPTION_NUMBER_OF_MESSAGES);
7273
$batchSize = (int)$input->getOption(self::OPTION_BATCH_SIZE);
7374
$areaCode = $input->getOption(self::OPTION_AREACODE);
74-
$pidFilePath = $input->getOption(self::PID_FILE_PATH);
7575

76-
if ($pidFilePath && $this->pidConsumerManager->isRun($pidFilePath)) {
77-
$output->writeln('<error>Consumer with the same PID is running</error>');
78-
return \Magento\Framework\Console\Cli::RETURN_FAILURE;
76+
if ($input->getOption(self::PID_FILE_PATH)) {
77+
$input->setOption(self::OPTION_SINGLE_THREAD, true);
7978
}
8079

81-
if ($pidFilePath) {
82-
$this->pidConsumerManager->savePid($pidFilePath);
80+
$singleThread = $input->getOption(self::OPTION_SINGLE_THREAD);
81+
82+
if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore
83+
$output->writeln('<error>Consumer with the same name is running</error>');
84+
return \Magento\Framework\Console\Cli::RETURN_FAILURE;
8385
}
8486

85-
if ($areaCode !== null) {
86-
$this->appState->setAreaCode($areaCode);
87-
} else {
88-
$this->appState->setAreaCode('global');
87+
if ($singleThread) {
88+
$this->lockManager->lock(md5($consumerName)); //phpcs:ignore
8989
}
9090

91+
$this->appState->setAreaCode($areaCode ?? 'global');
92+
9193
$consumer = $this->consumerFactory->get($consumerName, $batchSize);
9294
$consumer->process($numberOfMessages);
95+
96+
if ($singleThread) {
97+
$this->lockManager->unlock(md5($consumerName)); //phpcs:ignore
98+
}
99+
93100
return \Magento\Framework\Console\Cli::RETURN_SUCCESS;
94101
}
95102

96103
/**
97-
* {@inheritdoc}
104+
* @inheritdoc
98105
*/
99106
protected function configure()
100107
{
@@ -125,11 +132,17 @@ protected function configure()
125132
'The preferred area (global, adminhtml, etc...) '
126133
. 'default is global.'
127134
);
135+
$this->addOption(
136+
self::OPTION_SINGLE_THREAD,
137+
null,
138+
InputOption::VALUE_NONE,
139+
'This option prevents running multiple copies of one consumer simultaneously.'
140+
);
128141
$this->addOption(
129142
self::PID_FILE_PATH,
130143
null,
131144
InputOption::VALUE_REQUIRED,
132-
'The file path for saving PID'
145+
'The file path for saving PID (This option is deprecated, use --single-thread instead)'
133146
);
134147
$this->setHelp(
135148
<<<HELP
@@ -150,8 +163,12 @@ protected function configure()
150163
To specify the preferred area:
151164
152165
<comment>%command.full_name% someConsumer --area-code='adminhtml'</comment>
166+
167+
To do not run multiple copies of one consumer simultaneously:
168+
169+
<comment>%command.full_name% someConsumer --single-thread'</comment>
153170
154-
To save PID enter path:
171+
To save PID enter path (This option is deprecated, use --single-thread instead):
155172
156173
<comment>%command.full_name% someConsumer --pid-file-path='/var/someConsumer.pid'</comment>
157174
HELP

app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php

+22-40
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,13 @@
1313
use Magento\Framework\App\DeploymentConfig;
1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\Process\PhpExecutableFinder;
16-
use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager;
16+
use Magento\Framework\Lock\LockManagerInterface;
1717

1818
/**
1919
* Class for running consumers processes by cron
2020
*/
2121
class ConsumersRunner
2222
{
23-
/**
24-
* Extension of PID file
25-
*/
26-
const PID_FILE_EXT = '.pid';
27-
2823
/**
2924
* Shell command line wrapper for executing command in background
3025
*
@@ -53,13 +48,6 @@ class ConsumersRunner
5348
*/
5449
private $phpExecutableFinder;
5550

56-
/**
57-
* The class for checking status of process by PID
58-
*
59-
* @var PidConsumerManager
60-
*/
61-
private $pidConsumerManager;
62-
6351
/**
6452
* @var ConnectionTypeResolver
6553
*/
@@ -70,13 +58,20 @@ class ConsumersRunner
7058
*/
7159
private $logger;
7260

61+
/**
62+
* Lock Manager
63+
*
64+
* @var LockManagerInterface
65+
*/
66+
private $lockManager;
67+
7368
/**
7469
* @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed
7570
* for the PHP executable
7671
* @param ConsumerConfigInterface $consumerConfig The consumer config provider
7772
* @param DeploymentConfig $deploymentConfig The application deployment configuration
7873
* @param ShellInterface $shellBackground The shell command line wrapper for executing command in background
79-
* @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID
74+
* @param LockManagerInterface $lockManager The lock manager
8075
* @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver
8176
* @param LoggerInterface $logger Logger
8277
*/
@@ -85,15 +80,15 @@ public function __construct(
8580
ConsumerConfigInterface $consumerConfig,
8681
DeploymentConfig $deploymentConfig,
8782
ShellInterface $shellBackground,
88-
PidConsumerManager $pidConsumerManager,
83+
LockManagerInterface $lockManager,
8984
ConnectionTypeResolver $mqConnectionTypeResolver = null,
9085
LoggerInterface $logger = null
9186
) {
9287
$this->phpExecutableFinder = $phpExecutableFinder;
9388
$this->consumerConfig = $consumerConfig;
9489
$this->deploymentConfig = $deploymentConfig;
9590
$this->shellBackground = $shellBackground;
96-
$this->pidConsumerManager = $pidConsumerManager;
91+
$this->lockManager = $lockManager;
9792
$this->mqConnectionTypeResolver = $mqConnectionTypeResolver
9893
?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class);
9994
$this->logger = $logger
@@ -120,11 +115,9 @@ public function run()
120115
continue;
121116
}
122117

123-
$consumerName = $consumer->getName();
124-
125118
$arguments = [
126-
$consumerName,
127-
'--pid-file-path=' . $this->getPidFilePath($consumerName),
119+
$consumer->getName(),
120+
'--single-thread'
128121
];
129122

130123
if ($maxMessages) {
@@ -154,36 +147,25 @@ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $al
154147
return false;
155148
}
156149

157-
if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) {
150+
if ($this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore
158151
return false;
159152
}
160153

161154
$connectionName = $consumerConfig->getConnection();
162155
try {
163156
$this->mqConnectionTypeResolver->getConnectionType($connectionName);
164157
} catch (\LogicException $e) {
165-
$this->logger->info(sprintf(
166-
'Consumer "%s" skipped as required connection "%s" is not configured. %s',
167-
$consumerName,
168-
$connectionName,
169-
$e->getMessage()
170-
));
158+
$this->logger->info(
159+
sprintf(
160+
'Consumer "%s" skipped as required connection "%s" is not configured. %s',
161+
$consumerName,
162+
$connectionName,
163+
$e->getMessage()
164+
)
165+
);
171166
return false;
172167
}
173168

174169
return true;
175170
}
176-
177-
/**
178-
* Returns default path to file with PID by consumers name
179-
*
180-
* @param string $consumerName The consumers name
181-
* @return string The path to file with PID
182-
*/
183-
private function getPidFilePath($consumerName)
184-
{
185-
$sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname());
186-
187-
return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT;
188-
}
189171
}

0 commit comments

Comments
 (0)