Celery 재시도 로직과 Exponential Backoff and Jitter

Published
December 16, 2024
Last updated
Last updated December 27, 2024
Tistory
Category
Tags

개요

Celery는 파이썬에서 널리 사용되는 비동기 작업 큐 라이브러리로, 주로 분산 작업 처리와 백그라운드 작업 실행을 위해 활용됩니다.
비동기 작업에는 외부 API 호출이 포함되는 경우가 많으며, 이러한 작업은 외부 환경의 영향을 받기 쉽습니다. 네트워크 지연, 일시적인 API 장애, 서비스의 부하 등으로 인해 작업이 실패할 수 있습니다. 그러나 실패 원인이 일시적인 네트워크 문제인지, API 서비스의 장기적인 장애인지 즉시 판단하기는 어렵습니다.
이 글에서는 Exponential Backoff와 Jitter라는 안정성 보장 기법을 소개하고, 이를 Celery에 적용해 외부 API 통합 시스템의 신뢰성과 복원력을 강화하는 방법을 설명합니다.
 

Celery Task의 재시도 로직

Celery task를 정의할때는 아래와 같이 데코레이터를 이용한 방식을 주로 이용합니다.
@app.task def basic_task(): ... # 장고의 경우 @shared_task def django_task(): ...
/celery/app/task.py
 
이렇게 데코레이터를 통해 task 생성을 설정해주면 Task 객체가 만들어집니다.
class Task: ...
 
그럼 Task에서 재시도시 사용되는 Task.retry() 메소드에 대해 알아보겠습니다.

Task.retry()

Task.retry()는 task 실행중 오류 발생시 복구가 가능할 것으로 보이는 task를 재시도 하는데 사용됩니다.
다음은 retry의 예시입니다.
@app.task(bind=True) def send_twitter_status(self, oauth, tweet): try: twitter = Twitter(oauth) twitter.update_status(tweet) except (Twitter.FailWhaleError, Twitter.LoginError)as exc: raise self.retry(exc=exc)
위는 Twitter에 새로운 트윗을 게시하는 예시 코드입니다. Twitter와의 연동은 외부 시스템과의 통신이므로 오류 발생 가능성을 염두에 둔 것을 알 수 있습니다.
코드에서는 두 가지 주요 오류를 처리합니다.
  • Twitter.FailWhaleError: Twitter 서비스 과부하로 인해 발생하는 오류로, 시스템이 일시적으로 다운된 경우입니다.
  • Twitter.LoginError: 인증 실패 시 발생하는 오류로, OAuth 인증이 유효하지 않거나 만료된 경우입니다.
위와 같은 오류들은 대게 일시적인 오류임으로 일정 기간 후에 다시 시도할시 문제 없이 진행될 가능성이 높습니다. 이를 위해 코드에서 해당 문제가 발생하면 catch 하여 자동으로 작업을 재시도하도록 설정했습니다.

Task.retry() 설정

# Task.retry()의 시그니처 def retry(self, args=None, kwargs=None, exc=None, throw=True, eta=None, countdown=None, max_retries=None, **options): ...
그러나 서버가 복구되기 전에 즉시 재시도를 시도하거나, 일시적인 오류가 아닌 경우에도 지속적으로 재시도하면 문제가 발생할 수 있습니다. 이를 방지하기 위해 retry() 메서드를 사용할 때 별도의 설정이 없는 경우 기본적인 재시도 제한을 적용하고 있습니다.
또한 해당 재시도 제한을 직접 지정해줄 수도 있습니다.
  • 최대 재시도 횟수(max_retries: int)
    • max_retries 미지정시 self.max_retries (기본값 3)이 적용됩니다.
    • 즉 3번 재시도해 총 4번 시도합니다.
    • 초과시에 MaxRetriesExceededError 에러를 던집니다.
    • None으로 지정시에는 무한히 반복됩니다.
  • 재시도 전 대기 시간(countdown: float)
    • countdowneta 둘다 미지정시 self.default_retry_delay (기본값 30)이 적용됩니다.
  • 재시도할 특정 시간(eta: datetime)
    • 작업이 다시 실행될 정확한 시간을 지정할 수 있습니다.
    • countdown과 함께 사용할 수 없으며 동시에 지정하면 countdown은 무시되고 eta가 적용됩니다.

