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#
- 2026-07-13 14:55:58 KST — 사용자가
PUT /api/v1/phase_metrics/2/validate_formula호출 (request_id893d679b-635b-49bd-9108-286942f934ed, pid 2138722). - 2026-07-13 14:55:58 KST —
PhaseMetricRepository#validate_formula가 sample element 5건에 대해FormulaParser.parse!실행 → 모두 예외 발생 →rescue 'NaN'로 문자열"NaN"대입. - 2026-07-13 14:55:58 KST — 같은 loop 에서
FormulaParser.calculate!("NaN")5회 실행 → Dentaku 가NaN을 변수로 파싱, 값 미제공 예외로 5개의 error 로그 기록. - 2026-07-13 14:55:58 KST — 컨트롤러는 rescue 흡수 뒤 HTTP 200 반환.
Error Log#
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 으로 반환한다:
def validate_formula
response = repository_instance.validate_formula(params)
render_json 200, response
end
validate_formula 는 sample element 별로 두 단계 계산을 수행하며 각 단계에서 rescue 'NaN' 으로 예외를 흡수한다. parse 실패 시 결과가 이미 'NaN' 으로 확정되었음에도 곧바로 calculate!('NaN') 을 호출한다:
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 으로 해석하고 값이 없다는 예외를 던진다:
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 쿼리:
service:cupixworks-api status:error @environment:production "Invalid Formula: no value provided for variables: nan"
동일 request_id (893d679b-635b-49bd-9108-286942f934ed) 로 trace 재조회 (범위 좁힘):
service:cupixworks-api @request_id:893d679b-635b-49bd-9108-286942f934ed
결과 — 200 성공 응답과 5개의 error 로그가 같은 request 내에서 발생:
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" 임이 확인된다:
{
"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-24 의 rescue 'NaN' 패턴, 동일 request 안에서 5개 element × 1회 loop = 5개 로그 매치. |
— | Confirmed |
| H2 | 사용자가 formula 파라미터로 실제 NaN 문자열을 전송해 발생. |
로그의 formula 필드가 "NaN". |
컨트롤러 경로가 validate_formula 이며 set_formula 의 validate! 가 먼저 호출되었다면 잘못된 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'은 RubyFloat::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 쿼리:
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api::v1::phase_metrics_controller#validate_formula}.as_count()
logs("service:cupixworks-api status:error \"Invalid Formula: no value provided for variables: nan\"").index("*").rollup("count").by("environment,region")
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 메서드로 국한된다.