将SQS与多个Laravel队列读取器一起使用时出错

时间:2019-02-07 08:42:14

标签: php laravel amazon-sqs

我正在使用Laravel Jobs从SQS队列中读取消息(Laravel版本5.7)

在Laravel indications之后,我正在使用主管同时运行多个queue:work进程。

一切顺利,直到出现与消息可用性相关的SQS错误为止

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:

尤其是奇怪的是Message does not exist or is not available for visibility timeout change.

每个主管进程在没有command=php /home/application/artisan queue:work的情况下调用--sleep=3(我希望该进程是响应性的,如果队列中没有任何内容,则不等待3秒)或--tries=3 (我需要完成所有任务,所以我没有限制tries参数)

如果消息不存在(并且我不能排除这种可能性),为什么进程从队列中提取它?有什么我可以防止的吗?

1 个答案:

答案 0 :(得分:0)

我在生产中也间歇性地看到了这个错误,我们为单个SQS队列运行了大量的使用者。在我们的案例中,我非常确信该错误是由于SQS的at-least-once传递语义引起的。本质上,在极少数情况下,邮件可以传递两次或多次。

Laravel的队列工作程序命令不是严格等幂的,因为它在尝试释放或删除不再可用的SQS消息时会抛出异常(即,由于它已被另一个队列工作程序进程删除,该进程收到重复的消息)来自SQS的消息。

我们的解决方法是尝试检测何时收到重复的邮件,然后尝试将邮件安全地释放回队列。如果当前正在处理该消息的另一个队列工作器成功,它将删除该消息,并且不会再次收到该消息。如果另一个队列工作器失败,则该消息将被释放并稍后再次接收。像这样:

<?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"
           );
  }
}