Skip to content

chart: fix division by zero and step overflow in json engine - #2011

Merged
brharrington merged 2 commits into
Netflix:mainfrom
reginaldalfret:fix/json-graph-engine-subsecond-step-overflow
Nov 5, 2026
Merged

brharrington merged 2 commits into
Netflix:mainfrom
reginaldalfret:fix/json-graph-engine-subsecond-step-overflow

Conversation

@reginaldalfret

Copy link
Copy Markdown
Contributor

Summary

This PR addresses two edge-case issues in JsonGraphEngine:

  1. Sub-second step division by zero: When serializing the explain metadata block, graphDatapoints was computed as graphLines * ((end - start) / (config.step / 1000) + 1). When queries use sub-second step sizes (such as 500ms, 100ms, etc., which are valid in Step.allowedStepSizes), integer division (config.step / 1000) truncates to 0, resulting in ArithmeticException: / by zero. Dividing directly by config.step in milliseconds avoids division by zero while preserving the exact datapoint count.
  2. Large step integer overflow: config.step was cast to Int via .asInstanceOf[Int]. For large step sizes (> 24.8 days in milliseconds), this overflows to negative values, causing the values loop (timestamp += step) to decrease and loop infinitely. Keeping step as Long matches CsvGraphEngine and prevents overflow.
  3. Test additions: Added regression test coverage for explain stats, sub-second step handling, large step rollover, and stats.json in JsonGraphEngineSuite. Also normalized newline comparison in CsvGraphEngineSuite for cross-platform consistency.

Verification

All 335 tests in atlas-chart pass:

  • atlas-chart/test (335 passed, 0 failed)
  • atlas-chart/scalafmtCheck passed
  • checkLicenseHeaders passed

In JsonGraphEngine, graphDatapoints was calculated by dividing (config.step / 1000). For sub-second step sizes (such as 500ms or 100ms, supported in Step.allowedStepSizes), integer division evaluates to 0 and throws ArithmeticException: / by zero. Using config.step directly in milliseconds avoids division by zero while preserving the exact datapoint count.

Additionally, step was cast to Int, which overflows for large step intervals (> 24.8 days) and causes timestamp += step to decrease in an infinite loop. Keep step as Long.

Also add unit test coverage for stats.json and ensure test string stripping is newline-agnostic.

Signed-off-by: reginaldalfret <[email protected]>
@brharrington brharrington added this to the 1.9.0 milestone Nov 5, 2026
val end = config.endTime.toEpochMilli
val graphLines = config.plots.map(_.data.size).sum
val graphDatapoints = graphLines * ((end - start) / (config.step / 1000) + 1)
val graphDatapoints = graphLines * ((end - start) / config.step + 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endTime is exclusive and the values loop above emits (end - start) / step rows, so the + 1 overcounts by one per line. For example, the 3 minute window in the new test emits 3 rows but reports graphDatapoints = 4.

Suggest moving this into GraphDef, something like:

def numDatapoints: Long = (endTime.toEpochMilli - startTime.toEpochMilli) / step

and using it from both JsonGraphEngine and DefaultGraphEngine so they stay consistent.

val end = config.endTime.toEpochMilli / 1000
val start = config.startTime.toEpochMilli
val end = config.endTime.toEpochMilli
val graphLines = config.plots.map(_.data.size).sum

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plots.map(_.data.size) counts all data defs including spans and messages, but the output only includes lines. A graph with one line and a :vspan reports graphLines = 2. seriesList.size would match what is actually written.

@@ -74,10 +74,10 @@ class JsonGraphEngine extends GraphEngine {
gen.writeEndArray()

if (config.stats.inputLines > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, not from this PR: DefaultGraphEngine also checks config.loadTime > 0. Without that, this can emit "dataFetchTime": -1 when loadTime is left at the default.

|"metrics":[{"name":"42.0"}],
|"values":[[42.0],[42.0],[42.0]],
|"notices":[],
|"explain":{"dataFetchTime":123,"inputLines":10,"intermediateLines":20,"graphLines":1,"inputDatapoints":5,"intermediateDatapoints":10,"graphDatapoints":4}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This expects graphDatapoints = 4, but values has 3 rows. Same for the sub-second test below (2 rows, expects 3). Should be updated along with the off-by-one fix.

process(new StatsJsonGraphEngine, expected)
}

test("json with explain stats") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the new tests repeat the same build/write/compare block. Consider an overload of process that takes a GraphDef.

|2012-01-01T00:00:00Z,42.000000,NaN
|2012-01-01T00:01:00Z,42.000000,NaN
|""".stripMargin
|""".stripMargin.replace("\r\n", "\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CRLF handling seems unrelated to the fix and only covers a few suites, many others compare against stripMargin literals in the same way. If this is for Windows checkouts, a .gitattributes with *.scala text eol=lf would handle it repo wide. I'd suggest dropping these changes from this PR.

- Add GraphDef.numDatapoints and use it from both the JSON and
  PNG engines. End time is exclusive so the count no longer adds
  one, and the PNG engine no longer divides millis by seconds or
  fails for sub-second steps.
- Require step > 0 on GraphDef.
- Only count lines, not spans or messages, for graphLines.
- Only emit the JSON explain block when loadTime is known,
  consistent with the PNG engine.
- Bound test output so a non-terminating values loop fails fast
  and share the render helper across tests.
- Drop the CRLF test changes, unrelated to this fix.
- Update PNG golden images for the corrected datapoint count.
@brharrington
brharrington merged commit cb0fccf into Netflix:main Nov 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants