ES /docs

PullTaskWorker::perform | error on 9680 - Rate exceeded

RCA: PullTaskWorker::perform | error on 9680 - Rate exceeded

Overview#

What Happened#

2026-06-25 13:21:39 KST에 cupixworks-worker 서비스의 PullTaskWorker가 AwsTask id=9680 처리 중 AWS ECS API의 Rate exceeded 응답을 받아 단 1회 error 로그를 남겼다. ECS DescribeTasks API 호출이 throttling 한도에 걸린 것으로, 동일 메시지의 재발은 지난 7일 내 관측되지 않았다.

Quick Facts#

Field Value
exception.message Rate exceeded
top_frame app/workers/pull_task_worker.rb:14
origin AWS ECS DescribeTasks API throttling
env production, ap-southeast-1

Affected Teams#

Team / Domain Error Count Impact
cupixworks-worker (AwsTask 동기화) 1 AwsTask 9680의 ECS 상태 1회 polling 누락. Sidekiq retry: 1 설정으로 1회 재시도 가능.

Timeline#

  1. 2026-06-25 13:21:39 KSTPullTaskWorker::perform | begins on 9680 (info)
  2. 2026-06-25 13:21:39 KSTPullTaskWorker::perform | error on 9680 - Rate exceeded (error). 동일 task 9680에 대한 후속 begins/done 로그는 없음.

Error Log#

Datadog Logs

text
PullTaskWorker::perform | error on 9680 - Rate exceeded

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 1
  • 최초 발생: 2026-06-25 13:21:39 KST
  • 최근 발생: 2026-06-25 13:21:39 KST

지난 7일 기간에 "Rate exceeded"로 검색했을 때 본 클러스터의 1건이 유일하다 (service:cupixworks-worker "Rate exceeded" 쿼리 결과). AwsTask 9680을 한 cycle 누락한 수준으로, 다른 task ID(195575, 195597, 873985 등)는 정상적으로 begins/done이 짝지어 기록되었다. 동일 시간대 다른 PullTaskWorker 실행은 정상이었기 때문에 사용자 영향은 최소이거나 없음.

Root Cause Summary#

PullTaskWorker#performAwsTask#pull!fetch!를 통해 AWS ECS의 describe_tasks API를 호출하는데, 이 호출이 ECS API의 단위 시간당 호출 한도(ThrottlingException, message Rate exceeded)에 부딪혀 실패했다. Worker는 rescue StandardError 블록에서 에러를 로깅만 하고 종료하므로, 단발성 AWS throttling이 그대로 error 로그로 노출된 것이다. Sidekiq 옵션은 retry: 1이지만 rescue가 예외를 삼키기 때문에 Sidekiq의 재시도 큐로 진입하지 않는다.

Technical Analysis#

