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ülcü 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}