Skip to content

Commit 976d485

Browse files
committed
fix: bound observation buffering during collection
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent 05bf7e4 commit 976d485

5 files changed

Lines changed: 380 additions & 93 deletions

File tree

docs/apidiffs/current_vs_latest/prometheus-metrics-core.txt

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Buffer.java

Lines changed: 168 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,145 +2,232 @@
22

33
import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
44
import java.util.Arrays;
5+
import java.util.concurrent.TimeUnit;
56
import java.util.concurrent.atomic.AtomicLong;
67
import java.util.concurrent.locks.Condition;
78
import java.util.concurrent.locks.ReentrantLock;
89
import java.util.function.Consumer;
910
import java.util.function.Function;
1011
import java.util.function.Supplier;
12+
import javax.annotation.Nullable;
1113

1214
/**
13-
* Metrics support concurrent write and scrape operations.
15+
* Coordinates concurrent metric observations with collection.
1416
*
15-
* <p>This is implemented by switching to a Buffer when the scrape starts, and applying the values
16-
* from the buffer after the scrape ends.
17+
* <p>Collection activates a generation. Observations that start after activation are appended to
18+
* that generation while the collector waits for observations from the previous phase to finish. The
19+
* collector then creates a snapshot, deactivates the generation, and replays its buffered
20+
* observations into the live metric state.
1721
*/
1822
class Buffer {
19-
2023
private static final long bufferActiveBit = 1L << 63;
24+
private static final long DEFAULT_MAX_SPIN_WAIT_NANOS = TimeUnit.SECONDS.toNanos(1);
25+
private static final int DEFAULT_MAX_BUFFER_SIZE = 1_000_000;
26+
private static final int INITIAL_BUFFER_SIZE = 128;
27+
28+
/** Observations buffered during one collection cycle. */
29+
private static final class Generation {
30+
private double[] values = new double[0];
31+
private int size;
32+
private boolean active = true;
33+
}
34+
2135
// Tracking observation counts requires an AtomicLong for coordination between recording and
2236
// collecting. AtomicLong does much worse under contention than the LongAdder instances used
23-
// elsewhere to hold aggregated state. To improve, we stripe the AtomicLong into N instances,
24-
// where N is the number of available processors. Each record operation chooses the appropriate
25-
// instance to use based on the modulo of its thread id and N. This is a more naive / simple
26-
// implementation compared to the striping used under the hood in java.util.concurrent classes
27-
// like LongAdder - contention and hot spots can still occur if recording thread ids happen to
28-
// resolve to the same index. Further improvement is possible.
37+
// elsewhere to hold aggregated state. To reduce contention, the count is striped across the
38+
// available processors. This is simpler than the striping used by LongAdder, so hot spots remain
39+
// possible when several recording threads resolve to the same stripe.
2940
private final AtomicLong[] stripedObservationCounts;
30-
private double[] observationBuffer = new double[0];
31-
private int bufferPos = 0;
32-
private boolean reset = false;
33-
41+
// phaseTransition() makes appendPhase odd while the collector changes generations. An appender
42+
// only buffers its observation if it sees the same even phase before and after incrementing its
43+
// stripe. appendersInFlight closes the gap between those two phase reads.
44+
private final AtomicLong appendersInFlight = new AtomicLong();
45+
private final AtomicLong appendPhase = new AtomicLong();
46+
private final ReentrantLock observationLock = new ReentrantLock();
47+
private boolean reset;
48+
private long observationCountOffset;
49+
@Nullable private volatile Generation activeGeneration;
3450
ReentrantLock appendLock = new ReentrantLock();
3551
ReentrantLock runLock = new ReentrantLock();
36-
Condition bufferFilled = appendLock.newCondition();
52+
private final Condition bufferSpaceAvailable = appendLock.newCondition();
53+
private final long maxSpinWaitNanos;
54+
private final int maxBufferSize;
55+
private final Runnable beforeAppendLock;
3756

3857
Buffer() {
58+
this(DEFAULT_MAX_SPIN_WAIT_NANOS, DEFAULT_MAX_BUFFER_SIZE, () -> {});
59+
}
60+
61+
Buffer(long maxSpinWaitNanos) {
62+
this(maxSpinWaitNanos, DEFAULT_MAX_BUFFER_SIZE, () -> {});
63+
}
64+
65+
Buffer(long maxSpinWaitNanos, int maxBufferSize, Runnable beforeAppendLock) {
66+
if (maxBufferSize <= 0) {
67+
throw new IllegalArgumentException("maxBufferSize must be positive");
68+
}
69+
this.maxSpinWaitNanos = maxSpinWaitNanos;
70+
this.maxBufferSize = maxBufferSize;
71+
this.beforeAppendLock = beforeAppendLock;
3972
stripedObservationCounts = new AtomicLong[Runtime.getRuntime().availableProcessors()];
4073
for (int i = 0; i < stripedObservationCounts.length; i++) {
41-
stripedObservationCounts[i] = new AtomicLong(0);
74+
stripedObservationCounts[i] = new AtomicLong();
4275
}
4376
}
4477

4578
boolean append(double value) {
46-
int index = stripeIndex(Thread.currentThread().getId(), stripedObservationCounts.length);
47-
AtomicLong observationCountForThread = stripedObservationCounts[index];
48-
long count = observationCountForThread.incrementAndGet();
49-
if ((count & bufferActiveBit) == 0) {
50-
return false; // sign bit not set -> buffer not active.
51-
} else {
52-
doAppend(value);
79+
AtomicLong counter =
80+
stripedObservationCounts[
81+
stripeIndex(Thread.currentThread().getId(), stripedObservationCounts.length)];
82+
appendersInFlight.incrementAndGet();
83+
long phase = appendPhase.get();
84+
long count = counter.incrementAndGet();
85+
boolean phaseChanged = appendPhase.get() != phase;
86+
appendersInFlight.decrementAndGet();
87+
if (phaseChanged || (phase & 1L) != 0 || (count & bufferActiveBit) == 0) {
88+
return false;
89+
}
90+
Generation generation = activeGeneration;
91+
if (generation == null) {
92+
return false;
93+
}
94+
beforeAppendLock.run();
95+
appendLock.lock();
96+
try {
97+
Generation current = activeGeneration;
98+
if (current != generation || !generation.active) {
99+
return false;
100+
}
101+
while (generation.size >= maxBufferSize && generation.active) {
102+
bufferSpaceAvailable.awaitUninterruptibly();
103+
}
104+
if (!generation.active) {
105+
return false;
106+
}
107+
if (generation.size >= generation.values.length) {
108+
int doubled =
109+
generation.values.length > maxBufferSize / 2
110+
? maxBufferSize
111+
: generation.values.length * 2;
112+
generation.values =
113+
Arrays.copyOf(
114+
generation.values,
115+
Math.min(maxBufferSize, Math.max(INITIAL_BUFFER_SIZE, Math.max(1, doubled))));
116+
}
117+
generation.values[generation.size++] = value;
53118
return true;
119+
} finally {
120+
appendLock.unlock();
54121
}
55122
}
56123

57124
static int stripeIndex(long threadId, int stripeCount) {
58125
return (int) Math.floorMod(threadId, stripeCount);
59126
}
60127

61-
private void doAppend(double amount) {
62-
appendLock.lock();
63-
try {
64-
if (bufferPos >= observationBuffer.length) {
65-
observationBuffer = Arrays.copyOf(observationBuffer, observationBuffer.length + 128);
66-
}
67-
observationBuffer[bufferPos] = amount;
68-
bufferPos++;
128+
void reset() {
129+
reset = true;
130+
}
69131

70-
bufferFilled.signalAll();
132+
<T> T observeDirect(Supplier<T> observeFunction) {
133+
observationLock.lock();
134+
try {
135+
return observeFunction.get();
71136
} finally {
72-
appendLock.unlock();
137+
observationLock.unlock();
73138
}
74139
}
75140

76-
/** Must be called by the runnable in the run() method. */
77-
void reset() {
78-
reset = true;
141+
@SuppressWarnings({"NullAway", "ThreadPriorityCheck"})
142+
<T extends DataPointSnapshot> T run(
143+
Function<Long, Boolean> complete,
144+
Supplier<T> createResult,
145+
Consumer<Double> observeFunction) {
146+
return run(complete, createResult, observeFunction, true);
79147
}
80148

81-
@SuppressWarnings("ThreadPriorityCheck")
149+
@SuppressWarnings({"NullAway", "ThreadPriorityCheck"})
82150
<T extends DataPointSnapshot> T run(
83151
Function<Long, Boolean> complete,
84152
Supplier<T> createResult,
85-
Consumer<Double> observeFunction) {
153+
Consumer<Double> observeFunction,
154+
boolean failOnTimeout) {
155+
Generation generation = new Generation();
86156
double[] buffer;
87157
int bufferSize;
88-
T result;
89-
158+
boolean timedOut = false;
159+
T result = null;
90160
runLock.lock();
91161
try {
92-
// Signal that the buffer is active.
93-
long expectedCount = 0L;
94-
for (AtomicLong observationCount : stripedObservationCounts) {
95-
expectedCount += observationCount.getAndAdd(bufferActiveBit);
162+
phaseTransition();
163+
long expectedCount;
164+
appendLock.lock();
165+
try {
166+
activeGeneration = generation;
167+
long total = 0;
168+
for (AtomicLong counter : stripedObservationCounts) {
169+
total += counter.getAndAdd(bufferActiveBit);
170+
}
171+
expectedCount = total - observationCountOffset;
172+
} finally {
173+
appendLock.unlock();
96174
}
97-
175+
appendPhase.incrementAndGet();
176+
long deadline = System.nanoTime() + maxSpinWaitNanos;
98177
while (!complete.apply(expectedCount)) {
99-
// Wait until all in-flight threads have added their observations to the histogram /
100-
// summary.
101-
// we can't use a condition here, because the other thread doesn't have a lock as it's on
102-
// the fast path.
103-
Thread.yield();
104-
}
105-
result = createResult.get();
106-
107-
// Signal that the buffer is inactive.
108-
long expectedBufferSize = 0;
109-
if (reset) {
110-
for (AtomicLong observationCount : stripedObservationCounts) {
111-
expectedBufferSize += observationCount.getAndSet(0) & ~bufferActiveBit;
112-
}
113-
reset = false;
114-
} else {
115-
for (AtomicLong observationCount : stripedObservationCounts) {
116-
expectedBufferSize += observationCount.addAndGet(bufferActiveBit);
178+
if (System.nanoTime() - deadline >= 0) {
179+
timedOut = true;
180+
break;
117181
}
182+
Thread.yield();
118183
}
119-
expectedBufferSize -= expectedCount;
120-
121-
appendLock.lock();
184+
observationLock.lock();
122185
try {
123-
while (bufferPos < expectedBufferSize) {
124-
// Wait until all in-flight threads have added their observations to the buffer.
125-
bufferFilled.await();
126-
}
186+
result = timedOut ? null : createResult.get();
127187
} finally {
128-
appendLock.unlock();
188+
try {
189+
phaseTransition();
190+
appendLock.lock();
191+
try {
192+
generation.active = false;
193+
for (AtomicLong counter : stripedObservationCounts) {
194+
counter.addAndGet(bufferActiveBit);
195+
}
196+
if (reset) {
197+
observationCountOffset += expectedCount;
198+
reset = false;
199+
}
200+
activeGeneration = null;
201+
buffer = generation.values;
202+
bufferSize = generation.size;
203+
generation.values = new double[0];
204+
generation.size = 0;
205+
bufferSpaceAvailable.signalAll();
206+
} finally {
207+
appendLock.unlock();
208+
}
209+
appendPhase.incrementAndGet();
210+
for (int i = 0; i < bufferSize; i++) {
211+
observeFunction.accept(buffer[i]);
212+
}
213+
} finally {
214+
observationLock.unlock();
215+
}
129216
}
130-
131-
buffer = observationBuffer;
132-
bufferSize = bufferPos;
133-
observationBuffer = new double[0];
134-
bufferPos = 0;
135-
} catch (InterruptedException e) {
136-
throw new RuntimeException(e);
217+
if (timedOut && failOnTimeout) {
218+
throw new IllegalStateException("Timed out while waiting for in-flight observations.");
219+
}
220+
return result;
137221
} finally {
138222
runLock.unlock();
139223
}
224+
}
140225

141-
for (int i = 0; i < bufferSize; i++) {
142-
observeFunction.accept(buffer[i]);
226+
@SuppressWarnings("ThreadPriorityCheck")
227+
private void phaseTransition() {
228+
appendPhase.incrementAndGet();
229+
while (appendersInFlight.get() != 0) {
230+
Thread.yield();
143231
}
144-
return result;
145232
}
146233
}

0 commit comments

Comments
 (0)