BatchedFixedIntervalInvocationGate.java
/*
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2026, QOS.ch. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v2.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
*/
package ch.qos.logback.core.util;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* An {@link InvocationGate} that allows a fixed number of successive callers to
* proceed, then enforces a lull of a fixed {@link Duration}.
* <p>
* Callers on a hot path use {@link #isTooSoon(long)} to decide whether to skip a
* costly operation. Up to {@linkplain #batchSize N} invocations may receive
* {@code false} (proceed) in succession. After the N-th success, the gate enters
* a lull of {@linkplain #increment increment} milliseconds during which all
* callers receive {@code true} (skip). When the lull ends, another batch of N
* is available.
* </p>
* <p>
* This extends the idea of {@link FixedIntervalInvocationGate} (at most one
* success per interval) to batches of size N. Token accounting and the lull
* deadline use atomics so concurrent callers share one batch.
* </p>
*
* @author Ceki Gülcü
* @since 1.6.3
* @see InvocationGate
* @see FixedIntervalInvocationGate
*/
public class BatchedFixedIntervalInvocationGate implements InvocationGate {
/**
* Default batch size: number of successive allowed passages before a lull.
*/
public static final int DEFAULT_BATCH_SIZE = 4;
/**
* Default lull duration after a batch is exhausted (same as
* {@link FixedIntervalInvocationGate#DEFAULT_INCREMENT}).
*/
public static final Duration DEFAULT_INCREMENT = FixedIntervalInvocationGate.DEFAULT_INCREMENT;
/**
* Number of successive allowed invocations per open batch.
*/
final int batchSize;
/**
* Duration of the lull after a batch is exhausted.
*/
final Duration increment;
/**
* Remaining tokens in the current batch. Re-armed to {@link #batchSize}
* when the last token of a batch is taken (the lull deadline is set first).
*/
final AtomicInteger remaining;
/**
* Earliest time (milliseconds since the epoch) at which a new batch may be
* used. While {@code currentTime} is strictly less than this value, the
* gate is in lull and all callers are too soon.
*/
final AtomicLong atomicNext = new AtomicLong(0);
/**
* Creates a gate with {@link #DEFAULT_BATCH_SIZE} and
* {@link #DEFAULT_INCREMENT}.
*/
public BatchedFixedIntervalInvocationGate() {
this(DEFAULT_BATCH_SIZE, DEFAULT_INCREMENT);
}
/**
* Creates a gate with the given batch size and {@link #DEFAULT_INCREMENT}.
*
* @param batchSize number of successive allowed invocations per batch; must
* be at least 1
*/
public BatchedFixedIntervalInvocationGate(int batchSize) {
this(batchSize, DEFAULT_INCREMENT);
}
/**
* Creates a gate that allows {@code batchSize} successive passages, then
* lulls for {@code anIncrement}.
*
* @param batchSize number of successive allowed invocations per batch;
* must be at least 1
* @param anIncrement lull duration after a batch is exhausted; must not be
* {@code null}
*/
public BatchedFixedIntervalInvocationGate(int batchSize, Duration anIncrement) {
if (batchSize < 1) {
throw new IllegalArgumentException("batchSize must be at least 1, was " + batchSize);
}
if (anIncrement == null) {
throw new IllegalArgumentException("increment must not be null");
}
this.batchSize = batchSize;
this.increment = anIncrement;
this.remaining = new AtomicInteger(batchSize);
}
/**
* Returns {@code true} if the caller should skip further work; {@code false}
* if this call is allowed to proceed.
* <p>
* If {@code currentTime} is {@link InvocationGate#TIME_UNAVAILABLE}
* ({@code -1}), returns {@code false} so work can still run when the clock
* is unavailable.
* </p>
* <p>
* During a lull ({@code currentTime} before the next open time), returns
* {@code true}. Otherwise tries to consume one batch token. The first
* {@code batchSize} successful consumptions return {@code false}. The
* caller that takes the last token starts a lull of {@link #increment} and
* re-arms the batch for after the lull.
* </p>
*
* @param currentTime current time in milliseconds, or
* {@link InvocationGate#TIME_UNAVAILABLE} if unknown
* @return {@code true} if further work should be skipped; {@code false} if
* the caller may proceed
*/
@Override
public boolean isTooSoon(long currentTime) {
if (currentTime == -1) {
return false;
}
if (currentTime < atomicNext.get()) {
return true;
}
while (true) {
int left = remaining.get();
if (left <= 0) {
// Last token already taken; lull is being established or in force.
return true;
}
if (remaining.compareAndSet(left, left - 1)) {
if (left == 1) {
// Order matters: set lull deadline before re-arming tokens so
// concurrent callers see the lull and do not drain the new batch.
atomicNext.set(currentTime + increment.getMilliseconds());
remaining.set(batchSize);
}
return false;
}
}
}
}