001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util.concurrent;
018
019import java.util.List;
020import java.util.concurrent.AbstractExecutorService;
021import java.util.concurrent.Callable;
022import java.util.concurrent.ExecutorService;
023import java.util.concurrent.RejectedExecutionException;
024import java.util.concurrent.RunnableFuture;
025import java.util.concurrent.Semaphore;
026import java.util.concurrent.TimeUnit;
027import java.util.concurrent.atomic.LongAdder;
028
029/**
030 * An {@link ExecutorService} wrapper that enforces bounded concurrency via a {@link Semaphore}.
031 * <p>
032 * When virtual threads are enabled, Camel replaces the traditional {@link java.util.concurrent.ThreadPoolExecutor} with
033 * {@code Executors.newThreadPerTaskExecutor()}, which accepts every task immediately (unbounded). This wrapper limits
034 * the maximum number of tasks delegated to the underlying executor. Unlike {@code ThreadPoolExecutor} there is no
035 * distinction between pool threads and queued tasks — the semaphore enforces a flat concurrency cap on delegated tasks.
036 * <p>
037 * When the semaphore has no available permits, behavior depends on the configured {@link ThreadPoolRejectedPolicy}:
038 * <ul>
039 * <li><b>CallerRuns</b> (default): blocks until a permit is available or the timeout expires; on timeout, runs the task
040 * on the caller's thread. Tasks are never lost. Note that caller-run tasks execute outside semaphore accounting, so
041 * total system concurrency may temporarily exceed {@code maxConcurrent}.</li>
042 * <li><b>Abort</b>: blocks until a permit is available or the timeout expires; on timeout, throws
043 * {@link RejectedExecutionException}.</li>
044 * <li><b>Block</b>: blocks indefinitely until a permit becomes available. No timeout, no rejection.</li>
045 * </ul>
046 * <p>
047 * <b>Caller thread blocking:</b> while waiting for a permit, the calling thread is blocked. When callers are virtual
048 * threads this is inexpensive (the carrier thread is released). When callers are platform threads (e.g., HTTP server
049 * threads) the blocked thread is unavailable for other work — this is standard backpressure behavior but worth noting
050 * for capacity planning.
051 *
052 */
053public class BoundedExecutorService extends AbstractExecutorService {
054
055    private final ExecutorService delegate;
056    private final Semaphore semaphore;
057    private final int maxConcurrent;
058    private final long timeoutNanos;
059    private final ThreadPoolRejectedPolicy rejectedPolicy;
060    private final LongAdder callerRunsCount = new LongAdder();
061    private final LongAdder rejectedCount = new LongAdder();
062    private final LongAdder delegatedTaskCount = new LongAdder();
063
064    /**
065     * @param delegate       the underlying executor (typically {@code newThreadPerTaskExecutor})
066     * @param maxConcurrent  the maximum number of tasks delegated to the underlying executor concurrently
067     * @param acquireTimeout the maximum time to wait for a permit (ignored when policy is {@code Block})
068     * @param timeUnit       the time unit for {@code acquireTimeout}
069     * @param fair           {@code true} for FIFO permit ordering (predictable latency), {@code false} for barging
070     *                       (higher throughput)
071     * @param rejectedPolicy the policy to apply when no permit is available
072     */
073    public BoundedExecutorService(ExecutorService delegate, int maxConcurrent,
074                                  long acquireTimeout, TimeUnit timeUnit,
075                                  boolean fair, ThreadPoolRejectedPolicy rejectedPolicy) {
076        this.delegate = delegate;
077        this.maxConcurrent = maxConcurrent;
078        this.semaphore = new Semaphore(maxConcurrent, fair);
079        this.timeoutNanos = timeUnit.toNanos(acquireTimeout);
080        this.rejectedPolicy = rejectedPolicy;
081    }
082
083    @Override
084    public void execute(Runnable command) {
085        if (delegate.isShutdown()) {
086            throw new RejectedExecutionException("Executor has been shut down");
087        }
088
089        boolean acquired = false;
090        try {
091            if (rejectedPolicy == ThreadPoolRejectedPolicy.Block) {
092                semaphore.acquire();
093                acquired = true;
094            } else {
095                acquired = semaphore.tryAcquire(timeoutNanos, TimeUnit.NANOSECONDS);
096            }
097
098            if (!acquired) {
099                if (rejectedPolicy == ThreadPoolRejectedPolicy.CallerRuns) {
100                    callerRunsCount.increment();
101                    command.run();
102                    return;
103                }
104                rejectedCount.increment();
105                throw new RejectedExecutionException("Executor saturated: timed out waiting for a permit");
106            }
107
108            boolean submitted = false;
109            try {
110                delegate.execute(() -> {
111                    try {
112                        command.run();
113                    } finally {
114                        delegatedTaskCount.increment();
115                        semaphore.release();
116                    }
117                });
118                submitted = true;
119            } finally {
120                if (!submitted) {
121                    semaphore.release();
122                }
123            }
124        } catch (InterruptedException e) {
125            Thread.currentThread().interrupt();
126            throw new RejectedExecutionException("Interrupted while waiting for permit", e);
127        }
128    }
129
130    // -- Metrics --
131
132    /**
133     * The maximum number of tasks that can be delegated to the underlying executor concurrently. CallerRuns tasks
134     * execute outside this limit.
135     */
136    public int getMaxConcurrent() {
137        return maxConcurrent;
138    }
139
140    /**
141     * The number of permits currently available.
142     */
143    public int getAvailablePermits() {
144        return semaphore.availablePermits();
145    }
146
147    /**
148     * The number of tasks currently delegated to the underlying executor.
149     */
150    public int getActiveCount() {
151        return maxConcurrent - semaphore.availablePermits();
152    }
153
154    /**
155     * The number of threads currently blocked waiting for a permit.
156     */
157    public int getWaitingCount() {
158        return semaphore.getQueueLength();
159    }
160
161    /**
162     * The number of times the timeout expired and a task fell back to running on the caller's thread.
163     */
164    public long getCallerRunsCount() {
165        return callerRunsCount.sum();
166    }
167
168    /**
169     * The number of tasks rejected because no permit was available within the timeout.
170     */
171    public long getRejectedCount() {
172        return rejectedCount.sum();
173    }
174
175    /**
176     * The total number of tasks that completed via the underlying executor (excludes caller-runs).
177     */
178    public long getDelegatedTaskCount() {
179        return delegatedTaskCount.sum();
180    }
181
182    @Override
183    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
184        if (runnable instanceof Rejectable) {
185            return new RejectableFutureTask<>(runnable, value);
186        }
187        return super.newTaskFor(runnable, value);
188    }
189
190    @Override
191    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
192        if (callable instanceof Rejectable) {
193            return new RejectableFutureTask<>(callable);
194        }
195        return super.newTaskFor(callable);
196    }
197
198    @Override
199    public void shutdown() {
200        delegate.shutdown();
201    }
202
203    @Override
204    public List<Runnable> shutdownNow() {
205        return delegate.shutdownNow();
206    }
207
208    @Override
209    public boolean isShutdown() {
210        return delegate.isShutdown();
211    }
212
213    @Override
214    public boolean isTerminated() {
215        return delegate.isTerminated();
216    }
217
218    @Override
219    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
220        return delegate.awaitTermination(timeout, unit);
221    }
222
223    @Override
224    public String toString() {
225        return "BoundedExecutorService[active=" + getActiveCount()
226               + ", max=" + maxConcurrent
227               + ", waiting=" + getWaitingCount()
228               + ", callerRuns=" + callerRunsCount.sum()
229               + ", rejected=" + rejectedCount.sum()
230               + ", delegated=" + delegatedTaskCount.sum() + "]";
231    }
232}