기본 retry() 실험

다음과 같이 최대 3번, 3분 간격으로 재시도 하는것을 확인할 수 있습니다.
[2024-12-16 16:50:33,874: INFO/MainProcess] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] received [2024-12-16 16:50:33,878: WARNING/ForkPoolWorker-8] retry test task [2024-12-16 16:50:33,879: WARNING/ForkPoolWorker-8] 2024-12-16 16:50:33.878999+09:00 [2024-12-16 16:50:33,895: INFO/MainProcess] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] received [2024-12-16 16:50:33,902: INFO/ForkPoolWorker-8] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] retry: Retry in 180s [2024-12-16 16:53:33,884: WARNING/ForkPoolWorker-8] retry test task [2024-12-16 16:53:33,886: WARNING/ForkPoolWorker-8] 2024-12-16 16:53:33.885758+09:00 [2024-12-16 16:53:33,903: INFO/MainProcess] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] received [2024-12-16 16:53:33,915: INFO/ForkPoolWorker-8] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] retry: Retry in 180s [2024-12-16 16:56:33,895: WARNING/ForkPoolWorker-8] retry test task [2024-12-16 16:56:33,900: WARNING/ForkPoolWorker-8] 2024-12-16 16:56:33.899721+09:00 [2024-12-16 16:56:33,910: INFO/MainProcess] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] received [2024-12-16 16:56:33,928: INFO/ForkPoolWorker-8] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] retry: Retry in 180s [2024-12-16 16:59:33,906: WARNING/ForkPoolWorker-8] retry test task [2024-12-16 16:59:33,911: WARNING/ForkPoolWorker-8] 2024-12-16 16:59:33.910769+09:00 [2024-12-16 16:59:33,937: ERROR/ForkPoolWorker-8] Task ess.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] raised unexpected: MaxRetriesExceededError("Can't retry django_app.tasks.retry_test_task[0438fa03-693e-4ae9-ae9f-02766677b7b0] args:() kwargs:{}")
 

@app.task() 인자로 재시도 설정

재시도 간격, 횟수

또는 @app.task() 에서 키워드 인자를 지정해서 retry 옵션을 설정해줄 수도 있습니다.
@app.task(default_retry_delay=30 * 60, retry_kwargs={'max_retries': 5}) def custom_retry_task(user): ...
💡
이때 retry_kwargs로 지정해준 속성중 max_retriesretry(max_retries=…)로 지정해준 것보다 우선순위가 낮습니다. 또한 retry_kwargs["countdown"]retry_backoff 설정보다 우선순위가 낮습니다.

재시도 오류 종류

Task.retry()시에는 직접 except…raise로 오류 종류를 잡아줘야 했던것을, @app.task()에서 autoretry_for: list | tuple 속성을 지정해주는 것으로 대체할 수 있습니다.
즉 아래 두개의 코드는 똑같이 동작합니다.
@app.task(autoretry_for=(FailWhaleError,), retry_kwargs={'max_retries': 5}) def refresh_timeline(user): return twitter.refresh_timeline(user) # 위 코드와 아래 코드는 동일하게 동작 @app.task def refresh_timeline(user): try: twitter.refresh_timeline(user) except FailWhaleError as exc: raise refresh_timeline.retry(exc=exc, max_retries=5)
 
또한 5.3버전부터 반대의 역할을 하는 dont_autoretry_for가 추가되었습니다. dont_autoretry_for은 autoretry_for로 지정된 Exception의 하위에 속하는 Exception을 예외처리 해주기 위함임으로 당연히 autoretry_for보다 더 높은 우선순위를 가지고 있습니다.
@wraps(task.run) def run(*args, **kwargs): try: return task._orig_run(*args, **kwargs) except dont_autoretry_for: raise except autoretry_for as exc:
/celery/app/autoretry.py
 

Exponential Backoff and Jitter

Exponential Backoff

Jitter

Celery에서 Exponential Backoff and Jitter 적용

