ES /docs

InvalidParameterException on task_id: arn:aws:ecs:us-west-2:619071347432:task/cupix-tesla-ece/48ecc8

RCA: InvalidParameterException on stopped ECS task 48ecc8fc

Overview#

What Happened#

2026-07-01 11:55 KST부터 cupixvista-api-worker(tesla repo, LAUNCH_MODE=CUPIXVISTA)에서 ECS task 48ecc8fc146c40e6b7f1da84a352577b에 대해 AwsTask#stop!을 호출할 때마다 InvalidParameterException: The referenced task was not found가 발생하고 있다. 해당 task 자체는 2026-07-01 10:45:32 KST에 이미 ECS 상에서 STOPPED 상태가 되었고, 이후 ECS retention window(약 1시간)가 만료되면서 describe/stop 대상에서 사라졌다. 그럼에도 Cupix::Cron::Job.clean_running_jobs 크론이 10분마다 같은 task를 반복적으로 stop 시도하고 있다.

Quick Facts#

Field Value
exception.class Aws::ECS::Errors::InvalidParameterException
exception.message The referenced task was not found
top_frame app/models/aws_task.rb:172 (rescue in stop!)
runtime Ruby on Rails (Sidekiq/whenever cron via cupixvista-api-worker)
env production, region us-west-2, cluster cupix-tesla-ece

Affected Teams#

Team / Domain Error Count Impact
cupixvista-api-worker (tesla) 6 (14일 retention 내, 10분 간격 반복) 사용자 기능 영향 없음. 하지만 error 로그 노이즈 및 stale DB row(aws_tasks.last_status='RUNNING') 발생

Timeline#

  1. 2026-07-01 10:38:28 KST — ECS task 48ecc8fc 생성(connectivity_at), family:cupix-skat-master-production-arm.
  2. 2026-07-01 10:45:32 KST — ECS가 stop_code=UserInitiated, stopped_reason="Task has been stopped by Tesla"로 task를 STOPPED 처리 (execution_stopped_at, stopped_at).
  3. 2026-07-01 11:05:29 KST 이후 10분 간격Cupix::Cron::Job.clean_running_jobs cron이 매 사이클마다 AwsTask#stop!을 재호출. 이 시점에는 ECS에 아직 stopped task가 남아 있어 resp.task 덤프가 로그에 그대로 찍힘 (last_status=STOPPED).
  4. 2026-07-01 11:55:27 KST — ECS가 stopped task를 purge. 이후 stop_task 호출은 InvalidParameterException: The referenced task was not found로 실패 (첫 first_seen).
  5. 2026-07-01 12:45:27 KST — 6회째 동일 에러(last_seen). 계속 재발생 중.

Error Log#

Datadog Logs

text
InvalidParameterException on task_id: arn:aws:ecs:us-west-2:619071347432:task/cupix-tesla-ece/48ecc8fc146c40e6b7f1da84a352577b, message: The referenced task was not found

Impact#

  • Service: cupixvista-api-worker
  • 발생 횟수: 6 (10분 간격 반복, 자체 종료되지 않음)
  • 최초 발생: 2026-07-01 11:55 KST
  • 최근 발생: 2026-07-01 12:45 KST

사용자 요청 경로가 아닌 백그라운드 cron이므로 최종 사용자 영향은 없으나, 노이즈성 error 로그가 계속 쌓이고 원인이 해소되지 않으면 무기한 지속된다. 또한 실제로는 stopped 된 task에 매핑된 DB row(aws_tasks.last_status='RUNNING')가 계속 남아 Job.running 상태를 부정확하게 만든다.

Root Cause Summary#

AwsTask#stop!은 ECS의 stop_task 호출 후 내부적으로 fetch!(by_force: true)를 실행해 상태를 최신화하지만, fetch!는 ActiveRecord 속성을 write만 하고 save를 호출하지 않는다 (aws_task.rb:118-142, cf. pull!fetch! + save 조합). 그 결과 실제 ECS에서 task가 STOPPED가 된 뒤에도 DB row의 last_status='RUNNING'이 유지되고, AwsTask.running scope (where(last_status: [RUNNING], desired_status: [RUNNING, PENDING]))에 그대로 포함된다. 10분마다 실행되는 Cupix::Cron::Job.clean_running_jobs가 같은 task에 대해 stop!을 무한 반복 호출하고, ECS가 stopped task를 retention window 후 purge하면서 Aws::ECS::Errors::InvalidParameterException: The referenced task was not found가 매 사이클 발생한다. stop!의 rescue 절이 이 예외를 삼키고 조기 return하기 때문에 DB row도 STOPPED로 정정되지 않아 루프가 종료되지 않는다.

