Skip to content

Fix aggregate crashing on operations that report null metrics - #1096

Merged
OVI3D0 merged 1 commit into
opensearch-project:mainfrom
serhiy-bzhezytskyy:fix/aggregate-null-metrics-osb
Jul 29, 2026
Merged

Fix aggregate crashing on operations that report null metrics#1096
OVI3D0 merged 1 commit into
opensearch-project:mainfrom
serhiy-bzhezytskyy:fix/aggregate-null-metrics-osb

Conversation

@serhiy-bzhezytskyy

Copy link
Copy Markdown
Contributor

Description

calculate_weighted_average reduced min/max with value.get(metric_field, 0). That default only applies when the key is absent, so a key present with value None reached min()/max() and raised TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'. The percentile/median branch just below had the same flaw in a different shape — value * iterations on None.

calculate_rsd is a second, independent site, reached from build_aggregated_results_dict with the per-run mean values (TypeError: can't convert type 'NoneType' to numerator/denominator). Fixing only the first one moves the crash rather than removing it — that is how I found the second, by running against real data after unit tests for the first were already green.

The change:

  • extracts the weighted mean into a weighted_mean helper. The divisor now depends on which runs contributed a value, so it can no longer be one total summed up front, and the dict and scalar branches were computing it two different ways;
  • leaves nulls out of both the weighted sum and its divisor, so a run that measured nothing does not drag the mean toward zero;
  • propagates None when no run contributed a value, rather than substituting 0. 0 would read as "throughput was zero", which is a different claim from "not measured". This matches how aggregate_json_by_key in the same file already treats nulls;
  • returns NA from calculate_rsd when no values remain, as it already does for the single-value case.

Issues Resolved

Resolves #1093

Testing

  • New functionality includes testing

  • Two new tests in tests/aggregator_test.py: all-null metric fields, and partially null — a metric with samples in one run but not another, where the valid values must still aggregate, weighted only by the runs that contributed them.

  • Both verified red without the fix, by restoring main's aggregator.py under the new tests rather than by inspection.

  • Full suite: 1424 passed, 5 skipped (baseline on main is 1422). pylint clean.

  • End-to-end through the real CLI, on test-run files written by OSB's own TestRun.as_dict(): aggregate goes from ❌ FAILURE to ✅ SUCCESS, and the output is correct rather than merely non-crashing — a healthy operation aggregates to overall_min 40000.0, mean 42250.0, median 42150.0, overall_max 44500.0 with mean_rsd 0.8368, while the null operation reports null throughput and mean_rsd "NA".

The calculate_rsd site has no dedicated unit test here; it is covered by the end-to-end run, where it was the crash that surfaced once the first site was fixed. I can add a direct one if you'd prefer it in the suite.

Notes

Found while using Apache solr-orbit, a Python port of OSB whose aggregator.py is byte-identical to this one apart from the import module name, to run a multi-configuration benchmark campaign. The same fix is proposed there as apache/solr-orbit#58. Reported here because the defect is upstream, not port-specific.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit aa93387)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

@github-actions

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard RSD against zero mean division

statistics.mean will raise StatisticsError when mean is 0 during the subsequent RSD
division, or produce a division-by-zero. After filtering None values, if all
remaining values are 0 the RSD calculation std_dev / mean * 100 will fail. Consider
guarding against a zero mean by returning "NA" in that case as well.

osbenchmark/aggregator.py [256-265]

 if not values:
     raise ValueError(f"Cannot calculate RSD for metric '{metric_name}': empty list of values")
 # operations that produced no valid samples report None, which cannot contribute to a deviation
 values = [value for value in values if value is not None]
 if not values:
     return "NA"  # no test run measured this metric
 if len(values) == 1:
     return "NA"  # RSD is not applicable for a single value
 mean = statistics.mean(values)
+if mean == 0:
+    return "NA"  # RSD is not defined when mean is zero
 std_dev = statistics.stdev(values)
Suggestion importance[1-10]: 5

__

Why: The suggestion identifies a legitimate edge case where a zero mean would cause a division-by-zero in the subsequent RSD calculation. However, this is a pre-existing issue not directly related to the PR's null-handling changes, and zero means may be uncommon in practice.

Low

@OVI3D0 OVI3D0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@serhiy-bzhezytskyy tested and verified this works, LGTM, but looks like there is just a conflict on the test file if you can do a quick rebase

An operation that produced no valid samples reports its metric fields as null.
The geonames workload does this for `optimize`, which comes back with
error_rate 1.0 and a fully null throughput. Aggregating any test run that
contains such an operation fails:

  [ERROR] Cannot aggregate. '<' not supported between instances of
          'NoneType' and 'NoneType'.

calculate_weighted_average reached min()/max() with None values, and
`value.get(metric_field, 0)` did not help because the key is present with a
null value rather than absent.

Null values are now left out of the min, the max, and the weighted mean
instead of being coerced to 0, and the result is None when no test run
contributed a value, so an unmeasured metric stays distinct from a measured
zero. calculate_rsd has the same problem on a second, independent code path
and returns "NA" when nothing was measured.

The weighted-mean arithmetic moves into a helper because the divisor now
depends on which runs contributed, so it can no longer be a single total
computed up front.

Signed-off-by: Serhiy Bzhezytskyy <me@serhiy-bzhezytskyy.com>
@serhiy-bzhezytskyy
serhiy-bzhezytskyy force-pushed the fix/aggregate-null-metrics-osb branch from e402752 to aa93387 Compare July 28, 2026 18:36
@serhiy-bzhezytskyy

Copy link
Copy Markdown
Contributor Author

Rebased onto main — the conflict was in tests/aggregator_test.py, where #1097 and this PR each appended a test after the same anchor. Kept both; the suite is at 1425 passed / 5 skipped locally.

Thanks for reviewing and merging #1097.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit aa93387

@OVI3D0
OVI3D0 merged commit 2d70a51 into opensearch-project:main Jul 29, 2026
12 checks passed
OVI3D0 added a commit to OVI3D0/opensearch-benchmark that referenced this pull request Jul 29, 2026
…RSD)

