Signal processing in PHP, or cook delicious
When can we need signal processing in php?
- In any loaded project, sooner or later it is necessary to face the need to parallelize the process and the most common way is to use a message server such as RabbitMq, Gearman, Kafka and others. At this point, there is a need to create the so-called consumer. It consists of a loop, checking for new messages in it and processing them.
Now the situation itself: we updated the code and we need to restart the consumers - if you just send a SIGTERM signal, then there will be a chance of not having data consistency in the database or other problems due to a script break in the middle of processing, in which case signal processing will help us. - Also in the era of containerization, it’s good to be able to correctly extinguish a container with an application without losing any processed information.
- Well, the third option is specific tasks where you need a daemon and it was written in php, here we need a number of signals to update the configuration, complete the script, and other actions. An example .
The arsenal that php provides us with for working with signals is:
- pcntl_signal () php> = 4.1.0 - function for registering a signal handler.
- declare (ticks = 1) php <5.3 - indicates how many ticks the interpreter will check for a signal.
- pcntl_signal_dispatch () php> = 5.3.0 - manual start of checking for the presence of an unprocessed signal, as a more efficient alternative to declare.
- pcntl_async_signals () php> = 7.1.0 - asynchronous substitution of the signal handler on the call stack.
- pcntl_signal_get_handler () php> = 7.1.0 - get a signal handler function.
- pcntl_alarm () php> = 4.3.0 - send SIGALARM to yourself.
- pcntl_sigprocmask () php> = 5.3.0 - you can block, unlock the processing of given signals, also delete, replace the stack of blocked signals.
A bit of theory:
Для каждого сигнала который мы хотим обрабатывать нужно зарегистрировать функцию обработчик через pcntl_signal().
Важное замечание из php.net pcntl_signal() — не собирает обработчики сигналов в стек, а заменяет их, то есть если вы где-то в коде еще раз определите обработчик какого-то сигнала, то он просто перекроет предыдущий, именно по этому в сторонних библиотеках
После выполнения функции обработчика интерпретатор продолжает свою работу с места прерывания.
From previous versions of php, we know about the existence of the declare directive (ticks = 1), which told our script after each operation to see if we received a signal, respectively, it gave a noticeable overhead when executed, especially if there is a lot of code, it is described quite well here . But fortunately, in 2018, the language developers added an amazing thing - pcntl_async_signals (), this function allows the interpreter not to be distracted by checking the signal, in fact, it puts our signal handler next on the call stack for the function to be executed.
The synthetic performance test showed no differences with and without pcntl_async_signals ().
Now let's talk about the limitations of the handler, the signal will be processed only after the current function has finished executing, if this is accessing the api or database, then the time before the signal processing can be delayed, this should be taken into account, for example, if you use a supervisor or a docker, increase the timeout before sending SIGKILL for the time of your own long blocking call plus margin. I also want to say that at the time of testing I encountered an interesting behavior of the sleep () function, as it turned out to be documented, but I didn’t expect it - it is interrupted by a signal and returns the number of seconds that wasn’t enough, that is, if you suddenly need to use it and be sure of the sleep length, then this will look like this:
$sleep = 1000;
while ($sleep > 0) {
$sleep = sleep($sleep);
}I also want to focus on the fact that you will find examples with SIGKILL processing on many resources - but this does not work (worked on older versions of linux), now this signal kills the process on the part of the operating system, and this cannot be affected.
And some code
How it will look in the case of a cosyumer for RabbitMq:
This is our simplified manager whose task is to prepare a consumer and monitor the progress of tasks
class QueueManager
{
private $stopConsume = false;
public function stopConsume()
{
$this->stopConsume = true;
}
public function consume($consumerAlias)
{
$consumer = $this->getConsumerBuilder()->create($consumerAlias);
$channel = $consumer->getChannel();
while (\count($channel->callbacks) && $this->stopConsume !== true) {
$channel->wait();
}
}
}
And that’s all you need in the controller:
class SomeController
{
private $queueManager;
public function __construct()
{
$this->queueManager = new QueueManager();
pcntl_signal(SIGTERM, [$this->queueManager, 'stopConsume']);
}
public function consumeSomeQueue()
{
$this->queueManager->consume('SomeConsumer');
}
}
Upon receipt of the signal, the stopConsume method of the queueManager object will be called - it will in turn set the stopConsume parameter to true and will only have to go to the end of processing the current message, after which the cycle will end.
The purpose of the article is not to consider the side issues of demonizing processes such as maintaining connections, memory leaks, etc.
I hope this information was useful and interesting for you,
if you have any questions, I will gladly answer in the comments or supplement the article as necessary.
class SigHandler
{
private $handlers = [];
public function handle($sigNumber)
{
if (!empty($this->handlers[$sigNumber])) {
foreach ($this->handlers[$sigNumber] as $signalHandler) {
$signalHandler($sigNumber);
}
}
}
public function subscribe($sigNumber, $handler)
{
$this->handlers[$sigNumber][$this->getFunctionHash($handler)] = $handler;
}
public function unsubscribe($sigNumber, $handler)
{
unset($this->handlers[$sigNumber][$this->getFunctionHash($handler)]);
}
private function getFunctionHash($callable)
{
return spl_object_hash($callable);
}
}
Try it at work:
pcntl_async_signals(true);
$sigHandler = new SigHandler();
pcntl_signal(SIGTERM, [$sigHandler, 'handle']);
$sigHandler->subscribe(SIGTERM, function () {
echo 'sigterm_1', PHP_EOL;
});
$sigHandler->subscribe(SIGTERM, function () {
echo 'sigterm_2', PHP_EOL;
});
while (true) {
}