Technical Analysis#

Code Path#

  • Entry point: lib/cupix/cron/job.rb:12 — 10분 간격 cron Cupix::Cron::Job.clean_running_jobs
  • Selection: Job.running.where('state_updated_at < ?', 3.days.ago)job.aws_tasks.running.each(&:stop!)
  • AwsTask.running scope: app/models/aws_task.rb:20
  • Stop path: app/models/aws_task.rb:161-188
  • Failure point (예외 로그): app/models/aws_task.rb:172-173
  • 상태 미persist 원인: app/models/aws_task.rb:112-143 (fetch!_writesave 없음)
app/models/aws_task.rb:14-23ruby
PRE_RUNNING_STATUSES = [PROVISIONING, PENDING, ACTIVATING].freeze

belongs_to :job

scope :blank, -> { where(archived_at: nil).where(last_status: nil, desired_status: nil) }
scope :pending, -> { where(archived_at: nil).where(last_status: [PROVISIONING, PENDING], desired_status: [RUNNING]) }
scope :running, -> { where(archived_at: nil).where(last_status: [RUNNING], desired_status: [RUNNING, PENDING]) }
scope :stopping, -> { where(archived_at: nil).where(last_status: RUNNING, desired_status: STOPPED) }
scope :stopped, -> { where(archived_at: nil).where(last_status: STOPPED, desired_status: STOPPED) }

running scope는 DB의 last_statusRUNNING인 한 계속 매칭된다.

lib/cupix/cron/job.rb:12-20ruby
::Job.running.where('state_updated_at < ?', 3.days.ago).each do |job|
  if job.aws_tasks.running.exists?
    running_jobs_with_running_tasks_over_3days << job.id
    job.aws_tasks.running.each(&:stop!)
  else
    Cupix::Logger.info("[TES10003] Stopped job found: #{job.id}", ...)
    job.stopped_state_with_error('TES10003', 'Job has been stopped by system')
  end
end

DB 기준으로 aws_tasks.running이 존재하면 매 10분마다 stop!이 호출된다.

app/models/aws_task.rb:112-143ruby
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'] })
  resp_tasks = resp.tasks

  if resp_tasks.blank?
    if last_status.in?(PRE_RUNNING_STATUSES) && created_at > RECENTLY_CREATED_GRACE_PERIOD.ago
      Cupix::Logger.warn("Task not found but recently created (#{last_status}), skipping STOPPED transition - task_id: #{task_id}", ...)
      return
    end

    Cupix::Logger.warn("Task not found with task_id: #{task_id}", ...)
    self.update(last_status: STOPPED, desired_status: STOPPED, stopped_at: Time.current, stopped_reason: 'Task not found')
    return
  end

  task = resp_tasks.first.as_json
  _write(task)
  _set_instance_type(task)

  Cupix::Logger.info("Task fetched: #{task_id}", ...)
  task
end

정상 응답 경로(resp_tasks 존재)에서 _write는 attribute assignment만 수행하고 save가 없다. 응답이 STOPPED임에도 DB에는 반영되지 않는다. 반대로 resp_tasks.blank?(purge 이후) 분기에서만 update로 명시적으로 STOPPED로 저장한다. 그러나 이 경로는 stop! 내부에서 도달하지 못한다(아래 설명).

app/models/aws_task.rb:161-188ruby
def stop!
  begin
    resp = self.ecs_client.stop_task({
      task: task_id,
      cluster: $AWS[:ecs][:cluster_name],
      reason: 'Task has been stopped by Tesla'
    })
  rescue Aws::ECS::Errors::ClientException => e
    Cupix::Logger.error("ClientException on task_id: #{task_id}, message: #{e.message}", ...)
  rescue Aws::ECS::Errors::ClusterNotFoundException => e
    Cupix::Logger.error("ClusterNotFoundException on task_id: #{task_id}, message: #{e.message}", ...)
  rescue Aws::ECS::Errors::InvalidParameterException => e
    Cupix::Logger.error("InvalidParameterException on task_id: #{task_id}, message: #{e.message}", ...)
  rescue Aws::ECS::Errors::ServerException => e
    Cupix::Logger.error("ServerException on task_id: #{task_id}, message: #{e.message}", ...)
  end

  Cupix::Logger.info(resp, class: self.class.name, function: __method__, task: { task_id: task_id })
  Cupix::Logger.info("Task has been requested to stop on task_id: #{task_id}", ...)

  return if resp.blank? || resp.task.blank?

  fetched = fetch!(by_force: true)

  unless fetched
    clean_stale_task
  end
end

