ES /docs

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_spacetimesspacetimes.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 생성 시 SpacetimeEntitybefore_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#

  1. 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건 후 소멸.
  2. 2026-08-07 04:47 KST — contract migration 20260806194600 commit (618a61cd1, 2026-08-06 19:47:58 +0900) 이 배포 파이프라인에 반영.
  3. 2026-08-07 11:11 KST — APAC 리전(ap-southeast-1/2, ap-northeast-1) 배포 cutover, 재발 시작.
  4. 2026-08-07 11:39 KST — 마지막 발생 (last_seen). 이후 구 인스턴스 drain 완료로 자가 해소.
  5. 2026-08-07 (RCA 시점)now-2h 재검색 0건, 소멸 확인.

Error Log#

Datadog Logs

text
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_spacetimeSpacetime.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 로 위임한다.

app/controllers/api/v1/captures_controller.rb:45-46ruby
  def create
    @model = factory_instance.create!(params)

CaptureFactory#create!recordlevel (여기서는 level_id: 784) 를 model 에 세팅한다. record_id/level_id 가 세팅되면 SpacetimeEntity 의 콜백 조건이 성립한다.

app/factories/capture_factory.rb:38-45ruby
    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

CaptureSpacetimeEntity::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 을 실행한다.

app/models/concerns/spacetime_entity.rb:6-25ruby
    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 된다.

app/factories/base_factory.rb:123-141ruby
    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::SystemServerErrorControllerrescue_fromsystem_500_error 로 매핑하여 HTTP 500 을 반환한다.

app/controllers/concerns/server_error_controller.rb:7-8,38-40ruby
    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 순서에 대한 위험을 주석으로 명시하고 있다.

db/migrate/20260806194600_remove_summary_text_from_spacetimes.rb:1-10ruby
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 쿼리:

text
service:cupixworks-api "summary_text"

대표 로그 (last_seen, 2026-08-07 11:39 KST / 02:39:10.908Z):

json
{
  "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 에서 추출):

text
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 (commit 59d9d9517) 으로 이 원칙을 시도했으나, follow-up 드랍이 리전 rolling drain 완료 이전에 적용되었다.
  • 배포 cutover transient 에러(schema drift, service_jwt 회귀 등)를 Error Tracking 에서 자동으로 배포 이벤트와 상관지어 noise 로 분류하는 규칙을 검토한다.

Monitoring#

배포 후 재발 확인용 쿼리(리전별 cutover 창 종료 후 0 이어야 함):

text
service:cupixworks-api "Unknown column 'summary_text'"

capture 생성 500 추이:

text
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 후 자가 해소되고 코드 결함이 아니다.