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}