Celery 4.2부터 Celery에서는 자동 exponential backoff와 jitter를 지원합니다.
retry_backoff=True를 지정해주는 것으로 간단히 Exponential Backoff and Jitter를 Celery task에 적용할 수 있습니다.
from requests.exceptions import RequestException class BaseTaskWithRetry(Task): autoretry_for = (RequestException,) max_retries = 5 retry_backoff = True retry_backoff_max = 700 retry_jitter = False @app.task(autoretry_for=(RequestException,), retry_backoff=True) def x(): ...
  • retry_backoff: bool | int | float
    • exponential backoff 지정 여부. True면 1부터 2배 간격으로 시도
    • 정수를 지정하면 지정한 정수 n초 부터 시작해서 2배 간격으로 시도
      • n=5일때 예시: 5초 10초 20초 40초 80초…
    • default: False
    • retry_backoff 설정시 countdown 속성은 무시됩니다
  • retry_backoff_max: int | float
    • retry_backoff시 최대 반복 한도(초)
    • default: 600
    • retry_backoff 미설정시 적용안됨
  • retry_jitter: bool
    • jitter 적용 여부
    • default: True
    • retry_backoff 미설정시 적용안됨
💡
이때 주의할점으로 retry_backoff, retry_backoff_max, retry_jitter 등은 autoretry_for가 지정되어 있지 않으면 적용되지 않는다
 
 
 
  • rate_limit: int | float | str
    • worker별 태스크 타입별 초당 제한을 설정한다.
    • 숫자가 들어오면 초당 제한으로 지정된다."1/s", "1/m", "1/h"와 같이 제한할 수도 있다.
    • default: task_default_rate_limit == None
  • time_limit
  • soft_time_limit
 
 
 
if autoretry_for and not hasattr(task, '_orig_run'): @wraps(task.run) def run(*args, **kwargs): try: return task._orig_run(*args, **kwargs) except Ignore: # If Ignore signal occurs task shouldn't be retried, # even if it suits autoretry_for list raise except Retry: raise except dont_autoretry_for: raise except autoretry_for as exc: if retry_backoff: retry_kwargs['countdown'] = \ get_exponential_backoff_interval( factor=int(max(1.0, retry_backoff)), retries=task.request.retries, maximum=retry_backoff_max, full_jitter=retry_jitter) # Override max_retries if hasattr(task, 'override_max_retries'): retry_kwargs['max_retries'] = getattr(task, 'override_max_retries', task.max_retries) ret = task.retry(exc=exc, **retry_kwargs) # Stop propagation if hasattr(task, 'override_max_retries'): delattr(task, 'override_max_retries') raise ret

예시

Jitter 없이 exponential backoff로 시도

@shared_task( bind=True, autoretry_for=(Exception,), retry_backoff=1, retry_jitter=False, retry_kwargs={"max_retries": 20}, ) def exp_with_no_jitter_task(self, num: int | None = None): if num is None: num = random.randint(1, 100) logger.info(f"num: {num}") raise Exception("Exception!") """ [2024-12-27 14:38:37,668: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:38:37,674: INFO/ForkPoolWorker-8] app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096]: num: 83 [2024-12-27 14:38:37,703: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:38:37,726: INFO/ForkPoolWorker-8] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] retry: Retry in 1s: Exception('Exception!') [2024-12-27 14:38:38,679: INFO/ForkPoolWorker-8] app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096]: num: 63 [2024-12-27 14:38:38,683: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:38:38,687: INFO/ForkPoolWorker-8] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] retry: Retry in 2s: Exception('Exception!') [2024-12-27 14:38:40,689: INFO/ForkPoolWorker-8] app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096]: num: 12 [2024-12-27 14:38:40,700: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:38:40,723: INFO/ForkPoolWorker-8] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] retry: Retry in 4s: Exception('Exception!') [2024-12-27 14:38:44,709: INFO/ForkPoolWorker-8] app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096]: num: 38 [2024-12-27 14:38:44,719: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:38:44,729: INFO/ForkPoolWorker-8] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] retry: Retry in 8s: Exception('Exception!') [2024-12-27 14:38:52,718: INFO/ForkPoolWorker-8] app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096]: num: 48 [2024-12-27 14:38:52,726: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:38:52,739: INFO/ForkPoolWorker-8] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] retry: Retry in 16s: Exception('Exception!') [2024-12-27 14:39:08,727: INFO/ForkPoolWorker-8] app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096]: num: 14 [2024-12-27 14:39:08,744: INFO/MainProcess] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] received [2024-12-27 14:39:08,778: INFO/ForkPoolWorker-8] Task app.exp_with_no_jitter_task[98ec361d-39ac-4851-a30b-10128c049096] retry: Retry in 32s: Exception('Exception!') """
1, 2, 4, 8.. 초 간격으로 태스크를 재시도 하는 모습을 확인할 수 있다.

