net.sf.eBus.client.EClient Maven / Gradle / Ivy
//
// Copyright 2014 - 2016, 2020 Charles W. Rapp
//
// Licensed 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.
//
package net.sf.eBus.client;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import java.io.File;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import net.openhft.affinity.AffinityLock;
import net.sf.eBus.client.monitor.AlarmCondition;
import net.sf.eBus.client.monitor.EBusObjectInfo;
import net.sf.eBus.client.monitor.RQThreadInfo;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.DispatcherType;
import net.sf.eBus.config.EConfigure.ScheduledExecutorBuilder;
import net.sf.eBus.config.ThreadAffinity;
import net.sf.eBus.config.ThreadAffinityConfigure;
import net.sf.eBus.config.ThreadAffinityConfigure.AffinityType;
import net.sf.eBus.config.ThreadType;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.timer.EScheduledExecutor;
import net.sf.eBus.timer.EScheduledExecutor.IETimer;
import net.sf.eBus.util.IndexPool;
import org.jctools.queues.atomic.MpmcAtomicArrayQueue;
import org.jctools.queues.atomic.MpscAtomicArrayQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@code EClient} ties together eBus message routing and message
* processing. eBus views an application instance in three ways:
* 1) an implementation of one or more eBus interfaces
* (publisher, subscriber, requester, or replier), 2) the owner
* of open feeds and 3) the target of callback
* {@link Runnable tasks}. {@code EClient} stores the instance's
* open feeds and un-executed callback tasks.
*
* eBus has a client-oriented execution mode rather than
* task-oriented. When the client's task list is not empty, the
* {@code EClient} instance is placed on a run queue where it
* appears only once. {@code RQThread} threads remove
* clients from this run queue and execute the client's tasks.
* The {@code Dispatcher} continues executing the client's
* tasks until the client task queue or run quantum is exhausted.
* When the nanosecond run quantum reaches zero, the client is
* appended to the run queue if it has pending tasks.
* The run quantum reset to the configurable initial quantum
* value when a dispatcher thread acquires the client. Note: when
* the run quantum expires, eBus cannot preempt the client. It is
* possible for the client to run indefinitely, taking over the
* {@code Dispatcher} thread.
*
*
* {@code EClient} maintains a weak reference to the application
* instance. When eBus detects that the application instance is
* finalized, all feeds left open by the instance are closed. If
* the application instance is a replier, then all active
* requests will receive an error reply. The client's un-executed
* tasks are cleared.
*
* eBus Client Execution
* {@code EClient} maintains one or more {@code RQThread}
* groups where each group has a separate run queue. Each client
* is assigned to an executor group based on the encapsulated
* application class. If the application class is not assigned to
* a particular executor group, then the class is assigned to the
* default executor group. The executor groups are defined via
* {@link EConfigure configuration}. If no executors are defined,
* then there is one {@code Dispatcher} thread for each
* {@link Runtime#availableProcessors() core} using a single,
* blocking run queue. Executors block until there is an runnable
* {@code EClient} posted to group run queue. As stated
* previously, an {@code EClient} is assigned to only one client
* dispatcher thread at a time. This makes an {@code EClient}
* instance effectively single-threaded if that client
* is accessed only by dispatcher threads. While a client may be
* accessed by different dispatcher threads over time, it will be
* accessed by only one at any given moment in time.
*
* Each {@code EClient} is assigned a integer identifier which is
* guaranteed unique for the client lifespan. When the client
* is disposed, the client identifier is re-used and may be
* assigned to a new, future eBus client instance. An
* {@code EClient} is disposed when the encapsulated application
* instance is finalized.
*
*
* The reason for {@code EClient} is because eBus requires
* application objects to {@code implement} interfaces in order
* to interface with eBus, not {@code extend} an
* eBus abstract class. This class provides the functionality
* that an abstract client class would provide.
*
*
* @see EFeed
* @see ESubject
*
* @author Charles Rapp
*/
public final class EClient
extends WeakReference
implements Comparable
{
//---------------------------------------------------------------
// Enums.
//
/**
* An eBus client is either local to this JVM or in a remote
* JVM.
*/
public enum ClientLocation
{
/**
* Client resides in this JVM.
*/
LOCAL (0x1, "local"),
/**
* Client resides in another JVM.
*/
REMOTE (0x2, "remote");
//-----------------------------------------------------------
// Member data.
//
/**
* The location bit mask used with
* {@link EFeed.FeedScope}.
*/
public final int mask;
//-------------------------------------------------------
// Locals.
//
/**
* Human-readable text describing this zone.
*/
private final String mDescription;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a client location with for the given
* description.
* @param mask location bit mask.
* @param text client location description.
*/
private ClientLocation(final int mask,
final String text)
{
this.mask = mask;
this.mDescription = text;
} // end of ClientLocation(int, String)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (mDescription);
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of enum ClientLocation
/**
* An eBus object is either on a run queue thread or off.
*/
public enum ObjectState
{
/**
* eBus object is running on a run queue thread.
*/
ON_RQ_THREAD,
/**
* eBus object is not on a run queue thread.
*/
OFF_RQ_THREAD
} // end of enum ObjectState
/**
* Defines the eBus client run states. The client run
* state changes as tasks are posted and removed from the
* client task queue.
*/
public enum RunState
{
/**
* The eBus client has no pending tasks. Will not
* be on the run queue.
*/
IDLE,
/**
* The eBus client has pending tasks. Will be on the
* run queue, waiting for a {@code RQThread}
* to execute its oldest pending task.
*/
READY,
/**
* The eBus client is processing its oldest task. May or
* may not have pending tasks. Once the processing is
* completed, the client is placed back on the run queue
* if it still has pending tasks.
*/
RUNNING,
/**
* The eBus client is defunct because the underlying
* application instance is finalized. Once the eBus
* client enters this state, it never leaves.
*/
DEFUNCT
} // end of enum RunState
/**
* The eBus client state is independent of the
* {@link RunState} which tracks {@link #mTasks} status.
* This state tracks whether the eBus client's start-up or
* shutdown method was or is being executed.
*/
private enum ClientState
{
/**
* eBus client has not been started yet.
*
* Next state: {@code STARTING}
*
*/
NOT_STARTED,
/**
* eBus client start-up method is now being executed.
*
* Next state: {@code STARTED}
*
*/
STARTING,
/**
* eBus client is now started. This state is achieved
* regardless of whether the start-up method completed
* successfully or not.
*
* This is the initial state if the eBus client is
* registered due to opening a feed.
*
*
* Next state: {@code SHUTTING_DOWN}
*
*/
STARTED,
/**
* eBus client shutdown method is in-progress.
*
* Next state: NOT_STARTED
*
*/
SHUTTING_DOWN
} // end of ClientState
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* The default dispatcher name is {@value}.
* The default dispatcher type is
* {@link ThreadType#BLOCKING blocking}.
*/
private static final String DEFAULT_DISPATCHER =
"__DEFAULT__";
/**
* The eBus client executor thread names are prefixed with
* "eBus:executor-".
*/
private static final String EBUS_THREAD_NAME_PREFIX =
"eBus:dispatcher-";
/**
* Scheduled executor used by eBus core is named {@value}.
*/
public static final String CORE_EXECUTOR_NAME =
"CoreExecutor";
/**
* Core scheduled executor thread priority is {@value}.
*/
private static final int CORE_EXECUTOR_PRIORITY = 3;
/**
* Object used to check eBus object liveness.
*/
private static final String PINGER_NAME = "__EBUS_PINGER__";
/**
* No-op task does nothing. Its purpose is to verify that
* if an eBus object is marked as idle but has tasks to
* process (a contradictory state), that it will be put on
* to its run queue so it can process its tasks.
*/
private static final Runnable NO_OP_TASK = () -> {};
//-----------------------------------------------------------
// Statics.
//
/**
* eBus executor used by core class to schedule timers.
*/
public static final EScheduledExecutor sCoreExecutor;
/**
* Used to assign a unique 4-byte, signed integer index to
* each eBus client. This index is guaranteed to be unique
* for the client's lifespan only and may be re-used by
* multiple clients over the JVM execution.
*/
private static final IndexPool sClientPool;
/**
* Maps the eBus client instances to their matching
* {@link EClient} proxy.
*/
private static final List sClients;
/**
* When the referenced eBus target object is finalized, the
* {@code EClient} referencing the defunct target is placed
* on this reference queue for disposal.
*/
private static final ReferenceQueue super EObject> sGcQueue;
/**
* This thread uses the blocking
* {@link ReferenceQueue#remove()} to take the next defunct
* {@code EClient} from {@link #sGcQueue}, cleaning up after
* the finalized eBus client.
*/
private static final Thread sGcThread;
/**
* Maps the run queue name to its configuration information.
*/
private static final Map sDispatcherNames;
/**
* Maps an eBus client class name to its assigned run
* queue dispatcher.
*/
private static final Map sDispatchers;
/**
* All existing run queue threads. This list is mutable.
*/
private static final List sRQThreads;
/**
* The automatically loaded eBus configuration. Will be
* {@code null} if no eBus configuration file was specified
* in the command line.
*/
private static final EConfigure sEBusConfig;
/**
* If an eBus client class does not appear in
* {@link #sDispatchers}, then assign that class to the default
* run queue.
*/
private static DispatcherInfo sDefaultDispatcher;
/**
* Logging subsystem interface.
*/
private static final Logger sLogger;
/**
* eBus object liveness pinger.
*/
private static final Pinger sPinger;
// Static initialization block.
static
{
final String jsonFile =
System.getProperty(EConfigure.JSON_FILE_ENV);
EConfigure eConfig = null;
Map dispatchers =
Collections.emptyMap();
Duration pingRate = EConfigure.DEFAULT_PING_RATE;
sLogger = LoggerFactory.getLogger(EClient.class);
sClientPool = new IndexPool();
sClients = new ArrayList<>();
sGcQueue = new ReferenceQueue<>();
sGcThread =
new Thread("eBus:finalizeThread")
{
@Override
public void run()
{
EClient proxy;
while (true)
{
try
{
// Get the next defunct eBus client.
proxy = (EClient) sGcQueue.remove();
sLogger.debug(
"EClient: removing eBus client {}.",
proxy.clientId());
// Clean up the proxy resources.
proxy.cleanUp();
// Remove the client from the list
// and return its unique index to the
// pool.
sClients.remove(proxy);
sClientPool.returnIndex(
proxy.clientId());
}
catch (InterruptedException interrupt)
{}
}
// The point of no return.
}
};
sGcThread.start();
if (!Strings.isNullOrEmpty(jsonFile) &&
(eConfig = loadJsonFile(jsonFile)) != null)
{
dispatchers = eConfig.dispatchers();
}
sDispatcherNames = new HashMap<>();
sDispatchers = new HashMap<>();
sRQThreads = new ArrayList<>();
// Are any of the configured Dispatchers set to default?
if (!containsDefault(dispatchers.values()))
{
// No. Add a default default Dispatcher
// configuration.
int numProcs =
(Runtime.getRuntime()).availableProcessors();
final EConfigure.DispatcherBuilder builder =
EConfigure.dispatcherBuilder();
dispatchers = new HashMap<>();
dispatchers.put(
DEFAULT_DISPATCHER,
builder.name(DEFAULT_DISPATCHER)
.dispatcherType(DispatcherType.EBUS)
.runQueueType(ThreadType.BLOCKING)
.spinLimit(0L)
.parkTime(Duration.ZERO)
.priority(Thread.NORM_PRIORITY)
.quantum(EConfigure.DEFAULT_QUANTUM)
.numberThreads(numProcs)
.isDefault(true)
.build());
}
for (EConfigure.Dispatcher dispatcher :
dispatchers.values())
{
createDispatcher(dispatcher);
}
sEBusConfig = eConfig;
if (eConfig != null)
{
pingRate = eConfig.pingRate();
// Open eBus servers and unicast and multicast
// connections.
try
{
EServer.configure(eConfig);
ERemoteApp.configure(eConfig);
EMulticastConnection.configure(eConfig);
}
catch (IOException ioex)
{
sLogger.warn(
"Failure to open eBus remote connections:",
ioex);
}
}
// Create core eBus scheduled executor. This is a
// blocking executor.
final ScheduledExecutorBuilder execBuilder =
EConfigure.scheduledExecutorBuilder();
sCoreExecutor =
EScheduledExecutor.newScheduledExecutor(
execBuilder.name(CORE_EXECUTOR_NAME)
.threadType(ThreadType.BLOCKING)
.priority(CORE_EXECUTOR_PRIORITY)
.build());
// Start the ping timer.
sPinger = new Pinger(pingRate);
findOrCreateClient(sPinger, ClientLocation.LOCAL);
sPinger.startup();
} // end of Static initialization block.
//-----------------------------------------------------------
// Locals.
//
/**
* Store away the target class for reporting purposes.
*/
private final Class> mTargetClass;
/**
* The unique 4-byte, signed integer identifier for this
* client. This identifier is unique for the client lifespan
* only and may be re-used by future client instances.
*/
private final int mClientId;
/**
* The client location is either local or remote.
*/
private final ClientLocation mLocation;
/**
* Use an {@link IndexPool} to assign unique identifiers to
* the client feeds. When a feed is closed, its identifier is
* returned to this pool.
*/
private final IndexPool mFeedIdPool;
/**
* Stores eBus client's currently active feeds. When the
* client is finalized, all active feeds are immediately
* closed.
*/
private final List mFeeds;
/**
* Stores eBus client's currently active timers. When the
* client is finalized, all active timers are immediately
* closed.
*/
private final List mTimers;
/**
* Execute this task when starting up an eBus client.
*/
private final Runnable mStartupCallback;
/**
* Execute this task when shutting down an eBus client.
*/
private final Runnable mShutdownCallback;
/**
* Post this client when it is ready to run to this queue for
* later execution.
*/
private final Queue mRunQueue;
/**
* This client runs in this dispatcher.
*/
private final String mDispatcher;
/**
* An {@code EClient} may continue running on a
* {@link RQThread} as long as the client has pending
* tasks and up to this many nanoseconds.
*/
private final long mMaxQuantum;
/**
* The client's remaining run-time quantum. Initialized to
* {@link #mMaxQuantum} each time it is acquired by a
* {@link RQThread} thread.
*/
private long mQuantum;
/**
* Inverse of {@link #mQuantum} where total time client
* spends running.
*/
private long mRunTime;
//
// Executor data members.
//
/**
* The eBus client's pending callback tasks. If this queue is
* not empty and the client run state is not
* {@link RunState#RUNNING}, then this client will be in
* the dispatch table. When the encapsulated application
* instance is finalized, this queue is cleared.
*
* Set to {@code null} when another thread (like JavaFX GUI
* thread) is used to dispatch tasks.
*
*/
private final Queue mTasks;
/**
* This function points to the method for posting tasks to
* {@link #mTasks} or to a third-party thread for the purpose
* of executing the task.
*/
private final Consumer mDispatchHandle;
/**
* This client's current run state. This value is updated
* when new tasks are dispatched and when pending tasks are
* executed.
*/
private final AtomicReference mRunState;
/**
* Has this client been started yet or shutdown?
* Note: this data member must only be accessed from one of
* the client's run queue threads.
*/
private ClientState mClientState;
/**
* Marks time this {@code EClient} either:
*
* -
* transitioned to ready state when a task is added
* to an empty {@link #mTasks} queue,
*
* -
* when a ready client is removed from a run queue
* thread due to using up quantum, or
*
* -
* duration eBus object spent in ready state waiting for
* access to a run queue thread.
*
*
* This timestamp is used to detect when a runnable client
* is denied access to a run queue thread beyond the
* monitor time limit.
*/
private long mReadyTimestamp;
//
// Execution statistics.
//
/**
* Minimum nanoseconds spent processing messages.
*/
private long mMinimumRunTime;
/**
* Maximum nanoseconds spent processing messages.
*/
private long mMaximumRunTime;
/**
* Total nanoseconds spent processing messages.
*/
private long mTotalRunTime;
/**
* Number of times this client has been on core.
*/
private long mRunCount;
/**
* {@link #mMaxQuantum} overrun count by this client.
*/
private long mMaxQuantumOverrunCount;
/**
* Current thread denial condition.
*/
private AlarmCondition mThreadDenialCondition;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new eBus client instance for the given
* application instance, executor run queue, and nanosecond
* execution quantum.
* @param target the application instance.
* @param clientId eBus client identifier.
* @param location this client is either local or remote.
* @param startCb client start up callback.
* @param shutdownCb client shut down callback.
* @param taskQueueCapacity client task queue capacity.
* @param runQueue the assigned executor run queue. Will be
* {@code null} when a non-{@code null handle} is provided.
* @param handle use this function to post tasks to the
* dispatcher. Will be {@code null} when a
* non-{@code null runQueue} is provided.
* @param dispatcher Dispatcher's name.
* @param maxQuantum maximum nanosecond run quantum for this
* client.
* @param initialState eBus client initial state.
*/
@SuppressWarnings({"java:S107"})
private EClient(final EObject target,
final int clientId,
final ClientLocation location,
final Runnable startCb,
final Runnable shutdownCb,
final int taskQueueCapacity,
final Queue runQueue,
final Consumer handle,
final String dispatcher,
final long maxQuantum,
final ClientState initialState)
{
super (target, sGcQueue);
mTargetClass = target.getClass();
mClientId = clientId;
mLocation = location;
mStartupCallback = startCb;
mShutdownCallback = shutdownCb;
mRunQueue = runQueue;
mDispatchHandle =
(runQueue != null ? this::doDispatch : handle);
mDispatcher = dispatcher;
mMaxQuantum = maxQuantum;
mQuantum = maxQuantum;
mRunTime = 0L;
mFeedIdPool = new IndexPool();
mFeeds = new ArrayList<>();
mTimers = new ArrayList<>();
mTasks = new MpscAtomicArrayQueue<>(taskQueueCapacity);
mRunState = new AtomicReference<>(RunState.IDLE);
mClientState = initialState;
mReadyTimestamp = 0L;
mMinimumRunTime = 0L;
mMaximumRunTime = 0L;
mTotalRunTime = 0L;
mRunCount = 0L;
mMaxQuantumOverrunCount = 0L;
mThreadDenialCondition = AlarmCondition.CLEARED;
} // end of EClient(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Comparable Interface Implementation.
//
/**
* Returns an integer value <, equal to, or > zero
* based on whether {@code this EClient} instance's
* identifier is <, equal to, or > {@code client}'s
* identifier.
* @param client comparison object.
* @return integer value <, equal to, or > zero.
*/
@Override
public int compareTo(final EClient client)
{
return (mClientId - client.clientId());
} // end of compareTo(EClient)
//
// end of Comparable Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Object Method Overrides.
//
/**
* Returns {@code true} if {@code o} is a
* non-{@code null EClient} instance with the same client
* identifier as {@code this} instance; otherwise, returns
* {@code false}.
* @param o comparison object.
* @return {@code true} if the client identifiers are equal.
*/
@Override
public boolean equals(final Object o)
{
boolean retcode = (this == o);
if (!retcode && o instanceof EClient)
{
retcode = (mClientId == ((EClient) o).clientId());
}
return (retcode);
} // end of equals(Object)
/**
* Returns the unique client identifier as the hash code.
* @return client identifier.
*/
@Override
public int hashCode()
{
return (mClientId);
} // end of hashCode()
@Override
public String toString()
{
return ("Client-" + mClientId);
} // end of toString()
//
// end of Object Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get methods
//
/**
* Returns the target eBus object. May return {@code null} if
* the eBus object is finalized.
* @return eBus client.
*/
@Nullable
public EObject target()
{
return (this.get());
} // end of target()
/**
* Returns target eBus object name. If eBus object is
* finalized, then returns an empty string.
* @return eBus object name.
*/
public String targetName()
{
final EObject target = this.get();
return (target == null ? "" : target.name());
} // end of targetName()
/**
* Returns the unique eBus client identifier. This identifier
* is guaranteed unique for the client lifespan only.
* @return eBus client identifier.
*/
public int clientId()
{
return (mClientId);
} // end of clientId()
/**
* Returns the client location.
* @return client location.
*/
public ClientLocation location()
{
return (mLocation);
} // end of location()
/**
* Returns maximum allowed time a client is allowed to
* process messages.
* @return maximum run quantum.
*/
public long maximumQuantum()
{
return (mMaxQuantum);
} // end of maximumQuantum()
/**
* Returns eBus object current run state.
* @return eBus object run state.
*/
public RunState runState()
{
return (mRunState.get());
} // end of runState()
/**
* Returns timestamp when eBus object entered ready state.
* Also represents total time eBus object spent waiting
* for run queue thread access.
* @return eBus object ready state timestamp.
*/
public long readyTimestamp()
{
return (mReadyTimestamp);
} // end of readyTimestamp()
/**
* Returns the auto-loaded eBus configuration. Will return
* {@code null} if no eBus configuration was automatically
* loaded.
* @return auto-loaded eBus configuration.
*/
public static EConfigure eBusConfig()
{
return (sEBusConfig);
} // end of eBusConfig()
/**
* Returns run time statistics immutable list for extant eBus
* objects. Returns an empty list if there are no currently
* registered eBus objects.
*
* The following is an example output of the returned list:
*
* ConnectionPublisher (id 0)
min run time: 1,364 nanos
max run time: 32,743,678 nanos
total run time: 34,189,949 nanos
run count: 4
avg run time: 8,547,487 nanos
dispatcher: general
max quantum: 100,000 nanos
quantum overruns: 3
MulticastConnectionPublisher (id 1)
min run time: 613 nanos
max run time: 751,792 nanos
total run time: 763,513 nanos
run count: 3
avg run time: 254,504 nanos
dispatcher: general
max quantum: 100,000 nanos
quantum overruns: 1
PingPong Main (id 2)
min run time: 10,541 nanos
max run time: 3,700,790 nanos
total run time: 3,711,331 nanos
run count: 2
avg run time: 1,855,665 nanos
dispatcher: general
max quantum: 100,000 nanos
quantum overruns: 1
Ping! Pong! Timer (id 3)
min run time: 1,260 nanos
max run time: 9,877,401 nanos
total run time: 10,195,402 nanos
run count: 5
avg run time: 2,039,080 nanos
dispatcher: general
max quantum: 100,000 nanos
quantum overruns: 2
Pinger (id 4)
min run time: 61 nanos
max run time: 33,913,494 nanos
total run time: 953,601,532 nanos
run count: 338,447
avg run time: 2,817 nanos
dispatcher: ping
max quantum: 100,000 nanos
quantum overruns: 179
Ponger (id 5)
min run time: 164 nanos
max run time: 4,439,180 nanos
total run time: 926,228,288 nanos
run count: 132,905
avg run time: 6,969 nanos
dispatcher: pong
max quantum: 100,000 nanos
quantum overruns: 235
* @return run time statistics list.
*/
public static List runTimeStats()
{
final List clients;
final ImmutableList.Builder builder =
ImmutableList.builder();
synchronized (sClients)
{
clients = ImmutableList.copyOf(sClients);
}
for (EClient ec : clients)
{
// Does this client still have a target eBus
// object?
if (ec.target() != null)
{
// Yes. Collect the eBus object info.
builder.add(ec.generateInfo());
}
}
return (builder.build());
} // end of runTimeStats()
/**
* Returns the set of all currently registered eBus clients.
* The returned set is an immutable copy of the actual client
* list.
* @return currently registered eBus clients.
*/
public static List getClients()
{
final ImmutableList.Builder builder =
ImmutableList.builder();
synchronized (sClients)
{
builder.addAll(sClients);
}
return (builder.build());
} // end of getClients();
/**
* Returns immutable list of run queue threads. Used to
* monitor {@code RQThread} state.
* @return run queue threads list.
*/
public static List runQueueThreads()
{
return (sRQThreads);
} // end of runQueueThreads()
/**
* Returns the target object class.
* @return target object class.
*/
/* package */ Class> targetClass()
{
return (mTargetClass);
} // end of targetClass()
/**
* Returns the next available feed identifier. Returned
* values are ≥ zero. Feed identifiers are unique only
* within a given eBus client. Feed identifiers are re-used
* when a feed is closed.
* @return next client feed identifier.
*/
/* package */ int nextFeedId()
{
return (mFeedIdPool.nextIndex());
} // nextFeedId()
/**
* Returns the feed identifier to the pool for later re-use.
* @param feedId the no-longer-used feed identifier.
*/
/* package */ void returnFeedId(final int feedId)
{
mFeedIdPool.returnIndex(feedId);
} // end of returnFeedId(int)
/**
* Returns the eBus proxy for the given application object.
* Returns {@code null} if {@code client} is currently
* unknown to eBus.
* @param client search for this client's eBus proxy.
* @return eBus proxy for {@code client}.
*/
public static EClient findClient(final EObject client)
{
EClient retval = null;
synchronized (sClients)
{
final Iterator cit = sClients.iterator();
while (cit.hasNext() && retval == null)
{
retval = cit.next();
if (client != retval.get())
{
retval = null;
}
}
}
return (retval);
} // end of findClient(EObject)
/**
* Returns the current number of clients.
* @return current client count.
*/
/* package */ static int clientCount()
{
return (sClients.size());
} // end of clientCount()
/**
* Returns the default dispatcher name.
* @return default dispatcher name.
*/
/* package */ static String defaultDispatcher()
{
return (sDefaultDispatcher.name());
} // end of defaultDispatcher()
//
// end of Get methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set methods.
//
/**
* Sets eBus object's current thread denial condition.
* @param condition thread denial condition.
*/
public void threadDenial(final AlarmCondition condition)
{
mThreadDenialCondition = condition;
} // end of threadDenial(AlarmCondition)
/**
* Adds specified timer to eBus client timer list.
* @param timer add this timer to eBus client timer list.
*
* @see #removeTimer(EScheduledExecutor.IETimer)
*/
public void addTimer(final IETimer timer)
{
mTimers.add(timer);
} // end of addTimer(IETimer)
/**
* Removes specified timer from eBus client timer list.
* @param timer remove this timer from eBus client timer
* list.
*
* @see #addTimer(EScheduledExecutor.IETimer)
*/
public void removeTimer(final IETimer timer)
{
mTimers.remove(timer);
} // end of removeTimer(IETimer)
/**
* Adds specified feed to client feed list.
* @param feed adds this feed to client feed list.
*
* @see #addFeed(EAbstractMultikeyFeed)
* @see #removeFeed(EFeed)
*/
/* package */ void addFeed(final EFeed feed)
{
final int size;
synchronized (mFeeds)
{
mFeeds.add(feed);
size = mFeeds.size();
}
sLogger.trace("{}: reference count {}.",
mTargetClass.getName(),
size);
} // end of addFeed(EFeed)
/**
* Removes specified feed from eBus client feed list.
* @param feed remove this feed from eBus client feed list.
*
* @see #addFeed(EFeed)
* @see #removeFeed(EAbstractMultikeyFeed)
*/
/* package */ void removeFeed(final EFeed feed)
{
final int size;
synchronized (mFeeds)
{
mFeeds.remove(feed);
size = mFeeds.size();
}
sLogger.trace("{}: reference count {}.",
mTargetClass.getName(),
size);
} // end of removeFeed()
//
// end of Set methods.
//-----------------------------------------------------------
/**
* Posts the task to the client execution queue. This is
* performed by the {@link Consumer} method referenced by the
* dispatch handle.
* @param task post this task to the client queue.
*/
public void dispatch(final Runnable task)
{
mDispatchHandle.accept(task);
} // end of dispatch(Runnable)
/**
* Returns the eBus proxy for the given eBus client instance.
* If the proxy does not yet exist, then creates the proxy
* and places the proxy into the map. There is only one proxy
* created for each client independent of the number of eBus
* interfaces the client implements.
* @param client the eBus client.
* @param location {@link ClientLocation#LOCAL} if the client
* is local to this JVM and {@link ClientLocation#REMOTE} if
* it exists in a remote eBus application.
* @return the eBus client proxy.
*/
public static EClient findOrCreateClient(final EObject client,
final ClientLocation location)
{
EClient retval;
synchronized (sClients)
{
retval = findClient(client);
// Does this eBus client have a proxy?
if (retval == null)
{
// No, create it now.
// Find the dispatcher for this client.
final DispatcherInfo info =
findDispatcher(client);
// Use the default
retval =
new EClient(client,
sClientPool.nextIndex(),
location,
client::startup,
client::shutdown,
info.taskQueueCapacity(),
info.runQueue(),
info.dispatchHandle(),
info.name(),
info.maxQuantum(),
ClientState.STARTED);
sClients.add(retval);
sLogger.debug(
"EClient: created {} client {} -> {}",
location,
retval.clientId(),
client.name());
}
}
return (retval);
} // end of findOrCreateClient(EObject, ClientLocation)
/**
* Posts {@code task} to {@code client}'s task queue (this is
* the same task queue used for eBus message delivery). If
* the task queue was originally empty, then the client
* posted to its run queue for execution by a
* {@code RQThread}.
*
* If {@code client} is unknown to eBus, then a new
* {@code EClient} instance will be created with a
* {@link WeakReference weak reference} to {@code client}.
* {@code client} does not need to be an eBus
* publisher/ subscriber/requestor/replier.
*
* @param task post to the client task queue.
* @param client {@code task} is for this client.
* @throws NullPointerException
* if either {@code task} or {@code client} is {@code null}.
*/
public static void dispatch(final Runnable task,
final EObject client)
{
Objects.requireNonNull(task, "task is null");
Objects.requireNonNull(client, "client is null");
final EClient eClient =
findOrCreateClient(client, ClientLocation.LOCAL);
eClient.dispatch(task);
} // end of dispatch(Runnable, EObject)
/**
* Performs the actual work of posting the task to the client
* execution queue. If the client task is initially empty,
* then this eBus client is posted to its assigned run queue.
* @param task post this task to the client queue.
*/
private void doDispatch(final Runnable task)
{
// Put the task on the queue.
// Do not to check the return value since this is an
// unbounded ConcurrentLinkedQueue and
// ConcurrentLinkedQueue.offer always returns true.
mTasks.offer(task);
// If this client is currently idle, then change it
// to ready and post it to the dispatch table.
if (mRunState.compareAndExchange(
RunState.IDLE, RunState.READY) == RunState.IDLE)
{
mRunQueue.offer(this);
// Timestamp when this eBus object when back into
// ready state.
mReadyTimestamp = System.nanoTime();
}
// Else if this client is currently ready, then
// nothing has changed.
// If this client is currently running, then when
// the task completes, the client will be posted
// back to the dispatch table.
} // end of doDispatch(Runnable)
/**
* Returns the next task from this client's task queue if the
* task queue is not empty and the client's quantum
* is not exhausted. Otherwise, returns {@code null}.
*
* This method does not use synchronization because eBus
* dispatcher guarantees that the method is accessed from one
* thread at a time.
*
* @param timeUsed time most recently used for this client.
* Subtract this value from {@link #mQuantum} to get this
* client's remaining run time quantum.
* @return the client's next task or {@code null} if there
* are no more tasks or no more time to run tasks.
*/
// This method accesses EClient data which RQThread should
// not access.
@SuppressWarnings({"java:S3398"})
private Runnable nextTask(final long timeUsed)
{
Runnable retval = null;
mQuantum -= timeUsed;
mRunTime += timeUsed;
// Is this client still alive?
if (mRunState.get() == RunState.DEFUNCT)
{
// no-op.
}
// Are there any more tasks to run?
else if (mTasks.isEmpty())
{
// No tasks. The client is still active but idle.
mRunState.set(RunState.IDLE);
updateRunStats(mRunTime);
// Reset the quantum as well so we have a fresh start
// when the next task is posted.
mQuantum = mMaxQuantum;
mRunTime = 0L;
}
// Yes, there are more tasks to run.
// But is there time left to run the next task?
else if (mQuantum <= 0L)
{
// No, times up. Post this client to the back of the
// run queue, giving the other clients a chance.
mRunState.set(RunState.READY);
// Timestamp when this eBus object when back into
// ready state.
mReadyTimestamp = System.nanoTime();
updateRunStats(mRunTime);
// Reset this client's quantum to the allowed
// maximum and run time to zero.
mQuantum = mMaxQuantum;
mRunTime = 0L;
// Offer this client up for later sacrifice.
mRunQueue.offer(this);
}
// Yes, there is task to run and time left to run it.
// Take the next task from the queue and set this client
// running.
else
{
mRunState.set(RunState.RUNNING);
retval = mTasks.poll();
}
return (retval);
} // end of nextTask(long)
/**
* Updates client run-time statistics based on the latest
* run.
* @param runTime latest nanosecond run-time.
*/
private void updateRunStats(final long runTime)
{
if (runTime > 0L)
{
if (mMinimumRunTime == 0 ||
runTime < mMinimumRunTime)
{
mMinimumRunTime = runTime;
}
if (runTime > mMaximumRunTime)
{
mMaximumRunTime = runTime;
}
if (runTime > mMaxQuantum)
{
++mMaxQuantumOverrunCount;
}
mTotalRunTime += runTime;
++mRunCount;
}
} // end of updateRunStats(long)
/**
* Creates a new client dispatcher based on the given
* configuration.
*
* Note: if this dispatcher is marked as
* default and there is already a default dispatcher, then
* this new dispatcher will replace the existing default
* dispatcher.
*
* @param dispatcher dispatcher configuration.
* @throws IllegalStateException
* if a dispatcher with named
* {@link EConfigure.Dispatcher#name()} already exists.
*
* @see EConfigure.Dispatcher
* @see EConfigure.DispatcherBuilder
*/
/* package */ static void createDispatcher(final EConfigure.Dispatcher dispatcher)
{
final String dispatcherName = dispatcher.name();
final DispatcherInfo info;
int index;
String rqThreadName;
RQThread rqThread;
if (sDispatcherNames.containsKey(dispatcherName))
{
throw (
new IllegalStateException(
String.format(
"dispatcher named %s already exists",
dispatcherName)));
}
info = new DispatcherInfo(dispatcher);
sDispatcherNames.put(dispatcher.name(), info);
// Is this the default run queue?
if (dispatcher.isDefault())
{
// Yes.
sDefaultDispatcher = info;
}
// No. Get the dispatcher classes and create the
// class-to-run queue mappings.
else
{
for (Class> clazz : dispatcher.classes())
{
sDispatchers.put(clazz.getName(), info);
}
}
// Start the eBus Dispatchers running.
for (index = 0;
index < dispatcher.numberThreads();
++index)
{
rqThreadName =
(EBUS_THREAD_NAME_PREFIX +
dispatcher.name() +
"-" +
index);
rqThread = new RQThread(rqThreadName,
index,
info.runQueue(),
dispatcher);
sRQThreads.add(rqThread);
rqThread.start();
}
// Drop the previous affinity lock since it is now
// no longer used.
RQThread.sPreviousLock = null;
} // end of createDispatcher(Dispatcher)
/**
* Returns the dispatcher to which the client class is
* assigned. If the client class is not assigned to a
* specific dispatcher, then the default dispatcher is
* returned.
* @param client find the dispatcher for this eBus client.
* @return the client's assigned dispatcher.
*/
/* package */ static DispatcherInfo findDispatcher(final EObject client)
{
final String className = (client.getClass()).getName();
return (sDispatchers.containsKey(className) ?
sDispatchers.get(className) :
sDefaultDispatcher);
} // end of findDispatcher(EObject)
/**
* Returns an eBus proxy for the given application object,
* assigning the client to the given eBus dispatcher. This
* method allows individual objects to be assigned to a
* dispatcher rather than by class.
* @param client the application object.
* @param location the client is either local or remote.
* @param info assign {@code client} to this dispatcher.
* @param startupCb {@code client} start-up method callback.
* @param shutdownCb [@code client} shutdown method callback.
* @return the newly added eBus client proxy.
* @throws IllegalStateException
* if {@code client} is currently on the client list.
*/
/* package */ static EClient addClient(final EObject client,
final ClientLocation location,
final DispatcherInfo info,
final Runnable startupCb,
final Runnable shutdownCb)
{
final EClient retval;
synchronized (sClients)
{
// Is client already known to eBus?
if (findClient(client) != null)
{
// Yes, so it cannot be added a second time.
throw (
new IllegalStateException(
"client already registered with eBus"));
}
retval = new EClient(client,
sClientPool.nextIndex(),
location,
startupCb,
shutdownCb,
info.taskQueueCapacity(),
info.runQueue(),
info.dispatchHandle(),
info.name(),
info.maxQuantum(),
ClientState.NOT_STARTED);
sClients.add(retval);
sLogger.debug("EClient: created client {} -> {}",
retval.clientId(),
client.name());
}
return (retval);
} // end of addClient(EObject, ClientLocation, DispatcherInfo)
/**
* Returns the dispatcher information for the given name.
* Returns {@code null} if there is no dispatcher with that
* name.
* @param name dispatcher name.
* @return dispatcher information for that name.
*/
/* package */ static DispatcherInfo findDispatcher(final String name)
{
return (sDispatcherNames.get(name));
} // end of findDispatcher(String)
/**
* Starts the given eBus clients by calling each client's
* {@link #mStartupCallback} method from within the client's
* run queue.
* @param clients start up these clients.
*/
@SuppressWarnings ("AccessingNonPublicFieldOfAnotherObject")
/* package */ static void startup(final List clients)
{
clients.forEach(
client ->
client.dispatch(
new StartStopTask(client,
client.mStartupCallback,
ClientState.NOT_STARTED,
ClientState.STARTING,
ClientState.STARTED)));
} // end of startup(List<>)
/**
* Shuts down the given eBus clients by calling each client's
* {@link #mShutdownCallback} method from within the client's
* run queue.
* @param clients shut down these clients.
*/
@SuppressWarnings ("AccessingNonPublicFieldOfAnotherObject")
/* package */ static void shutdown(final List clients)
{
clients.forEach(
client ->
client.dispatch(
new StartStopTask(client,
client.mShutdownCallback,
ClientState.STARTED,
ClientState.SHUTTING_DOWN,
ClientState.NOT_STARTED)));
} // end of shutdown(List<>)
/**
* Returns an eBus object information instance based on this
* client's settings.
* @return eBus object information instance.
*/
private EBusObjectInfo generateInfo()
{
final EBusObjectInfo.Builder builder =
EBusObjectInfo.builder();
return (builder.objectName(targetName())
.clientId(mClientId)
.runState(mRunState.get())
.taskQueueSize(mTasks.size())
.minimumRunTime(mMinimumRunTime)
.maximumRunTime(mMaximumRunTime)
.totalRunTime(mTotalRunTime)
.runCount(mRunCount)
.dispatcher(mDispatcher)
.maxQuantum(mMaxQuantum)
.maxQuantumOverrunCount(
mMaxQuantumOverrunCount)
.alarmCondition(
mThreadDenialCondition)
.build());
} // end of generateInfo()
/**
* When an eBus client is finalized, close any and all active
* feeds associated with that defunct client, dispose of
* all un-executed tasks, and mark this client as defunct.
*
* There is no need to use synchronization in this method
* since the client is now garbage.
*
*/
// This method accesses EClient data which finalizer thread
// should not access.
@SuppressWarnings({"java:S3398"})
private void cleanUp()
{
mRunState.set(RunState.DEFUNCT);
mFeeds.stream().forEach(feed -> feed.close());
mFeeds.clear();
// Remove this client from subject listeners - in case
// it is so registered.
ESubject.removeListener(this);
// Cancel all active client timers.
for (IETimer timer : mTimers)
{
try
{
timer.close();
}
catch (Exception jex)
{
// Ignore.
}
}
mTimers.clear();
mTasks.clear();
mThreadDenialCondition = AlarmCondition.DEFUNCT;
} // end of removeClient(EClient)
/**
* Returns the Dispatcher run queue associated with the
* dispatcher and thread types.
* @param config Dispatcher configuration.
* @return Dispatcher run queue.
*/
// This method accesses EClient data which RQThread should
// not access.
@SuppressWarnings({"java:S3398"})
private static Queue runQueue(final EConfigure.Dispatcher config)
{
final DispatcherType dType = config.dispatchType();
final Queue retval;
if (dType == DispatcherType.EBUS)
{
// If the thread type is blocking, then use a
// blocking queue.
if (config.runQueueType() == ThreadType.BLOCKING)
{
retval = new LinkedBlockingQueue<>();
}
// All other thread types use a non-blocking
// queue.
else
{
retval =
new MpmcAtomicArrayQueue<>(
config.runQueueCapacity());
}
}
else
{
retval = null;
}
return (retval);
} // end of runQueue(DispatcherType)
/**
* Returns {@code true} if {@code dispatchers} contains an
* entry marked as the default Dispatcher. Otherwise, returns
* {@code false}.
* @param dispatchers search collection for default
* Dispatcher.
* @return {@code true} if {@code dispatchers} contains a
* default.
*/
private static boolean
containsDefault(final Collection dispatchers)
{
final Iterator dit =
dispatchers.iterator();
boolean retcode = false;
while (dit.hasNext() && !retcode)
{
retcode = (dit.next()).isDefault();
}
return (retcode);
} // end of containsDefault(Collection<>)
/**
* Returns eBus configuration load from given JSON file.
* @param jsonFileName JSON configuration file name.
* @return eBus configuration.
* @throws ConfigException
* if {@code jsonFileName} is not a valid configuration
* file.
*/
private static EConfigure loadJsonFile(final String jsonFileName)
{
final File jsonFile = new File(jsonFileName);
final Config eBusConfig =
ConfigFactory.parseFile(jsonFile);
return (EConfigure.load(eBusConfig));
} // end of loadJsonFile(String)
//---------------------------------------------------------------
// Inner classes.
//
/**
* A Dispatcher run queue thread watches a given
* {@link java.util.concurrent.ConcurrentLinkedQueue run queue}
* for {@link EClient} instances ready to run, attempting to
* acquire the next ready client. When the thread
* successfully acquires a client, this thread has the client
* execute its pending tasks until either 1) the client has
* no more tasks or 2) the client exhausts its run quantum.
* Each run queue has a configurable run quantum (default is
* {@link EConfigure#DEFAULT_QUANTUM}). When a client
* exhausts this quantum and still has tasks to run, the
* client is placed at the end of the run queue and its
* quantum is replenished.
*
* A client instance is referenced by only one dispatcher
* thread at time. This means a client is effectively
* single-threaded at any given instance while it may be
* accessed by multiple, different dispatcher threads over
* time.
*
*
* Note: this only applies to eBus threads.
* Non-eBus threads may still access the client object at the
* same time.
*
*/
public static final class RQThread
extends Thread
{
//-----------------------------------------------------------
// Member enums.
//
/**
* A run queue thread is either idle (waiting for an
* available eBus object) or busy (running an eBus
* object).
*/
public enum RQThreadState
{
/**
* Run queue thread is waiting for the next
* available eBus object to run.
*/
IDLE,
/**
* Run queue thread is busy running a eBus object
* tasks.
*/
BUSY
} // end of enum RQThreadState
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Constants.
//
/**
* Set eBus object name to {@value} when run queue thread
* is idle.
*/
public static final String NO_EOBJECT = "(idle)";
//-------------------------------------------------------
// Statics.
//
/**
* If dispatcher has multiple run queue threads
* and defined thread affinity, then this is the
* affinity lock assigned to the previous run queue
* thread. This lock is used when affinity type is a
* CPU selection strategy.
*/
private static AffinityLock sPreviousLock = null;
//-------------------------------------------------------
// Locals.
//
/**
* This executor takes ready eBus clients from this run
* queue
*/
private final Queue mRunQueue;
/**
* Use this method to extract clients from
* {@link #mRunQueue}.
*/
private final PollInterface mPollMethod;
/**
* Spin limit used when {@link #spinSleepPoll()} or
* {@link #spinYieldPoll()} poll method is used.
*/
private final long mSpinLimit;
/**
* Nanosecond park time used when
* {@link #spinSleepPoll()} poll method is used.
*/
private final long mParkTime;
/**
* Thread affinity configuration. Used to associate thread
* with a CPU. Set to {@code null} if the run queue
* thread has no CPU affinity.
*/
private final ThreadAffinityConfigure mAffinity;
/**
* Run quantum maximum in nanoseconds.
*/
private final long mMaxQuantum;
/**
* Current run queue thread stats. Used to detect if an
* eBus object has been running on the thread longer than
* maximum quantum allows. The contained value will
* not be set to {@code null}.
*/
private final AtomicReference mStats;
//
// Performance statistics.
//
/**
* Current run queue thread state.
*/
private RQThreadState mState;
/**
* Timestamp when {@code RQThread} started running.
*/
private Instant mStartTime;
/**
* Total number of eBus objects run on this thread.
*/
private long mRunCount;
/**
* Total time this run queue thread has spent processing
* eBus object tasks.
*/
private long mBusyTimeNanos;
/**
* Minimum eBus object run time on thread.
*/
private long mMinRunTimeNanos;
/**
* Maximum eBus object run time on thread.
*/
private long mMaxRunTimeNanos;
/**
* if {@link #mState} is {@link RQThreadState#BUSY}, then
* this is the name of the eBus object running on the
* thread.
*/
private String mEObjectName;
/**
* Thread maximum quantum overrun alarm condition.
*/
private AlarmCondition mAlarmCondition;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Create a new run queue thread instance.
* @param name run queue thread name.
* @param index run queue thread index.
* @param runQueue monitor this queue for runnable
* eBus clients.
* @param config run queue configuration.
*/
private RQThread(final String name,
final int index,
final Queue runQueue,
final EConfigure.Dispatcher config)
{
super (name);
mRunQueue = runQueue;
mSpinLimit = config.spinLimit();
mParkTime = (config.parkTime()).toNanos();
mAffinity = findAffinity(index, config);
mMaxQuantum = (config.quantum()).toNanos();
mStats =
new AtomicReference<>(
new RQStats(name,
mMaxQuantum,
ObjectState.OFF_RQ_THREAD));
switch (config.runQueueType())
{
case BLOCKING:
mPollMethod = this::blockingPoll;
break;
case SPINNING:
mPollMethod = this::spinningPoll;
break;
case SPINPARK:
mPollMethod = this::spinSleepPoll;
break;
default:
mPollMethod = this::spinYieldPoll;
}
mState = RQThreadState.IDLE;
mRunCount = 0L;
mBusyTimeNanos = 0L;
mMinRunTimeNanos = 0L;
mMaxRunTimeNanos = 0L;
mEObjectName = NO_EOBJECT;
mAlarmCondition = AlarmCondition.CLEARED;
this.setDaemon(true);
} // end of Dispatcher(String, int, Queue, Dispatcher)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Thread Method Overrides.
//
/**
* A client executor thread continues processing ready
* eBus clients until the JVM exits. On each iteration,
* this thread acquires the next available client. Then
* iterates over the client's task list and executing
* each task. This continues until the client has no
* more tasks or has used up its runtime quantum. The
* client is put back on the run queue (if the quantum
* is expired, the value is reset to the configured
* maximum) and this thread goes back to the loop's top
* and acquires the next available client.
*
* Because an eBus client is posted to only one, unique
* run queue and each {@code Dispatcher} thread works
* with only one run queue, once a dispatcher thread
* acquires an eBus client, it is guaranteed that no
* other dispatcher thread has access to the
* client. Therefore, from an eBus perspective, eBus
* client access is single-threaded.
*
*
* (Non-eBus threads may still access a client
* simultaneously as a dispatcher thread. This is an
* application design decision.)
*
*/
@SuppressWarnings ({"java:S2696", "java:S3776"})
@Override
public void run()
{
final String name = this.getName();
final RQStats offThread = mStats.get();
final RQStats onThread =
new RQStats(name,
mMaxQuantum,
ObjectState.ON_RQ_THREAD);
EClient client;
long busyStart;
long busyStop;
long busyTime;
Runnable task;
long startTime;
long timeUsed;
mStartTime = Instant.now();
// If run queue thread is configured for thread
// affinity then put that affinity into place here.
if (mAffinity != null)
{
// Is this a strategy affinity?
if (mAffinity.affinityType() ==
AffinityType.CPU_STRATEGIES)
{
// Yes. Use the previous lock when selecting
// this next lock.
sPreviousLock =
ThreadAffinity.acquireLock(
sPreviousLock, mAffinity);
}
// No. Apply the CPU selection independent of
// previous lock.
else
{
sPreviousLock =
ThreadAffinity.acquireLock(mAffinity);
}
}
sLogger.debug("{}: running.", name);
sLogger.trace("{}: polling run queue {}.",
name,
mRunQueue);
// Keep going until perdition.
while (true)
{
// Get the next client.
if ((client = mPollMethod.poll()) != null)
{
timeUsed = 0L;
mState = RQThreadState.BUSY;
mEObjectName = client.targetName();
busyStart = System.nanoTime();
onThread.updateStats(
mRunCount, client.target(), busyStart);
mStats.set(onThread);
// Continue processing this client until it
// either runs out of tasks or out of time.
while (
(task = client.nextTask(timeUsed))
!= null)
{
// Yes, there is a task to perform.
// So, perform it.
// eBus tasks catch any client-thrown
// exceptions, so a try-catch block is not
// needed here.
startTime = System.nanoTime();
task.run();
timeUsed =
(System.nanoTime() - startTime);
// Do not reset the client to null here
// but continue processing the client's
// tasks until the client either has no
// tasks to run or until its time quantum
// is used up.
}
busyStop = System.nanoTime();
busyTime = (busyStop - busyStart);
offThread.updateStats(
mRunCount, client.target(), busyStop);
mStats.set(offThread);
mBusyTimeNanos += busyTime;
mEObjectName = NO_EOBJECT;
mState = RQThreadState.IDLE;
++mRunCount;
// If busy time is set (> zero) and either
// min run time not yet set or busy time
// is less than min run time, then set min
// run time to busy time.
if (busyTime > 0L &&
(mMinRunTimeNanos == 0L ||
busyTime < mMinRunTimeNanos))
{
mMinRunTimeNanos = busyTime;
}
if (busyTime > mMaxRunTimeNanos)
{
mMaxRunTimeNanos = busyTime;
}
}
}
// Point of no return.
} // end of run()
//
// end of Thread Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get methods.
//
/**
* Returns this run queue thread's current stats. May
* return {@code null}.
* @return current run queue stats.
*/
public RQStats currentStats()
{
return (mStats.get());
} // end of currentStats()
/**
* Returns this run queue thread's current information.
* @return run queue thread information.
*/
public RQThreadInfo currentInfo()
{
final RQThreadInfo.Builder builder =
RQThreadInfo.builder();
return (builder.threadName(this.getName())
.startTime(mStartTime)
.threadState(mState)
.eBusObject(mEObjectName)
.totalRunTime(mBusyTimeNanos)
.runCount(mRunCount)
.minimumRunTime(mMinRunTimeNanos)
.maximumRunTime(mMaxRunTimeNanos)
.alarmCondition(mAlarmCondition)
.build());
} // end of currentInfo()
/**
* Returns configured maximum quantum for this run queue
* thread.
* @return run queue thread maximum quantum.
*/
public long maximumQuantum()
{
return (mMaxQuantum);
} // end of maximumQuantum()
//
// end of Get methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set methods.
//
/**
* Sets maximum quantum overrun alarm condition.
* @param alarmState thread overrun alarm condition.
*/
public void alarmCondition(final AlarmCondition alarmState)
{
mAlarmCondition = alarmState;
} // end of alarmCondition(AlarmCondition)
//
// end of Set methods.
//-------------------------------------------------------
/**
* Returns the next available client from the run queue,
* blocking until the client arrives.
* @return next available client. Does not return
* {@code null}.
*/
private EClient blockingPoll()
{
EClient retval = null;
while (retval == null)
{
try
{
retval =
((LinkedBlockingQueue)
mRunQueue).take();
}
catch (InterruptedException interrupt)
{}
}
return (retval);
} // end of blockingPoll()
/**
* Actively spins calling
* {@link ConcurrentLinkedQueue#poll()} to extract the
* next available client from the run queue.
* @return next available client. Does not return
* {@code null}.
*/
private EClient spinningPoll()
{
EClient retval = null;
while (retval == null)
{
retval = mRunQueue.poll();
}
return (retval);
} // end of spinningPoll()
/**
* Spins a fixed number of times calling
* {@link ConcurrentLinkedQueue#poll()} to extract the
* next available client from the run queue. When the
* spin limit is reached, then parks for a fixed number
* of nanoseconds.
* @return next available client. Does not return
* {@code null}.
*/
public EClient spinSleepPoll()
{
long counter = mSpinLimit;
EClient retval = null;
while (retval == null)
{
// Spin limit reached?
if (counter == 0)
{
// Yes. Take a nap before continuing.
LockSupport.parkNanos(mParkTime);
counter = mSpinLimit;
}
retval = mRunQueue.poll();
--counter;
}
return (retval);
} // end of spinSleepPoll()
/**
* Spins a fixed number of times calling
* {@link ConcurrentLinkedQueue#poll()} to extract the
* next available client from the run queue. When the
* spin limit is reached, then this Dispatcher thread
* yields.
* @return next available client. Does not return
* {@code null}.
*/
public EClient spinYieldPoll()
{
long counter = mSpinLimit;
EClient retval = null;
while (retval == null)
{
// Spin limit reached?
if (counter == 0)
{
// Yes. Take a nap before continuing.
LockSupport.park();
counter = mSpinLimit;
}
retval = mRunQueue.poll();
--counter;
}
return (retval);
} // end of spinYieldPoll()
/**
* Returns thread affinity configuration for the given
* run queue thread index. May return {@code Null} if
* dispatcher does not support thread affinity.
* @param index run queue thread index.
* @param config eBus dispatcher configuration.
* @return run queue thread affinity.
*/
@Nullable
private static ThreadAffinityConfigure findAffinity(final int index,
final EConfigure.Dispatcher config)
{
final ThreadAffinityConfigure[] affinities =
config.affinity();
final int numAffinity =
(affinities == null ? 0 : affinities.length);
final ThreadAffinityConfigure retval;
// Are affinities specified?
if (numAffinity == 0)
{
// No. Return null affinity.
retval = null;
}
// Is the affinity count greater than this index?
else if (numAffinity > index)
{
// Yes. Use the thread affinity matching the
// thread index.
retval = affinities[index];
}
// No, there are fewer affinities than threads. Use
// the final affinity.
else
{
retval = affinities[numAffinity - 1];
}
return (retval);
} // end of findAffinity(int, Dispatcher)
} // end of class RQThread
/**
* Contains the {@link RQThread} current condition. This
* includes: thread name, maximum run quantum allowed,
* current run index, eBus object being run, and eBus
* object run start time in nanoseconds. This information
* is used to determine if an eBus object is exceeding the
* maximum allowed run time.
*/
public static final class RQStats
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* {@code RQThread} name.
*/
private final String mName;
/**
* Maximum run quantum.
*/
private final long mMaxQuantum;
/**
* eBus object is either running on an {@code RQThread}
* or not on a thread.
*/
private final ObjectState mState;
/**
* Current run queue thread run count.
*/
private long mRunCount;
/**
* eBus Object currently running on thread.
*/
private EObject mEObject;
/**
* Time {@code EObject} started running on thread.
*/
private long mTimestamp;
/**
* Current overrun state where {@code true} means the
* run quque thread is experiencing an overrun condition.
*/
private boolean mIsOverrun;
/**
* Update message sent for this current RQThread stat.
* May be {@code null}.
*/
private @Nullable ENotificationMessage mUpdate;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new run queue state object for the given
* name, quantum, and thread state.
* @param name RQThread name.
* @param maxQuantum maximum run quantum.
* @param state eBus object thread state.
*/
private RQStats(final String name,
final long maxQuantum,
final ObjectState state)
{
mName = name;
mMaxQuantum = maxQuantum;
mState = state;
mRunCount = 0L;
mEObject = null;
mTimestamp = System.nanoTime();
mIsOverrun = false;
} // end of RQStats(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
final StringBuilder retval = new StringBuilder();
return (retval.append("[name=").append(mName)
.append(", max quantum=")
.append(mMaxQuantum)
.append(", run count=")
.append(mRunCount)
.append(", object=")
.append((mEObject == null ?
"null" :
mEObject.name()))
.append(", state=").append(mState)
.append(", is overrun=")
.append(mIsOverrun)
.append("]")
.toString());
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns run queue thread name.
* @return run queue thread name.
*/
public String name()
{
return (mName);
} // end of name()
/**
* Returns maximum run quantum.
* @return maximum run quantum.
*/
public long maximumQuantum()
{
return (mMaxQuantum);
} // end of maximumQuantum()
/**
* Returns number of eBus objects "run" by the given
* run queue thread.
* @return eBus object run count.
*/
public long runCount()
{
return (mRunCount);
} // end of runCount()
/**
* Returns eBus object currently being run by thread.
* Will be {@code null} if thread is not running an
* object.
* @return eBus object.
*/
public EObject eBusObject()
{
return (mEObject);
} // end of eBusObject()
/**
* Returns time eBus object started or stopped running on
* thread.
* @return eBus object run start time.
*/
public long timestamp()
{
return (mTimestamp);
} // end of timestamp()
/**
* Returns eBus object state: either on running an
* {@code RQThread} or off thread.
* @return eBus object thread state.
*/
public ObjectState state()
{
return (mState);
} // end of state()
/**
* Returns current maximum quantum overrun status where
* {@code true} means the run queue thread is currently
* exceeding overrun state.
* @return overrun status.
*/
public boolean isOverrun()
{
return (mIsOverrun);
} // end of isOverrun()
/**
* Returns {@code true} if this thread contains an eBus
* object which is overrunning the maximum run quantum
* at this very moment. A thread is overrunning if
*
* -
* object state is {@link ObjectState#ON_RQ_THREAD}
* and
*
* -
* the difference between now and the object
* on-thread start time is ≥ maximum time
* quantum plus the given overrun limit.
*
*
* The reason for an overrun limit is to prevent
* repeated alarms raised for minor quantum infractions.
* @param now current nanosecond timestamp.
* @param overrunLimit object must overrun maximum
* time quantum by this nanosecond amount before
* declaring an overrun.
* @return {@code true} if a maximum run quantum overrun
* is in progress.
*/
public boolean isOverrun(final long now,
final long overrunLimit)
{
final long delta = (now - mTimestamp);
final long limit = (mMaxQuantum + overrunLimit);
mIsOverrun = (mState == ObjectState.ON_RQ_THREAD &&
delta >= limit);
return (mIsOverrun);
} // end of isOverrun(long, long)
/**
* Returns {@code RQThreadUpdate} message associated with
* this {@code RQThread} stat. May return {@code null}.
* @return eBus notification message.
*/
@Nullable
public ENotificationMessage getUpdate()
{
return (mUpdate);
} // end of getUpdate()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Stores the {@code RQThreadUpdate} message posted
* for this current {@code RQThread} stat.
* @param update eBus notification message.
*/
public void setUpdate(final ENotificationMessage update)
{
mUpdate = update;
} // end of setUpdate(ENotificationMessage)
/**
* Updates the run-time stats for this run queue thread.
* @param runCount number of objects run so far.
* @param eObject eBus object. May be {@code null}.
* @param timestamp run start or stop time.
*/
private void updateStats(final long runCount,
final EObject eObject,
final long timestamp)
{
mRunCount = runCount;
mEObject = eObject;
mTimestamp = timestamp;
} // end of updateStats(long, EObject, long)
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class RQStats
/**
* This immutable class stores the dispatcher name,
* associated run queue, and maximum quantum for a given
* eBus client dispatcher. This class is used to create a new
* {@code EClient} instance.
*/
@SuppressWarnings ("PackageVisibleInnerClass")
/* package */ static final class DispatcherInfo
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* The unique dispatcher name.
*/
private final String mName;
/**
* Client task queue capacity.
*/
private final int mTaskQueueCapacity;
/**
* The dispatcher run queue.
*/
private final Queue mRunQueue;
/**
* References the method used to post a task.
*/
private final Consumer mDispatchHandle;
/**
* The maximum nanosecond run quantum for this
* dispatcher.
*/
private final long mMaxQuantum;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates Dispatcher information for the given
* configuration.
* @param config Dispatcher configuration.
*/
private DispatcherInfo(final EConfigure.Dispatcher config)
{
mName = config.name();
mTaskQueueCapacity = config.taskQueueCapacity();
mRunQueue = EClient.runQueue(config);
mDispatchHandle =
(config.dispatchType()).dispatchHandle();
mMaxQuantum = (config.quantum()).toNanos();
} // end of DispatcherInfo(EConfigure.Dispatcher)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns the dispatcher name.
* @return dispatcher name.
*/
public String name()
{
return (mName);
} // end of name()
/**
* Returns client task queue capacity.
* @return client task queue capacity.
*/
public int taskQueueCapacity()
{
return (mTaskQueueCapacity);
} // end of taskQueueCapacity()
/**
* Returns the dispatcher run queue.
* @return run queue.
*/
public Queue runQueue()
{
return (mRunQueue);
} // end of runQueue()
/**
* Returns the dispatch method handle.
* @return dispatch method handle.
*/
public Consumer dispatchHandle()
{
return (mDispatchHandle);
} // end of dispatchHandle()
/**
* Returns the dispatcher maximum nanosecond quantum.
* @return nanosecond time.
*/
public long maxQuantum()
{
return (mMaxQuantum);
} // end of maxQuantum()
//
// end of Get Methods.
//-------------------------------------------------------
} // end of class DispatcherInfo
/**
* This runnable is used to either start up or shutdown
* the given client.
*/
private static final class StartStopTask
implements Runnable
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Execute {@link #mTask} if-and-only-if the client's
* current state matches {@link #mInitialState}.
*/
private final EClient mClient;
/**
* The client start-up or shutdown task.
*/
private final Runnable mTask;
/**
* A client's current state must match this state in
* order to execute {@link #mTask}.
*/
private final ClientState mInitialState;
/**
* Set the client state to this value just before
* executing {@link #mTask}.
*/
private final ClientState mIntermediateState;
/**
* Set the client state to this value just after
* executing {@link #mTask}, regardless of whether the
* task succeeds or fails.
*/
private final ClientState mFinalState;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private StartStopTask(final EClient client,
final Runnable task,
final ClientState initState,
final ClientState betweenState,
final ClientState finalState)
{
mClient = client;
mTask = task;
mInitialState = initState;
mIntermediateState = betweenState;
mFinalState = finalState;
} // end of StartStopTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementation.
//
/**
* Executes each client's startup or shutdown method
* if the client is in the expected initial
* state.
*/
@Override
@SuppressWarnings ("AccessingNonPublicFieldOfAnotherObject")
public void run()
{
final EObject target = mClient.get();
if (target != null &&
mClient.mClientState == mInitialState)
{
mClient.mClientState = mIntermediateState;
sLogger.debug(
"{} client {} ({}).",
(mIntermediateState == ClientState.STARTING ?
"Starting" :
"Stopping"),
mClient.mClientId,
target.name());
try
{
mTask.run();
}
catch (Exception jex)
{
sLogger.warn(
"start-up/shutdown exception:", jex);
}
mClient.mClientState = mFinalState;
}
} // end of run()
//
// end of Runnable Interface Implementation.
//-------------------------------------------------------
} // end of class StartStopTask
/**
* Allows various methods to be substituted for
* a {@link Queue#poll()}.
*
* @param the {@code Queue} item type.
*/
@FunctionalInterface
private interface PollInterface
{
/**
* Returns the item removed from the queue's head.
* @return the queue's head or {@code null} if queue is
* empty.
*/
T poll();
} // end of interface PollInterface
/**
* Ping is responsible for posting a no-op task to all extant
* eBus clients on the chance that the client is marked as
* idle but has pending tasks on its queue (an invalid but
* possible state). Posting a no-op (or do nothing) task on
* the client's task queue will result in the client being
* posted to its run queue and finally getting a chance to
* process its pending tasks.
*/
private static final class Pinger
implements EObject
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Repeated ping eBus clients are this rate.
*/
private final Duration mPingRate;
/**
* Post {@link #NO_OP_TASK no-op task} to all extant
* clients when this timer expires.
*/
private IETimer mPingTimer;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private Pinger(final Duration pingRate)
{
mPingRate = pingRate;
} // end of Pinger(Duration)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// EObject Interface Implementation.
//
/**
* Returns {@link #PINGER_NAME}.
* @return {@link #PINGER_NAME}.
*/
@Override
public String name()
{
return (PINGER_NAME);
} // end of name()
/**
* Schedules repeating ping timer.
*/
@Override
public void startup()
{
mPingTimer =
sCoreExecutor.scheduleWithFixedDelay(
this::doPing, this, mPingRate, mPingRate);
sLogger.debug("{}: pinger started.", PINGER_NAME);
} // end of startup()
@Override
public void shutdown()
{
final IETimer timer = mPingTimer;
mPingTimer = null;
if (timer != null)
{
try
{
timer.close();
}
catch (Exception jex)
{
sLogger.debug(
"Error closing ping timer.", jex);
}
sLogger.debug("{}: pinger stopped.", PINGER_NAME);
}
} // end of shutdown()
//
// end of EObject Interface Implementation.
//-------------------------------------------------------
//-------------------------------------------------------
// Timer Expiration.
//
/**
* Dispatches a no-op task to each existing eBus client.
*/
private void doPing()
{
sClients.forEach(ec -> ec.dispatch(NO_OP_TASK));
} // end of doPing()
//
// end of Timer Expiration.
//-------------------------------------------------------
} // end of class Pinger
} // end of class EClient