Code Path#

  • Entry point: app/workers/pull_task_worker.rb:5 (PullTaskWorker#perform)
  • Throttling 발생 지점: app/models/aws_task.rb:118 (ecs_client.describe_tasks)
  • Failure log line: app/workers/pull_task_worker.rb:14

Worker는 단일 task ID를 받아 ECS API를 1회 호출한다. 동일 시점에 다수의 PullTaskWorker가 enqueue되면 각각 독립된 describe_tasks 호출을 발생시켜 throttling이 발생할 수 있다.

app/workers/pull_task_worker.rb:1-16ruby
class PullTaskWorker
  include Sidekiq::Worker
  sidekiq_options queue: :default, retry: 1

  def perform(id)
    Cupix::Logger.info("PullTaskWorker::perform | begins on #{id}", class: self.class.name, function: __method__)
    task = ::AwsTask.find_by_id(id)
    return if task.nil?

    task.pull!

    Cupix::Logger.info("PullTaskWorker::perform | done on #{id}", class: self.class.name, function: __method__, task: { task_id: task.task_id })
  rescue StandardError => e
    Cupix::Logger.error("PullTaskWorker::perform | error on #{id} - #{e.message}", class: self.class.name, function: __method__, task: { task_id: task.task_id })
  end
end

pull!은 ECS API를 직접 호출한다.

app/models/aws_task.rb:95-122ruby
def pull!
  self.fetch!
  self.save
end

# ...

def fetch!(by_force: false)
  unless fetchable?
    Cupix::Logger.info("Task is not fetchable with task_id: #{task_id}, last status: #{last_status}", ...)
    return
  end

  resp = self.ecs_client.describe_tasks({
    tasks: [task_id],
    cluster: $AWS[:ecs][:cluster_name],
    include: ['TAGS']
  })

PullTaskWorker는 두 경로에서 enqueue된다.

app/repositories/job_repository.rb:214-218ruby
def pull
  @model.aws_tasks.each do |task|
    PullTaskWorker.perform_async(task.id)
  end
end
app/models/concerns/statable/job.rb:86-89ruby
after_transition any => :stopped do |job, transition|
  job.aws_tasks.each do |task|
    PullTaskWorker.perform_in(20.second, task.id)
  end

기대 동작: describe_tasks 호출 성공 → _writesave → done 로그. 실제 동작: AWS ECS가 ThrottlingException (Rate exceeded)을 반환 → rescue StandardError가 예외를 잡고 error 로그만 남김 → task 상태는 다음 cron(Cupix::Cron::AwsTask.pull_* batch job)에서 재동기화될 때까지 stale.

Log Evidence#

사용한 Datadog 쿼리.

text
service:cupixworks-worker "PullTaskWorker" "Rate exceeded"
text
service:cupixworks-worker "PullTaskWorker" "9680"
text
service:cupixworks-worker "Rate exceeded"

해당 task 9680에 대한 begins/error pair 외에는 동일 task에 대한 후속 로그가 없다.

text
2026-06-25 13:21:39  info   PullTaskWorker::perform | begins on 9680
2026-06-25 13:21:39  error  PullTaskWorker::perform | error on 9680 - Rate exceeded

같은 시각 전후 동일 worker의 다른 호출은 모두 정상.

text
2026-06-25 13:29:48  info   PullTaskWorker::perform | begins on 9681
2026-06-25 13:29:48  info   PullTaskWorker::perform | done on 9681
2026-06-25 13:30:11  info   PullTaskWorker::perform | begins on 195596
2026-06-25 13:30:11  info   PullTaskWorker::perform | done on 195596

7일 범위 "Rate exceeded" 검색 결과는 본 클러스터 1건뿐이다 → 만성적 throttling이 아닌 spike성 single-event.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 AWS ECS DescribeTasks API throttling으로 Rate exceeded 예외가 발생 (root cause) fetch!가 직접 ecs_client.describe_tasks 호출 (app/models/aws_task.rb:118); AWS SDK의 표준 throttling 메시지가 정확히 Rate exceeded; worker의 rescue StandardError 블록 (pull_task_worker.rb:13)이 메시지를 그대로 interpolate. Confirmed
H2 Sidekiq 재시도 폭주로 동일 task ID에서 반복 에러 발생 동일 worker class 동일 task 9680에 대한 begins/error는 13:21:39 단 1쌍, retry 흔적 없음 (service:cupixworks-worker "PullTaskWorker" "9680" 쿼리 결과 2건만 반환). rescue가 예외를 삼키므로 Sidekiq retry 진입 자체가 불가. Rejected
H3 AwsTask 9680이 archive되었거나 nil이 반환되어 task: { task_id: nil } 직렬화에서 발생 find_by_id가 nil이면 line 8에서 즉시 return하므로 error log에 도달하지 않음. begins on 9680 info 로그가 먼저 남았다는 사실은 find_by_id가 record를 반환했음을 의미. Rejected
H4 외부 의존성 광역 장애(예: AWS ap-southeast-1 ECS 부분 outage) 클러스터 region 태그가 ap-southeast-1 status-board dep:* 활성 incident 없음; 같은 시간대 다른 PullTaskWorker 호출은 정상 완료; 7일 내 동일 메시지가 단 1건. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 즉시 조치 불필요. 단발성 throttling이며 사용자 영향이 사실상 없고, 다음 cron cycle에서 동일 task가 batch 경로(AwsTask.batch_pull!)로 자연스럽게 재동기화된다.

단기 개선 (1주 이내)#

  • app/workers/pull_task_worker.rb:13-14 — AWS throttling은 운영상 회복 가능한 transient 상태이므로 error 레벨로 alarm 노이즈를 만들 필요가 없다. Aws::ECS::Errors::ThrottlingException (혹은 message가 Rate exceeded인 케이스)을 별도로 rescue하여 warn으로 다운그레이드하는 방향 검토. 기존 rescue StandardError는 유지하되 throttling만 분기.
  • 같은 파일에서 rescue StandardError가 예외를 삼켜 Sidekiq retry: 1 옵션이 사실상 작동하지 않는다. Throttling에 대해서는 예외를 re-raise하여 Sidekiq의 지수 백오프 재시도가 동작하도록 하는 안을 고려. 단, 재시도 시점도 throttle 상태일 수 있으므로 Sidekiq의 retry interval과 함께 평가 필요.

장기 개선 (재발 방지)#

  • app/repositories/job_repository.rb:214-218, app/models/concerns/statable/job.rb:86-89 — 한 Job의 모든 aws_tasks를 task별로 개별 PullTaskWorker로 fan-out하는 패턴은 task 수에 비례해 ECS API 호출이 증가한다. AwsTask.batch_pull! (app/models/aws_task.rb:45-89)이 이미 describe_tasksBATCH_SIZE: 100으로 묶는 batch 경로를 제공하므로, 동일 Job에 속한 task들을 1회 batch 호출로 묶는 worker로 리팩터링하면 throttling 표면적이 크게 줄어든다.
  • AWS SDK 레벨의 client-side rate limiting (예: Aws.config[:retry_mode] = 'adaptive') 적용 검토. ECS client 인스턴스가 호출 시마다 새로 생성되고 있어 (ecs_client 메서드, line 91-93) SDK 내장 재시도/back-off 정책이 호출 간 학습되지 않는다.

Monitoring#

  • ECS API throttling 추세 추적용 쿼리:
text
service:cupixworks-worker "Rate exceeded"
  • PullTaskWorker 에러율 시계열:
text
service:cupixworks-worker status:error @class:PullTaskWorker
  • PullTaskWorker 처리량과 에러를 동시에 보기 위한 baseline:
text
service:cupixworks-worker @class:PullTaskWorker

위 쿼리는 모두 timeseries widget에서 그대로 사용 가능한 facet 기반 expression이며, monitor-only 문법(| stats, count by(...))을 포함하지 않는다.

Risk Assessment#

  • Risk level: low — 단발 1회, 사용자 영향 미미, 다음 cron이 자가 복구.
  • 예상 복잡도: trivial (throttling만 warn으로 분리) — standard (fan-out을 batch_pull!로 통합하는 단기/장기 개선까지 진행 시).