ES /docs

Invalid Formula: no value provided for variables: nan

RCA: Invalid Formula: no value provided for variables: nan

Overview#

What Happened#

2026-07-13 14:55 KST, cupixworks-api (ap-southeast-1, production) 에서 PUT /api/v1/phase_metrics/2/validate_formula 요청 한 건이 처리되는 동안 Cupix::Util::FormulaParser.calculate! 가 5회 연속으로 Invalid Formula: no value provided for variables: nan 에러를 로깅했다. HTTP 응답 자체는 200 으로 성공 반환됐지만, 샘플 element 5개에 대해 formula 계산이 모두 실패해 응답 body 의 value 필드가 모두 "NaN" 으로 채워졌다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter (내부 rescue로 흡수)
exception.message Invalid Formula: no value provided for variables: nan
top_frame lib/cupix/util/formula_parser.rb:24 (calculate!)
deploy production-ap-southeast-1-20260713T0510Z0-4f01ffc0-cupixworks
env production, ap-southeast-1
tenant cupix
endpoint PUT /api/v1/phase_metrics/2/validate_formula

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (site-insights / phase_metric) 5 특정 phase_metric(id=2) 의 formula 검증 API 응답에서 샘플 element 5개의 value 가 모두 "NaN" 으로 표시됨. 사용자는 formula 미리보기가 유효하지 않다고 인식.

Timeline#

  1. 2026-07-13 14:55:58 KST — 사용자가 PUT /api/v1/phase_metrics/2/validate_formula 호출 (request_id 893d679b-635b-49bd-9108-286942f934ed, pid 2138722).
  2. 2026-07-13 14:55:58 KSTPhaseMetricRepository#validate_formula 가 sample element 5건에 대해 FormulaParser.parse! 실행 → 모두 예외 발생 → rescue 'NaN' 로 문자열 "NaN" 대입.
  3. 2026-07-13 14:55:58 KST — 같은 loop 에서 FormulaParser.calculate!("NaN") 5회 실행 → Dentaku 가 NaN 을 변수로 파싱, 값 미제공 예외로 5개의 error 로그 기록.
  4. 2026-07-13 14:55:58 KST — 컨트롤러는 rescue 흡수 뒤 HTTP 200 반환.

Error Log#

Datadog Logs

text
Invalid Formula: no value provided for variables: nan

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 5
  • 최초 발생: 2026-07-13 14:55 KST
  • 최근 발생: 2026-07-13 14:55 KST
  • 범위: 단일 request (request_id 893d679b-635b-49bd-9108-286942f934ed) 에서 sample element 5건 loop 로그. 다른 tenant/facility 로의 확산 증거 없음.
  • 사용자 영향: formula 미리보기 응답의 value"NaN" 문자열로 노출됨. 실제 저장/계산 파이프라인이 아닌 검증(validate) API 이므로 데이터 손상 없음.

Root Cause Summary#

PhaseMetricRepository#validate_formula 는 sample element 배열을 순회하며 FormulaParser.parse! 결과를 rescue 'NaN' 으로 흡수하고, 그 결과를 그대로 FormulaParser.calculate! 에 전달한다. parse! 가 실패한 경우 반환되는 값은 파싱된 수식이 아니라 리터럴 문자열 "NaN" 이며, 이를 받은 Dentaku::Calculator#evaluate!NaN 을 미정의 변수(nan)로 해석해 Dentaku::UnboundVariableError: no value provided for variables: nan 예외를 던진다. calculate! 는 이 예외를 잡아 Cupix::Logger.error("Invalid Formula: ...") 로 기록한 뒤 다시 raise 하고, 상위 loop 의 rescue 'NaN' 이 두 번째 예외까지 흡수한다. 결과적으로 parse 단계에서 이미 결과가 결정된 케이스에서도 calculate 단계까지 진행되어, sample element 개수만큼 error 로그가 중복 생성된다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/phase_metrics_controller.rb:30 (validate_formula)
  • Loop 지점: app/repositories/phase_metric_repository.rb:22-41
  • Failure point: lib/cupix/util/formula_parser.rb:24 (calculate!) — 로깅 발생 지점
  • 실패 유발 값: parse!rescue 'NaN' (문자열)

컨트롤러는 repository 로 위임한 뒤 결과를 200 으로 반환한다:

app/controllers/api/v1/phase_metrics_controller.rb:30-34ruby
def validate_formula
  response = repository_instance.validate_formula(params)

  render_json 200, response
end

validate_formula 는 sample element 별로 두 단계 계산을 수행하며 각 단계에서 rescue 'NaN' 으로 예외를 흡수한다. parse 실패 시 결과가 이미 'NaN' 으로 확정되었음에도 곧바로 calculate!('NaN') 을 호출한다:

