Skip to content

Commit 4f9fe71

Browse files
authored
Merge pull request #615 from open-telemetry/main
merged from upstream
2 parents 43d4b84 + 47d0351 commit 4f9fe71

File tree

2 files changed

+160
-2
lines changed

2 files changed

+160
-2
lines changed

sdk/src/metrics/aggregation/base2_exponential_histogram_aggregation.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,11 @@ std::unique_ptr<Aggregation> Base2ExponentialHistogramAggregation::Merge(
310310
auto neg_max_index =
311311
(std::max)(low_res.negative_buckets_->EndIndex(), high_res.negative_buckets_->EndIndex());
312312

313-
if (static_cast<size_t>(pos_max_index) >
313+
// The range [pos_min_index, pos_max_index] contains (pos_max_index - pos_min_index + 1)
314+
// buckets. We need to downscale if this count exceeds max_buckets_.
315+
if (static_cast<size_t>(pos_max_index) >=
314316
static_cast<size_t>(pos_min_index) + result_value.max_buckets_ ||
315-
static_cast<size_t>(neg_max_index) >
317+
static_cast<size_t>(neg_max_index) >=
316318
static_cast<size_t>(neg_min_index) + result_value.max_buckets_)
317319
{
318320
// We need to downscale the buckets to fit into the new max_buckets_.

sdk/test/metrics/aggregation_test.cc

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,3 +451,159 @@ TEST(Aggregation, Base2ExponentialHistogramAggregationMerge)
451451
expected_scale, expected_max_buckets);
452452
}
453453
}
454+
455+
// Test that verifies the count invariant is maintained during merge operations that require
456+
// downscaling. The invariant is: count == zero_count + sum(positive_bucket_counts) +
457+
// sum(negative_bucket_counts)
458+
//
459+
TEST(Aggregation, Base2ExponentialHistogramAggregationMergeCountInvariant)
460+
{
461+
// Helper to sum all bucket counts
462+
auto sum_bucket_counts = [](const Base2ExponentialHistogramPointData &point) -> uint64_t {
463+
uint64_t total = 0;
464+
if (point.positive_buckets_ && !point.positive_buckets_->Empty())
465+
{
466+
for (int32_t i = point.positive_buckets_->StartIndex();
467+
i <= point.positive_buckets_->EndIndex(); ++i)
468+
{
469+
total += point.positive_buckets_->Get(i);
470+
}
471+
}
472+
if (point.negative_buckets_ && !point.negative_buckets_->Empty())
473+
{
474+
for (int32_t i = point.negative_buckets_->StartIndex();
475+
i <= point.negative_buckets_->EndIndex(); ++i)
476+
{
477+
total += point.negative_buckets_->Get(i);
478+
}
479+
}
480+
return total;
481+
};
482+
483+
// Helper to verify the count invariant
484+
auto verify_invariant = [&sum_bucket_counts](const Base2ExponentialHistogramPointData &point,
485+
uint64_t expected_count, const std::string &phase) {
486+
uint64_t bucket_sum = sum_bucket_counts(point);
487+
uint64_t calculated_count = point.zero_count_ + bucket_sum;
488+
489+
EXPECT_EQ(point.count_, expected_count) << "Count mismatch at " << phase << ": expected "
490+
<< expected_count << ", got " << point.count_;
491+
EXPECT_EQ(point.count_, calculated_count)
492+
<< "Invariant violation at " << phase << ": count(" << point.count_ << ") != zero_count("
493+
<< point.zero_count_ << ") + bucket_sum(" << bucket_sum << ") = " << calculated_count;
494+
};
495+
496+
// Use scale 0 for easy bucket index reasoning: value 2^N -> index N-1
497+
// Use max_buckets=5 so we can trigger the bug with small powers of 2
498+
Base2ExponentialHistogramAggregationConfig config;
499+
config.max_scale_ = 0;
500+
config.max_buckets_ = 5;
501+
502+
std::unique_ptr<Aggregation> cumulative =
503+
std::make_unique<Base2ExponentialHistogramAggregation>(&config);
504+
505+
uint64_t expected_count = 0;
506+
507+
// === Cycle 1: values 2,4,8,16,32 -> indices 0,1,2,3,4 (5 buckets, fits in max_buckets=5) ===
508+
{
509+
Base2ExponentialHistogramAggregation delta(&config);
510+
delta.Aggregate(2.0, {}); // 2^1 -> index 0
511+
delta.Aggregate(4.0, {}); // 2^2 -> index 1
512+
delta.Aggregate(8.0, {}); // 2^3 -> index 2
513+
delta.Aggregate(16.0, {}); // 2^4 -> index 3
514+
delta.Aggregate(32.0, {}); // 2^5 -> index 4
515+
expected_count += 5;
516+
517+
cumulative = cumulative->Merge(delta);
518+
519+
auto point = nostd::get<Base2ExponentialHistogramPointData>(cumulative->ToPoint());
520+
verify_invariant(point, expected_count, "Cycle 1: indices 0-4");
521+
522+
// Verify bucket positions
523+
EXPECT_EQ(point.positive_buckets_->StartIndex(), 0);
524+
EXPECT_EQ(point.positive_buckets_->EndIndex(), 4);
525+
}
526+
527+
// === Cycle 2: values 4,8,16,32,64 -> indices 1,2,3,4,5 (5 buckets, fits individually) ===
528+
// But combined with Cycle 1: indices 0-5 = 6 buckets = max_buckets + 1
529+
// This triggers the off-by-one bug as of commit
530+
// https://github.com/open-telemetry/opentelemetry-cpp/commit/5fc4707a8b7820f6bdbc782ccdffac7ccafbe80d.
531+
{
532+
Base2ExponentialHistogramAggregation delta(&config);
533+
delta.Aggregate(4.0, {}); // 2^2 -> index 1
534+
delta.Aggregate(8.0, {}); // 2^3 -> index 2
535+
delta.Aggregate(16.0, {}); // 2^4 -> index 3
536+
delta.Aggregate(32.0, {}); // 2^5 -> index 4
537+
delta.Aggregate(64.0, {}); // 2^6 -> index 5
538+
expected_count += 5;
539+
540+
cumulative = cumulative->Merge(delta);
541+
542+
auto point = nostd::get<Base2ExponentialHistogramPointData>(cumulative->ToPoint());
543+
verify_invariant(point, expected_count, "Cycle 2: combined indices 0-5");
544+
}
545+
546+
// === Cycle 3: values 8,16,32,64,128 -> indices 2,3,4,5,6 ===
547+
{
548+
Base2ExponentialHistogramAggregation delta(&config);
549+
delta.Aggregate(8.0, {}); // 2^3 -> index 2
550+
delta.Aggregate(16.0, {}); // 2^4 -> index 3
551+
delta.Aggregate(32.0, {}); // 2^5 -> index 4
552+
delta.Aggregate(64.0, {}); // 2^6 -> index 5
553+
delta.Aggregate(128.0, {}); // 2^7 -> index 6
554+
expected_count += 5;
555+
556+
cumulative = cumulative->Merge(delta);
557+
558+
auto point = nostd::get<Base2ExponentialHistogramPointData>(cumulative->ToPoint());
559+
verify_invariant(point, expected_count, "Cycle 3: indices 2-6");
560+
}
561+
562+
// === Cycle 4: values 16,32,64,128,256 -> indices 3,4,5,6,7 ===
563+
{
564+
Base2ExponentialHistogramAggregation delta(&config);
565+
delta.Aggregate(16.0, {}); // 2^4 -> index 3
566+
delta.Aggregate(32.0, {}); // 2^5 -> index 4
567+
delta.Aggregate(64.0, {}); // 2^6 -> index 5
568+
delta.Aggregate(128.0, {}); // 2^7 -> index 6
569+
delta.Aggregate(256.0, {}); // 2^8 -> index 7
570+
expected_count += 5;
571+
572+
cumulative = cumulative->Merge(delta);
573+
574+
auto point = nostd::get<Base2ExponentialHistogramPointData>(cumulative->ToPoint());
575+
verify_invariant(point, expected_count, "Cycle 4: indices 3-7");
576+
}
577+
578+
// === Cycle 5: values 32,64,128,256,512 -> indices 4,5,6,7,8 ===
579+
{
580+
Base2ExponentialHistogramAggregation delta(&config);
581+
delta.Aggregate(32.0, {}); // 2^5 -> index 4
582+
delta.Aggregate(64.0, {}); // 2^6 -> index 5
583+
delta.Aggregate(128.0, {}); // 2^7 -> index 6
584+
delta.Aggregate(256.0, {}); // 2^8 -> index 7
585+
delta.Aggregate(512.0, {}); // 2^9 -> index 8
586+
expected_count += 5;
587+
588+
cumulative = cumulative->Merge(delta);
589+
590+
auto point = nostd::get<Base2ExponentialHistogramPointData>(cumulative->ToPoint());
591+
verify_invariant(point, expected_count, "Cycle 5: indices 4-8");
592+
}
593+
594+
// === Cycle 6: values 64,128,256,512,1024 -> indices 5,6,7,8,9 ===
595+
{
596+
Base2ExponentialHistogramAggregation delta(&config);
597+
delta.Aggregate(64.0, {}); // 2^6 -> index 5
598+
delta.Aggregate(128.0, {}); // 2^7 -> index 6
599+
delta.Aggregate(256.0, {}); // 2^8 -> index 7
600+
delta.Aggregate(512.0, {}); // 2^9 -> index 8
601+
delta.Aggregate(1024.0, {}); // 2^10 -> index 9
602+
expected_count += 5;
603+
604+
cumulative = cumulative->Merge(delta);
605+
606+
auto point = nostd::get<Base2ExponentialHistogramPointData>(cumulative->ToPoint());
607+
verify_invariant(point, expected_count, "Cycle 6: indices 5-9");
608+
}
609+
}

0 commit comments

Comments
 (0)