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#
- 2026-07-01 10:38:28 KST — ECS task
48ecc8fc생성(connectivity_at),family:cupix-skat-master-production-arm. - 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). - 2026-07-01 11:05:29 KST 이후 10분 간격 —
Cupix::Cron::Job.clean_running_jobscron이 매 사이클마다AwsTask#stop!을 재호출. 이 시점에는 ECS에 아직 stopped task가 남아 있어resp.task덤프가 로그에 그대로 찍힘 (last_status=STOPPED). - 2026-07-01 11:55:27 KST — ECS가 stopped task를 purge. 이후
stop_task호출은InvalidParameterException: The referenced task was not found로 실패 (첫first_seen). - 2026-07-01 12:45:27 KST — 6회째 동일 에러(
last_seen). 계속 재발생 중.
Error Log#
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분 간격 cronCupix::Cron::Job.clean_running_jobs - Selection:
Job.running.where('state_updated_at < ?', 3.days.ago)→job.aws_tasks.running.each(&:stop!) AwsTask.runningscope: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!—_write후save없음)
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_status가 RUNNING인 한 계속 매칭된다.
::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!이 호출된다.
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! 내부에서 도달하지 못한다(아래 설명).
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가 갱신되지 않는다.
def pull!
self.fetch!
self.save
end
pull!은 명시적으로 save를 호출하지만 stop!의 하위 호출 경로에는 pull!이 없다.
기대 동작 vs 실제 동작
- 기대:
stop!호출 이후 DBlast_status가STOPPED로 동기화되어 이후 cron scope에서 제외되어야 함. - 실제:
stop!→fetch!(정상) 경로는 save를 하지 않고,stop!→ rescue(InvalidParameter) 경로는 fetch! 자체를 건너뛰어 stale row가 유지됨. 결과적으로 cron이 무기한 반복 실패.
Log Evidence#
Datadog query (재현용):
service:cupixvista-api-worker "48ecc8fc146c40e6b7f1da84a352577b"
정상 응답 사이클(11:15:25 KST) — task가 이미 ECS에서 STOPPED 상태로 반환되고 있음:
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! 미실행:
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_status가 RUNNING으로 유지되고, 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분 주기로 정확히 반복될 수 없고, JobOperation은 job.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(InvalidParameterExceptionrescue) 및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.runningscope에 잡히지 않아 반복이 종료된다. - 로그 레벨 조정: 저장 후에도 남길 경우
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_runningcron(Cupix::Cron::AwsTask.pull_stopping,schedule.rb:23)이 주석 처리되어 있는 이유를 재검토. 활성화하면 DBdesired_status=STOPPED, last_status=RUNNING인 row도 주기적으로 재조회되어 자연스럽게 STOPPED로 동기화된다.
Monitoring#
Datadog dashboard timeseries widget용 쿼리 (writing-datadog-monitoring-queries 규칙 준수, 파이프/stats/count by(...) 미사용):
동일 task_id에 대한 반복 InvalidParameterException 감지:
sum:trace.error.hits{service:cupixvista-api-worker,resource_name:AwsTask#stop!}.as_count()
로그 기반 카운트 (로그 인덱스 메트릭 활용 시):
logs("service:cupixvista-api-worker status:error \"InvalidParameterException\" \"AwsTask\"").index("*").rollup("count").last("1h")
Stale aws_tasks.running row 수 (DB metric — 별도 exporter 필요) — 신규 추가 권장:
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!을 호출하도록 바꾸는 최소 변경이 더 안전할 수 있다.