app/repositories/phase_metric_repository.rb:21-33ruby
element_ids = ::Element.untrashed.joins(category: :phases).where(category: { phases: { id: @model.phase_id } }).pluck(:id).sample(sample_size)
elements = ::Element.where(id: element_ids).map do |element|
  parsed_formula = Cupix::Util::FormulaParser.parse!(@model.formula, bim_element: element) rescue 'NaN'
  value = Cupix::Util::FormulaParser.calculate!(parsed_formula) rescue 'NaN'

  {
    id: element.id,
    name: element.name,
    bim_external_id: element.bim_external_id,
    bim_element_id: element.bim_element_id,
    bim: element._bim,
    parsed_formula: parsed_formula,
    value: value.to_s,

calculate! 는 Dentaku 결과를 .to_f!.round(5) 로 강제 변환하며, 실패 시 Cupix::Logger.error("Invalid Formula: ...") 를 기록한 뒤 재-raise 한다. Dentaku 3.5.2 는 식별자 NaN 을 대소문자 무시하여 변수 nan 으로 해석하고 값이 없다는 예외를 던진다:

lib/cupix/util/formula_parser.rb:19-28ruby
def calculate!(expression)
  calculator = ::Dentaku::Calculator.new
  begin
    calculator.evaluate!(expression).to_f!.round(5)
  rescue StandardError => e
    Cupix::Logger.error("Invalid Formula: #{e.message}", class: self.name, function: __method__, formula: expression)

    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Invalid Formula: #{e.message}")
  end
end

기대 동작: parse 단계가 실패해 결과가 'NaN' 으로 확정되면 calculate 단계를 skip 하고 바로 value = 'NaN' 을 사용해야 한다. 실제 동작: parse 결과와 무관하게 calculate 를 시도해 error 로그가 이중으로 발생한다. 또한 calculate! 자체가 실패해 로그를 남기는 경로는, 호출자가 예외를 무시하는 미리보기/샘플 검증 컨텍스트에서도 항상 error 레벨로 기록된다.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-api status:error @environment:production "Invalid Formula: no value provided for variables: nan"

동일 request_id (893d679b-635b-49bd-9108-286942f934ed) 로 trace 재조회 (범위 좁힘):

text
service:cupixworks-api @request_id:893d679b-635b-49bd-9108-286942f934ed

결과 — 200 성공 응답과 5개의 error 로그가 같은 request 내에서 발생:

text
2026-07-13 14:55:58  info   [200] PUT /api/v1/phase_metrics/2/validate_formula (Api::V1::PhaseMetricsController#validate_formula)
2026-07-13 14:55:58  error  Invalid Formula: no value provided for variables: nan  (calculate!)
2026-07-13 14:55:58  error  Invalid Formula: no value provided for variables: nan  (calculate!)
2026-07-13 14:55:58  error  Invalid Formula: no value provided for variables: nan  (calculate!)
2026-07-13 14:55:58  error  Invalid Formula: no value provided for variables: nan  (calculate!)
2026-07-13 14:55:58  error  Invalid Formula: no value provided for variables: nan  (calculate!)

Cupix::Logger 가 함께 기록한 구조화 필드에서 formula 값이 리터럴 "NaN" 임이 확인된다:

json
{
  "message": "Invalid Formula: no value provided for variables: nan",
  "class": "Cupix::Util::FormulaParser",
  "function": "calculate!",
  "formula": "NaN",
  "request_id": "893d679b-635b-49bd-9108-286942f934ed",
  "environment": "production",
  "service": "cupixworks-api",
  "@timestamp": "2026-07-13T05:55:58.245Z"
}

이 로그의 formula: "NaN"parse! 가 실패해 rescue 'NaN' 으로 문자열 대체가 된 뒤 그 값이 calculate! 로 전달되었음을 직접 증명한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 parse! 실패 → rescue 'NaN' 문자열이 calculate! 로 전달되어 Dentaku 가 미정의 변수 nan 예외를 던진다. 로그의 구조화 필드 formula: "NaN" (Datadog raw), Dentaku 에러 문구가 정확히 no value provided for variables: nan, phase_metric_repository.rb:23-24rescue 'NaN' 패턴, 동일 request 안에서 5개 element × 1회 loop = 5개 로그 매치. Confirmed
H2 사용자가 formula 파라미터로 실제 NaN 문자열을 전송해 발생. 로그의 formula 필드가 "NaN". 컨트롤러 경로가 validate_formula 이며 set_formulavalidate! 가 먼저 호출되었다면 잘못된 formula 는 Cupix::Errors::Parameter 로 400/422 를 반환해야 함. 그러나 응답은 200 이고 5회 반복 로그가 발생 — validate! 가 아니라 validate_formula (sample 계산) 경로임. 또한 validate!!{}/${}/@{} 브라켓을 '1 ' 로 치환 후 calculate! 을 호출하므로 파라미터가 NaN 이었다면 검증 단계에서 이미 거부된다. Rejected
H3 Dentaku 업그레이드로 인한 회귀. Gemfile.lock 에 dentaku (3.5.2) 기록. 코드 변경 없이 이전에도 동일 조건에서 실패해 왔을 것 — 회귀를 뒷받침할 배포/버전 차이 로그가 없음. formula: "NaN" 값이 명시된 리터럴이므로 라이브러리 회귀보다 코드 상 rescue 문자열이 직접 원인. Rejected
H4 외부 종속(예: DB, cache) 지연으로 element 속성이 nil 이 되어 parse 가 실패. 5개 element 전부 실패 — 광범위한 데이터 결측일 수 있음. 이는 parse! 실패의 원인 가설이며, calculate! 에서 5회 로그가 나오는 근본 원인(문자열 전달)과는 층이 다름. parse 실패 원인은 후속 조사 대상이며, 본 클러스터의 지문(no value provided for variables: nan)은 calculate 경로에서만 발생. Inconclusive (별도 조사 필요)

Fix Recommendation#

즉시 조치 (Critical)#

  • app/repositories/phase_metric_repository.rb:22-41 의 map 블록에서 parse 결과가 'NaN' (즉, parse 실패로 대체된 sentinel) 이면 calculate! 를 skip 하고 value 를 곧바로 'NaN' 으로 세팅하도록 조건 분기 추가. 이로써 이미 결정된 실패 케이스에서 error 로그가 중복 발생하는 것을 막을 수 있다.
  • 대안으로 sentinel 을 문자열 'NaN' 대신 nil 또는 명시적 :parse_failed 심볼로 바꾸고, 이후 로직에서 그 값이면 value = 'NaN' 을 반환하도록 분리. 문자열 'NaN' 이 Dentaku 파서에 재유입되지 않도록 보장하는 것이 핵심.

단기 개선 (1주 이내)#

  • FormulaParser.calculate! 의 로그 레벨을 호출 컨텍스트에 따라 조정. 미리보기/샘플 검증에서는 예외를 재-raise 하지만 로깅은 warn 으로 낮추거나, 호출자가 raise_on_error: false 옵션을 넘길 수 있도록 시그니처 확장. 미리보기 실패는 시스템 에러가 아닌 사용자 입력 검증 결과에 가깝다.
  • Sample size 만큼 반복되는 실패에 대해 파싱 실패의 근본 원인(예: element 의 dimension nil, custom_property 결측) 을 로그에 함께 남겨 후속 조사가 가능하도록 개선. 현재는 parse! 의 rescue 가 예외 정보 없이 사라진다 (rescue 'NaN').

장기 개선 (재발 방지)#

  • Sentinel-이-문자열 안티패턴 전면 검토. 문자열 'NaN' 은 Ruby Float::NAN 도, Dentaku 예약어도 아니어서 계산 경로에서 재차 evaluate 될 위험이 있다. 도메인 상 "계산 불가"를 나타내는 별도 값 객체(예: PhaseMetric::UnavailableValue)를 도입해 계산 파이프라인 전체에 일관되게 흐르게 한다.
  • PhaseMetricRepository#validate_formula 의 사용 목적(미리보기)에 맞춰 error → warn 정책과 응답 스키마(status: "ok" | "parse_failed" | "calc_failed") 를 명세화한다. 프런트엔드가 value === "NaN" 문자열 매칭에 의존하고 있는지 확인 필요 (uncertain -- needs verification).

Monitoring#

Datadog release dashboard 에 추가할 timeseries 쿼리:

text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api::v1::phase_metrics_controller#validate_formula}.as_count()
text
logs("service:cupixworks-api status:error \"Invalid Formula: no value provided for variables: nan\"").index("*").rollup("count").by("environment,region")
text
logs("service:cupixworks-api @class:Cupix::Util::FormulaParser @function:calculate\\! @formula:NaN").index("*").rollup("count").by("environment")
  • 첫 번째 쿼리: validate_formula 액션의 traced error 추이.
  • 두 번째 쿼리: 지문 매칭 로그 카운트를 environment/region 별 시계열로.
  • 세 번째 쿼리: formula 구조화 필드가 리터럴 "NaN" 인 경우만 골라 sentinel-leak 재발을 조기 감지.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 사용자 대면 API 는 200 을 유지하므로 서비스 영향은 관측성 노이즈와 응답의 value: "NaN" UX 이슈 수준. 수정 범위는 단일 repository 메서드로 국한된다.