You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel retryUntil任务重试4次后无故消失且未触发失败逻辑问题排查求助

Troubleshooting Laravel 6 + SQS Job Disappearing Without Triggering Failed Logic

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:

  1. maxTries Takes Priority Over retryUntil() for Manual Releases
    Laravel 6 treats manual release() calls differently than exception-driven retries. If your job's maxTries (either set on the class or in config/queue.php) is set to 3, that means:

    • 1 initial execution + 3 retries = 4 total attempts
      When you hit the 4th attempt and call release(), 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.
  2. retryUntil() Doesn't Work with Manual release()
    The retryUntil() 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

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 15:07:37