• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Hyshmily / hotkey / 28290463292

27 Jun 2026 01:22PM UTC coverage: 91.227% (-0.7%) from 91.882%
28290463292

push

github

Hyshmily
test: fix CI tests

Signed-off-by: Hyshmily <cxm8607@outlook.com>

1250 of 1427 branches covered (87.6%)

Branch coverage included in aggregate %.

3544 of 3828 relevant lines covered (92.58%)

4.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.16
/common/src/main/java/io/github/hyshmily/hotkey/reporting/HotKeyReporter.java
1
/*
2
 * Copyright 2026 Hyshmily. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package io.github.hyshmily.hotkey.reporting;
17

18
import com.github.benmanes.caffeine.cache.Cache;
19
import com.github.benmanes.caffeine.cache.Caffeine;
20
import io.github.hyshmily.hotkey.sharding.ClusterHealthView;
21
import io.github.hyshmily.hotkey.sharding.RingManager;
22
import java.util.*;
23
import java.util.concurrent.BlockingQueue;
24
import java.util.concurrent.LinkedBlockingQueue;
25
import java.util.concurrent.ScheduledExecutorService;
26
import java.util.concurrent.TimeUnit;
27
import java.util.concurrent.atomic.AtomicBoolean;
28
import java.util.concurrent.atomic.AtomicLong;
29
import java.util.concurrent.atomic.LongAdder;
30
import lombok.Setter;
31
import lombok.extern.slf4j.Slf4j;
32

33

34
/**
35
 * Periodically aggregates per-key access counts and publishes them
36
 * to the Worker via {@link ReportPublisher}.
37
 *
38
 * <p>Uses a Caffeine cache as a temporary counter store; entries are
39
 * evicted after 30 seconds of inactivity to bound memory usage.
40
 * Flushed to the appropriate shard at a fixed interval.
41
 *
42
 * <p>Keys are routed via the {@link RingManager} consistent-hash ring, ensuring the
43
 * same key always maps to the same Worker node even as the cluster scales.
44
 * The RingManager also tracks Worker liveness via heartbeat, so dead shards are
45
 * automatically excluded from routing.
46
 *
47
 * <p>Burst absorption and backpressure are provided by a bounded
48
 * {@link LinkedBlockingQueue} between the flush loop and the
49
 * RabbitMQ publisher.  When the queue is full, {@code flush()} drops
50
 * batches after a configurable timeout, and the Caffeine eviction
51
 * provides natural rate-limiting.
52
 *
53
 * <p>When a {@link BbrRateLimiter} is configured, each flush cycle is submitted
54
 * to the BBR for admission control.  If the pipeline is saturated (high CPU
55
 * and/or excessive in-flight batches), the flush is skipped and counters
56
 * remain in the local Caffeine cache for the next cycle.  This provides
57
 * adaptive back-pressure proportional to system load.
58
 */
