Cupix::Errors::System: Mysql2::Error: Unknown column 'summary_text' in 'field list'
RCA: Cupix::Errors::System: Mysql2::Error: Unknown column 'summary_text' in 'field list'
Overview#
TSLA-13207 의 후속 contract migration 20260806194600_remove_summary_text_from_spacetimes 가 spacetimes.summary_text 컬럼을 제거하는 rolling deploy 도중, 아직 드랍 이전에 커넥션을 맺은 구 인스턴스가 stale schema cache 로 spacetimes 테이블 전체 컬럼(제거된 summary_text 포함)을 SELECT 하여 발생한 transient 에러다. 리전별로 순차 배포되며 각 리전의 cutover 시간창에서만 나타났고, 인스턴스 drain 후 자가 해소되었다.
What Happened#
2026-08-06 (us-west-2) 와 2026-08-07 (APAC) 두 차례 production 배포 cutover 시간창에서 POST /api/v1/captures (capture 생성) 요청이 HTTP 500 으로 실패했다. capture 생성 시 SpacetimeEntity 의 before_save :set_spacetime 콜백이 Spacetime.find_or_create_by! 로 spacetimes 를 조회하는데, 방금 드랍된 summary_text 컬럼을 여전히 SELECT 목록에 포함한 구 인스턴스에서 Unknown column 'summary_text' in 'field list' 가 발생했다. 총 15건, 전부 production.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::System |
| exception.message | Mysql2::Error: Unknown column 'summary_text' in 'field list' |
| error.code | SYS50000 |
| top_frame | app/factories/base_factory.rb:140 (rescue → re-raise) |
| origin | app/models/concerns/spacetime_entity.rb:15 (set_spacetime) |
| runtime | Rails 7.2 / mysql2 |
| deploy | 20260806194600_remove_summary_text_from_spacetimes (commit 618a61cd1) |
| env | production (us-west-2, ap-northeast-1, ap-southeast-1, ap-southeast-2) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| kinden-kyoto (ap-northeast-1) | 5 | capture 생성 요청 500 (배포 cutover 창 한정) |
| naylorlove, leighsconstruction (ap-southeast-2) | 6 | capture 생성 요청 500 |
| structon (ap-southeast-1) | 1 | capture 생성 요청 500 |
| demokr (us-west-2) | 3 | capture 생성 요청 500 |
Timeline#
- 2026-08-06 14:16 KST — us-west-2 배포 cutover, 첫 발생 (host
ip-10-1-16-191.us-west-2,ip-10-1-147-149.us-west-2), 14:20 KST 까지 3건 후 소멸. - 2026-08-07 04:47 KST — contract migration
20260806194600commit (618a61cd1,2026-08-06 19:47:58 +0900) 이 배포 파이프라인에 반영. - 2026-08-07 11:11 KST — APAC 리전(ap-southeast-1/2, ap-northeast-1) 배포 cutover, 재발 시작.
- 2026-08-07 11:39 KST — 마지막 발생 (last_seen). 이후 구 인스턴스 drain 완료로 자가 해소.
- 2026-08-07 (RCA 시점) —
now-2h재검색 0건, 소멸 확인.
Error Log#
Mysql2::Error: Unknown column 'summary_text' in 'field list'
Impact#
- Service:
cupixworks-api - 발생 횟수: 15
- 최초 발생: 2026-08-06 14:16 KST
- 최근 발생: 2026-08-07 11:39 KST
capture 생성 요청이 cutover 시간창 동안 500 을 반환했다. 사용자 관점에서는 해당 시간창에 캡처 생성이 실패했으나, 구 인스턴스가 drain 되면 즉시 정상화되는 transient 장애다. 데이터 손상이나 영구적 기능 손실은 없다.
Root Cause Summary#
spacetimes.summary_text 는 TSLA-13207 에서 코드 참조가 전부 제거된 뒤 후속 contract migration 20260806194600_remove_summary_text_from_spacetimes (remove_column :spacetimes, :summary_text, :text, if_exists: true) 로 드랍되었다. 이 migration 은 리전별로 순차 실행되는데, 실행 시점에 아직 구 코드로 동작 중이면서 드랍 이전에 DB 커넥션을 맺어 spacetimes 의 컬럼 목록을 캐시한 인스턴스가 남아 있었다. tesla 는 committed schema_cache.yml 이 없어 ActiveRecord 가 커넥션 시점에 lazy 로 컬럼 목록을 로드하므로, 그 구 인스턴스는 SELECT 시 이미 존재하지 않는 summary_text 를 여전히 SELECT 목록에 포함한다. capture 생성 시 SpacetimeEntity#set_spacetime 의 Spacetime.find_or_create_by! 가 spacetimes 를 조회하면서 이 stale 컬럼 목록을 사용해 Unknown column 'summary_text' 가 발생했고, BaseFactory#create! 의 rescue StandardError 가 이를 Cupix::Errors::System SYS50000 으로 재-raise 하여 500 으로 매핑되었다. 구 인스턴스가 drain 되면 자가 해소되는 배포 cutover transient 이며, 코드 결함이 아니다.
Technical Analysis#
Code Path#
Entry point: app/controllers/api/v1/captures_controller.rb:45 (CapturesController#create) 가 factory 로 위임한다.
def create
@model = factory_instance.create!(params)
CaptureFactory#create! 는 record 와 level (여기서는 level_id: 784) 를 model 에 세팅한다. record_id/level_id 가 세팅되면 SpacetimeEntity 의 콜백 조건이 성립한다.
if params[:level_id].present?
level = ::Level.find_by(id: params[:level_id])
raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: 'Level not found') if level.nil?
raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid level_id; facility does not match') if level.facility_id != self.model.facility_id
self.model.level = level
Capture 는 SpacetimeEntity::Capture 를 include 하고(app/models/capture.rb:36), 그 안에서 SpacetimeEntity base concern 을 include 한다(app/models/concerns/spacetime_entity/capture.rb:10). base concern 은 save 직전 record_id/level_id 가 변경되면 set_spacetime 을 실행한다.
before_save :set_spacetime, if: :need_to_change_spacetime?
def need_to_change_spacetime?
record_id_changed? || level_id_changed?
end
def set_spacetime
_spacetime = Spacetime.find_or_create_by!({
team_id: team_id,
workspace_id: workspace_id,
facility_id: facility_id,
record_id: record_id,
level_id: level_id
}) do |st|
st.building_id = level&.building_id
end
Failure point: Spacetime.find_or_create_by! 는 먼저 SELECT spacetimes.* FROM spacetimes WHERE ... LIMIT 1 을 발행한다. ActiveRecord 는 spacetimes.* 을 스키마 캐시에 로드된 컬럼 목록으로 확장하는데, 드랍 이전에 커넥션을 맺은 구 인스턴스의 캐시에는 summary_text 가 남아 있어 없는 컬럼을 SELECT 하고 Mysql2::Error: Unknown column 'summary_text' in 'field list' 로 실패한다. 이 예외는 ActiveRecord::StatementInvalid (StandardError 자손, Cupix::Errors::BaseError 아님) 로 전파되어 factory 의 마지막 rescue 에서 재-raise 된다.
begin
self.model.save!
self.model
rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
raise e
rescue NoMethodError => e
raise Cupix::Errors::System.new(code: 'SYS10003', reason: e.message)
rescue ActiveRecord::RecordInvalid => e
raise Cupix::Errors::Entity.new(code: 'ENT10005', reason: e.message)
rescue ActiveRecord::ValueTooLong => e
raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid argument', message: e.message)
rescue Aws::Errors::ServiceError => e
raise Cupix::Errors::BadGateway.new(code: 'BG10004', reason: e.message)
rescue StandardError => e
raise e if e.is_a?(Cupix::Errors::BaseError)
raise Cupix::Errors::System.new(code: 'SYS50000', reason: e.message)
end
재-raise 된 Cupix::Errors::System 은 ServerErrorController 의 rescue_from 이 system_500_error 로 매핑하여 HTTP 500 을 반환한다.
rescue_from Cupix::Errors::System,
Cupix::Errors::Argument, with: :system_500_error
def system_500_error(exception)
raise_error(500, exception)
end
드랍을 수행한 migration 은 rolling deploy 순서에 대한 위험을 주석으로 명시하고 있다.
class RemoveSummaryTextFromSpacetimes < ActiveRecord::Migration[7.2]
def change
# Follow-up (contract) migration. summary_text is no longer referenced by any
# code (removed in TSLA-13207). Run only after every instance has rolled over
# to the new code so the rolling deploy never leaves an old instance selecting
# a dropped column. if_exists guards regions where the column was already
# dropped (e.g. US, where the earlier drop migration had run).
remove_column :spacetimes, :summary_text, :text, if_exists: true
end
end
기대 동작은 "모든 인스턴스가 신 코드로 전환되고 커넥션이 재수립된 뒤" 컬럼이 드랍되어야 하는 것이다. 실제 동작은 migration 이 old-color 인스턴스 drain 이전에 적용되어, 드랍 이전에 커넥션을 맺은 구 인스턴스가 stale 컬럼 목록으로 SELECT 를 발행한 것이다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api "summary_text"
대표 로그 (last_seen, 2026-08-07 11:39 KST / 02:39:10.908Z):
{
"error": {
"reason": "Mysql2::Error: Unknown column 'summary_text' in 'field list'",
"code": "SYS50000",
"message": "Mysql2::Error: Unknown column 'summary_text' in 'field list'",
"class": "Cupix::Errors::System"
},
"action": "create",
"controller": "Api::V1::CapturesController",
"environment": "production",
"http": { "url_details": { "path": "/api/v1/captures" }, "status_code": 500, "method": "POST" },
"params": { "level_id": 784, "facility_key": "o0bt8a" },
"host": { "name": "ip-10-1-145-183.ap-northeast-1.compute.internal" },
"team": { "domain": "kinden-kyoto", "id": 38 }
}
리전/호스트/시각 분포 (전체 15건, --raw 에서 추출):
2026-08-06 05:16-05:20 UTC us-west-2 ip-10-1-16-191 / ip-10-1-147-149 demokr 3건
2026-08-07 02:11 UTC ap-southeast-1 ip-10-1-82-169 structon 1건
2026-08-07 02:19-02:29 UTC ap-southeast-2 ip-10-1-81-193 / ip-10-1-147-131 naylorlove/leighsconstruction 6건
2026-08-07 02:33-02:39 UTC ap-northeast-1 ip-10-1-16-169 / ip-10-1-145-183 kinden-kyoto 5건
두 개의 뚜렷한 리전별 cutover 창(us-west-2 는 08-06, APAC 3개 리전은 08-07)으로 나뉘며, 각 창은 5~30분 내에 종료된다. now-2h 재검색은 0건으로 마지막 발생 이후 자가 해소를 확인했다. 전 건 environment: production, mysql2 status:error span 검색은 예외가 factory 에서 rescue 되어 request info 로그로만 남기 때문에 span 검색으로는 잡히지 않는다(추가로 span API 는 429 로 미확인).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | destructive contract migration 이 rolling deploy cutover 중 구 인스턴스의 stale schema cache 로 드랍된 컬럼을 SELECT (transient noise) | migration 20260806194600 주석이 정확히 이 위험을 서술; 2개 리전별 cutover 창 한정 발생, now-2h 0건 자가해소; committed schema_cache.yml 없음(lazy per-connection 컬럼 로드); 전 건 production |
— | Confirmed |
| H2 | a89a3762 와 동일한 additive-migration version-ordering schema drift (backdated migration skip → 컬럼 부재, 결정론적 BUG) | 동일 컬럼명 summary_text |
정반대 메커니즘: 이번은 컬럼을 제거하는 migration; 발생이 결정론적 지속이 아니라 cutover 창 한정 후 자가해소; 에러가 spacetimes.summary_text(400 ARG10001)가 아니라 bare summary_text(500 SYS50000, capture create) |
Rejected |
| H3 | 코드가 실제로 summary_text 를 명시적으로 SELECT (코드-스키마 불일치 BUG) |
— | git grep summary_text (app/lib) 결과 컬럼 참조 없음; summary_text_for_search (summary_validatable.rb:63) 는 summary json 을 조합하는 Ruby 메서드로 컬럼 read 아님; searchable/capture.rb:324 도 그 메서드 결과를 json 에 넣을 뿐 |
Rejected |
| H4 | ispring users.ispring_user_id 클러스터(acdd2caa)와 동일 ET 이슈로 병합 대상 |
같은 cutover 계열 메커니즘 | et_issue_id 상이(fb6fc2f2 vs ispring), 컬럼/테이블/엔드포인트 상이 → Datadog ET 는 컬럼별 별개 이슈로 집계, cross-merge 금지 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 코드 변경 불필요. 마지막 발생(2026-08-07 11:39 KST) 이후 구 인스턴스 drain 으로 자가 해소되었고
now-2h재검색 0건이다. Error Tracking 이슈fb6fc2f2는 IGNORE 처리를 권장한다.
단기 개선 (1주 이내)#
- destructive
remove_column배포는 expand/contract 순서를 강제한다. contract migration (20260806194600) 은 신 코드가 모든 리전 인스턴스에 전환되고 DB 커넥션이 재수립된 것을 확인한 뒤에만 실행되도록 배포 파이프라인(리전별 순차 + 인스턴스 drain 완료 게이트)을 조정한다. migration 주석의 의도가 파이프라인 순서로 강제되지 않은 것이 문제다. - committed
schema_cache.yml(또는bin/rails db:schema:cache:dump산출물) 도입을 검토한다. 배포 아티팩트에 컬럼 목록을 고정하면 커넥션 시점 lazy 로드에 의한 stale 캐시 창을 줄일 수 있다. 다만 이 경우 캐시-스키마 동기화 배포 순서가 별도 관리 대상이 된다.
장기 개선 (재발 방지)#
- destructive migration 전용 릴리즈 체크리스트를 표준화한다: (1) 코드 참조 제거 배포 → (2) 전 인스턴스 전환·재커넥션 확인 → (3) 다음 릴리즈에서 컬럼 드랍. TSLA-13207 은 이미
keep column, defer drop to follow-up(commit59d9d9517) 으로 이 원칙을 시도했으나, follow-up 드랍이 리전 rolling drain 완료 이전에 적용되었다. - 배포 cutover transient 에러(schema drift, service_jwt 회귀 등)를 Error Tracking 에서 자동으로 배포 이벤트와 상관지어 noise 로 분류하는 규칙을 검토한다.
Monitoring#
배포 후 재발 확인용 쿼리(리전별 cutover 창 종료 후 0 이어야 함):
service:cupixworks-api "Unknown column 'summary_text'"
capture 생성 500 추이:
service:cupixworks-api @http.url_details.path:/api/v1/captures @http.status_code:500
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (코드 수정 없음; 배포 프로세스 조정만)
Noise Verdict#
noise — spacetimes.summary_text 컬럼을 제거하는 contract migration 이 리전별 rolling deploy cutover 중 구 인스턴스의 stale schema cache 로 인해 발생시킨 transient 에러이며, 인스턴스 drain 후 자가 해소되고 코드 결함이 아니다.