Skip to content

Commit b0be362

Browse files
Publish completed step values on close before scheduled publish (StepRegistry) (#3864)
When a registry was closed before the scheduled publish in the step, the values were lost from the previous step that would have been published during the scheduled publish. This adds logic to detect this scenario and do an extra publish before rolling over values for the partial step to publish with closing. Resolves gh-3863
1 parent 4811b7e commit b0be362

File tree

5 files changed

+190
-47
lines changed

5 files changed

+190
-47
lines changed

implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,29 @@ protected DistributionStatisticConfig defaultHistogramConfig() {
237237
public void close() {
238238
stop();
239239
if (!isPublishing() && isDelta()) {
240+
if (!isDataPublishedForCurrentStep()) {
241+
// Data was not published for the current step. So, we should flush that
242+
// first.
243+
try {
244+
this.publish();
245+
}
246+
catch (Throwable e) {
247+
logger.warn(
248+
"Unexpected exception thrown while publishing metrics for " + getClass().getSimpleName(),
249+
e);
250+
}
251+
}
240252
getMeters().forEach(this::closingRollover);
241253
}
242254
super.close();
243255
}
244256

257+
private boolean isDataPublishedForCurrentStep() {
258+
long currentTimeInMillis = clock.wallTime();
259+
return (getLastScheduledPublishStartTime() / config.step().toMillis()) >= (currentTimeInMillis
260+
/ config.step().toMillis());
261+
}
262+
245263
// Either we do this or make StepMeter public
246264
// and still call OtlpStepTimer and OtlpStepDistributionSummary separately.
247265
private void closingRollover(Meter meter) {

implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpDeltaMeterRegistryTest.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ void finalPushHasPartialStep() {
550550
assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
551551

552552
stepOverNStep(1);
553-
registry.publish();
553+
registry.scheduledPublish();
554554

555555
assertThat(registry.publishedCounterCounts).hasSize(1);
556556
assertThat(registry.publishedCounterCounts.pop()).isOne();
@@ -684,6 +684,8 @@ private class TestOtlpMeterRegistry extends OtlpMeterRegistry {
684684

685685
Deque<Double> publishedFunctionTimerTotals = new ArrayDeque<>();
686686

687+
private long lastScheduledPublishStartTime = 0L;
688+
687689
public TestOtlpMeterRegistry() {
688690
super(OtlpDeltaMeterRegistryTest.this.otlpConfig(), OtlpDeltaMeterRegistryTest.this.clock);
689691
}
@@ -696,6 +698,16 @@ protected void publish() {
696698
.collect(Collectors.toList());
697699
}
698700

701+
private void scheduledPublish() {
702+
this.lastScheduledPublishStartTime = clock.wallTime();
703+
this.publish();
704+
}
705+
706+
@Override
707+
protected long getLastScheduledPublishStartTime() {
708+
return lastScheduledPublishStartTime;
709+
}
710+
699711
private Timer publishTimer(Timer timer) {
700712
publishedTimerCounts.add(timer.count());
701713
publishedTimerSumMilliseconds.add(timer.totalTime(MILLISECONDS));

micrometer-core/src/main/java/io/micrometer/core/instrument/push/PushMeterRegistry.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public abstract class PushMeterRegistry extends MeterRegistry {
4141

4242
private final AtomicBoolean publishing = new AtomicBoolean(false);
4343

44+
private long lastScheduledPublishStartTime = 0L;
45+
4446
@Nullable
4547
private ScheduledExecutorService scheduledExecutorService;
4648

@@ -60,6 +62,7 @@ protected PushMeterRegistry(PushRegistryConfig config, Clock clock) {
6062
// VisibleForTesting
6163
void publishSafely() {
6264
if (this.publishing.compareAndSet(false, true)) {
65+
this.lastScheduledPublishStartTime = clock.wallTime();
6366
try {
6467
publish();
6568
}
@@ -85,6 +88,14 @@ protected boolean isPublishing() {
8588
return publishing.get();
8689
}
8790

91+
/**
92+
* Returns the time when the last scheduled publish was started by
93+
* {@link PushMeterRegistry#publishSafely()}.
94+
*/
95+
protected long getLastScheduledPublishStartTime() {
96+
return lastScheduledPublishStartTime;
97+
}
98+
8899
/**
89100
* @deprecated Use {@link #start(ThreadFactory)} instead.
90101
*/

micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeterRegistry.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package io.micrometer.core.instrument.step;
1717

1818
import io.micrometer.common.lang.Nullable;
19+
import io.micrometer.common.util.internal.logging.InternalLogger;
20+
import io.micrometer.common.util.internal.logging.InternalLoggerFactory;
1921
import io.micrometer.core.instrument.*;
2022
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
2123
import io.micrometer.core.instrument.distribution.HistogramGauges;
@@ -40,6 +42,8 @@
4042
*/
4143
public abstract class StepMeterRegistry extends PushMeterRegistry {
4244

45+
private static final InternalLogger logger = InternalLoggerFactory.getInstance(StepMeterRegistry.class);
46+
4347
private final StepRegistryConfig config;
4448

4549
@Nullable
@@ -133,15 +137,41 @@ public void stop() {
133137
@Override
134138
public void close() {
135139
stop();
140+
136141
if (!isPublishing()) {
137-
getMeters().stream()
138-
.filter(StepMeter.class::isInstance)
139-
.map(StepMeter.class::cast)
140-
.forEach(StepMeter::_closingRollover);
142+
if (!isDataPublishedForCurrentStep()) {
143+
// Data was not published for the current step. So, we should flush that
144+
// first.
145+
try {
146+
this.publish();
147+
}
148+
catch (Throwable e) {
149+
logger.warn(
150+
"Unexpected exception thrown while publishing metrics for " + getClass().getSimpleName(),
151+
e);
152+
}
153+
}
154+
closeStepMeters();
141155
}
142156
super.close();
143157
}
144158

159+
private boolean isDataPublishedForCurrentStep() {
160+
long currentTimeInMillis = clock.wallTime();
161+
return (getLastScheduledPublishStartTime() / config.step().toMillis()) >= (currentTimeInMillis
162+
/ config.step().toMillis());
163+
}
164+
165+
/**
166+
* Performs closing rollover on StepMeters.
167+
*/
168+
private void closeStepMeters() {
169+
getMeters().stream()
170+
.filter(StepMeter.class::isInstance)
171+
.map(StepMeter.class::cast)
172+
.forEach(StepMeter::_closingRollover);
173+
}
174+
145175
/**
146176
* This will poll the values from meters, which will cause a roll over for Step-meters
147177
* if past the step boundary. This gives some control over when roll over happens

micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepMeterRegistryTest.java

Lines changed: 114 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,7 @@ void shortLivedPublish() {
175175
.register(registry);
176176

177177
// before step rollover
178-
assertThat(counter.count()).isZero();
179-
assertThat(timer.count()).isZero();
180-
assertThat(timer.totalTime(MILLISECONDS)).isZero();
181-
assertThat(summary.count()).isZero();
182-
assertThat(summary.totalAmount()).isZero();
183-
assertThat(functionCounter.count()).isZero();
184-
assertThat(functionTimer.count()).isZero();
185-
assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
178+
assertBeforeRollover(counter, timer, summary, functionCounter, functionTimer);
186179

187180
registry.close();
188181

@@ -224,17 +217,10 @@ void finalPushHasPartialStep() {
224217
.register(registry);
225218

226219
// before step rollover
227-
assertThat(counter.count()).isZero();
228-
assertThat(timer.count()).isZero();
229-
assertThat(timer.totalTime(MILLISECONDS)).isZero();
230-
assertThat(summary.count()).isZero();
231-
assertThat(summary.totalAmount()).isZero();
232-
assertThat(functionCounter.count()).isZero();
233-
assertThat(functionTimer.count()).isZero();
234-
assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
220+
assertBeforeRollover(counter, timer, summary, functionCounter, functionTimer);
235221

236-
clock.add(config.step());
237-
registry.publish();
222+
addTimeWithRolloverOnStepStart(clock, registry, config, config.step());
223+
registry.scheduledPublish();
238224

239225
assertThat(registry.publishedCounterCounts).hasSize(1);
240226
assertThat(registry.publishedCounterCounts.pop()).isOne();
@@ -254,7 +240,7 @@ void finalPushHasPartialStep() {
254240
assertThat(registry.publishedFunctionTimerTotals.pop()).isEqualTo(53);
255241

256242
// set clock to middle of second step
257-
clock.add(config.step().dividedBy(2));
243+
addTimeWithRolloverOnStepStart(clock, registry, config, config.step().dividedBy(2));
258244
// record some more values in new step interval
259245
counter.increment(2);
260246
timer.record(6, MILLISECONDS);
@@ -309,14 +295,7 @@ void publishOnCloseCrossesStepBoundary() {
309295
.register(registry);
310296

311297
// before rollover
312-
assertThat(counter.count()).isZero();
313-
assertThat(timer.count()).isZero();
314-
assertThat(timer.totalTime(MILLISECONDS)).isZero();
315-
assertThat(summary.count()).isZero();
316-
assertThat(summary.totalAmount()).isZero();
317-
assertThat(functionCounter.count()).isZero();
318-
assertThat(functionTimer.count()).isZero();
319-
assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
298+
assertBeforeRollover(counter, timer, summary, functionCounter, functionTimer);
320299

321300
// before publishing, simulate a step boundary being crossed after forced rollover
322301
// on close and before/during publishing
@@ -342,6 +321,57 @@ void publishOnCloseCrossesStepBoundary() {
342321
assertThat(registry.publishedFunctionTimerTotals.pop()).isEqualTo(53);
343322
}
344323

324+
@Test
325+
@Issue("#3863")
326+
void shouldPublishLastCompletedStepWhenClosingBeforeScheduledPublish() {
327+
Counter counter = Counter.builder("counter_3863").register(registry);
328+
Timer timer = Timer.builder("timer_3863").register(registry);
329+
DistributionSummary summary = DistributionSummary.builder("summary_3863").register(registry);
330+
331+
AtomicLong functionValue = new AtomicLong(0);
332+
FunctionCounter functionCounter = FunctionCounter
333+
.builder("counter.function_3863", functionValue, AtomicLong::get)
334+
.register(registry);
335+
FunctionTimer functionTimer = FunctionTimer
336+
.builder("timer.function_3863", this, obj -> 3, obj -> 53, MILLISECONDS)
337+
.register(registry);
338+
339+
counter.increment();
340+
timer.record(5, MILLISECONDS);
341+
summary.record(5);
342+
functionValue.set(1);
343+
344+
// before rollover
345+
assertBeforeRollover(counter, timer, summary, functionCounter, functionTimer);
346+
347+
addTimeWithRolloverOnStepStart(clock, registry, config, Duration.ofSeconds(60));
348+
349+
// All new recordings now belong to next step.
350+
counter.increment(2);
351+
timer.record(10, MILLISECONDS);
352+
summary.record(10);
353+
functionValue.incrementAndGet();
354+
355+
// Simulating the application close behaviour before actual publishing happens.
356+
registry.close();
357+
358+
assertThat(registry.publishedCounterCounts).hasSize(2);
359+
assertThat(registry.sumAllPublishedValues(registry.publishedCounterCounts)).isEqualTo(3);
360+
assertThat(registry.publishedTimerCounts).hasSize(2);
361+
assertThat(registry.sumAllPublishedValues(registry.publishedTimerCounts)).isEqualTo(2);
362+
assertThat(registry.sumAllPublishedValues(registry.publishedTimerSumMilliseconds)).isEqualTo(15);
363+
assertThat(registry.publishedSummaryCounts).hasSize(2);
364+
assertThat(registry.sumAllPublishedValues(registry.publishedSummaryCounts)).isEqualTo(2);
365+
assertThat(registry.sumAllPublishedValues(registry.publishedSummaryTotals)).isEqualTo(15);
366+
367+
assertThat(registry.publishedFunctionCounterCounts).hasSize(2);
368+
assertThat(registry.sumAllPublishedValues(registry.publishedFunctionCounterCounts)).isEqualTo(2);
369+
370+
assertThat(registry.publishedFunctionTimerCounts).hasSize(2);
371+
assertThat(registry.sumAllPublishedValues(registry.publishedFunctionTimerCounts)).isEqualTo(3);
372+
assertThat(registry.sumAllPublishedValues(registry.publishedFunctionTimerTotals)).isEqualTo(53);
373+
}
374+
345375
@Test
346376
@Issue("#2818")
347377
void scheduledRollOver() {
@@ -357,28 +387,17 @@ void scheduledRollOver() {
357387
.register(registry);
358388

359389
// before rollover
360-
assertThat(counter.count()).isZero();
361-
assertThat(timer.count()).isZero();
362-
assertThat(timer.totalTime(MILLISECONDS)).isZero();
363-
assertThat(summary.count()).isZero();
364-
assertThat(summary.totalAmount()).isZero();
365-
assertThat(functionCounter.count()).isZero();
366-
assertThat(functionTimer.count()).isZero();
367-
assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
368-
369-
clock.addSeconds(60);
370-
// simulate this being scheduled at the start of the step
371-
registry.pollMetersToRollover();
390+
assertBeforeRollover(counter, timer, summary, functionCounter, functionTimer);
372391

373-
clock.addSeconds(1);
392+
addTimeWithRolloverOnStepStart(clock, registry, config, Duration.ofSeconds(60));
374393
// these recordings belong to the current step and should not be published
375394
counter.increment();
376395
timer.record(5, MILLISECONDS);
377396
summary.record(8);
378-
clock.addSeconds(10);
397+
addTimeWithRolloverOnStepStart(clock, registry, config, Duration.ofSeconds(10));
379398

380399
// recordings that happened in the previous step should be published
381-
registry.publish();
400+
registry.scheduledPublish();
382401
assertThat(registry.publishedCounterCounts).hasSize(1);
383402
assertThat(registry.publishedCounterCounts.pop()).isOne();
384403
assertThat(registry.publishedTimerCounts).hasSize(1);
@@ -415,6 +434,8 @@ private class MyStepMeterRegistry extends StepMeterRegistry {
415434

416435
Deque<Double> publishedFunctionTimerTotals = new ArrayDeque<>();
417436

437+
private long lastScheduledPublishStartTime = 0L;
438+
418439
@Nullable
419440
Runnable prePublishAction;
420441

@@ -438,6 +459,16 @@ protected void publish() {
438459
.collect(Collectors.toList());
439460
}
440461

462+
private void scheduledPublish() {
463+
this.lastScheduledPublishStartTime = clock.wallTime();
464+
this.publish();
465+
}
466+
467+
@Override
468+
protected long getLastScheduledPublishStartTime() {
469+
return lastScheduledPublishStartTime;
470+
}
471+
441472
private Timer publishTimer(Timer timer) {
442473
publishedTimerCounts.add(timer.count());
443474
publishedTimerSumMilliseconds.add(timer.totalTime(MILLISECONDS));
@@ -466,11 +497,52 @@ private DistributionSummary publishSummary(DistributionSummary summary) {
466497
return summary;
467498
}
468499

500+
<T extends Number> double sumAllPublishedValues(Deque<T> deque) {
501+
double sum = 0;
502+
while (!deque.isEmpty()) {
503+
sum += deque.pop().doubleValue();
504+
}
505+
return sum;
506+
}
507+
469508
@Override
470509
protected TimeUnit getBaseTimeUnit() {
471510
return TimeUnit.SECONDS;
472511
}
473512

474513
}
475514

515+
private static void assertBeforeRollover(final Counter counter, final Timer timer,
516+
final DistributionSummary summary, final FunctionCounter functionCounter,
517+
final FunctionTimer functionTimer) {
518+
assertThat(counter.count()).isZero();
519+
assertThat(timer.count()).isZero();
520+
assertThat(timer.totalTime(MILLISECONDS)).isZero();
521+
assertThat(summary.count()).isZero();
522+
assertThat(summary.totalAmount()).isZero();
523+
assertThat(functionCounter.count()).isZero();
524+
assertThat(functionTimer.count()).isZero();
525+
assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
526+
}
527+
528+
/**
529+
* This method simulates the behaviour StepRegistry will exhibit when rollOver is
530+
* scheduled on a thread. This calls {@link StepMeterRegistry#pollMetersToRollover()}
531+
* as soon as the step is crossed.
532+
*/
533+
private void addTimeWithRolloverOnStepStart(MockClock clock, StepMeterRegistry registry, StepRegistryConfig config,
534+
Duration timeToAdd) {
535+
536+
long currentTime = clock.wallTime();
537+
long boundaryForNextStep = ((currentTime / config.step().toMillis()) + 1) * config.step().toMillis();
538+
long timeToNextStep = boundaryForNextStep - currentTime;
539+
if (timeToAdd.toMillis() >= timeToNextStep) {
540+
clock.add(timeToNextStep, MILLISECONDS);
541+
registry.pollMetersToRollover();
542+
clock.add((timeToAdd.toMillis() - timeToNextStep), MILLISECONDS);
543+
return;
544+
}
545+
clock.add(timeToAdd);
546+
}
547+
476548
}

0 commit comments

Comments
 (0)