Found via live e2e: 'aggregate' of two normal index-only runs crashed with
ValueError 'Cannot calculate RSD ... empty list of values'.

Root cause was a collision between two prior changes:
- the robustness fix pre-filtered None mean values at the call site, and
- the opensearch-project#1096 merge made calculate_rsd tolerate None *inside* a list but still
  raise on a genuinely empty list.
Together, an all-None metric (e.g. index-only throughput.mean) produced an
empty list that hit the raise.

Fix:
- build_aggregated_results_dict passes raw v.get('mean') values (still avoids
  the original KeyError on a missing 'mean' key) and lets calculate_rsd handle
  None/empty.
- calculate_rsd returns 'NA' for an empty/all-None list instead of raising, so
  one unmeasured metric no longer aborts the whole aggregation.

Adds a regression test for empty/all-None/single-value RSD.

Signed-off-by: Michael Oviedo <mikeovi@amazon.com>
OVI3D0 added a commit to OVI3D0/opensearch-benchmark that referenced this pull request Jul 30, 2026
…RSD)

Found via live e2e: 'aggregate' of two normal index-only runs crashed with
ValueError 'Cannot calculate RSD ... empty list of values'.

Root cause was a collision between two prior changes:
- the robustness fix pre-filtered None mean values at the call site, and
- the opensearch-project#1096 merge made calculate_rsd tolerate None *inside* a list but still
  raise on a genuinely empty list.
Together, an all-None metric (e.g. index-only throughput.mean) produced an
empty list that hit the raise.

Fix:
- build_aggregated_results_dict passes raw v.get('mean') values (still avoids
  the original KeyError on a missing 'mean' key) and lets calculate_rsd handle
  None/empty.
- calculate_rsd returns 'NA' for an empty/all-None list instead of raising, so
  one unmeasured metric no longer aborts the whole aggregation.

Adds a regression test for empty/all-None/single-value RSD.

Signed-off-by: Michael Oviedo <mikeovi@amazon.com>
OVI3D0 added a commit to OVI3D0/opensearch-benchmark that referenced this pull request Aug 3, 2026
…RSD)

Found via live e2e: 'aggregate' of two normal index-only runs crashed with
ValueError 'Cannot calculate RSD ... empty list of values'.

Root cause was a collision between two prior changes:
- the robustness fix pre-filtered None mean values at the call site, and
- the opensearch-project#1096 merge made calculate_rsd tolerate None *inside* a list but still
  raise on a genuinely empty list.
Together, an all-None metric (e.g. index-only throughput.mean) produced an
empty list that hit the raise.

Fix:
- build_aggregated_results_dict passes raw v.get('mean') values (still avoids
  the original KeyError on a missing 'mean' key) and lets calculate_rsd handle
  None/empty.
- calculate_rsd returns 'NA' for an empty/all-None list instead of raising, so
  one unmeasured metric no longer aborts the whole aggregation.

Adds a regression test for empty/all-None/single-value RSD.

Signed-off-by: Michael Oviedo <mikeovi@amazon.com>
OVI3D0 added a commit to OVI3D0/opensearch-benchmark that referenced this pull request Aug 3, 2026
…RSD)

Found via live e2e: 'aggregate' of two normal index-only runs crashed with
ValueError 'Cannot calculate RSD ... empty list of values'.

Root cause was a collision between two prior changes:
- the robustness fix pre-filtered None mean values at the call site, and
- the opensearch-project#1096 merge made calculate_rsd tolerate None *inside* a list but still
  raise on a genuinely empty list.
Together, an all-None metric (e.g. index-only throughput.mean) produced an
empty list that hit the raise.

Fix:
- build_aggregated_results_dict passes raw v.get('mean') values (still avoids
  the original KeyError on a missing 'mean' key) and lets calculate_rsd handle
  None/empty.
- calculate_rsd returns 'NA' for an empty/all-None list instead of raising, so
  one unmeasured metric no longer aborts the whole aggregation.

Adds a regression test for empty/all-None/single-value RSD.

Signed-off-by: Michael Oviedo <mikeovi@amazon.com>
OVI3D0 added a commit to OVI3D0/opensearch-benchmark that referenced this pull request Aug 3, 2026
…RSD)

Found via live e2e: 'aggregate' of two normal index-only runs crashed with
ValueError 'Cannot calculate RSD ... empty list of values'.

Root cause was a collision between two prior changes:
- the robustness fix pre-filtered None mean values at the call site, and
- the opensearch-project#1096 merge made calculate_rsd tolerate None *inside* a list but still
  raise on a genuinely empty list.
Together, an all-None metric (e.g. index-only throughput.mean) produced an
empty list that hit the raise.

Fix:
- build_aggregated_results_dict passes raw v.get('mean') values (still avoids
  the original KeyError on a missing 'mean' key) and lets calculate_rsd handle
  None/empty.
- calculate_rsd returns 'NA' for an empty/all-None list instead of raising, so
  one unmeasured metric no longer aborts the whole aggregation.

Adds a regression test for empty/all-None/single-value RSD.

Signed-off-by: Michael Oviedo <mikeovi@amazon.com>
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.

[Bug]: aggregate fails with TypeError when an operation reports null metrics

2 participants