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

Hyshmily / hotkey / 28356107387

29 Jun 2026 07:35AM UTC coverage: 91.168% (-0.01%) from 91.178%
28356107387

push

github

Hyshmily
test : fix CI tests

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

1276 of 1461 branches covered (87.34%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 4 files covered. (100.0%)

117 existing lines in 15 files now uncovered.

3596 of 3883 relevant lines covered (92.61%)

4.24 hits per line

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

78.05
/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 static io.github.hyshmily.hotkey.util.TimeSource.currentTimeMillis;
19

20
import io.github.hyshmily.hotkey.hotkeydetector.doublebuffer.BufferedCounter;
21
import io.github.hyshmily.hotkey.sharding.ClusterHealthView;
22
import io.github.hyshmily.hotkey.sharding.RingManager;
23
import java.util.*;
24
import java.util.concurrent.BlockingQueue;
25
import java.util.concurrent.LinkedBlockingQueue;
26
import java.util.concurrent.ScheduledExecutorService;
27
import java.util.concurrent.TimeUnit;
28
import java.util.concurrent.atomic.AtomicBoolean;
29
import java.util.concurrent.atomic.AtomicLong;
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 {@link BufferedCounter} double-buffer as a temporary counter store;
39
 * the active buffer accepts lock-free writes while the standby buffer is drained
40
 * and reset on each flush cycle. Flushed to the appropriate shard at a fixed
41
 * interval.
42
 *
43
 * <p>Keys are routed via the {@link RingManager} consistent-hash ring, ensuring the
44
 * same key always maps to the same Worker node even as the cluster scales.
45
 * The RingManager also tracks Worker liveness via heartbeat, so dead shards are
46
 * automatically excluded from routing.
47
 *
48
 * <p>Burst absorption and backpressure are provided by a bounded
49
 * {@link LinkedBlockingQueue} between the flush callback and the
50
 * RabbitMQ publisher.  When the queue is full, {@code onFlush()} drops
51
 * batches after a configurable timeout.
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 flushed batch is dropped and
56
 * the counts are lost — at most one {@code reportIntervalMs} window.
57
 */
58
@Slf4j
4✔
59
public class HotKeyReporter {
60

61
  /** Maximum distinct keys in the BufferedCounter before eager swap. */
62
  private static final int MAX_BUFFER_SIZE = 100_000;
63

64
  /** Fraction of maxBufferSize that triggers an eager buffer swap. */
65
  private static final double EAGER_SWAP_RATIO = 0.8;
66

67
  /** Double-buffered counter aggregating per-key access counts between flushes. */
68
  private final BufferedCounter bufferedCounter;
69

70
  /** Publishes aggregated reports to RabbitMQ. */
71
  private final ReportPublisher reportPublisher;
72
  /** Scheduler for the periodic flush loop. */
73
  private final ScheduledExecutorService scheduler;
74
  /** Fixed delay between report flushes in milliseconds. */
75
  private final long reportIntervalMs;
76
  /** Name of this application instance, included in report messages. */
77
  private final String appName;
78
  /** Maximum capacity of the dispatcher work queue. */
79
  private final int queueCapacity;
80
  /** Timeout (ms) for offering a batch to the dispatcher queue before dropping. */
81
  private final int queueOfferTimeoutMs;
82
  /** Number of consumer threads draining the dispatcher queue. */
83
  private final int consumerCount;
84
  /** Consistent-hashing ring manager for Worker node routing. */
85
  private final RingManager ringManager;
86
  /** Cluster health view for filtering dead Workers. */
87
  private final ClusterHealthView healthView;
88

89
  private volatile int lastNodeCount = -1;
3✔
90

91
  /** Optional BBR adaptive rate limiter; null disables BBR gating. */
92
  @Setter
93
  private volatile BbrRateLimiter bbrRateLimiter;
94

95
  /** Guards start() idempotency. */
96
  private final AtomicBoolean started = new AtomicBoolean(false);
6✔
97
  /** The report dispatcher instance; created on start(). */
98
  private ReportDispatcher dispatcher;
99

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

142
  /**
143
   * A batch of key-count mappings destined for a single Worker target.
144
   *
145
   * @param target    the Worker nodeId for this batch
146
   * @param timestamp wall-clock ms when the batch was assembled
147
   * @param counts    non-zero per-key counts accumulated since the last flush
148
   */
149
  record ShardBatch(String target, long timestamp, Map<String, Long> counts) {}
12✔
150

151
  /**
152
   * Record one access for the given cache key.
153
   *
154
   * <p>Idempotent per-key local counter increment.  The counter is stored in
155
   * a double-buffered {@link BufferedCounter} and flushed periodically to the
156
   * configured Workers.
157
   *
158
   * @param cacheKey the accessed key
159
   */
160
  public void record(String cacheKey) {
161
    bufferedCounter.count(cacheKey, 1);
5✔
162
  }
1✔
163

164
  /**
165
   * Start the periodic flush scheduler and the report dispatcher.
166
   * Idempotent — subsequent calls are silently ignored.
167
   *
168
   * <p>The flush callback drains the {@link BufferedCounter}, groups entries by
169
   * target (shard index or nodeId), and enqueues them.  Actual publishing
170
   * to RabbitMQ runs on dedicated consumer threads, decoupling the flush
171
   * callback from network I/O.
172
   */
173
  public void start() {
174
    if (!started.compareAndSet(false, true)) {
6✔
175
      log.debug("HotKeyReporter already started, skip");
3✔
176
      return;
1✔
177
    }
178
    try {
179
      dispatcher = new ReportDispatcher();
6✔
180
      dispatcher.start();
3✔
181

182
      bufferedCounter.afterPropertiesSet();
3✔
183

184
      log.info(
14✔
185
        "HotKeyReporter started: appName={}, intervalMs={}, queueCapacity={}, consumers={}",
186
        appName,
187
        reportIntervalMs,
6✔
188
        queueCapacity,
6✔
189
        dispatcher.consumerCount()
3✔
190
      );
UNCOV
191
    } catch (Exception e) {
×
UNCOV
192
      log.error(
×
193
        "Failed to start HotKeyReporter; per-key counts will not be flushed to Worker. " +
194
          "Application continues but Worker hot-key detection will be blind to this instance.",
195
        e
196
      );
197
    }
1✔
198
  }
1✔
199

200
  /**
201
   * Gracefully shut down the report dispatcher.
202
   *
203
   * <p>Interrupts all consumer threads and waits for them to finish
204
   * (with a 2-second timeout per thread). Any batches remaining in the
205
   * work queue are discarded. This method is typically registered as
206
   * the Spring bean {@code destroyMethod}.
207
   *
208
   * <p>After shutdown, the reporter no longer publishes reports.
209
   * The periodic flush loop continues to run but its output is silently
210
   * dropped because the dispatcher queue is no longer being consumed.
211
   *
212
   * <p>Idempotent — safe to call multiple times.
213
   */
214
  public void stop() {
215
    bufferedCounter.destroy();
3✔
216
    if (dispatcher != null) {
3✔
217
      dispatcher.shutdown();
3✔
218
    }
219
  }
1✔
220

221
  /**
222
   * Callback invoked by the {@link BufferedCounter} on each scheduled flush.
223
   * Groups the drained key-count map by target Worker via consistent-hash
224
   * routing and enqueues one {@link ShardBatch} per target.
225
   *
226
   * <p>When BBR rate limiting is active, the batch is dropped if the
227
   * limiter rejects the cycle.  Because the double-buffer has already
228
   * drained the counts, at most one {@code reportIntervalMs} window of
229
   * counts is lost — an acceptable trade-off for the simpler and more
230
   * predictable double-buffer design.
231
   *
232
   * <p>If no Workers are alive, the batch is silently discarded.
233
   */
234
  private void onFlush(Map<String, Long> keyCounts) {
235
    if (keyCounts.isEmpty()) {
3!
UNCOV
236
      return;
×
237
    }
238

239
    try {
240
      ringManager.reconcileFromHealthView(healthView);
5✔
241

242
      Set<String> aliveNodes = healthView.getAliveWorkerIds();
4✔
243
      if (aliveNodes.isEmpty()) {
3✔
244
        log.warn("No alive Worker nodes for routing; dropping {} keys in this flush", keyCounts.size());
6✔
245
        return;
1✔
246
      }
247

248
      if (bbrRateLimiter != null) {
3✔
249
        // sync minInFlight floor to current Worker count; only updates when count changes
250
        int currentCount = ringManager.nodeCount();
4✔
251
        if (currentCount != lastNodeCount) {
4!
252
          lastNodeCount = currentCount;
3✔
253
          bbrRateLimiter.setMinInFlight(currentCount);
4✔
254
        }
255
        if (!bbrRateLimiter.tryAcquire()) {
4!
UNCOV
256
          bbrRateLimiter.onGateDrop();
×
UNCOV
257
          return;
×
258
        }
259
      }
260

261
      long now = currentTimeMillis();
2✔
262
      Map<String, Map<String, Long>> sharded = new HashMap<>();
4✔
263

264
      keyCounts.forEach((key, val) -> {
6✔
265
        if (val > 0) {
5!
266
          String target = ringManager.routeNode(key, aliveNodes);
6✔
267
          if (target != null) {
2!
268
            sharded.computeIfAbsent(target, t -> new HashMap<>()).put(key, val);
13✔
269
          }
270
        }
271
      });
1✔
272

273
      sharded.forEach((target, counts) -> {
5✔
274
        if (!dispatcher.enqueue(new ShardBatch(target, now, counts))) {
10!
UNCOV
275
          long dropped = dispatcher.dropped();
×
UNCOV
276
          if (dropped % 100 == 0 || dropped == 1) {
×
UNCOV
277
            log.warn(
×
278
              "report queue full, dropped target={} keys={}, depth={}/{}, cumulativeDrops={}",
279
              target,
280
              counts.size(),
×
281
              dispatcher.depth(),
×
UNCOV
282
              queueCapacity,
×
UNCOV
283
              dropped
×
284
            );
285
          }
286
        } else if (bbrRateLimiter != null) {
3✔
287
          bbrRateLimiter.onEnqueue();
3✔
288
        }
289
      });
1✔
UNCOV
290
    } catch (Exception e) {
×
UNCOV
291
      log.error("Flush callback failed", e);
×
292
    }
1✔
293
  }
1✔
294

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

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

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

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

337
  /**
338
   * Return the approximate number of unique keys currently buffered in the
339
   * local double-buffer counter store.
340
   *
341
   * <p>This is the sum of distinct keys in both the active and standby
342
   * buffers and may not reflect the exact count under concurrent access.
343
   *
344
   * @return estimated number of unique keys with pending access counts
345
   */
346
  public long getPendingKeyCount() {
347
    return bufferedCounter.estimatedSizeOfKeysCount();
4✔
348
  }
349

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

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

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

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

397
  /**
398
   * Manages a bounded work queue and a fixed pool of consumer threads that
399
   * drain batches and publish them via {@link ReportPublisher}.
400
   *
401
   * <p>Burst absorption: the bound on {@code LinkedBlockingQueue} prevents
402
   * the flush callback from overwhelming RabbitMQ.  When the queue is
403
   * persistently full, batches are dropped after a configurable timeout.
404
   */
405
  class ReportDispatcher {
5✔
406

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

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

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

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

498
    /**
499
     * Return the current number of batches waiting in the work queue.
500
     *
501
     * @return current queue depth (non-negative)
502
     */
503
    int depth() {
504
      return queue.size();
4✔
505
    }
506

507
    /**
508
     * Return the maximum number of batches the work queue can hold.
509
     *
510
     * @return the configured queue capacity
511
     */
512
    int capacity() {
513
      return queueCapacity;
4✔
514
    }
515

516
    /**
517
     * Return the number of actively running consumer threads.
518
     *
519
     * @return current consumer thread count
520
     */
521
    int consumerCount() {
522
      return consumers.size();
4✔
523
    }
524

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

535
    /**
536
     * Return the total number of batches that were rejected because the
537
     * queue was full since startup.
538
     *
539
     * @return total dropped batch count
540
     */
541
    long dropped() {
542
      return droppedCount.get();
4✔
543
    }
544

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

572
        // 5s stale check — discard data that waited too long in the queue
573
        if (currentTimeMillis() - batch.timestamp() > 5_000) {
7!
UNCOV
574
          expiredCount.incrementAndGet();
×
UNCOV
575
          if (bbrRateLimiter != null) {
×
UNCOV
576
            bbrRateLimiter.onConsumerDrop();
×
577
          }
578
          continue;
579
        }
580

581
        try {
582
          reportPublisher.publish(batch.target(), new ReportMessage(appName, batch.timestamp(), batch.counts()));
16✔
583
          if (bbrRateLimiter != null) {
4✔
584
            bbrRateLimiter.onSuccess(currentTimeMillis() - batch.timestamp());
8✔
585
          }
UNCOV
586
        } catch (Exception e) {
×
UNCOV
587
          log.error("Failed to publish report batch for target={}, keys={}", batch.target(), batch.counts().size(), e);
×
UNCOV
588
          if (bbrRateLimiter != null) {
×
UNCOV
589
            bbrRateLimiter.onConsumerDrop();
×
590
          }
591
        }
1✔
592
      }
1✔
593
    }
1✔
594
  }
595
}
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