Bulk operation failed
RCA: Bulk operation failed
Overview#
What Happened#
2026-06-29 07:32 KST부터 약 45분 동안 cupixworks-worker (us-west-2)에서 BimRevision.bulk_operation이 29회 연속 실패했다. 모든 실패의 근본 원인은 동일하며, Elasticsearch bulk 인덱싱 도중 BimRevision의 as_indexed_json 직렬화 경로가 s3.me-south-1.amazonaws.com:443로 TCP 연결을 시도하다 timeout(execution expired)되었다. 동일 시간대에 별도 워커 Record.flush_geo_coordinate도 같은 me-south-1 endpoint에서 동일한 Seahorse::Client::NetworkingError로 실패하고 있어, BimRevision 코드 결함이 아니라 us-west-2 worker → me-south-1 S3 endpoint 구간의 네트워크 도달성 문제로 판단된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Seahorse::Client::NetworkingError |
| exception.message | Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired) |
| top_frame | app/models/concerns/searchable.rb:173 (rescued in bulk_operation) |
| originating_call | app/models/concerns/bim_revision_result.rb:48-57 (bim_revision_result_urls) |
| operation | update (Elasticsearch bulk update) |
| batch_size | 300 BimRevision IDs per call |
| deploy | production-us-west-2-20260628T2222Z0-bfdc5ebd-cupixworks |
| env | production, region us-west-2, host ip-10-1-18-149.us-west-2.compute.internal |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
BIM / Search indexing (BimRevision) |
29 (cluster) | me-south-1 tenant의 BimRevision Elasticsearch 인덱스 갱신이 지연/누락. 사용자가 검색·목록에서 최신 상태를 보지 못할 수 있음 |
Record geo-coordinate (Record.flush_geo_coordinate) |
동시 발생 (별도 cluster) | 같은 root cause로 worker 실패. capture/record 지오 좌표 동기화 지연 |
영향은 storage가 me-south-1에 있는 단일(또는 소수) tenant 범위로 추정되나, tenant ID는 로그에 포함되어 있지 않다 — uncertain, needs verification.
Timeline#
- 2026-06-29 07:32 KST — 첫
Bulk operation failed발생 (first_seen) - 2026-06-29 07:32–08:17 KST — 약 45분간 29건 연속 발생. 모두 동일 host
ip-10-1-18-149.us-west-2에서 me-south-1 endpoint로 TCP 연결 timeout - 2026-06-29 08:17 KST — 클러스터 마지막 발생 (
last_seen). 동일 시간대에 별도 cluster (Record.flush_geo_coordinate)도 같은 endpoint로 실패 중
Error Log#
Bulk operation failed
Impact#
- Service:
cupixworks-worker - 발생 횟수: 29
- 최초 발생: 2026-06-29 07:32 KST
- 최근 발생: 2026-06-29 08:17 KST
Root Cause Summary#
us-west-2 worker가 BimRevision.bulk_operation을 처리하면서 as_indexed_json → BimRevisionSerializer → bim_revision_result_urls 경로를 거치는데, 이 경로의 StorageOption#set_bucket_url이 me-south-1 tenant의 S3 bucket URL을 해석하기 위해 s3.me-south-1.amazonaws.com에 HTTP(S) 호출을 시도한다. 인시던트 구간 동안 us-west-2 worker에서 me-south-1 S3 endpoint로의 TCP 연결이 모두 timeout(execution expired)되어 Seahorse::Client::NetworkingError가 발생했고, searchable.rb의 광역 rescue StandardError가 이를 잡아 Bulk operation failed로 기록한 채 false를 리턴하면서 Sidekiq retry도 트리거되지 않았다. 같은 시간대에 동일 endpoint를 향한 Record.flush_geo_coordinate 워커도 실패하고 있어, BimRevision 코드의 결함이 아니라 us-west-2 ↔ me-south-1 구간의 네트워크/AWS 가용성 문제가 root cause다.
Technical Analysis#
Code Path#
- Entry point:
app/workers/bulk_index_worker.rb:14— Sidekiq worker가BimRevision.bulk_operation!호출 - Bulk 래퍼:
app/models/concerns/searchable.rb:170—bulk_operation이bulk_operation!을 감싸고StandardError를 rescue - 직렬화 루프:
app/models/concerns/searchable.rb:207-216—records.find_each안에서 각 record의as_indexed_json호출 - BimRevision serializer:
app/models/concerns/searchable/bim_revision.rb:91-123—BimRevisionSerializer로 직렬화 - S3 호출 발생:
app/models/concerns/bim_revision_result.rb:48-57—bim_revision_result_urls가 storage_option을 사용해 URL 생성 - Failure point:
app/models/storage_option.rb:118-126—set_bucket_url이Cupix::StorageService.bucket(...)을 통해 me-south-1 S3 endpoint에 도달 시도
bulk_operation의 rescue 핸들러 (로그 메시지를 만드는 지점):
def bulk_operation(ids, operation = 'index', refresh_cached = false)
bulk_operation!(ids, operation, refresh_cached)
rescue StandardError => e
Cupix::Logger.error('Bulk operation failed',
class: self.name, function: __method__,
operation: operation,
ids_count: ids&.size,
ids_first: ids&.first,
ids_last: ids&.last,
error_class: e.class.name,
error_message: e.message)
false
else
true
end
as_indexed_json 직렬화 도중 외부 S3 호출이 발생하는 경로:
def bim_revision_result_urls
return nil if revision.zero?
bim_revision_objects.map do |path|
Cupix::StorageService.object_url(
storage_option: storage_option,
key: "#{bim_revision_basepath(revision)}/#{path}"
)
end
end
object_url 자체는 cf_hostname/endpoint만 있으면 문자열 조합으로 끝나지만, storage_option이 s3_source_bucket_url을 캐시하지 못한 경우 StorageOption 초기화 단계에서 다음 경로가 실행되어 AWS SDK가 me-south-1 endpoint로 TCP 연결을 시도한다:
def set_bucket_url
raise Cupix::Errors::Argument.new(code: 'ARG10000', reason: 's3_source_bucket_name is required') if @s3_source_bucket_name.blank?
raise Cupix::Errors::Argument.new(code: 'ARG10000', reason: 's3_bucket_region is required') if @s3_bucket_region.blank?
cache_key = "bucket_url_#{@s3_source_bucket_name}_#{@s3_bucket_region}"
cached = Rails.cache.read(cache_key)
if cached.present?
@s3_source_bucket_url = cached
return
end
begin
bucket = Cupix::StorageService.bucket(name: @s3_source_bucket_name, storage_option: self)
_url = bucket.url
Rails.cache.write(cache_key, _url)
@s3_source_bucket_url = _url
rescue => e
raise e
end
end
def bucket(storage_option: nil, **kwargs)
opts = parse_storage_option(storage_option).merge(kwargs)
opts[:force_path_style] = true
check_required_params(opts, %i[name region])
Aws::S3::Bucket.new(opts)
end
기대 동작: ES 인덱싱 직렬화(as_indexed_json)는 in-memory 변환이어야 하며 외부 네트워크 호출이 발생하지 않아야 한다. 실제 동작: tenant storage가 me-south-1일 때 us-west-2 worker가 me-south-1 S3 endpoint로 TCP 연결을 시도하고, 인시던트 구간에는 해당 endpoint가 도달 불가하여 timeout. batch 처리 중 한 record에서 예외가 raise되면 동일 batch 내 다른 record는 처리되지 않은 채 batch 전체(300개)가 실패한다.
또한 bulk_operation은 예외를 false로 swallow하므로 Sidekiq의 재시도(BulkIndexWorker의 retry: 5)가 트리거되지 않는다:
class BulkIndexWorker
include Sidekiq::Worker
sidekiq_options queue: :default, retry: 5
def perform(model_name, ids, operation = 'index', refresh_cached = false)
return if ids.blank?
return unless model_name.is_a?(String)
model_name.classify.constantize.bulk_operation!(ids, operation, refresh_cached)
end
end
위 워커는 bulk_operation!(예외 전파 버전)을 호출하므로 자체적으로는 재시도가 동작한다. 그러나 동일한 me-south-1 timeout이 5번 재시도되는 동안에도 같은 endpoint가 계속 불통이라면 결과적으로 모든 재시도가 실패한다(retry interval: 300, 600, 1200, 2400, 2400초). 그리고 모델의 after_commit 콜백을 거쳐 들어오는 단일 record 인덱싱(_index_document → BulkIndexWorker.perform_async(self.class.name, [id], 'index'))은 동일 클래스의 bulk_operation!로 가지만, 본 클러스터에 기록된 ids_count: 300 배치는 재시도되지 않고 swallow된 것으로 보인다 — needs verification (어느 호출 site에서 bulk_operation(swallow)을 부르는지 추가 확인 필요).
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-worker "Bulk operation failed" @class:BimRevision
대표 로그 (가장 최근 발생, raw JSON):
{
"timestamp": "2026-06-28T23:17:13.296Z",
"level": "error",
"message": "Bulk operation failed",
"class": "BimRevision",
"function": "bulk_operation",
"operation": "update",
"ids_count": 300,
"ids_first": 19530,
"ids_last": 19829,
"error_class": "Seahorse::Client::NetworkingError",
"error_message": "Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)",
"service": "cupixworks-worker",
"environment": "production",
"host": "ip-10-1-18-149.us-west-2.compute.internal",
"version": "production-us-west-2-20260628T2222Z0-bfdc5ebd-cupixworks"
}
연속 batch ID 분포 (300개씩 진행되며 모두 동일 endpoint timeout):
ids_first=18930 ids_last=19229 (@timestamp 22:14:45Z = 07:14 KST 다음 날 — 직전 batch)
ids_first=19230 ids_last=19529 (@timestamp 23:15:49Z = 08:15 KST)
ids_first=19530 ids_last=19829 (@timestamp 23:17:13Z = 08:17 KST)
동일 시간대 다른 코드 경로에서도 같은 endpoint timeout이 관찰되어 BimRevision 한정 문제가 아님:
service:cupixworks-worker "execution expired" "s3."
{
"timestamp": "2026-06-28T23:29:13Z",
"level": "error",
"class": "Record",
"function": "flush_geo_coordinate",
"message": "flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)"
}
us-west-2 worker가 me-south-1 S3에 성공적으로 도달한 info 레벨 로그는 인시던트 시간대(2026-06-28 22:00–23:30 UTC)에 발견되지 않았다:
service:cupixworks-worker "me-south-1" status:info → 0 logs
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | us-west-2 worker → me-south-1 S3 endpoint 네트워크 도달 불가 (또는 AWS 측 endpoint 이슈) | 29건 모두 Seahorse::Client::NetworkingError: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired). 같은 시간대 별도 클래스(Record.flush_geo_coordinate)도 동일 endpoint로 동일 에러. 단일 host(ip-10-1-18-149.us-west-2)에서 집중 발생 |
— | Confirmed |
| H2 | BimRevision/bim_revision_result_urls 코드 로직 결함 (nil reference, 직렬화 오류 등) |
— | error_class가 Seahorse::Client::NetworkingError로 네트워크 계층 오류임이 명확. 같은 직렬화 경로가 다른 region에서는 평소 정상 동작 |
Rejected |
| H3 | Elasticsearch 클러스터 장애 | bulk_operation이 ES 호출을 포함하므로 ES 장애일 가능성 검토 |
error message가 s3.me-south-1.amazonaws.com:443 (S3 endpoint). ES 관련 에러 클래스(Elasticsearch::Transport::Transport::*)나 timeout 로그 없음 |
Rejected |
| H4 | Worker host의 일반 outbound 네트워크 장애 (모든 region 영향) | 단일 host에서 집중 발생 | timeout이 me-south-1 endpoint로 한정. 같은 host의 다른 outbound 실패 로그 없음 — 문제 범위가 region-pair 한정 | Rejected (scope is region-pair, not host-wide) |
| H5 | tenant storage_option에 잘못된 region 값(오설정)으로 me-south-1으로 잘못 라우팅 | — | me-south-1는 StorageOption#set_region_code (line 99-100)에 정상 정의된 region. 인시던트 이전 정상 동작 여부 확인 어려움 |
Inconclusive (needs tenant storage_option verification) |
Fix Recommendation#
즉시 조치 (Critical)#
- 외부 의존성 상태 확인: AWS Health Dashboard에서
me-south-1region(특히 S3)의 인시던트 여부를 확인한다. 인시던트 발생 구간(2026-06-28 22:32–23:17 UTC)과 일치한다면 별도 코드 수정 없이 모니터링으로 종결 가능. 과거 episodecc2e2887에서 cross-region 시나리오는 종종 일시적 운영 조건이라는 학습이 있다. - 단일 host 격리 점검: 영향 host
ip-10-1-18-149.us-west-2.compute.internal이 us-west-2 worker fleet 중 일부 인스턴스에 국한된 문제인지 확인 (VPC route table, NAT/Egress, EC2 ENI). 동일 fleet 다른 인스턴스에서는 정상이라면 해당 인스턴스를 교체/재시작.
단기 개선 (1주 이내)#
bim_revision_result_urls에서 동기 S3 호출 제거: ES 인덱싱(as_indexed_json)은 in-memory 변환이어야 하며 외부 S3 endpoint에 동기 호출이 일어나지 않아야 한다. URL은 사전 계산해 DB/cache에 저장된 값을 사용하거나,cf_hostname/endpoint가 모두 없으면 nil/빈 배열을 리턴하고 background에서 계산하도록 변경한다 (app/models/concerns/bim_revision_result.rb:48-57,app/models/storage_option.rb:106-130).- Rescue 범위 축소 및 로그 메시지 정확화:
app/models/concerns/searchable.rb:172의rescue StandardError는 ES bulk 호출 실패뿐 아니라 직렬화 단계의 외부 호출 실패까지 같은 로그 메시지(Bulk operation failed)로 묶는다. 메시지에 어느 단계에서 실패했는지(serialization vs ES bulk call) 명시하거나,Seahorse::Client::NetworkingError/Faraday::TimeoutError등 일시적 네트워크 오류는 별도 로그(예:warn)로 분리한다. - 재시도 전략 검토: 호출 site가
bulk_operation(swallow, 재시도 없음)을 부르는 경우와bulk_operation!(예외 전파, Sidekiq retry 5회)을 부르는 경우가 혼재한다. 일시적 네트워크 오류는 재시도되도록 호출 site를 정리한다.
장기 개선 (재발 방지)#
- 인덱싱 경로에서 외부 의존성 제거: ES 인덱싱 데이터는 모델 자체 컬럼 또는 캐시된 derived 값만 사용. 외부 서비스 URL 생성은 read-time(컨트롤러/serializer)에 수행하거나 별도 background job에서 채워둔다.
- Cross-region S3 호출 회로 차단(circuit breaker): tenant 별 region 도달성이 일정 시간 실패하면 회로를 차단해 worker thread time을 소진하지 않도록 한다.
- Per-region health 모니터: us-west-2 worker가 사용하는 각 tenant region에 대한 outbound 연결 상태를 별도 metric으로 노출.
Monitoring#
- 추가할 메트릭/알림:
Seahorse::Client::NetworkingError발생률을service/@error_class기준으로 추적- me-south-1 endpoint timeout 발생 시 worker 알림
Datadog 쿼리 예시 (release dashboard timeseries widget 용):
service:cupixworks-worker @error_class:Seahorse::Client::NetworkingError
service:cupixworks-worker "Bulk operation failed" @class:BimRevision
service:cupixworks-worker "s3.me-south-1.amazonaws.com"
Risk Assessment#
- Risk level: medium — 단일 region/tenant 범위의 외부 의존성 문제이나 ES 인덱스 정합성에 영향을 주며, 일부 호출 site의 swallow 패턴으로 인해 일시적 장애가 인덱싱 누락으로 굳어질 수 있음.
- 예상 복잡도: standard — 즉시 조치는 외부 의존성 확인 + 인스턴스 격리. 단기 개선(rescue 범위/메시지, 인덱싱 경로의 동기 S3 호출 제거)은 모델/serializer 레이어 리팩토링 수준.