59
@Slf4j
4✔
60
public class HotKeyReporter {
61

62
  /** Caffeine cache acting as a temporary counter store; entries evict after 30 s of inactivity. */
63
  private final Cache<String, LongAdder> counters = Caffeine.newBuilder()
4✔
64
    .expireAfterAccess(30, TimeUnit.SECONDS)
2✔
65
    .maximumSize(100_000)
1✔
66
    .build();
2✔
67
  /** Publishes aggregated reports to RabbitMQ. */
68
  private final ReportPublisher reportPublisher;
69
  /** Scheduler for the periodic flush loop. */
70
  private final ScheduledExecutorService scheduler;
71
  /** Fixed delay between report flushes in milliseconds. */
72
  private final long reportIntervalMs;
73
  /** Name of this application instance, included in report messages. */
74
  private final String appName;
75
  /** Maximum capacity of the dispatcher work queue. */
76
  private final int queueCapacity;
77
  /** Timeout (ms) for offering a batch to the dispatcher queue before dropping. */
78
  private final int queueOfferTimeoutMs;
79
  /** Number of consumer threads draining the dispatcher queue. */
80
  private final int consumerCount;
81
  /** Consistent-hashing ring manager for Worker node routing. */
82
  private final RingManager ringManager;
83
  /** Cluster health view for filtering dead Workers. */
84
  private final ClusterHealthView healthView;
85

86
  /** Optional BBR adaptive rate limiter; null disables BBR gating. */
87
  @Setter
88
  private volatile BbrRateLimiter bbrRateLimiter;
89

90
  /** Guards start() idempotency. */
91
  private final AtomicBoolean started = new AtomicBoolean(false);
6✔
92
  /** The report dispatcher instance; created on start(). */
93
  private ReportDispatcher dispatcher;
94

95
  /**
96
   * Creates a new reporter that periodically flushes access counts to RabbitMQ.
97
   *
98
   * @param reportPublisher     the publisher used to send report messages to RabbitMQ
99
   * @param scheduler           the scheduler for periodic flush cycles
100
   * @param reportIntervalMs    fixed delay between consecutive flushes in milliseconds
101
   * @param appName             name of this application instance, included in report messages
102
   * @param queueCapacity       maximum capacity of the dispatcher work queue
103
   * @param queueOfferTimeoutMs timeout for offering a batch to the dispatcher queue before dropping
104
   * @param consumerCount       number of consumer threads draining the dispatcher queue
105
   * @param ringManager         consistent-hashing ring manager for Worker node routing
106
   * @param healthView          cluster health view for filtering dead Workers
107
   */
108
  public HotKeyReporter(
109
    ReportPublisher reportPublisher,
110
    ScheduledExecutorService scheduler,
111
    long reportIntervalMs,
112
    String appName,
113
    int queueCapacity,
114
    int queueOfferTimeoutMs,
115
    int consumerCount,
116
    RingManager ringManager,
117
    ClusterHealthView healthView
118
  ) {
2✔
119
    this.reportPublisher = reportPublisher;
3✔
120
    this.scheduler = scheduler;
3✔
121
    this.reportIntervalMs = reportIntervalMs;
3✔
122
    this.appName = appName;
3✔
123
    this.queueCapacity = queueCapacity;
3✔
124
    this.queueOfferTimeoutMs = queueOfferTimeoutMs;
3✔
125
    this.consumerCount = consumerCount;
3✔
126
    this.ringManager = ringManager;
3✔
127
    this.healthView = healthView;
3✔
128
  }
1✔
129

130
  /**
131
   * A batch of key-count mappings destined for a single Worker target.
132
   *
133
   * @param target    the Worker nodeId for this batch
134
   * @param timestamp wall-clock ms when the batch was assembled
135
   * @param counts    non-zero per-key counts accumulated since the last flush
136
   */
137
  record ShardBatch(String target, long timestamp, Map<String, Long> counts) {}
12✔
138

139
  /**
140
   * Record one access for the given cache key.
141
   *
142
   * <p>Idempotent per-key local counter increment.  The counter is stored in
143
   * a Caffeine cache that evicts after 30 s of inactivity, so low-frequency
144
   * keys are naturally forgotten without explicit cleanup.
145
   *
146
   * @param cacheKey the accessed key
147
   */
148
  public void record(String cacheKey) {
149
    counters.get(cacheKey, k -> new LongAdder()).increment();
11✔
150
  }
1✔
151

152
  /**
153
   * Start the periodic flush scheduler and the report dispatcher.
154
   * Idempotent — subsequent calls are silently ignored.
155
   *
156
   * <p>The flush loop drains the Caffeine counter map, groups entries by
157
   * target (shard index or nodeId), and enqueues them.  Actual publishing
158
   * to RabbitMQ runs on dedicated consumer threads, decoupling the flush
159
   * loop from network I/O.
160
   */
161
  public void start() {
162
    if (!started.compareAndSet(false, true)) {
6✔
163
      log.debug("HotKeyReporter already started, skip");
3✔
164
      return;
1✔
165
    }
166
    try {
167
      dispatcher = new ReportDispatcher();
6✔
168
      dispatcher.start();
3✔
169
      scheduler.scheduleAtFixedRate(this::flush, reportIntervalMs, reportIntervalMs, TimeUnit.MILLISECONDS);
11✔
170
      log.info(
14✔
171
        "HotKeyReporter started: appName={}, intervalMs={}, queueCapacity={}, consumers={}",
172
        appName,
173
        reportIntervalMs,
6✔
174
        queueCapacity,
6✔
175
        dispatcher.consumerCount()
3✔
176
      );
177
    } catch (Exception e) {
1✔
178
      log.error(
4✔
179
        "Failed to start HotKeyReporter; per-key counts will not be flushed to Worker. " +
180
          "Application continues but Worker hot-key detection will be blind to this instance.",
181
        e
182
      );
183
    }
1✔
184
  }
1✔
185

186
  /**
187
   * Gracefully shut down the report dispatcher.
188
   *
189
   * <p>Interrupts all consumer threads and waits for them to finish
190
   * (with a 2-second timeout per thread). Any batches remaining in the
191
   * work queue are discarded. This method is typically registered as
192
   * the Spring bean {@code destroyMethod}.
193
   *
194
   * <p>After shutdown, the reporter no longer publishes reports.
195
   * The periodic flush loop continues to run but its output is silently
196
   * dropped because the dispatcher queue is no longer being consumed.
197
   *
198
   * <p>Idempotent — safe to call multiple times.
199
   */
200
  public void stop() {
201
    if (dispatcher != null) {
3✔
202
      dispatcher.shutdown();
3✔
203
    }
204
  }
1✔
205

206
  /**
207
   * Drain all locally accumulated counters, group by target, and enqueue
208
   * one {@link ShardBatch} per target for the consumer threads to publish.
209
   *
210
   * <p>In consistent-hashing mode, the ring is reconciled with the current
211
   * set of alive Worker nodes before grouping.
212
   *
213
   * <p>If the dispatcher queue is full, the batch for that target is
214
   * dropped (logged at WARN).  Caffeine eviction provides additional
215
   * backpressure — counters for cold keys are silently discarded.
216
   *
217
   * <p>When BBR rate limiting is active, {@code flush()} first checks
218
   * {@link BbrRateLimiter#tryAcquire()}.  If the limiter rejects the cycle,
219
   * the counters remain in the local cache and are merged with the next
220
   * flush.  This provides adaptive back-pressure proportional to system load.
221
   *
222
   * <p>Called periodically by the scheduler at {@code reportIntervalMs}.
223
   */
224
  private void flush() {
225
    try {
226
      if (counters.estimatedSize() == 0) {
6✔
227
        log.trace("Reporter flush tick: counters empty, no-op");
3✔
228
        return;
1✔
229
      }
230

231
      // Reconcile ring with alive Worker nodes (from heartbeat state), then route via consistent hash
232
      ringManager.reconcileFromHealthView(healthView);
5✔
233

234
      // Snapshot the alive-Worker set ONCE before iterating the counters map;
235
      // the previous version called RouteNode per-key, which re-fetched
236
      // getAliveWorkerIds() from ClusterHealthView each iteration — allocating
237
      // a fresh Set (ConcurrentHashMap.values snapshot) per key — up to
238
      // 100 000 keys per flush tick.
239
      Set<String> aliveNodes = healthView.getAliveWorkerIds();
4✔
240
      if (aliveNodes.isEmpty()) {
3✔
241
        log.warn(
5✔
242
          "No alive Worker nodes available for routing; dropping all {} keys in this flush",
243
          counters.estimatedSize()
2✔
244
        );
245
        return;
1✔
246
      }
247

248
      // Set BBR minInFlight to the current number of Workers so that multi-Worker deployments
249
      // don't trigger false-positive drops (each Worker produces one ShardBatch per flush).
250
      if (bbrRateLimiter != null) {
3✔
251
        bbrRateLimiter.setMinInFlight(ringManager.nodeCount());
6✔
252
        if (!bbrRateLimiter.tryAcquire()) {
4!
253
          bbrRateLimiter.onGateDrop();
×
254
          return;
×
255
        }
256
      }
257

258
      Map<String, Map<String, Long>> sharded = new HashMap<>();
4✔
259
      long now = System.currentTimeMillis();
2✔
260

261
      counters
2✔
262
        .asMap()
5✔
263
        .forEach((key, adder) -> {
1✔
264
          long val = adder.sum();
3✔
265

266
          if (val > 0) {
4✔
267
            String target = ringManager.routeNode(key, aliveNodes);
6✔
268

269
            if (target != null) {
2!
270
              adder.sumThenReset();
3✔
271
              sharded.computeIfAbsent(target, t -> new HashMap<>()).put(key, val);
14✔
272
            }
273
          }
274
        });
1✔
275

276
      sharded.forEach((target, counts) -> {
5✔
277
        if (!dispatcher.enqueue(new ShardBatch(target, now, counts))) {
10!
278
          long dropped = dispatcher.dropped();
×
279

280
          if (dropped % 100 == 0 || dropped == 1) {
×
281
            log.warn(
×
282
              "report queue full, dropped target={} keys={}, depth={}/{}, cumulativeDrops={}",
283
              target,
284
              counts.size(),
×
285
              dispatcher.depth(),
×
286
              queueCapacity,
×
287
              dropped
×
288
            );
289
          }
290
        } else if (bbrRateLimiter != null) {
3✔
291
          bbrRateLimiter.onEnqueue();
3✔
292
        }
293
      });
1✔
294
    } catch (Exception e) {
×
295
      log.error("Scheduled reporter flush failed", e);
×
296
    }
1✔
297
  }
1✔
298

299
  /**
300
   * Return the current number of batches waiting in the dispatcher work queue.
301
   *
302
   * @return queue depth (number of enqueued but not yet consumed batches),
303
   *         or {@code -1} if the dispatcher has not been started
304
   */
305
  public int dispatcherDepth() {
306
    return dispatcher == null ? -1 : dispatcher.depth();
9✔
307
  }
308

309
  /**
310
   * Return the maximum capacity of the dispatcher work queue.
311
   *
312
   * @return the queue capacity as configured via {@code queueCapacity},
313
   *         or {@code -1} if the dispatcher has not been started
314
   */
315
  public int dispatcherCapacity() {
316
    return dispatcher == null ? -1 : dispatcher.capacity();
9✔
317
  }
318

319
  /**
320
   * Return the total number of batches that were discarded because they
321
   * waited longer than 5 seconds in the dispatcher queue (staleness expiry).
322
   *
323
   * @return total expired batch count since startup, or {@code -1} if the
324
   *         dispatcher has not been started
325
   */
326
  public long dispatcherExpired() {
327
    return dispatcher == null ? -1 : dispatcher.expired();
7!
328
  }
329

330
  /**
331
   * Return the total number of batches rejected because the dispatcher
332
   * queue was full (back-pressure drops from the flush loop).
333
   *
334
   * @return total dropped batch count since startup, or {@code -1} if the
335
   *         dispatcher has not been started
336
   */
337
  public long dispatcherDropped() {
338
    return dispatcher == null ? -1 : dispatcher.dropped();
7!
339
  }
340

341
  /**
342
   * Return the approximate number of unique keys currently buffered in the
343
   * local Caffeine counter store.
344
   *
345
   * <p>This is an estimate provided by {@link Cache#estimatedSize()} and
346
   * may not reflect the exact count due to the concurrent nature of the
347
   * underlying data structure.
348
   *
349
   * @return estimated number of unique cache keys with pending access counts
350
   */
351
  public long getPendingKeyCount() {
352
    return counters.estimatedSize();
4✔
353
  }
354

355
  /**
356
   * Return the total number of flush cycles that were permitted by the BBR
357
   * rate limiter since startup.
358
   *
359
   * @return total passed count, or {@code -1} if BBR rate limiting is disabled
360
   *         ({@link #bbrRateLimiter} is {@code null})
361
   */
362
  public long bbrPassed() {
363
    return bbrRateLimiter == null ? -1 : bbrRateLimiter.getTotalPassed();
9✔
364
  }
365

366
  /**
367
   * Return the total number of flush cycles that were dropped by the BBR
368
   * rate limiter since startup (both gate drops and consumer drops).
369
   *
370
   * @return total dropped count, or {@code -1} if BBR rate limiting is disabled
371
   */
372
  public long bbrDropped() {
373
    return bbrRateLimiter == null ? -1 : bbrRateLimiter.getTotalDropped();
9✔
374
  }
375

376
  /**
377
   * Return the current number of in-flight batches tracked by the BBR rate
378
   * limiter — those enqueued for publishing but not yet completed or dropped.
379
   *
380
   * @return current in-flight count (non-negative), or {@code -1} if BBR
381
   *         rate limiting is disabled
382
   */
383
  public long bbrInFlight() {
384
    return bbrRateLimiter == null ? -1 : bbrRateLimiter.getInFlight();
9✔
385
  }
386

387
  /**
388
   * Return the current BBR-computed maximum concurrency budget (max in-flight).
389
   *
390
   * <p>This value is derived from the sliding-window maxPASS and minRT
391
   * metrics via Little's Law. It represents the limiter's estimate of the
392
   * optimal number of concurrent in-flight batches before the pipeline
393
   * becomes congested.
394
   *
395
   * @return the computed max in-flight budget, or {@code -1} if BBR rate
396
   *         limiting is disabled
397
   */
398
  public long bbrMaxInFlight() {
399
    return bbrRateLimiter == null ? -1 : bbrRateLimiter.getCurrentMaxInFlight();
9✔
400
  }
401

402
  /**
403
   * Manages a bounded work queue and a fixed pool of consumer threads that
404
   * drain batches and publish them via {@link ReportPublisher}.
405
   *
406
   * <p>Burst absorption: the bound on {@code LinkedBlockingQueue} prevents
407
   * the flush loop from overwhelming RabbitMQ.  Backpressure propagates to
408
   * Caffeine eviction when the queue is persistently full.
409
   */
410
  class ReportDispatcher {
5✔
411

412
    /** Bounded work queue between the flush loop and the consumer threads. */
413
    private final BlockingQueue<ShardBatch> queue = new LinkedBlockingQueue<>(queueCapacity);
8✔
414
    /** Count of batches discarded due to staleness (>5 s wait in queue). */
415
    private final AtomicLong expiredCount = new AtomicLong();
5✔
416
    /** Count of batches rejected because the queue was full. */
417
    private final AtomicLong droppedCount = new AtomicLong();
5✔
418
    /** Active consumer threads draining the work queue. */
419
    private final List<Thread> consumers = new ArrayList<>();
6✔
420
    /** Lifecycle flag; false after shutdown. */
421
    private volatile boolean running;
422

423
    /**
424
     * Start the consumer threads that drain batches from the bounded work
425
     * queue and publish them via {@link ReportPublisher}.
426
     *
427
     * <p>Each consumer runs in a named daemon thread
428
     * ({@code "report-consumer-N"}) so they do not prevent JVM shutdown.
429
     * The number of consumer threads is determined by the
430
     * {@code consumerCount} configuration.
431
     */
432
    void start() {
433
      running = true;
3✔
434
      for (int i = 0; i < consumerCount; i++) {
9✔
435
        Thread t = new Thread(this::consumeLoop, "report-consumer-" + i);
8✔
436
        t.setDaemon(true);
3✔
437
        t.start();
2✔
438
        consumers.add(t);
5✔
439
      }
440
    }
1✔
441

442
    /**
443
     * Offer a batch to the bounded work queue with a timeout.
444
     *
445
     * <p>Blocks for up to {@code queueOfferTimeoutMs} milliseconds waiting
446
     * for space to become available. If the queue remains full after the
447
     * timeout, the batch is rejected and the drop counter is incremented.
448
     *
449
     * <p>If the calling thread is interrupted while waiting, the batch is
450
     * discarded and {@code false} is returned.
451
     *
452
     * @param batch the sharded batch to enqueue for publishing
453
     * @return {@code true} if the batch was accepted into the queue;
454
     *         {@code false} if the queue was full or the thread was
455
     *         interrupted
456
     */
457
    boolean enqueue(ShardBatch batch) {
458
      try {
459
        boolean accepted = queue.offer(batch, queueOfferTimeoutMs, TimeUnit.MILLISECONDS);
10✔
460
        if (!accepted) {
2!
461
          droppedCount.incrementAndGet();
×
462
        }
463
        return accepted;
2✔
464
      } catch (InterruptedException e) {
×
465
        Thread.currentThread().interrupt();
×
466
        return false;
×
467
      }
468
    }
469

470
    /**
471
     * Gracefully shut down all consumer threads.
472
     *
473
     * <p>Signals all consumers to stop via the {@code running} flag and
474
     * thread interruption, then waits up to 2 seconds for each thread to
475
     * finish. After shutdown, the queue may still contain unconsumed
476
     * batches — they are discarded.
477
     *
478
     * <p>This method is called from {@link HotKeyReporter#stop()} and is
479
     * idempotent. However, after shutdown the dispatcher cannot be
480
     * restarted.
481
     */
482
    void shutdown() {
483
      running = false;
3✔
484
      for (Thread t : consumers) {
11✔
485
        t.interrupt();
2✔
486
      }
1✔
487
      for (Thread t : consumers) {
11✔
488
        try {
489
          t.join(2000);
3✔
490
        } catch (InterruptedException e) {
×
491
          Thread.currentThread().interrupt();
×
492
        }
1✔
493
      }
1✔
494
      consumers.clear();
3✔
495
      log.info(
9✔
496
        "ReportDispatcher stopped, remaining queue={}, expired={}, dropped={}",
497
        queue.size(),
7✔
498
        expiredCount.get(),
7✔
499
        droppedCount.get()
3✔
500
      );
501
    }
1✔
502

503
    /**
504
     * Return the current number of batches waiting in the work queue.
505
     *
506
     * @return current queue depth (non-negative)
507
     */
508
    int depth() {
509
      return queue.size();
4✔
510
    }
511

512
    /**
513
     * Return the maximum number of batches the work queue can hold.
514
     *
515
     * @return the configured queue capacity
516
     */
517
    int capacity() {
518
      return queueCapacity;
4✔
519
    }
520

521
    /**
522
     * Return the number of actively running consumer threads.
523
     *
524
     * @return current consumer thread count
525
     */
526
    int consumerCount() {
527
      return consumers.size();
4✔
528
    }
529

530
    /**
531
     * Return the total number of batches that were discarded due to
532
     * staleness (waited longer than 5 seconds in the queue) since startup.
533
     *
534
     * @return total expired batch count
535
     */
536
    long expired() {
537
      return expiredCount.get();
4✔
538
    }
539

540
    /**
541
     * Return the total number of batches that were rejected because the
542
     * queue was full since startup.
543
     *
544
     * @return total dropped batch count
545
     */
546
    long dropped() {
547
      return droppedCount.get();
4✔
548
    }
549

550
    /**
551
     * Consumer thread body: poll batches from the queue and publish them.
552
     *
553
     * <p>Staleness guard: batches that waited longer than 5 s in the queue
554
     * are discarded rather than published, preventing the Worker from receiving
555
     * stale access patterns during prolonged backpressure.
556
     *
557
     * <p>When BBR rate limiting is active:
558
     * <ul>
559
     *   <li>Successful publish records round-trip time via {@link BbrRateLimiter#onSuccess(long)}</li>
560
     *   <li>Stale batches (5s+ wait) or publish failures trigger ,
561
     *       allowing BBR to back off the send rate and prevent cascading overload</li>
562
     *   <li>InterruptedException is handled separately (break, not logged as error)
563
     *       to avoid noise during orderly shutdown</li>
564
     * </ul>
565
     */
566
    private void consumeLoop() {
567
      while (running) {
3✔
568
        ShardBatch batch;
569
        try {
570
          batch = queue.take();
5✔
571
        } catch (InterruptedException e) {
1✔
572
          log.warn("ReportDispatcher consumer interrupted, shutting down");
3✔
573
          Thread.currentThread().interrupt();
2✔
574
          break;
1✔
575
        }
1✔
576

577
        // 5s stale check — discard data that waited too long in the queue
578
        if (System.currentTimeMillis() - batch.timestamp() > 5_000) {
7!
579
          expiredCount.incrementAndGet();
×
580
          if (bbrRateLimiter != null) {
×
581
            bbrRateLimiter.onConsumerDrop();
×
582
          }
583
          continue;
584
        }
585

586
        try {
587
          reportPublisher.publish(batch.target(), new ReportMessage(appName, batch.timestamp(), batch.counts()));
16✔
588
          if (bbrRateLimiter != null) {
4✔
589
            bbrRateLimiter.onSuccess(System.currentTimeMillis() - batch.timestamp());
8✔
590
          }
591
        } catch (Exception e) {
×
592
          log.error("Failed to publish report batch for target={}, keys={}", batch.target(), batch.counts().size(), e);
×
593
          if (bbrRateLimiter != null) {
×
594
            bbrRateLimiter.onConsumerDrop();
×
595
          }
596
        }
1✔
597
      }
1✔
598
    }
1✔
599
  }
600
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc