package bes.injector;/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import bes.concurrent.WaitQueue;
public class InjectionExecutor extends AbstractExecutorService
{
/**
* Called directly before the task is executed.
* This method should ensure no exceptions can be thrown during its execution.
*/
protected void beforeExecute(Runnable task) { }
/**
* Called after the task has been executed, successfully or not.
* This method should ensure no exceptions can be thrown during its execution.
*
* @param task the task that was executed
* @param failure null if success; otherwise the exception that caused the task to fail
*/
protected void afterExecute(Runnable task, Throwable failure) { }
// non-final so this class can be extended by users, whilst not exposing this internal detail
Injector injector;
private final int maxWorkers;
private final int maxTasksQueued;
// stores both a set of work permits and task permits:
// bottom 48 bits are number of queued tasks, in the range [0..maxTasksQueued] (initially 0)
// top 16 bits are number of work permits available in the range [0..maxWorkers] (initially maxWorkers)
private final AtomicLong permits = new AtomicLong();
final Queue<Runnable> tasks;
final Work asWork = new Work(this);
// producers wait on this when there is no room on the queue
private final WaitQueue hasRoom = new WaitQueue();
private final AtomicLong totalBlocked = new AtomicLong();
private final AtomicInteger currentlyBlocked = new AtomicInteger();
private final CountDownLatch terminated = new CountDownLatch(1);
private volatile boolean shutdown = false;
protected InjectionExecutor(int maxWorkers, int maxTasksQueued)
{
this(maxWorkers, maxTasksQueued, new ConcurrentLinkedQueue<Runnable>());
}
protected InjectionExecutor(int maxWorkers, int maxTasksQueued, Queue<Runnable> tasks)
{
if (maxWorkers >= 1 << 16)
throw new IllegalArgumentException("Unsupported thread count: max is 65535");
this.maxWorkers = maxWorkers;
this.maxTasksQueued = maxTasksQueued;
this.permits.set(maxWorkers * WORK_PERMIT);
this.tasks = tasks;
}
// schedules another worker for this injector if there is work outstanding and there are no spinning threads that
// will self-assign to it in the immediate future
boolean maybeSchedule(boolean internal)
{
if (injector.spinningCount.get() > 0 || !takeWorkPermit(true))
return false;
injector.schedule(asWork, internal);
return true;
}
/**
* Execute the provided Runnable as soon as a worker becomes available. The task may be executed
* on any of the Injector's shared worker threads, however the caller will not typically schedule
* the thread, only doing so if there is currently no active worker capable of promptly serving it
* in the injector, or this executor's task queue is full.
*
* @param task the task to execute
*/
public void execute(Runnable task)
{
if (task == null)
throw new NullPointerException();
// we check the shutdown status initially to ensure we never accept a task submitted after shutdown()
// or shutdownNow() have exited. otherwise, if we are not terminated, a worker could grab it from the queue
// before we repair after the fact
if (shutdown)
throw new RejectedExecutionException();
// we add to the queue first, so that when a worker takes a task permit it can be certain there is a task available
// this permits us to schedule threads non-spuriously; it also means work is serviced fairly
tasks.add(task);
/**
* we recheck the shutdown status after adding to the queue, to ensure we haven't raced.
* if we are shutdown, and we fail to remove ourselves, we were added out of order with another
* task that successfully incremented the task permits before the shutdown flag was set _and_
* we have already been dequeued by a worker; in this case we cannot reject the task, since it's
* being (or has been) processed, but we also must honour the prior task's successful submission,
* so we have to increment our permit count
*/
if (shutdown && tasks.remove(task))
throw new RejectedExecutionException();
long update;
while (true)
{
long current = this.permits.get();
update = current + 1;
// because there is no difference in practical terms between the work permit being added or not
// (the work is already in existence), we always add our permit, but block after the fact if we
// breached the queue limit
if (permits.compareAndSet(current, update))
break;
}
if (taskPermits(update) == 1)
{
// we only need to schedule a thread if there are no tasks already waiting to be processed, as the prior
// enqueue that moved the permit count from zero will have already started a spinning worker (if necessary),
// and spinning workers multiply if they encounter work, and only spin down if there is no work available
injector.maybeStartSpinningWorker(false);
}
// we consider any available work permits to count towards our queue limit, since the resource use is the same;
// the work can be considered allocated to each of the available permits, and effectively 'not queued'
else if (taskPermits(update) > maxTasksQueued + workPermits(update))
{
// register to receive a signal once a task is processed bringing the queue below its threshold
WaitQueue.Signal s = hasRoom.register();
// we will only be signalled once the queue drops below full, so this creates equivalent external behaviour
// however the advantage is that we never wake-up spuriously
long latest = permits.get();
if (taskPermits(latest) > maxTasksQueued + workPermits(latest))
{
// if we're blocking, we might as well directly schedule a worker if we aren't already at max
if (takeWorkPermit(true))
injector.schedule(asWork, false);
totalBlocked.incrementAndGet();
currentlyBlocked.incrementAndGet();
s.awaitUninterruptibly();
currentlyBlocked.decrementAndGet();
}
else // don't propagate our signal when we cancel, just cancel
s.cancel();
}
}
/**
* takes permission to perform a task, if any are available; once taken it is guaranteed
* that a proceeding call to tasks.poll() will return some work
*/
boolean takeTaskPermit()
{
while (true)
{
long current = permits.get();
long update = current - 1;
if (taskPermits(current) == 0)
return false;
if (permits.compareAndSet(current, update))
{
if (taskPermits(update) == maxTasksQueued && hasRoom.hasWaiters())
hasRoom.signalAll();
return true;
}
}
}
// takes a work permit and (optionally) a task permit simultaneously; if one of the two is unavailable, returns false
boolean takeWorkPermit(boolean takeTaskPermit)
{
long delta = WORK_PERMIT + (takeTaskPermit ? 1 : 0);
while (true)
{
long current = permits.get();
if ((workPermits(current) == 0) | (taskPermits(current) == 0))
return false;
long update = current - delta;
if (permits.compareAndSet(current, update))
{
if (takeTaskPermit && taskPermits(update) == maxTasksQueued && hasRoom.hasWaiters())
hasRoom.signalAll();
return true;
}
}
}
boolean hasTasks()
{
return taskPermits(permits.get()) > 0;
}
// gives up a work permit
void returnWorkPermit()
{
while (true)
{
long current = permits.get();
long update = current + WORK_PERMIT;
if (permits.compareAndSet(current, update))
return;
}
}
// only to be called on encountering an *unexpected* exception (i.e. bug, bad VM state, OOM, etc.)
void returnTaskPermit()
{
while (true)
{
long current = permits.get();
long update = current + 1;
if (permits.compareAndSet(current, update))
return;
}
}
/**
* The calling thread, if there is a work permit available, temporarily becomes a worker
* in this pool and executes the task inline. If there are no work permits available,
* this call behaves like a normal invocation of execute(), placing the task on the queue
* and letting one of the pool's workers handle it when available.
*
* @param task the task to execute
*/
public void maybeExecuteInline(Runnable task)
{
if (!takeWorkPermit(false))
{
execute(task);
}
else
{
try
{
executeInternal(task);
}
finally
{
returnWorkPermit();
maybeSchedule(false);
}
}
}
void executeInternal(Runnable task)
{
Throwable failure = null;
try
{
beforeExecute(task);
task.run();
}
catch (Throwable t)
{
failure = t;
}
afterExecute(task, failure);
}
/**
* Stops further tasks from being submitted to the executor.
* This is only guaranteed to stop submissions that were initiated after this call exits;
* any execute() call entered even fractionally before this has a chance of being submitted
* for processing after we exit.
*/
public synchronized void shutdown()
{
shutdown = true;
maybeTerminate();
}
/**
* Stops further tasks from being submitted to the executor.
* This is only guaranteed to stop submissions that were initiated after this call exits;
* any execute() call entered even fractionally before this has a chance of being submitted
* for processing after we exit.
*
* @return the tasks that were prevented from executing
*/
public synchronized List<Runnable> shutdownNow()
{
shutdown = true;
List<Runnable> aborted = new ArrayList<>();
// we busy-spin trying to take a permit if the queue is non-empty, to reduce the window
// in which an execution may be submitted after this call exits successfully
while (!tasks.isEmpty())
while (takeTaskPermit())
aborted.add(tasks.poll());
maybeTerminate();
return aborted;
}
// once shutdown = true, this is called after any state change that might
// result in the executor having terminated and, if so, signals termination
void maybeTerminate()
{
while (true)
{
// we read tasks.isEmpty() before reading our permit state;
// the alternative ordering permits a permit to be added
// and a task to be dequeued, and for us not to notice
boolean safeToTerminate = tasks.isEmpty();
long permits = this.permits.get();
// once we see a state with an empty task queue, we know we are _safe_ to terminate
// because all execute() calls that insert to an empty queue after the shutdown flag
// is set are guaranteed to be able to remove themselves without their task executing
// the only question is: _are_ we terminated? this answers that
if (taskPermits(permits) > 0 || workPermits(permits) < maxWorkers)
return;
if (safeToTerminate)
{
injector.executors.remove(this);
terminated.countDown();
return;
}
// however if we appear to be terminated, but don't have an empty task queue,
// the termination state will resolve shortly but right now cannot be determined.
// so we suspend a brief period to let any threads that raced on execute() to repair
LockSupport.parkNanos(10000);
}
}
public boolean isShutdown()
{
return shutdown;
}
public boolean isTerminated()
{
return terminated.getCount() == 0;
}
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
{
terminated.await(timeout, unit);
return isTerminated();
}
public long getPendingTasks()
{
return Math.max(0, taskPermits(permits.get()));
}
public int getActiveTasks()
{
return maxWorkers - (int) workPermits(permits.get());
}
public long getAllTimeBlockedProducers()
{
return totalBlocked.get();
}
public int getCurrentlyBlockedProducers()
{
return currentlyBlocked.get();
}
public int getMaxThreads()
{
return maxWorkers;
}
private static long WORK_PERMIT = 1L << 48;
private static long TASK_BITMASK = -1L >>> 16;
private static long taskPermits(long permits)
{
return permits & TASK_BITMASK;
}
private static long workPermits(long permits)
{
return permits >>> 48;
}
}