InvalidParameterException이 발생하면 resp는 nil이므로 return if resp.blank?가 걸린다. fetch!가 호출되지 않으므로 "task not found" 분기(self.update(last_status: STOPPED, ...))에도 도달하지 못한다. 결과적으로 DB row는 영구히 RUNNING으로 남는다. 또한 정상 응답이 있었던 사이클(11:05~11:45)에서도 fetch!가 save를 하지 않아 DB가 갱신되지 않는다.

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

pull!은 명시적으로 save를 호출하지만 stop!의 하위 호출 경로에는 pull!이 없다.

기대 동작 vs 실제 동작

  • 기대: stop! 호출 이후 DB last_statusSTOPPED로 동기화되어 이후 cron scope에서 제외되어야 함.
  • 실제: stop!fetch!(정상) 경로는 save를 하지 않고, stop! → rescue(InvalidParameter) 경로는 fetch! 자체를 건너뛰어 stale row가 유지됨. 결과적으로 cron이 무기한 반복 실패.

Log Evidence#

Datadog query (재현용):

text
service:cupixvista-api-worker "48ecc8fc146c40e6b7f1da84a352577b"

정상 응답 사이클(11:15:25 KST) — task가 이미 ECS에서 STOPPED 상태로 반환되고 있음:

text
2026-07-01 11:15:25  info  AwsTask#stop!
{:task=>{..., :desired_status=>"STOPPED",
         :execution_stopped_at=>"2026-07-01T01:45:32.960+00:00",
         :last_status=>"STOPPED",
         :stop_code=>"UserInitiated",
         :stopped_at=>"2026-07-01T01:45:32.983+00:00",
         :stopped_reason=>"Task has been stopped by Tesla",
         :task_arn=>"arn:aws:ecs:us-west-2:619071347432:task/cupix-tesla-ece/48ecc8fc146c40e6b7f1da84a352577b"}}

2026-07-01 11:15:25  info  AwsTask#stop!    Task has been requested to stop on task_id: ...48ecc8fc...
2026-07-01 11:15:25  info  AwsTask#fetch!   Task fetched: ...48ecc8fc...

ECS purge 이후 사이클(11:55:27 KST 이후) — task가 사라지고 InvalidParameterException 발생, fetch! 미실행:

text
2026-07-01 11:55:27  error  AwsTask#stop!  InvalidParameterException on task_id: arn:aws:ecs:us-west-2:619071347432:task/cupix-tesla-ece/48ecc8fc146c40e6b7f1da84a352577b, message: The referenced task was not found
2026-07-01 11:55:27  info   AwsTask#stop!  Task has been requested to stop on task_id: ...48ecc8fc...

이후 12:05, 12:15, 12:25, 12:35, 12:45 KST 매 10분마다 동일 패턴 반복(총 6회, 14일 retention 내). Task fetched: 로그가 사라진 것이 fetch!가 rescue 이후 return으로 건너뛰어졌다는 증거이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 stop!의 정상 경로가 ECS 응답을 fetch만 하고 save하지 않아 DB의 last_statusRUNNING으로 유지되고, Cupix::Cron::Job.clean_running_jobs가 10분마다 재시도. ECS retention 만료 후 InvalidParameterException으로 이어짐. fetch!save 없음 (aws_task.rb:112-143), pull!save 포함(:95-98). 로그상 stop!이 10분 간격으로 반복(clean_running_jobs cron 주기와 일치, schedule.rb:67). ECS 응답 덤프에 last_status=STOPPED가 이미 존재하지만 다음 사이클에서도 여전히 stop! 대상. stop! rescue 분기는 조기 return하므로 stale row 자정 불가. Confirmed
H2 사용자/API 요청이 매 10분마다 JobOperation.stop_aws_task를 호출하여 stop!을 트리거. 사용자 요청은 10분 주기로 정확히 반복될 수 없고, JobOperationjob.aws_tasks.last.stop!을 호출하지 특정 task를 반복 지목하지 않음 (app/operations/job_operation.rb:8). 로그에 사용자/컨트롤러 컨텍스트 흔적 없음. Rejected
H3 ECS 측 IAM/권한 문제로 stop_task가 실패. 에러 메시지는 The referenced task was not found이며, 앞선 사이클(11:15~11:45 KST)에서는 동일 task에 대해 describe/stop이 정상 반환(resp.task 덤프 존재). 권한 오류라면 응답 자체가 아예 실패했어야 함. AccessDenied 등 다른 예외 클래스도 없음. Rejected
H4 AWS/ECS 리전 장애로 인한 일시적 오류. 다른 task는 로그에 영향 없음(동일 서비스 status:error 검색 결과가 이 task 하나만). status-board에서 dep:* active 인시던트 없음(svc:cupixvista-api-worker::unknown, active:null). Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 대상 파일: app/models/aws_task.rb:172-173 (InvalidParameterException rescue) 및 app/models/aws_task.rb:161-188 (stop! 전체).
  • 방향: Aws::ECS::Errors::InvalidParameterException — 특히 메시지가 "The referenced task was not found"인 경우 — 는 사실상 "이미 stopped/purged된 task"이므로 error가 아니라 정상 종료로 취급해야 한다. rescue 안에서 DB row를 STOPPED로 정정하고 warn 레벨로 다운그레이드. 예: self.update(last_status: STOPPED, desired_status: STOPPED, stopped_at: stopped_at || Time.current, stopped_reason: stopped_reason || 'Task purged from ECS') 후 조기 return. 이렇게 하면 다음 cron 사이클에서 이 task가 aws_tasks.running scope에 잡히지 않아 반복이 종료된다.
  • 로그 레벨 조정: 저장 후에도 남길 경우 Cupix::Logger.warn으로 낮춘다. 이는 memory의 "true bug vs expected operational scenario" 원칙에 부합한다. task가 ECS retention window 만료로 사라지는 것은 정상 이벤트이다.