Jitter 포함 exponential backoff로 시도

@shared_task( bind=True, autoretry_for=(Exception,), retry_backoff=1, retry_kwargs={"max_retries": 20}, ) def exp_with_jitter_task(self, num: int | None = None): if num is None: num = random.randint(1, 100) logger.info(f"num: {num}") raise Exception("Exception!") """ [2024-12-27 14:42:24,834: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:24,842: INFO/ForkPoolWorker-8] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 95 [2024-12-27 14:42:24,880: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:24,897: INFO/ForkPoolWorker-8] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 1s: Exception('Exception!') [2024-12-27 14:42:25,850: INFO/ForkPoolWorker-8] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 96 [2024-12-27 14:42:25,853: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:25,861: INFO/ForkPoolWorker-8] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 0s: Exception('Exception!') [2024-12-27 14:42:25,860: INFO/ForkPoolWorker-1] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 88 [2024-12-27 14:42:25,887: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:25,900: INFO/ForkPoolWorker-1] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 2s: Exception('Exception!') [2024-12-27 14:42:27,871: INFO/ForkPoolWorker-8] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 100 [2024-12-27 14:42:27,882: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:27,895: INFO/ForkPoolWorker-8] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 5s: Exception('Exception!') [2024-12-27 14:42:32,893: INFO/ForkPoolWorker-8] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 63 [2024-12-27 14:42:32,904: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:32,913: INFO/ForkPoolWorker-8] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 3s: Exception('Exception!') [2024-12-27 14:42:35,899: INFO/ForkPoolWorker-8] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 34 [2024-12-27 14:42:35,903: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:35,905: INFO/ForkPoolWorker-8] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 12s: Exception('Exception!') [2024-12-27 14:42:47,909: INFO/ForkPoolWorker-8] app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48]: num: 80 [2024-12-27 14:42:47,926: INFO/MainProcess] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] received [2024-12-27 14:42:47,940: INFO/ForkPoolWorker-8] Task app.exp_with_jitter_task[b0a7a096-80c3-473a-81af-8653f54f6d48] retry: Retry in 53s: Exception('Exception!') """
원래 1, 2, 4, 8, 16, 32, 64 초 간격으로 재시도 할것을 1, 0, 2, 5, 3, 12, 53초 만큼 대기하는 모습을 볼 수 있는데, 이는 Jitter 적용시 jitter 공식
if full_jitter: countdown = random.randrange(countdown + 1)
에 따라서 기존 countdown 이하의 무작위 정수 초 만큼 대기하기 때문이다.

retry_backoff_max와 함께 시도

