How to manually ack messages in RabbitMQ? - php

I need to know how to manually ack the messages on the queue direct from the Consumer I created and to set a retry strategy of 5 times, each attemp increasing the time like second try 5min, third try, 10min after second try failed, fourth 15min...
I'm kinda lost in the Rabbit documentation, I learned a bit of the concept but the practical use is still a mistery to me...
I'm using Symfony 6.1 and my old_sound_rabbit_mq.yaml looks like this:
old_sound_rabbit_mq:
connections:
default:
host: '%rabbitmqHost%'
port: '%rabbitmqPort%'
user: '%rabbitmqUser%'
password: '%rabbitmqPassword%'
vhost: '%rabbitmqVhost%'
consumers:
upload_file:
connection: default
exchange_options: { name: 'upload_file_exchange', type: direct, durable: true, auto_delete: false }
queue_options: { name: 'upload_file_queue', durable: true, auto_delete: false, arguments: { 'x-max-priority': [ 'I', 20 ] } }
callback: App\Consumer\UploadFileConsumer
qos_options: { prefetch_size: 0, prefetch_count: 1, global: false }
This is my consumer:
<?php
declare(strict_types=1);
namespace App\Consumer;
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
use PhpAmqpLib\Message\AMQPMessage;
class UploadFileConsumer implements ConsumerInterface
{
public function execute(AMQPMessage $msg): void
{
try {
// do something with $msg, if all is good then ack the msg and remove from queue
} catch (\Exception $e) {
// keep message in queue, don't ack it, keep it in queue retry 5 times then stop consumer if no success
}
}
}