단기 개선 (1주 이내)#

  • 대상 파일: app/models/aws_task.rb:112-143 (fetch!).
  • 방향: stop!이 사용하는 fetch!(by_force: true)에서도 _write 이후 결과가 실제로 persist 되도록 save를 추가하거나, stop!fetch! 대신 pull!(fetch! + save, :95-98)을 호출하도록 변경한다. by_force 인자가 현재 정의만 되어 있고 실제로 사용되지 않는 것도 함께 정리. 이 변경만으로도 정상 stop 이후 DB가 즉시 STOPPED로 동기화되어 반복 stop! 자체가 발생하지 않는다.
  • 부수 확인: AwsTask 테이블에서 최근 24시간 이상 last_status='RUNNING'이지만 실제 ECS 응답이 STOPPED이거나 not-found인 stale row가 얼마나 있는지 조사. 필요시 일회성 backfill job으로 정리.

장기 개선 (재발 방지)#

  • AwsTask의 상태 동기화 계약을 통일: "ECS 응답을 반영하는 모든 코드 경로는 save까지 책임진다"는 원칙을 fetch!/batch_pull!/pull! 세 메서드에 일관되게 적용하고, save 없는 write는 private helper로 격리.
  • Cupix::Cron::Job.clean_running_jobs 로직 검토: DB 기준으로 aws_tasks.running.exists?를 판단하되, 실제 stop 시도 전 최신 batch_pull!로 상태 재확인을 선행하도록 하여 stale scope에 의한 무한 재시도를 방지.
  • stop_running cron(Cupix::Cron::AwsTask.pull_stopping, schedule.rb:23)이 주석 처리되어 있는 이유를 재검토. 활성화하면 DB desired_status=STOPPED, last_status=RUNNING인 row도 주기적으로 재조회되어 자연스럽게 STOPPED로 동기화된다.

Monitoring#

Datadog dashboard timeseries widget용 쿼리 (writing-datadog-monitoring-queries 규칙 준수, 파이프/stats/count by(...) 미사용):

동일 task_id에 대한 반복 InvalidParameterException 감지:

text
sum:trace.error.hits{service:cupixvista-api-worker,resource_name:AwsTask#stop!}.as_count()

로그 기반 카운트 (로그 인덱스 메트릭 활용 시):

text
logs("service:cupixvista-api-worker status:error \"InvalidParameterException\" \"AwsTask\"").index("*").rollup("count").last("1h")

Stale aws_tasks.running row 수 (DB metric — 별도 exporter 필요) — 신규 추가 권장:

text
avg:tesla.aws_tasks.running.stale_over_24h{env:production}

알림 임계값 예:

  • 동일 task_id의 InvalidParameterException이 30분 내 3회 이상 → Slack #api-alerts-debug-production 알림 (반복 실패의 조기 감지).

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (즉시 조치는 rescue 분기 하나에 update 한 줄 + return 처리)

즉시 조치는 이미 존재하는 "task not found" 분기(aws_task.rb:132-134)의 로직을 rescue 경로에도 동일하게 적용하는 형태로, side effect 표면적이 매우 좁다. 단기 개선(fetch!에 save 추가)은 stop! 이외에도 fetch! 직접 호출 지점이 있는지 확인이 필요하다 — 현재 stop! 내부 호출 외에 다른 진입점은 발견되지 않았으나, 부작용 회피를 위해 stop!pull!을 호출하도록 바꾸는 최소 변경이 더 안전할 수 있다.