|
8 | 8 | import java.util.ArrayList; |
9 | 9 | import java.util.List; |
10 | 10 | import java.util.concurrent.CountDownLatch; |
| 11 | +import java.util.concurrent.ExecutorService; |
| 12 | +import java.util.concurrent.Executors; |
| 13 | +import java.util.concurrent.Future; |
11 | 14 | import java.util.concurrent.TimeUnit; |
12 | 15 | import java.util.concurrent.atomic.AtomicBoolean; |
13 | 16 | import java.util.concurrent.atomic.AtomicLong; |
@@ -180,83 +183,123 @@ void interruptedAppenderLeavesBoundedBufferWait() throws InterruptedException { |
180 | 183 | } |
181 | 184 |
|
182 | 185 | @Test |
183 | | - void lateAppenderCannotBeAddedToTheNextGeneration() throws InterruptedException { |
184 | | - CountDownLatch firstRunStarted = new CountDownLatch(1); |
185 | | - CountDownLatch firstRunMayFinish = new CountDownLatch(1); |
186 | | - CountDownLatch stalled = new CountDownLatch(1); |
187 | | - CountDownLatch release = new CountDownLatch(1); |
| 186 | + void lateAppenderCountedByNextGenerationMustNotBeBufferedAgain() throws Exception { |
| 187 | + assertLateAppenderHandoff(false); |
| 188 | + } |
| 189 | + |
| 190 | + @Test |
| 191 | + void lateAppenderHandoffUsesAbsoluteStripeCountsAfterReset() throws Exception { |
| 192 | + assertLateAppenderHandoff(true); |
| 193 | + } |
| 194 | + |
| 195 | + private static void assertLateAppenderHandoff(boolean reset) throws Exception { |
| 196 | + CountDownLatch firstSnapshotStarted = new CountDownLatch(1); |
| 197 | + CountDownLatch finishFirstSnapshot = new CountDownLatch(1); |
| 198 | + CountDownLatch observationCounted = new CountDownLatch(1); |
| 199 | + CountDownLatch readGeneration = new CountDownLatch(1); |
188 | 200 | CountDownLatch secondRunStarted = new CountDownLatch(1); |
189 | | - AtomicBoolean appended = new AtomicBoolean(); |
190 | 201 | AtomicLong completedObservations = new AtomicLong(); |
| 202 | + AtomicLong secondExpectedCount = new AtomicLong(); |
| 203 | + AtomicBoolean pauseFirstAppender = new AtomicBoolean(true); |
191 | 204 | Buffer buffer = |
192 | 205 | new Buffer( |
193 | | - TimeUnit.SECONDS.toNanos(1), |
| 206 | + TimeUnit.SECONDS.toNanos(5), |
194 | 207 | 16, |
195 | 208 | () -> { |
196 | | - stalled.countDown(); |
197 | | - try { |
198 | | - release.await(); |
199 | | - } catch (InterruptedException e) { |
200 | | - Thread.currentThread().interrupt(); |
| 209 | + if (pauseFirstAppender.compareAndSet(true, false)) { |
| 210 | + observationCounted.countDown(); |
| 211 | + awaitLatch(readGeneration); |
201 | 212 | } |
202 | 213 | }); |
203 | | - Thread firstRun = |
204 | | - new Thread( |
205 | | - () -> |
206 | | - buffer.run( |
207 | | - ignored -> { |
208 | | - firstRunStarted.countDown(); |
209 | | - return firstRunMayFinish.getCount() == 0; |
210 | | - }, |
211 | | - () -> new CounterSnapshot.CounterDataPointSnapshot(0, Labels.EMPTY, null, 0), |
212 | | - ignored -> {}), |
213 | | - "buffer-first-runner"); |
214 | | - firstRun.setDaemon(true); |
215 | | - firstRun.start(); |
216 | | - assertThat(firstRunStarted.await(5, TimeUnit.SECONDS)).isTrue(); |
| 214 | + if (reset) { |
| 215 | + assertThat(buffer.append(1.0)).isFalse(); |
| 216 | + buffer.observeDirect(completedObservations::incrementAndGet); |
| 217 | + } |
| 218 | + ExecutorService executor = Executors.newFixedThreadPool(2); |
| 219 | + try { |
| 220 | + Future<CounterSnapshot.CounterDataPointSnapshot> firstRun = |
| 221 | + executor.submit( |
| 222 | + () -> |
| 223 | + buffer.run( |
| 224 | + expectedCount -> completedObservations.get() == expectedCount, |
| 225 | + () -> { |
| 226 | + firstSnapshotStarted.countDown(); |
| 227 | + awaitLatch(finishFirstSnapshot); |
| 228 | + CounterSnapshot.CounterDataPointSnapshot snapshot = |
| 229 | + new CounterSnapshot.CounterDataPointSnapshot( |
| 230 | + completedObservations.get(), Labels.EMPTY, null, 0); |
| 231 | + if (reset) { |
| 232 | + completedObservations.set(0); |
| 233 | + buffer.reset(); |
| 234 | + } |
| 235 | + return snapshot; |
| 236 | + }, |
| 237 | + ignored -> completedObservations.incrementAndGet())); |
| 238 | + awaitLatch(firstSnapshotStarted); |
217 | 239 |
|
218 | | - Thread appender = |
219 | | - new Thread( |
220 | | - () -> { |
221 | | - appended.set(buffer.append(1.0)); |
222 | | - if (!appended.get()) { |
223 | | - buffer.observeDirect( |
224 | | - () -> { |
225 | | - completedObservations.incrementAndGet(); |
226 | | - return null; |
227 | | - }); |
228 | | - } |
229 | | - }, |
230 | | - "buffer-late-appender"); |
231 | | - appender.setDaemon(true); |
232 | | - appender.start(); |
233 | | - assertThat(stalled.await(5, TimeUnit.SECONDS)).isTrue(); |
| 240 | + // Increment while generation A is active, but do not read activeGeneration yet. |
| 241 | + Future<Boolean> appender = |
| 242 | + executor.submit( |
| 243 | + () -> { |
| 244 | + boolean appended = buffer.append(1.0); |
| 245 | + if (!appended) { |
| 246 | + buffer.observeDirect(completedObservations::incrementAndGet); |
| 247 | + } |
| 248 | + return appended; |
| 249 | + }); |
| 250 | + awaitLatch(observationCounted); |
| 251 | + finishFirstSnapshot.countDown(); |
| 252 | + assertThat(firstRun.get(10, TimeUnit.SECONDS).getValue()).isEqualTo(reset ? 1 : 0); |
234 | 253 |
|
235 | | - firstRunMayFinish.countDown(); |
236 | | - firstRun.join(5_000); |
237 | | - assertThat(firstRun.isAlive()).isFalse(); |
| 254 | + Future<CounterSnapshot.CounterDataPointSnapshot> secondRun = |
| 255 | + executor.submit( |
| 256 | + () -> |
| 257 | + buffer.run( |
| 258 | + expectedCount -> { |
| 259 | + secondExpectedCount.set(expectedCount); |
| 260 | + secondRunStarted.countDown(); |
| 261 | + return completedObservations.get() == expectedCount; |
| 262 | + }, |
| 263 | + () -> |
| 264 | + new CounterSnapshot.CounterDataPointSnapshot( |
| 265 | + completedObservations.get(), Labels.EMPTY, null, 0), |
| 266 | + ignored -> completedObservations.incrementAndGet())); |
| 267 | + awaitLatch(secondRunStarted); |
| 268 | + assertThat(secondExpectedCount).hasValue(1); |
| 269 | + // An observation arriving after B's activation still belongs in B's buffer. It must not |
| 270 | + // appear in B's snapshot and must be replayed exactly once before the following collection. |
| 271 | + assertThat(buffer.append(1.0)).isTrue(); |
238 | 272 |
|
239 | | - Thread secondRun = |
240 | | - new Thread( |
241 | | - () -> |
242 | | - buffer.run( |
243 | | - expectedCount -> { |
244 | | - secondRunStarted.countDown(); |
245 | | - return completedObservations.get() == expectedCount; |
246 | | - }, |
247 | | - () -> new CounterSnapshot.CounterDataPointSnapshot(0, Labels.EMPTY, null, 0), |
248 | | - ignored -> {}), |
249 | | - "buffer-second-runner"); |
250 | | - secondRun.setDaemon(true); |
251 | | - secondRun.start(); |
252 | | - assertThat(secondRunStarted.await(5, TimeUnit.SECONDS)).isTrue(); |
253 | | - release.countDown(); |
254 | | - appender.join(5_000); |
255 | | - secondRun.join(5_000); |
| 273 | + // B includes the paused observation in expectedCount. Buffering it in B would make B wait |
| 274 | + // until its own timeout/replay; it must instead complete via the direct observation path. |
| 275 | + readGeneration.countDown(); |
| 276 | + assertThat(secondRun.get(10, TimeUnit.SECONDS).getValue()).isEqualTo(1); |
| 277 | + assertThat(appender.get(10, TimeUnit.SECONDS)).isFalse(); |
| 278 | + assertThat(completedObservations).hasValue(2); |
| 279 | + assertThat( |
| 280 | + buffer |
| 281 | + .run( |
| 282 | + expectedCount -> completedObservations.get() == expectedCount, |
| 283 | + () -> |
| 284 | + new CounterSnapshot.CounterDataPointSnapshot( |
| 285 | + completedObservations.get(), Labels.EMPTY, null, 0), |
| 286 | + ignored -> completedObservations.incrementAndGet()) |
| 287 | + .getValue()) |
| 288 | + .isEqualTo(2); |
| 289 | + } finally { |
| 290 | + finishFirstSnapshot.countDown(); |
| 291 | + readGeneration.countDown(); |
| 292 | + executor.shutdownNow(); |
| 293 | + assertThat(executor.awaitTermination(10, TimeUnit.SECONDS)).isTrue(); |
| 294 | + } |
| 295 | + } |
256 | 296 |
|
257 | | - assertThat(appender.isAlive()).isFalse(); |
258 | | - assertThat(secondRun.isAlive()).isFalse(); |
259 | | - assertThat(appended).isFalse(); |
260 | | - assertThat(completedObservations).hasValue(1); |
| 297 | + private static void awaitLatch(CountDownLatch latch) { |
| 298 | + try { |
| 299 | + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 300 | + } catch (InterruptedException e) { |
| 301 | + Thread.currentThread().interrupt(); |
| 302 | + throw new RuntimeException(e); |
| 303 | + } |
261 | 304 | } |
262 | 305 | } |
0 commit comments