AMQPMessage provides both ack() and nack() methods for this purpose.
https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Message/AMQPMessage.php#L98-L128
So likely what you want is:
<?php
declare(strict_types=1);
namespace App\Consumer;
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
use PhpAmqpLib\Message\AMQPMessage;
class UploadFileConsumer implements ConsumerInterface
{
public function execute(AMQPMessage $msg): void
{
try {
// do something with $msg
$msg->ack();
} catch (\Exception $e) {
// keep message in queue, don't ack it, keep it in queue retry
$msg->nack(true);
}
}
}
Though I'm not familiar with a way to limit the number of times a message is re-queued without modifying the headers/payload and re-queueing it as a new message. Alternatively, you can set a TTL value and messages will eventually time out of the queue. You can also create a dead-letter exchange if you want to inspect nack'ed/expired messages. [just make sure to clean it out, otherwise you'll have new problems]
If I had to kludge in a "re-queue X times" I'd suggest a cache with a built-in TTL like Redis, the key is the message ID, and the value is the number of retries.
Edit:
Spitballing some workflows for "Task A must complete before Task B can begin", in order of decreasing preference:
If Tasks A and B can never happen without each other, then consolidate Task A and Task B into a single task, as they are not independent.
If Task B cannot happen without A, have B invoke A in synchronous/RPC fashion.
Create new async Task C that calls A and B in synchronous/RPC fashion.
Task A ends with submitting Task B to the queue. [this still has the smell of "A and B" are actually a single work unit]
Track task status externally [eg: in a cache like Redis] and have Task B nack/requeue if Task A is not yet completed.
and always beware of "infinite requeue" if your queue does not have a defined TTL, as messages will continue to build up, and your consumers will be working constantly on tasks that may never be completed successfully.

Related

How does a Node.js server (like Express) manage memory as opposed to a PHP server?

From what I understand, basically, PHP server-side apps (PHP-FPM) load the entire app from scratch on every request and then close it down at the end of a request. Meaning that variables, containers, config and everything else gets read and built from zero in each separate request and there is no crossover. I can use this knowledge to structure the app better. For example, I would know that class statics hold their data only for the duration of the request and each new request will have its own value.
A Node.js server like Express.js works very differently, however. It is a single Node.js process that is running continually and listens for any new requests and passes them along to the correct handlers. This requires a different approach to development, as there is data that is kept in memory between requests. For example, class statics in such a case sound like they would hold data for the entire duration of the server uptime, not just for the duration of a single request.
So I have some questions about this:
Does it make sense to pre-load some data during Express.js startup (like reading private keys from file) so that it is already in memory when needed by a request and it would get re-used each time without being re-read from file? In a PHP server framework this wouldn't matter that much as everything gets built from 0 with each request.
How do I properly handle exceptions in a Node.js server process? If a PHP server script throws a fatal exception only that specific request dies, all other requests and any new ones run fine. If a fatal error happens in a Node.js server, it sounds like it would kill the entire process and thus all requests with it.
If you have any resources about how this topic, it'd be great if you could share them also.
1-
Does it make sense to pre-load some data during Express.js startup (like reading private keys from file) so that it is already in memory when needed by a request and it would get re-used each time without being re-read from file? In a PHP server framework this wouldn't matter that much as everything gets built from 0 with each request.
Yes, totally. You would bootstrap connections to databases, data read for files and similar tasks at application startup, so they are always available in every request.
There are some things to consider in this scenario:
During application startup, you can safely call synchronous methods, like fs.readFileSync etc, because there are not concurrent request on the single thread at this point.
CommonJS modules does cache their first value exported. So if you choose to use a dedicate module to handle secrets read from a file, database connections etc., you can:
secrets.js
const fs = require('fs');
const gmailSecretApiKey = fs.readFileSync('path_to_file');
const mailgunSecretApiKey = fs.readFileSync('path_to_file');
...
module.exports = {
gmailSecretApiKey,
mailgunSecretApiKey,
...
}
Then require this as your application startup. After this, any modules that does:
const gmailKey = require('.../secrets').gmailSecretApiKey won't read from file again. The results are cached in the module.
This is important because allow you to use require and import for consuming configuration in your controllers and modules, without bothering passing extra parameters to your http controllers or adding them to req objects.
Depending upon infrastructure, you may not be able to allow your application to not handling requests during startup (i.e. you have only one machine up and don't want to give service unavailble to your clients). In such cases, you can expose all the configuration and shared resources in promises, and bootstrap your web controllers as fast as possible, waiting for the promises inside. Let's say we need kafka up and running when handling a request on '/user':
kafka.js
function kafka() {
// return some promise of an object that can publish and read from kafka in a given port etc. etc.
}
module.exports = kafka();
So now in:
userController.js
const kafka = require('.../kafka');
router.get('/user', (req,res) => {
kafka.then(k => {
k.publish(req.user, 'userTopic'); // or whatever. This is just an example.
});
})
In this way, in the event an user makes a request during bootstrap, the request will still be handled (but will take some time). Requests made when the promise is already resolved won't notice anything.
There's no such thing as multiple threads in node. Anything you declare in a commonJS module or you write to process will be available in every request.
2-
How do I properly handle exceptions in a Node.js server process? If a PHP server script throws a fatal exception only that specific request dies, all other requests and any new ones run fine. If a fatal error happens in a Node.js server, it sounds like it would kill the entire process and thus all requests with it.
This really depends in the kind of exception that you find. It is specifically related to the request being processed, or is something critical for the whole application?
In the former case, you want to catch the exception and don't allow the whole thread to die. Now, 'catch the exception' in javascript is tricky, because you cannot catch asynchronous exceptions/errors, and you would likely use process.on('unhandledRejection') to handle that, like:
// main.js
try {
bootstrapMongoDb();
bootstrapKafka();
bootstrapSecrets();
... wahtever
bootstrapExpress();
} catch(e){
// read what `e` brings and decide.
// however, is worth to mention that errors raised during handling
// http request won't ever get handled here, because they are
// asynchronous. try/catch in javascript don't catch asynchronous errors.
}
process.on('unhandledRejection', e => {
// now here we are treating unhandled promise rejections, and errors that raise
// in express controllers are likely end up here. of course, I'm talking about
// promise rejections. I am not sure if this can catch Errors thrown in callbacks.
// You should never `throw new Error` inside an asynchronous callback.
});
Handling errors in node application is a whole topic on its own, too broad to be considered here. However some tips shouldn't do harm:
Never throw errors in callbacks. throw is synchronous. Callbacks and asynchrony should rely on an error parameter or a promise rejection.
You better get used to promises. Promises really improve error management in asynchronous code.
Javascript errors can be decorated with extra fields, so you can fill in trace id's and other id's that may be useful when reading logs of your system, given you will log your unhandled errors.
Now, in the latter case... sometimes there are failures that are totally disastrous for your app. Maybe you totally need a connection to a kafka or a mongo server, and if it is broken, then you may want to kill your application so clients receive a 503 when trying to connect.
Then, in some scenarios, you may want to kill your app, then let another service to reboot it when database is available again. This depends a lot on infrastructure and you may as well not kill your app never.
If you don't have a infrastructure that handles the health and reboot of your web service for you, it is probably safer to never let your application die. Said so, it's a good thing to at least use tools like nodemon or PM2 to ensure your app will relaunch after going down.
Bonus: why you should not throw errors in callbacks
Thrown errors propagates through the callstack. You have, let's say, function A who calls B, who in turn then calls C. Then C throw an Error. All of them only have synchronous code.
In such scenario, error propagates to B and, if it don't catch it, it propagates to A, and so on.
Now let's say that, instead, C doesn't throw an error by itself, but do call fs.readFile(path, callback). In the callback function, an error is thrown.
Here, when the callback is invoked, and the error thrown, A is already done and left the stack long ago, hundreds of milliseconds ago, maybe even more.
This means that any catch block in A won't catch the error, because is not even there already:
function bootstrapTimeout() {
try {
setTimeout(() => {
throw new Error('foo');
console.log('paco');
}, 200);
} catch (e) {
console.log('error trapped!');
}
}
function bootstrapInterval() {
setInterval(() => {
console.log('interval')
}, 50);
}
console.log('start');
bootstrapTimeout();
bootstrapInterval();
If you run that snippet, you would see how the error reach the top level and kill the process, even if the throw new Error('foo'); line was placed within a try/catch block.
error, result interface
Instead of using Errors to handle exceptions in asynchronous code, node.js has the standard behavior of expose an (error, result) interface for every callback you pass to an asynchronous method. If, for instance, fs.readFile happens to go wrong because the filename did not exist, it does not throw an error, it invokes the callback with the corresponding Error as the error parameter.
Like:
fs.readFile('notexists.png', (error, callback) => {
if(error){
// foo
}
else {
http.post('http://something.com', result, (error, callback) => {
if(error){
// oops, something went wrong with an http request
} else {
// keep working
// etc.
// maybe more callbacks, always with the dreadful 'if (error)'...
}
})
}
});
You always control errors in async operations in the callback, you should never throw.
Now this is a pain in the ass. Promises allow for much better error control because you can control async errors in one single catch block:
fsReadFilePromise('something.png')
.then(res => someHttpRequestPromise(res))
.then(httpResponse => someOtherAsyncMethod(httpResponse))
.then(_ => maybeSomeLoggingOrWhatever() )
.catch(e => {
// here you can control any error thrown in the previous chain.
});
And there's also async/await that allow you to mix async and sync code and treat promise rejections in catch blocks:
await function main() {
try {
a(); // some sync code
await b(); // some promise
} catch(e) {
console.log(e); // either an error throw in a() or a promise rejection reason in b();
}
}
However keep in mind that await is no magic and you really need to understand promises and asynchrony well in order to use it properly.
At the end, you always end up with one error control flow for synchronous errors via try/catch, and another for asynchronous errors, via callback parameters or promise rejections.
Callbacks can use try/catch when consuming synchronous api's, but should never throw. Any function can use catch to handle synchronous errors, but cannot rely on catch blocks to handle asynchronous errors. Kinda messy.
Does it make sense to pre-load some data during Express.js startup (like reading private keys from file) so that it is already in memory when needed by a request and it would get re-used each time without being re-read from file?
Yes it make sense if you structure your code to let these data be available in the request handler. In the following example, based on what i know, the staticResponse is readed only one time.
const express = require('express');
const staticResponse = fs.readFileSync('./data');
const app = express();
app.get('/', function (req, res) {
res.json(staticResponse);
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
How do I properly handle exceptions in a Node.js server process? If a fatal error happens in a Node.js server, it sounds like it would kill the entire process and thus all requests with it.
Exactly, an unhandled exception make the entire nodejs process crash. There are multiple ways to manage error, there isn't 'the one for all' solution. Depends on how you write you're code.
all requests with it => keep in mind that nodejs is single thread.
app.post('/', function (req, res, next) {
try {
const data = JSON.parse(req.body.stringedData);
// use data
res.sendStatus(200);
} catch (err) {
return next(err);
}
});

Error using SQS with multiple Laravel queue readers

I am using Laravel Jobs to read messages from an SQS queue (Laravel version 5.7)
Following Laravel indications I am using supervisor to run multiple queue:work processes at the same time.
All goes well until I get this SQS error related to the message availability:
InvalidParameterValue (client): Value
... for parameter ReceiptHandle is invalid. Reason: Message does not exist or
is not available for visibility timeout change. - <?xml version="1.0"?>
<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/"><Error>
<Type>Sender</Type><Code>InvalidParameterValue</Code><Message>Value ...
for parameter ReceiptHandle is invalid. Reason: Message does not exist or is
not available for visibility timeout change.</Message><Detail/></Error>
<RequestId>8c1d28b7-a02c-5059-8b65-7c6292a0e56e</RequestId></ErrorResponse>
{"exception":"[object] (Aws\\Sqs\\Exception\\SqsException(code: 0): Error
executing \"ChangeMessageVisibility\" on \"https://sqs.eu-central-
1.amazonaws.com/123123123123/myQueue\"; AWS HTTP error: Client error: `POST
https://sqs.eu-central-1.amazonaws.com/123123123123/myQueue` resulted in a
`400 Bad Request` response:
In particular, the strange thing is Message does not exist or is not available for visibility timeout change.
Each supervisor process calls command=php /home/application/artisan queue:work without a --sleep=3 (I'd like the process to be reactive and not waiting for 3 seconds in case nothing was in the queue) nor a --tries=3 (I need all the tasks to be completed, so I don't put a limit to the tries parameter)
In case the message is not existing (and I can't exclude this possibility) why does the process fetches it from the queue ? Is there anything I can do to prevent it ?
I've seen this error intermittently in production too, where we run a good number of consumers for a single SQS queue. In our case, I'm pretty convinced that the error is due to SQS's at-least-once delivery semantics. Essentially, a message can be delivered twice or more on rare occasions.
Laravel's queue worker command isn't strictly idempotent because it will throw an exception when trying to release or delete an SQS message that is no longer available (i.e., because it has been deleted by another queue worker process, which received a duplicate of the message from SQS).
Our workaround is to try to detect when a duplicate message has been received, and then attempt to safely release the message back onto the queue. If the other queue worker that is currently working on the message succeeds, it will delete the message, and it won't be received again. If the other queue worker fails, then the message will be released and received again later. Something like this:
<?php
use Aws\Sqs\Exception\SqsException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $jobId;
public function __construct($jobId)
{
$this->jobId = $jobId;
}
public function handle()
{
$acquired = Cache::lock("process-podcast-$this->jobId")->get(function () {
// Process the podcast (NB: this should be idempotent)
});
if (!$acquired) {
$this->releaseDuplicateMessage($delay = 60);
}
}
private function releaseDuplicateMessage($delay)
{
try {
$this->release($delay);
} catch (Exception $ex) {
if (!$this->causedByMessageNoLongerAvailable($ex)) {
throw $ex;
}
}
}
private function causedByMessageNoLongerAvailable(Exception $ex): bool
{
return $ex instanceof SqsException &&
Str::contains(
$ex->getAwsErrorMessage(),
"Message does not exist or is not available for visibility timeout change"
);
}
}
Another potential for these duplicate messages is that SQS has a default Visibility Timeout of 30secs.
Visibility timeout sets the length of time that a message received from a queue (by one consumer) will not be visible to the other message consumers.
So if one worker reads a message from the queue and it takes longer than 30secs to process, the message will become visible again and another worker will start processing.
When the first worker finishes, it will delete it from the queue. Then when the second worker finishes processing the same message and tries to delete it, it can't because the first worker already deleted it.
We're having the same issue at the moment and are implementing a fix/workaround similar to Louis. Will post our version when done and confirmed working.
Note: You can increase the Visibility Timeout on SQS.

Dead lettering with php-amqplib and RabbitMQ?

I'm just starting out in using php-amqplib and RabbitMQ and want a way to handle messages that, for whatever reason, can't be processed and are nack'd. I thought that one way people handle this is with a dead letter queue. I'm trying to set this up but have not had any luck so far and hope someone could offer some suggestions.
My initiation of the queues looks a little something like:
class BaseAbstract
{
/** #var AMQPStreamConnection */
protected $connection;
/** #var AMQPChannel */
protected $channel;
/** #var array */
protected $deadLetter = [
'exchange' => 'dead_letter',
'type' => 'direct',
'queue' => 'delay_queue',
'ttl' => 10000 // in milliseconds
];
protected function initConnection(array $config)
{
try {
$this->connection = AMQPStreamConnection::create_connection($config);
$this->channel = $this->connection->channel();
// Setup dead letter exchange and queue
$this->channel->exchange_declare($this->deadLetter['exchange'], $this->deadLetter['type'], false, true, false);
$this->channel->queue_declare($this->deadLetter['queue'], false, true, false, false, false, new AMQPTable([
'x-dead-letter-exchange' => $this->deadLetter['exchange'],
'x-dead-letter-routing-key' => $this->deadLetter['queue'],
'x-message-ttl' => $this->deadLetter['ttl']
]));
$this->channel->queue_bind($this->deadLetter['queue'], $this->deadLetter['exchange']);
// Set up regular exchange and queue
$this->channel->exchange_declare($this->getExchangeName(), $this->getExchangeType(), true, true, false);
$this->channel->queue_declare($this->getQueueName(), true, true, false, false, new AMQPTable([
'x-dead-letter-exchange' => $this->deadLetter['exchange'],
'x-dead-letter-routing-key' => $this->deadLetter['queue']
]));
if (method_exists($this, 'getRouteKey')) {
$this->channel->queue_bind($this->getQueueName(), $this->getExchangeName(), $this->getRouteKey());
} else {
$this->channel->queue_bind($this->getQueueName(), $this->getExchangeName());
}
} catch (\Exception $e) {
throw new \RuntimeException('Cannot connect to the RabbitMQ service: ' . $e->getMessage());
}
return $this;
}
// ...
}
which I thought should set up my dead letter exchange and queue, and then also set up my regular exchange and queue (with the getRouteKey, getQueueName, and getExchangeName/Type methods provided by extending classes)
When I try to handle a message like:
public function process(AMQPMessage $message)
{
$msg = json_decode($message->body);
if (empty($msg->payload) || empty($msg->payload->run)) {
$message->delivery_info['channel']->basic_nack($message->delivery_info['delivery_tag'], false, true);
return;
}
// removed for post brevity, but compose $cmd variable
exec($cmd, $output, $returned);
if ($returned !== 0) {
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
} else {
$message->delivery_info['channel']->basic_nack($message->delivery_info['delivery_tag']);
}
}
But I get back the error Something went wrong: Cannot connect to the RabbitMQ service: PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange' for queue 'delay_queue' in vhost '/': received 'dead_letter' but current is ''
Is this the way I should be setting up dead lettering? Different examples I've seen around all seem to show a bit of a different way of handling it, none of which seem to work for me. So I've obviously misunderstood something here and am appreciative of any advice. :)
Setting up (permanent) queues and exchanges is something you want to do once, when deploying code, not every time you want to use them. Think of them like your database schema - although the protocol provides "declare" rather than "create", you should generally be writing code that assumes things are configured a particular way. You could build the first part of your code into a setup script, or use the web- and CLI-based management plugin to manage these using a simple JSON format.
The error you are seeing is probably a result of trying to declare the same queue at different times with different parameters - the "declare" won't replace or reconfigure an existing queue, it will treat the arguments as "pre-conditions" to be checked. You'll need to drop and recreate the queue, or manage it via the management UI, to change its existing parameters.
Where run-time declares become more useful is when you want to dynamically create items in your broker. You can either give them names you know will be unique to that purpose, or pass null as the name to receive a randomly-generated name back (people sometimes refer to creating an "anonymous queue", but every queue in RabbitMQ has a name, even if you didn't choose it).
If I'm reading it correctly, your "schema" looks likes this:
# Dead Letter eXchange and Queue
Exchange: DLX
Queue: DLQ; dead letter exchange: DLX, with key "DLQ"; automatic expiry
Binding: copy messages arriving in DLX to DLQ
# Regular eXchange and Queue
Exchange: RX
Queue: RQ; dead letter exchange: DLX, with key "DLQ"
Binding: copy messages from RX to RQ, optionally filtered by routing key
When a message is "nacked" in RQ, it will be passed to DLX, with its routing key overwritten to be "DLQ". It will then be copied to DLQ. If it is nacked from DLQ, or waits in that queue too long, it will be routed round to itself.
I would simplify in two ways:
Remove the dead letter exchange and TTL from the "dead letter queue" (which I've labelled DLQ); that loop's likely to be more confusing than useful.
Remove the x-dead-letter-routing-key option from the regular queue (which I've labelled RQ). The configuration of the regular queue shouldn't need to know whether the Dead Letter Exchange has zero, one, or several queues attached to it, so shouldn't know the name of that other queue. If you want nacked messages to go straight to one queue, just make it a "fanout exchange" (which ignores routing keys) or a "topic exchange" with the binding key set to # (which is a wildcard matching all routing keys).
An alternative might be to set x-dead-letter-routing-key to the name of the regular queue, i.e. to label which queue it came from. But until you have a use case for that, I'd keep it simple and leave the message with its original routing key.

How to handle deadlock in Doctrine?

I have a mobile application and server based on Symfony which gives API for the mobile app.
I have a situation, where users can like Post. When users like Post I add an entry in ManyToMany table that this particular user liked this particular Post (step 1). Then in Post table I increase likesCounter (step 2). Then in User table I increase gamification points for user (because he liked the Post) (step 3).
So there is a situation where many users likes particular Post at the same time and deadlock occurs (on Post table or on User table).
How to handle this? In Doctrine Docs I can see solution like this:
<?php
try {
// process stuff
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
// retry the processing
}
but what should I do in catch part? Retry the whole process of liking (steps 1 to 3) for instance 3 times and if failed return BadRequest to the mobile application? Or something else?
I don't know if this is a good example cause maybe I could try to rebuild the process so the deadlock won't happen but I would like to know what should I do if they actually happen?
I disagree with Stefan, deadlocks are normal as the MySQL documentation says:
Normally, you must write your applications so that they are always prepared to re-issue a transaction if it gets rolled back because of a deadlock.
See: MySQL documentation
However, the loop suggested by Stefan is the right solution. Except that it lacks an important point: after Doctrine has thrown an Exception, the EntityManager becomes unusable and you must create a new one in the catch clause with resetManager() from the ManagerRegistry instance.
When I had exactly the same concern as you, I searched the web but couldn't find any completely satisfactory answer. So I got my hands dirty and came back with an article where you'll find an implementation exemple of what I said above:
Thread-safe business logic with Doctrine
What I'd do is post all likes on a queue and consume them using a batch consumer so that you can group the updates on a single post.
If you insist on keeping you current implementation you could go down the road you yourself suggested like this:
<?php
for ($i = 0; $i < $retryCount; $i++) {
try {
// try updating
break;
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
// you could also add a delay here
continue;
}
}
if ($i === $retryCount) {
// throw BadRequest
}
This is an ugly solution and I wouldn't suggest it. Deadlocks shouldn't be "avoided" by retrying or using delays. Also have a look at named locks and use the same retry system, but don't wait for the deadlock to happen.
The problem is that after Symfony Entity Manager fails - it closes db connection and you can't continue you work with db even if you catch the ORMException.
First good solution is to process your 'likes' async, with rabbitmq or other queue implementation.
Step-by-step:
Create message like {type: 'like', user:123, post: 456}
Publish it in queue
Consume it and update 'likes' count.
You can have several consumers that try to obtain lock on based on postId. If two consumers try to update same post - one of them will fail obtaining the lock. But it's ok, you can consume failed message after.
Second solution is to have special table e.g. post_likes (userId, postId, timestamp). Your endpoint could create new rows in this table synchronously. And you can count 'likes' on some post with this table. Or you can write some cron script, which will update post likes count by this table.
I've made a special class to retry on deadlock (I'm on Symfony 4.4).
Here it is :
class AntiDeadlockService
{
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function safePush(): void
{
// to retry on deadlocks or other retryable exceptions
$connection = $this->em->getConnection();
$retry = 0;
$maxRetries = 3;
while ($retry < $maxRetries) {
try {
if (!$this->em->isOpen()) {
$this->em = $this->em->create(
$connection = $this->em->getConnection(),
$this->em->getConfiguration()
);
}
$connection->beginTransaction(); // suspend auto-commit
$this->em->flush();
$connection->commit();
break;
} catch (RetryableException $exception) {
$connection->rollBack();
$retry++;
if ($retry === $maxRetries) {
throw $exception;
}
}
}
}
}
Use this safePush() method instead of the $entityManager->push() one ;)

Laravel 4.2 Queue - force job fail

I want to do something like this in my fire method:
class MyClass{
public function fire($job) {
if(something) {
$job->fail();
}else {
//processing
}
$job->delete();
}
There is no such method as fail(), is it possible to do something like this?
There is no such thing as fail a job but what you can do:
release it back to the queue with
$job->release();
After defined number of attempts it will end up in failed jobs table.
throw an exception. The job will be released back to the queue on it's own.
if you're using beanstalkd as a queue driver you can bury a job
$job->bury();
If your condition is unrecoverable you can log this fact and simply delete the job.

Categories