package org.threadly.concurrent.wrapper.limiter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import org.threadly.concurrent.AbstractSubmitterExecutor;
import org.threadly.concurrent.RunnableCallableAdapter;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.concurrent.SubmitterExecutor;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.concurrent.future.ListenableRunnableFuture;
import org.threadly.concurrent.lock.StripedLock;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.StringUtils;
/**
* Abstract implementation for keyed limiters. Unlike the other limiters we can not extend off of
* each one, and just add extra functionality. Instead they must extend these abstract classes.
* The reason for that being that these types of limiters use the other limiters, rather than
* extend functionality off each other.
*
* @since 4.6.0 (since 4.3.0 at org.threadly.concurrent.limiter)
* @param <T> Type of limiter stored internally
*/
abstract class AbstractKeyedLimiter<T extends ExecutorLimiter> {
protected static final short DEFAULT_LOCK_PARALISM = 32;
protected static final float CONCURRENT_HASH_MAP_LOAD_FACTOR = 0.75f; // 0.75 is ConcurrentHashMap default
protected static final short CONCURRENT_HASH_MAP_MIN_SIZE = 8;
protected static final short CONCURRENT_HASH_MAP_MAX_INITIAL_SIZE = 64;
protected static final short CONCURRENT_HASH_MAP_MIN_CONCURRENCY_LEVEL = 4;
protected static final short CONCURRENT_HASH_MAP_MAX_CONCURRENCY_LEVEL = 32;
protected final Executor executor;
protected final int maxConcurrency;
protected final String subPoolName;
protected final boolean addKeyToThreadName;
protected final StripedLock sLock;
protected final ConcurrentHashMap<Object, LimiterContainer> currentLimiters;
protected AbstractKeyedLimiter(Executor executor, int maxConcurrency,
String subPoolName, boolean addKeyToThreadName,
int expectedTaskAdditionParallism) {
ArgumentVerifier.assertGreaterThanZero(maxConcurrency, "maxConcurrency");
ArgumentVerifier.assertNotNull(executor, "executor");
this.executor = executor;
this.maxConcurrency = maxConcurrency;
// make sure this is non-null so that it 'null' wont appear
this.subPoolName = StringUtils.nullToEmpty(subPoolName);
this.addKeyToThreadName = addKeyToThreadName;
this.sLock = new StripedLock(expectedTaskAdditionParallism);
int mapInitialSize = Math.min(sLock.getExpectedConcurrencyLevel(),
CONCURRENT_HASH_MAP_MAX_INITIAL_SIZE);
if (mapInitialSize < CONCURRENT_HASH_MAP_MIN_SIZE) {
mapInitialSize = CONCURRENT_HASH_MAP_MIN_SIZE;
}
int mapConcurrencyLevel = Math.max(CONCURRENT_HASH_MAP_MIN_CONCURRENCY_LEVEL,
Math.min(sLock.getExpectedConcurrencyLevel() / 2,
CONCURRENT_HASH_MAP_MAX_CONCURRENCY_LEVEL));
if (mapConcurrencyLevel < 1) {
mapConcurrencyLevel = 1;
}
this.currentLimiters = new ConcurrentHashMap<>(mapInitialSize,
CONCURRENT_HASH_MAP_LOAD_FACTOR, mapConcurrencyLevel);
}
/**
* Check how many threads may run in parallel for a single unique key.
*
* @return maximum concurrent tasks to be run
*/
public int getMaxConcurrencyPerKey() {
return maxConcurrency;
}
/**
* Check how many keys are currently being restricted or monitored.
*
* @return The number of task keys being monitored
*/
public int getTrackedKeyCount() {
return currentLimiters.size();
}
/**
* Check how many tasks are currently being limited, and not submitted yet for a given key.
* This can be useful for knowing how backed up a specific key is.
*
* @param taskKey Key which would be limited
* @return Quantity of tasks being held back inside the limiter, and thus still queued
*/
public int getUnsubmittedTaskCount(Object taskKey) {
ArgumentVerifier.assertNotNull(taskKey, "taskKey");
LimiterContainer lc = currentLimiters.get(taskKey);
return lc == null ? 0 : lc.limiter.getUnsubmittedTaskCount();
}
/**
* Get a map of all the keys and how many tasks are held back (queued) in each limiter per key.
* This map is generated without locking. Due to that, this may be inaccurate as task queue
* sizes changed while iterating all key's limiters.
* <p>
* Because this requires an iteration of all limiters, if only a single limiters unsubmitted
* count is needed, use {@link #getUnsubmittedTaskCount(Object)} as a cheaper alternative.
*
* @return Map of task key's to their respective task queue size
*/
public Map<Object, Integer> getUnsubmittedTaskCountMap() {
Map<Object, Integer> result = new HashMap<>();
for (Map.Entry<Object, LimiterContainer> e : currentLimiters.entrySet()) {
int taskCount = e.getValue().limiter.getUnsubmittedTaskCount();
if (taskCount > 0) {
result.put(e.getKey(), taskCount);
}
}
return result;
}
/**
* Provide a task to be run with a given thread key.
* <p>
* See also: {@link SubmitterExecutor#execute(Runnable)}
*
* @param taskKey object key where {@code equals()} will be used to determine execution thread
* @param task Task to be executed
*/
public void execute(Object taskKey, Runnable task) {
ArgumentVerifier.assertNotNull(taskKey, "taskKey");
ArgumentVerifier.assertNotNull(task, "task");
getLimiterContainer(taskKey).execute(task);
}
/**
* Submit a task to be run with a given thread key.
* <p>
* See also: {@link SubmitterExecutor#submit(Runnable)}
*
* @param taskKey object key where {@code equals()} will be used to determine execution thread
* @param task Task to be executed
* @return Future to represent when the execution has occurred
*/
public ListenableFuture<?> submit(Object taskKey, Runnable task) {
return submit(taskKey, task, null);
}
/**
* Submit a task to be run with a given thread key.
* <p>
* See also: {@link SubmitterExecutor#submit(Runnable, Object)}
*
* @param <TT> type of result returned from the future
* @param taskKey object key where {@code equals()} will be used to determine execution thread
* @param task Runnable to be executed
* @param result Result to be returned from future when task completes
* @return Future to represent when the execution has occurred and provide the given result
*/
public <TT> ListenableFuture<TT> submit(Object taskKey, Runnable task, TT result) {
return submit(taskKey, new RunnableCallableAdapter<>(task, result));
}
/**
* Submit a callable to be run with a given thread key.
* <p>
* See also: {@link SubmitterExecutor#submit(Callable)}
*
* @param <TT> type of result returned from the future
* @param taskKey object key where {@code equals()} will be used to determine execution thread
* @param task Callable to be executed
* @return Future to represent when the execution has occurred and provide the result from the callable
*/
public <TT> ListenableFuture<TT> submit(Object taskKey, Callable<TT> task) {
ArgumentVerifier.assertNotNull(taskKey, "taskKey");
ArgumentVerifier.assertNotNull(task, "task");
ListenableRunnableFuture<TT> rf = new ListenableFutureTask<>(false, task);
getLimiterContainer(taskKey).execute(rf);
return rf;
}
/**
* Get the current limiter in a thread safe way. If the limiter does not exist it will be
* created in a thread safe way. In addition the limiters handling task count will be
* incremented in expectation for execution. If not accessing for execution
* {@link #currentLimiters} should just be accessed directly.
*
* @param taskKey Key used to identify execution limiter
* @return Container with limiter and associated state data
*/
protected LimiterContainer getLimiterContainer(Object taskKey) {
LimiterContainer lc;
Object lock = sLock.getLock(taskKey);
synchronized (lock) {
lc = currentLimiters.get(taskKey);
if (lc == null) {
lc = new LimiterContainer(taskKey, makeLimiter(subPoolName +
(addKeyToThreadName ? taskKey.toString() : "")));
currentLimiters.put(taskKey, lc);
}
// must increment while in lock to prevent early removal
lc.handlingTasks.incrementAndGet();
}
return lc;
}
/**
* Constructs a new limiter that is specific for the given type.
*
* @param limiterThreadName Name for threads inside subpool
* @return A newly constructed limiter
*/
protected abstract T makeLimiter(String limiterThreadName);
/**
* Returns an executor implementation where all tasks submitted on this executor will run on the
* provided key. Tasks executed on the returned scheduler will be limited by the key
* submitted on this instance equally with ones provided through the returned instance.
*
* @param taskKey object key where {@code equals()} will be used to determine execution thread
* @return Executor which will only execute with reference to the provided key
*/
public SubmitterExecutor getSubmitterExecutorForKey(Object taskKey) {
ArgumentVerifier.assertNotNull(taskKey, "taskKey");
return new KeyedSubmitterExecutor(taskKey);
}
/**
* Submitter executor which delegates to this instance with a constructed permits and task key.
*
* @since 4.9.0
*/
protected class KeyedSubmitterExecutor extends AbstractSubmitterExecutor {
protected final Object taskKey;
protected KeyedSubmitterExecutor(Object taskKey) {
this.taskKey = taskKey;
}
@Override
protected void doExecute(Runnable task) {
getLimiterContainer(taskKey).execute(task);
}
}
/**
* Small class to hold the limiter and state associated with the limiter.
*
* @since 4.3.0
*/
protected class LimiterContainer {
public final Object taskKey;
public final T limiter;
public final AtomicInteger handlingTasks;
public LimiterContainer(Object taskKey, T limiter) {
this.taskKey = taskKey;
this.limiter = limiter;
this.handlingTasks = new AtomicInteger(0);
}
public Runnable wrap(Runnable task) {
return new LimiterCleaner(task);
}
public void execute(Runnable task) {
limiter.doExecute(wrap(task));
}
/**
* Small class to handle tracking as tasks finish. Once the last task of a limiter finishes
* the limiter is removed for GC. This wraps the runnable to handle that cleanup if needed.
*
* @since 4.3.0
*/
private class LimiterCleaner implements Runnable, RunnableContainer {
private final Runnable wrappedTask;
protected LimiterCleaner(Runnable wrappedTask) {
this.wrappedTask = wrappedTask;
}
@Override
public void run() {
try {
wrappedTask.run();
} finally {
if (handlingTasks.decrementAndGet() == 0) {
synchronized (sLock.getLock(taskKey)) {
// must verify removal in lock so that map gets are atomic with removals
if (handlingTasks.get() == 0) {
currentLimiters.remove(taskKey);
}
}
}
}
}
@Override
public Runnable getContainedRunnable() {
return wrappedTask;
}
}
}
}