Laravel retryUntil任务重试4次后无故消失且未触发失败逻辑问题排查求助
Let's break down what's happening with your job and walk through fixes to get it behaving as expected.
Core Issue Analysis
Your job vanishes after 4 attempts, skipping the failed() method and failing to log to failed_jobs—even though you've set a 24-hour retry window with retryUntil(). The key clue here is that you're manually calling release() instead of throwing exceptions to trigger retries. Here's why that's causing problems:
maxTriesTakes Priority OverretryUntil()for Manual Releases
Laravel 6 treats manualrelease()calls differently than exception-driven retries. If your job'smaxTries(either set on the class or inconfig/queue.php) is set to 3, that means:- 1 initial execution + 3 retries = 4 total attempts
When you hit the 4th attempt and callrelease(), Laravel discards the job immediately (since it's exceeded the max allowed tries) without triggering failure logic—because failure hooks only fire when exceptions exhaust retry limits.
- 1 initial execution + 3 retries = 4 total attempts
retryUntil()Doesn't Work with Manualrelease()
TheretryUntil()method is designed to work with Laravel's built-in exception retry flow. When you manually release a job, Laravel doesn't check if the current time is past your retry window, so you lose that safeguard unless you implement it yourself.
Fixes to Try
Option 1: Switch to Exception-Driven Retries (Recommended)
This aligns with Laravel's intended retry behavior, ensuring retryUntil() and maxTries work as expected, and triggers failed() when retries are exhausted.
First, create a custom exception for throttling:
class TrackDownloadThrottledException extends \Exception {}
Then update your job code to throw this exception instead of calling release() directly:
public function handle() { /** @var Track $track */ $track = Track::idOrUuId($this->trackId); $this->logger->info('Downloading track', [ 'trackId' => $track->getId(), 'attempt' => $this->attempts(), 'retryUntil' => $this->job->timeoutAt(), 'maxTries' => $this->job->maxTries(), ]); $throttleKey = sprintf('track.download.%s', $track->getUser()->getTeamId()); // Check if we're throttled first if ($this->rateLimiter->tooManyAttempts($throttleKey, self::MAX_ALLOWED_JOBS)) { $this->logger->info('Throttling track download.', [ 'trackId' => $track->getId(), 'delay' => $this->retryAfter() ]); throw new TrackDownloadThrottledException("Throttled; retrying after delay"); } // Proceed with download if not throttled $this->downloadTrack($track); $this->rateLimiter->hit($throttleKey, 60); } // Define retry delay for Laravel's retry system public function retryAfter() { $track = Track::idOrUuId($this->trackId); $throttleKey = sprintf('track.download.%s', $track->getUser()->getTeamId()); return random_int(10, 100) + $this->rateLimiter->availableIn($throttleKey); } // Keep your existing retryUntil and failed methods public function retryUntil(): DateTimeInterface { return now()->addHours(24); } public function failed(Exception $exception) { $this->logger->info('Job failed', ['exception' => $exception->getMessage()]); Sentry::captureException($exception); }
Option 2: Manually Control Retries (If You Prefer release())
If you want to stick with manual release() calls, disable the default maxTries limit and add a check for your retryUntil() window to handle failures manually:
// Add this at the top of your job class to disable default max tries public $maxTries = 0; public function handle() { /** @var Track $track */ $track = Track::idOrUuId($this->trackId); // Check if we've exceeded the retry window first if (now()->isAfter($this->retryUntil())) { $this->logger->info('Retry window expired for track download', ['trackId' => $track->getId()]); // Manually trigger failure logic $this->failed(new \Exception('Retry window expired')); return; } $this->logger->info('Downloading track', [ 'trackId' => $track->getId(), 'attempt' => $this->attempts(), 'retryUntil' => $this->job->timeoutAt(), 'maxTries' => $this->job->maxTries(), ]); $throttleKey = sprintf('track.download.%s', $track->getUser()->getTeamId()); if (!$this->rateLimiter->tooManyAttempts($throttleKey, self::MAX_ALLOWED_JOBS)) { $this->downloadTrack($track); $this->rateLimiter->hit($throttleKey, 60); } else { $delay = random_int(10, 100) + $this->rateLimiter->availableIn($throttleKey); $this->logger->info('Throttling track download.', [ 'trackId' => $track->getId(), 'delay' => $delay, ]); $this->release($delay); } } // Keep your existing retryUntil and failed methods public function retryUntil(): DateTimeInterface { return now()->addHours(24); } public function failed(Exception $exception) { $this->logger->info('Job failed', ['exception' => $exception->getMessage()]); Sentry::captureException($exception); }
Option 3: Verify Global Queue Config
Double-check your config/queue.php SQS connection settings—if a global max_tries is set here, it will override any job-specific settings unless you explicitly define $maxTries on your job class:
'sqs' => [ 'driver' => 'sqs', // ... other config 'max_tries' => 3, // This is the default that's likely causing your 4-attempt limit ],
内容的提问来源于stack exchange,提问作者darighteous1

