Fix RemoteReplyResolvePipeline, make unique until processing

pull/7398/head
Daniel Supernault 2 days ago
parent 56b96359c3
commit 226785f085
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -5,6 +5,7 @@ namespace App\Jobs\StatusPipeline;
use App\Models\Profile;
use App\Util\ActivityPub\Helpers;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
@ -23,8 +24,15 @@ use Illuminate\Support\Facades\Log;
* Attempts are tracked on the job and re-dispatched with a delay instead of
* using release()/tries, so an unreachable parent does not end up in
* failed_jobs.
*
* Two guards keep this to one queued job per reply. The pending key spans
* the whole chain and stops a redelivered activity from parking a second
* one. The unique lock covers each queued attempt, whatever dispatched it.
* It has to be ShouldBeUniqueUntilProcessing: plain ShouldBeUnique holds the
* lock until handle() returns, so the re-dispatch in retryOrDrop() would
* fail to acquire it and be discarded without an error.
*/
class RemoteReplyResolvePipeline implements ShouldQueue
class RemoteReplyResolvePipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
@ -36,10 +44,13 @@ class RemoteReplyResolvePipeline implements ShouldQueue
public const array BACKOFF = [180, 900, 3600, 21600];
/**
* How long a reply is considered pending, a little over the sum of
* BACKOFF. Stops a redelivered activity from starting a second chain.
* Added to the delay of the attempt being scheduled to get the TTL of
* the pending key and the unique lock. Has to cover how far the low
* queue can run behind, plus one attempt's $timeout. If the queue lags
* more than this, both expire before the attempt runs and a redelivered
* activity can start a second chain.
*/
private const int PENDING_TTL = 28800;
private const int LAG_MARGIN = 3600;
public $timeout = 300;
@ -71,7 +82,7 @@ class RemoteReplyResolvePipeline implements ShouldQueue
return false;
}
if (! Cache::add(self::pendingKey($id), 1, self::PENDING_TTL)) {
if (! Cache::add(self::pendingKey($id), 1, self::ttlFor(0))) {
return false;
}
@ -87,6 +98,33 @@ class RemoteReplyResolvePipeline implements ShouldQueue
return 'pf:ap:reply-resolve:pending:'.hash('sha256', $id);
}
/**
* Seconds the pending key and the unique lock are held while waiting for
* the given attempt.
*/
private static function ttlFor(int $attempt): int
{
return (self::BACKOFF[$attempt] ?? max(self::BACKOFF)) + self::LAG_MARGIN;
}
/**
* One queued attempt per reply. The lock is taken on dispatch and
* released as the attempt starts processing.
*/
public function uniqueId(): string
{
return hash('sha256', (string) Helpers::pluckval($this->object['id'] ?? null));
}
/**
* Safety expiry for the lock, in case the queued attempt is lost (queue
* cleared, payload evicted) and never gets to release it.
*/
public function uniqueFor(): int
{
return self::ttlFor($this->attempt);
}
public function handle(): void
{
$id = Helpers::pluckval($this->object['id'] ?? null);
@ -142,6 +180,11 @@ class RemoteReplyResolvePipeline implements ShouldQueue
return;
}
// Re-arm the pending key for the wait ahead. It is sized per attempt
// instead of once for the whole chain, so a chain that runs late
// cannot outlive it.
Cache::put(self::pendingKey($id), 1, self::ttlFor($next));
self::dispatch($this->object, $this->profileId, $next)
->delay(now()->addSeconds(self::BACKOFF[$next]))
->onQueue('low');

Loading…
Cancel
Save