@shared_task( bind=True, autoretry_for=(Exception,), retry_backoff=1, retry_backoff_max=12, retry_jitter=False, retry_kwargs={"max_retries": 20}, ) def exp_with_backoff_max_task(self, num: int | None = None): if num is None: num = random.randint(1, 100) logger.info(f"num: {num}") raise Exception("Exception!") """ [2024-12-27 14:52:50,198: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:52:50,209: INFO/ForkPoolWorker-8] app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548]: num: 21 [2024-12-27 14:52:50,238: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:52:50,265: INFO/ForkPoolWorker-8] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] retry: Retry in 1s: Exception('Exception!') [2024-12-27 14:52:51,215: INFO/ForkPoolWorker-8] app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548]: num: 42 [2024-12-27 14:52:51,220: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:52:51,226: INFO/ForkPoolWorker-8] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] retry: Retry in 2s: Exception('Exception!') [2024-12-27 14:52:53,229: INFO/ForkPoolWorker-8] app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548]: num: 100 [2024-12-27 14:52:53,236: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:52:53,241: INFO/ForkPoolWorker-8] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] retry: Retry in 4s: Exception('Exception!') [2024-12-27 14:52:57,244: INFO/ForkPoolWorker-8] app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548]: num: 72 [2024-12-27 14:52:57,254: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:52:57,265: INFO/ForkPoolWorker-8] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] retry: Retry in 8s: Exception('Exception!') [2024-12-27 14:53:05,254: INFO/ForkPoolWorker-8] app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548]: num: 46 [2024-12-27 14:53:05,267: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:53:05,276: INFO/ForkPoolWorker-8] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] retry: Retry in 12s: Exception('Exception!') [2024-12-27 14:53:17,268: INFO/ForkPoolWorker-8] app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548]: num: 49 [2024-12-27 14:53:17,281: INFO/MainProcess] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] received [2024-12-27 14:53:17,306: INFO/ForkPoolWorker-8] Task app.exp_with_backoff_max_task[fbe6226e-20d0-4a69-82a8-402c23b83548] retry: Retry in 12s: Exception('Exception!') """
최대 반복 한도 retry_backoff_max=12로 지정하니 1, 2, 4, 8로 증가 하던 재시도 간격이 12에서 더이상 증가하지 않는 것을 확인할 수 있다.

직접 retry 호출

@shared_task(bind=True) def custom_task(self, num: int | None = None): self.max_retries = 6 self.retry_backoff = 5 self.retry_jitter = False self.retry_backoff_max = 12 if num is None: num = random.randint(1, 100) try: logger.info(f"num: {num}") raise Exception("Exception!") except Exception as e: delay = get_exponential_backoff_interval( factor=int(max(1.0, self.retry_backoff)), retries=self.request.retries, maximum=self.retry_backoff_max, full_jitter=self.retry_jitter, ) self.retry(exc=e, countdown=delay, args=(num,)) """ [2024-12-27 15:04:54,619: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:04:54,685: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:04:54,769: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:04:54,852: INFO/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] retry: Retry in 5s: Exception('Exception!') [2024-12-27 15:04:59,716: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:04:59,723: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:04:59,732: INFO/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] retry: Retry in 10s: Exception('Exception!') [2024-12-27 15:05:09,728: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:05:09,736: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:05:09,752: INFO/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] retry: Retry in 12s: Exception('Exception!') [2024-12-27 15:05:21,748: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:05:21,767: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:05:21,798: INFO/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] retry: Retry in 12s: Exception('Exception!') [2024-12-27 15:05:33,764: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:05:33,788: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:05:33,811: INFO/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] retry: Retry in 12s: Exception('Exception!') [2024-12-27 15:05:45,784: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:05:45,795: INFO/MainProcess] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] received [2024-12-27 15:05:45,834: INFO/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] retry: Retry in 12s: Exception('Exception!') [2024-12-27 15:05:57,796: INFO/ForkPoolWorker-8] app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de]: num: 99 [2024-12-27 15:05:57,828: ERROR/ForkPoolWorker-8] Task app.custom_task[e62cfd1e-a266-477d-953a-e2d35cf247de] raised unexpected: Exception('Exception!') """
데코레이터에 옵션으로 주지 않아도, 직접 수동으로 self.retry()Exception 처리를 할 수 있다. 이 경우에 Exception 종류에 따라서 다르게 retry()를 설정할 수도 있다.
또한 수동으로 retry를 호출하면 재시도할 태스크에 매게변수를 args또는 kwargs로 지정해줄 수도 있다. 위의 사례에서 최초 실행 시 랜덤으로 정해진 숫자를 다음 태스크에도 계속 넘기는 것을 확인할 수 있다.
이 때 self.max_retries는 인스턴스 변수로 지정해주면 값이 적용되지만, retry_backoff, retry_jitter, retry_backoff_max등은 지정하는 것만으로 자동으로 사용되지 않는다. 이는 retry()시 backoff와 관련된 로직은 즉시 적용되는 것이 아닌 add_autoretry_behaviour() 라는 함수로 지정이 되는데 self.retry()를 직접 호출시에는 해당 함수가 실행되지 않기 때문이다. 그렇기에 해당 값들을 이용해서 직접 delay를 계산 후, retry()countdown에 전달해주어야 한다.

Reference