001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.util;
015
016import java.util.concurrent.atomic.AtomicInteger;
017import java.util.concurrent.atomic.AtomicLong;
018
019/**
020 * An {@link InvocationGate} that allows a fixed number of successive callers to
021 * proceed, then enforces a lull of a fixed {@link Duration}.
022 * <p>
023 * Callers on a hot path use {@link #isTooSoon(long)} to decide whether to skip a
024 * costly operation. Up to {@linkplain #batchSize N} invocations may receive
025 * {@code false} (proceed) in succession. After the N-th success, the gate enters
026 * a lull of {@linkplain #increment increment} milliseconds during which all
027 * callers receive {@code true} (skip). When the lull ends, another batch of N
028 * is available.
029 * </p>
030 * <p>
031 * This extends the idea of {@link FixedIntervalInvocationGate} (at most one
032 * success per interval) to batches of size N. Token accounting and the lull
033 * deadline use atomics so concurrent callers share one batch.
034 * </p>
035 *
036 * @author Ceki G&uuml;lc&uuml;
037 * @since 1.6.3
038 * @see InvocationGate
039 * @see FixedIntervalInvocationGate
040 */
041public class BatchedFixedIntervalInvocationGate implements InvocationGate {
042
043    /**
044     * Default batch size: number of successive allowed passages before a lull.
045     */
046    public static final int DEFAULT_BATCH_SIZE = 4;
047
048    /**
049     * Default lull duration after a batch is exhausted (same as
050     * {@link FixedIntervalInvocationGate#DEFAULT_INCREMENT}).
051     */
052    public static final Duration DEFAULT_INCREMENT = FixedIntervalInvocationGate.DEFAULT_INCREMENT;
053
054    /**
055     * Number of successive allowed invocations per open batch.
056     */
057    final int batchSize;
058
059    /**
060     * Duration of the lull after a batch is exhausted.
061     */
062    final Duration increment;
063
064    /**
065     * Remaining tokens in the current batch. Re-armed to {@link #batchSize}
066     * when the last token of a batch is taken (the lull deadline is set first).
067     */
068    final AtomicInteger remaining;
069
070    /**
071     * Earliest time (milliseconds since the epoch) at which a new batch may be
072     * used. While {@code currentTime} is strictly less than this value, the
073     * gate is in lull and all callers are too soon.
074     */
075    final AtomicLong atomicNext = new AtomicLong(0);
076
077    /**
078     * Creates a gate with {@link #DEFAULT_BATCH_SIZE} and
079     * {@link #DEFAULT_INCREMENT}.
080     */
081    public BatchedFixedIntervalInvocationGate() {
082        this(DEFAULT_BATCH_SIZE, DEFAULT_INCREMENT);
083    }
084
085    /**
086     * Creates a gate with the given batch size and {@link #DEFAULT_INCREMENT}.
087     *
088     * @param batchSize number of successive allowed invocations per batch; must
089     *                  be at least 1
090     */
091    public BatchedFixedIntervalInvocationGate(int batchSize) {
092        this(batchSize, DEFAULT_INCREMENT);
093    }
094
095    /**
096     * Creates a gate that allows {@code batchSize} successive passages, then
097     * lulls for {@code anIncrement}.
098     *
099     * @param batchSize   number of successive allowed invocations per batch;
100     *                    must be at least 1
101     * @param anIncrement lull duration after a batch is exhausted; must not be
102     *                    {@code null}
103     */
104    public BatchedFixedIntervalInvocationGate(int batchSize, Duration anIncrement) {
105        if (batchSize < 1) {
106            throw new IllegalArgumentException("batchSize must be at least 1, was " + batchSize);
107        }
108        if (anIncrement == null) {
109            throw new IllegalArgumentException("increment must not be null");
110        }
111        this.batchSize = batchSize;
112        this.increment = anIncrement;
113        this.remaining = new AtomicInteger(batchSize);
114    }
115
116    /**
117     * Returns {@code true} if the caller should skip further work; {@code false}
118     * if this call is allowed to proceed.
119     * <p>
120     * If {@code currentTime} is {@link InvocationGate#TIME_UNAVAILABLE}
121     * ({@code -1}), returns {@code false} so work can still run when the clock
122     * is unavailable.
123     * </p>
124     * <p>
125     * During a lull ({@code currentTime} before the next open time), returns
126     * {@code true}. Otherwise tries to consume one batch token. The first
127     * {@code batchSize} successful consumptions return {@code false}. The
128     * caller that takes the last token starts a lull of {@link #increment} and
129     * re-arms the batch for after the lull.
130     * </p>
131     *
132     * @param currentTime current time in milliseconds, or
133     *                    {@link InvocationGate#TIME_UNAVAILABLE} if unknown
134     * @return {@code true} if further work should be skipped; {@code false} if
135     *         the caller may proceed
136     */
137    @Override
138    public boolean isTooSoon(long currentTime) {
139        if (currentTime == -1) {
140            return false;
141        }
142
143        if (currentTime < atomicNext.get()) {
144            return true;
145        }
146
147        while (true) {
148            int left = remaining.get();
149            if (left <= 0) {
150                // Last token already taken; lull is being established or in force.
151                return true;
152            }
153            if (remaining.compareAndSet(left, left - 1)) {
154                if (left == 1) {
155                    // Order matters: set lull deadline before re-arming tokens so
156                    // concurrent callers see the lull and do not drain the new batch.
157                    atomicNext.set(currentTime + increment.getMilliseconds());
158                    remaining.set(batchSize);
159                }
160                return false;
161            }
162        }
163    }
164}