org.eclipse.jetty.util.thread.ReservedThreadExecutor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-util Show documentation
Show all versions of jetty-util Show documentation
Utility classes for Jetty
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util.thread;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.VirtualThreads;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A TryExecutor using pre-allocated/reserved threads from an external Executor.
* Calls to {@link #tryExecute(Runnable)} on ReservedThreadExecutor will either
* succeed with a reserved thread immediately being assigned the task, or fail if
* no reserved thread is available.
* Threads are reserved lazily, with new reserved threads being allocated from the external
* {@link Executor} passed to the constructor. Whenever 1 or more reserved threads have been
* idle for more than {@link #getIdleTimeoutMs()} then one reserved thread will return to
* the external Executor.
*/
@ManagedObject("A pool for reserved threads")
public class ReservedThreadExecutor extends ContainerLifeCycle implements TryExecutor, Dumpable
{
private static final Logger LOG = LoggerFactory.getLogger(ReservedThreadExecutor.class);
private final Executor _executor;
private final ThreadIdPool _threads;
private final AtomicInteger _pending = new AtomicInteger();
private final int _minSize;
private final int _maxPending;
private ThreadPoolBudget.Lease _lease;
private long _idleTimeoutMs;
/**
* @param executor The executor to use to obtain threads
* @param capacity The number of threads that can be reserved. If less than 0 then capacity
* is calculated based on a heuristic from the number of available processors and
* thread pool type.
*/
public ReservedThreadExecutor(Executor executor, int capacity)
{
this(executor, capacity, -1);
}
/**
* @param executor The executor to use to obtain threads
* @param capacity The number of threads that can be reserved. If less than 0 then capacity
* is calculated based on a heuristic from the number of available processors and
* thread pool type.
* @param minSize The minimum number of reserve Threads that the algorithm tries to maintain, or -1 for a heuristic value.
*/
public ReservedThreadExecutor(Executor executor, int capacity, int minSize)
{
this(executor, capacity, minSize, -1);
}
/**
* @param executor The executor to use to obtain threads
* @param capacity The number of threads that can be reserved. If less than 0 then capacity
* is calculated based on a heuristic from the number of available processors and
* thread pool type.
* @param minSize The minimum number of reserve Threads that the algorithm tries to maintain, or -1 for a heuristic value.
* @param maxPending The maximum number of reserved Threads to start, or -1 for no limit.
*/
public ReservedThreadExecutor(Executor executor, int capacity, int minSize, int maxPending)
{
_executor = executor;
_threads = new ThreadIdPool<>(reservedThreads(executor, capacity));
_minSize = minSize < 0 ? Math.min(1, _threads.capacity()) : minSize;
if (_minSize > _threads.capacity())
throw new IllegalArgumentException("minSize larger than capacity");
_maxPending = maxPending;
if (_maxPending == 0)
throw new IllegalArgumentException("maxPending cannot be 0");
if (LOG.isDebugEnabled())
LOG.debug("{}", this);
installBean(_executor);
installBean(_threads);
}
/**
* Get the heuristic number of reserved threads.
*
* @param executor The executor to use to obtain threads
* @param capacity The number of threads to preallocate, If less than 0 then capacity
* is calculated based on a heuristic from the number of available processors and
* thread pool size.
* @return the number of reserved threads that would be used by a ReservedThreadExecutor
* constructed with these arguments.
*/
public static int reservedThreads(Executor executor, int capacity)
{
if (capacity >= 0)
return capacity;
if (VirtualThreads.isUseVirtualThreads(executor))
return 0;
int cpus = ProcessorUtils.availableProcessors();
if (executor instanceof ThreadPool.SizedThreadPool)
{
int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
return Math.max(1, TypeUtil.ceilToNextPowerOfTwo(Math.min(cpus, threads / 8)));
}
return cpus;
}
public Executor getExecutor()
{
return _executor;
}
/**
* @return the maximum number of reserved threads
*/
@ManagedAttribute(value = "max number of reserved threads", readonly = true)
public int getCapacity()
{
return _threads.capacity();
}
/**
* @return the number of threads available to {@link #tryExecute(Runnable)}
*/
@ManagedAttribute(value = "available reserved threads", readonly = true)
public int getAvailable()
{
return _threads.size();
}
@ManagedAttribute(value = "pending reserved threads (deprecated)", readonly = true)
@Deprecated
public int getPending()
{
return 0;
}
@ManagedAttribute(value = "idle timeout in ms", readonly = true)
public long getIdleTimeoutMs()
{
return _idleTimeoutMs;
}
/**
* Set the idle timeout for shrinking the reserved thread pool
*
* @param idleTime Time to wait before shrinking, or 0 for default timeout.
* @param idleTimeUnit Time units for idle timeout
*/
public void setIdleTimeout(long idleTime, TimeUnit idleTimeUnit)
{
if (isRunning())
throw new IllegalStateException();
_idleTimeoutMs = idleTime <= 0 ? 0 : idleTimeUnit.toMillis(idleTime);
}
@Override
public void doStart() throws Exception
{
_lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, getCapacity());
super.doStart();
}
@Override
public void doStop() throws Exception
{
if (_lease != null)
_lease.close();
super.doStop();
_threads.removeAll().forEach(ReservedThread::stop);
}
@Override
public void execute(Runnable task) throws RejectedExecutionException
{
_executor.execute(task);
}
/**
* Executes the given task if and only if a reserved thread is available.
*
* @param task the task to run
* @return true if and only if a reserved thread was available and has been assigned the task to run.
*/
@Override
public boolean tryExecute(Runnable task)
{
if (task == null)
return false;
ReservedThread reserved = _threads.take();
if (reserved != null)
{
reserved.wakeup(task);
return true;
}
startReservedThread();
if (LOG.isDebugEnabled())
LOG.debug("{} tryExecute failed for {}", this, task);
return false;
}
private void startReservedThread()
{
if (_maxPending > 0 && _pending.incrementAndGet() >= _maxPending)
{
_pending.decrementAndGet();
return;
}
try
{
ReservedThread thread = new ReservedThread();
_executor.execute(thread);
}
catch (Throwable e)
{
if (LOG.isDebugEnabled())
LOG.debug("ignored", e);
}
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
Dumpable.dumpObjects(out, indent, this);
}
@Override
public String toString()
{
return String.format("%s@%x{capacity=%d,threads=%s}",
getClass().getSimpleName(),
hashCode(),
getCapacity(),
_threads);
}
private class ReservedThread implements Runnable
{
private final Semaphore _semaphore = new Semaphore(0);
private volatile Runnable _task;
private volatile Thread _thread;
@Override
public void run()
{
_thread = Thread.currentThread();
try
{
_pending.decrementAndGet();
while (true)
{
int slot = _threads.offer(this);
if (LOG.isDebugEnabled())
LOG.debug("offered to slot " + slot);
if (slot < 0)
// no slot available
return;
if (!isRunning() && _threads.remove(this, slot))
return;
Runnable task = waitForTask();
while (task == null)
{
if (!isRunning())
return;
// Shrink if we are already removed or there are other reserved threads;
// there is a small chance multiple threads will shrink below minSize.
if (getAvailable() > _minSize && _threads.remove(this, slot))
{
if (LOG.isDebugEnabled())
LOG.debug("{} reservedThread shrank {}", ReservedThreadExecutor.this, this);
return;
}
task = waitForTask();
}
try
{
if (LOG.isDebugEnabled())
LOG.debug("{} reservedThread run {} on {}", ReservedThreadExecutor.this, task, this);
task.run();
}
catch (Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("{} task {} failure", ReservedThreadExecutor.this, task, t);
}
finally
{
// clear interrupted status between reserved thread iterations
if (Thread.interrupted() && LOG.isDebugEnabled())
LOG.debug("{} was interrupted", _thread);
}
}
}
catch (Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("{} reservedThread {} failure", ReservedThreadExecutor.this, this, t);
}
finally
{
_thread = null;
}
}
private void wakeup(Runnable task)
{
_task = task;
_semaphore.release();
}
private Runnable waitForTask()
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("waiting for task");
if (_idleTimeoutMs <= 0)
_semaphore.acquire();
else if (!_semaphore.tryAcquire(_idleTimeoutMs, TimeUnit.MILLISECONDS))
return null;
Runnable task = _task;
_task = null;
return task;
}
catch (Throwable e)
{
if (LOG.isDebugEnabled())
LOG.debug("wait failed ", e);
}
return null;
}
private void stop()
{
// If we are stopping, the reserved thread may already have stopped. So just interrupt rather than
// expect an exchange rendezvous.
_semaphore.release();
Thread thread = _thread;
if (thread != null)
{
if (LOG.isDebugEnabled())
LOG.debug("interrupting thread {} for stop", thread);
thread.interrupt();
}
}
@Override
public String toString()
{
return String.format("%s@%x{thread=%s}",
getClass().getSimpleName(),
hashCode(),
_thread);
}
}
}