
net.sf.eBus.client.EClient Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright 2014 - 2016. Charles W. Rapp
// All Rights Reserved.
//
package net.sf.eBus.client;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.DispatcherType;
import net.sf.eBus.config.ENetConfigure;
import net.sf.eBus.config.ThreadType;
import net.sf.eBus.util.IndexPool;
import net.sf.eBus.util.Properties;
/**
* {@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. {@link 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 {@link 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
{
LOCAL (0x1, "local"),
REMOTE (0x2, "remote");
//-----------------------------------------------------------
// 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._description = text;
} // end of ClientLocation(int, String)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (_description);
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
/**
* The location bit mask used with
* {@link EFeed.FeedScope}.
*/
public final int mask;
/**
* Human-readable text describing this zone.
*/
private final String _description;
} // end of enum ClientLocation
/**
* 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
/**
* Defines the eBus client run states. The client run
* state changes as tasks are posted and removed from the
* client task queue.
*/
private 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 {@link 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
//---------------------------------------------------------------
// 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-";
//-----------------------------------------------------------
// Statics.
//
/**
* 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 sRunQueues;
/**
* Maps an eBus client class to its assigned run queue.
*/
private static final Map, DispatcherInfo> sDispatchers;
/**
* 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;
// Static initialization block.
static
{
final String configFile =
System.getProperty(EConfigure.CONFIG_FILE_ENV);
EConfigure eConfig = null;
Map dispatchers =
new HashMap<>();
DispatcherInfo info;
int index;
String name;
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();
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"EClient: removing eBus client %d.",
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();
sLogger = Logger.getLogger(EClient.class.getName());
if (configFile != null && !configFile.isEmpty())
{
try
{
final Properties props =
Properties.loadProperties(configFile);
ENetConfigure.load(props);
eConfig = EConfigure.load(props);
dispatchers = eConfig.dispatchers();
}
catch (IOException |
IllegalArgumentException |
MissingResourceException |
NullPointerException jex)
{
sLogger.log(
Level.WARNING,
"Failure to load eBus configuration:",
jex);
}
}
sRunQueues = new HashMap<>();
sDispatchers = new HashMap<>();
// 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.put(
DEFAULT_DISPATCHER,
builder.name(DEFAULT_DISPATCHER)
.dispatcherType(DispatcherType.EBUS)
.threadType(ThreadType.BLOCKING)
.spinLimit(0L)
.parkTime(0L)
.priority(Thread.NORM_PRIORITY)
.quantum(EConfigure.DEFAULT_QUANTUM)
.numberThreads(numProcs)
.isDefault(true)
.build());
}
for (EConfigure.Dispatcher dispatcher :
dispatchers.values())
{
info = new DispatcherInfo(dispatcher);
sRunQueues.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, info);
}
}
// Start the eBus Dispatchers running.
for (index = 0;
index < dispatcher.numberThreads();
++index)
{
name =
(EBUS_THREAD_NAME_PREFIX +
dispatcher.name() +
"-" +
index);
(new RQThread(name,
info.runQueue(),
dispatcher)).start();
}
}
if (eConfig != null)
{
// Open eBus servers and connections.
try
{
EServer.configure(eConfig);
ERemoteApp.configure(eConfig);
}
catch (IOException ioex)
{
sLogger.log(
Level.WARNING,
"Failure to open eBus remote connections:",
ioex);
}
}
} // 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 the eBus client's currently active feeds. If the
* client is finalized, all active feeds are immediately
* closed.
*/
private final List mFeeds;
/**
* 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;
/**
* 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;
//
// 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 RunState 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;
//---------------------------------------------------------------
// 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 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 maxQuantum maximum nanosecond run quantum for this
* client.
* @param initialState eBus client initial state.
*/
private EClient(final EObject target,
final int clientId,
final ClientLocation location,
final Runnable startCb,
final Runnable shutdownCb,
final Queue runQueue,
final Consumer handle,
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);
mMaxQuantum = maxQuantum;
mQuantum = maxQuantum;
mFeedIdPool = new IndexPool();
mFeeds = new ArrayList<>();
mTasks = new ArrayDeque<>();
mRunState = RunState.IDLE;
mClientState = initialState;
} // 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 client. May return {@code null} if the
* target client is finalized.
* @return eBus client.
*/
public EObject target()
{
return (this.get());
} // end of target()
/**
* 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 the target object class.
* @return target object class.
*/
/* package */ Class> targetClass()
{
return (mTargetClass);
} // end of targetClass()
/**
* Returns {@code true} if the client is local to this JVM
* and {@code false} if the client is a proxy for a remote
* eBus client.
* @return {@code true} if the client is local.
*/
/* package */ boolean isLocal()
{
return (mLocation == ClientLocation.LOCAL);
} // end of isLocal()
/**
* 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);
return;
} // end of returnFeedId(int)
/**
* Returns {@code true} if {@code client} is a known eBus
* client and {@code false} otherwise.
* @param client check if this object is a known eBus client.
* @return {@code true} if {@code client} is known to eBus.
*/
/* package */ static boolean hasClient(final EObject client)
{
boolean retcode = false;
synchronized (sClients)
{
final Iterator cit = sClients.iterator();
while (cit.hasNext() && !retcode)
{
retcode = (client == (cit.next()).get());
}
}
return (retcode);
} // end of hasClient(EObject)
/**
* 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}.
*/
/* package */ 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 set of all currently registered eBus clients.
* The returned set is a copy of the actual client list.
* @return currently registered eBus clients.
*/
/* package */ static List getClients()
{
final List retval;
synchronized (sClients)
{
retval = new ArrayList<>(sClients);
}
return (retval);
} // end of getClients();
/**
* 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.
//
/**
* Adds the specified feed to the client feed list.
* @param feed adds this feed to the client feed list.
*
* @see #addFeed(EAbstractMultikeyFeed)
* @see #removeFeed(EFeed)
*/
/* package */ synchronized void addFeed(final EFeed feed)
{
mFeeds.add(feed);
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: reference count %,d.",
mTargetClass.getName(),
mFeeds.size()));
}
return;
} // end of addFeed(EFeed)
/**
* Removes the specified feed from the eBus client feed list.
* @param feed remove this feed from the eBus client feed
* list.
*
* @see #addFeed(EFeed)
* @see #removeFeed(EAbstractMultikeyFeed)
*/
/* package */ synchronized void removeFeed(final EFeed feed)
{
mFeeds.remove(feed);
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: reference count %,d.",
mTargetClass.getName(),
mFeeds.size()));
}
return;
} // end of removeFeed()
//
// end of Set methods.
//-----------------------------------------------------------
/**
* 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.runQueue(),
info.dispatchHandle(),
info.maxQuantum(),
ClientState.STARTED);
sClients.add(retval);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"EClient: created client %d -> %s",
retval.clientId(),
client));
}
}
}
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
* {@link 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);
return;
} // end of dispatch(Runnable, EObject)
/**
* Posts the task to the client execution queue. This is
* performed indirected by the method referenced by the
* dispatch handle.
* @param task post this task to the client queue.
*/
/* package */ synchronized void dispatch(final Runnable task)
{
mDispatchHandle.accept(task);
return;
} // end of dispatch(Runnable)
/**
* 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.
if (!mTasks.offer(task))
{
sLogger.warning(
String.format(
"client %d: failed to add %s to task queue.",
mClientId,
(task.getClass()).getName()));
}
// If this client is currently idle, then change it
// to ready and post it to the dispatch table.
else if (mRunState == RunState.IDLE)
{
setState(RunState.READY);
mRunQueue.offer(this);
}
// 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.
return;
} // 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}.
* @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.
*/
private synchronized Runnable nextTask(final long timeUsed)
{
Runnable retval = null;
mQuantum -= timeUsed;
// Is this client still alive?
if (mRunState == RunState.DEFUNCT)
{
// no-op.
}
// Are there any more tasks to run?
else if (mTasks.isEmpty())
{
// No tasks. The client is still active but idle.
setState(RunState.IDLE);
// Reset the quantum as well so we have a fresh start
// when the next task is posted.
mQuantum = mMaxQuantum;
}
// 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.
setState(RunState.READY);
// Reset this client's quantum to the allowed
// maximum.
mQuantum = mMaxQuantum;
// 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
{
setState(RunState.RUNNING);
retval = mTasks.poll();
}
return (retval);
} // end of nextTask(long)
/**
* Sets the client run state, logging this fact.
* @param nextState set the client run state to this value.
*/
private void setState(final RunState nextState)
{
mRunState = nextState;
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"client %d: run state %s.",
mClientId,
mRunState));
}
return;
} // end of setState(RunState)
/**
* 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 Class> clazz = client.getClass();
return ((sDispatchers.containsKey(clazz) ?
sDispatchers.get(clazz) :
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)
throws IllegalStateException
{
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.runQueue(),
info.dispatchHandle(),
info.maxQuantum(),
ClientState.NOT_STARTED);
sClients.add(retval);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"EClient: created client %d -> %s",
retval.clientId(),
client));
}
}
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 (sRunQueues.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));
});
return;
} // 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));
});
return;
} // end of shutdown(List<>)
/**
* When an eBus client is finalized, close any and all active
* feeds associated with that defunct client, dispose of
* all unexecuted tasks, and mark this client as defunct.
*/
private synchronized void cleanUp()
{
mFeeds.stream().forEach(feed -> feed.close());
mFeeds.clear();
mTasks.clear();
setState(RunState.DEFUNCT);
return;
} // end of removeClient(EClient)
/**
* Returns the Dispatcher run queue associated with the
* dispatcher and thread types.
* @param config Dispatcher configuration.
* @return Dispatcher run queue.
*/
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 ConcurrentLinkedQueue<>();
}
}
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<>)
//---------------------------------------------------------------
// 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.
*
*/
private static final class RQThread
extends Thread
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// 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 #yieldingPoll()} poll method is used.
*/
private final long mSpinLimit;
/**
* Nanosecond park time used when
* {@link #spinSleepPoll()} poll method is used.
*/
private final long mParkTime;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private RQThread(final String name,
final Queue runQueue,
final EConfigure.Dispatcher config)
{
super (name);
mRunQueue = runQueue;
mSpinLimit = config.spinLimit();
mParkTime = config.parkTime();
switch (config.runQueueType())
{
case BLOCKING:
mPollMethod = this::blockingPoll;
break;
case SPINNING:
mPollMethod = this::spinningPoll;
break;
case SPINPARK:
mPollMethod = this::spinSleepPoll;
break;
default:
mPollMethod = this::yieldingPoll;
}
this.setDaemon(true);
} // end of Dispatcher(String, 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.)
*
*/
@Override
public void run()
{
EClient client;
Runnable task;
long startTime;
long timeUsed;
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: running.", this.getName()));
}
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: polling run queue %s.",
this.getName(),
mRunQueue));
}
// Keep going until perdition.
while (true)
{
// Get the next client.
if ((client = mPollMethod.poll()) != null)
{
timeUsed = 0L;
// 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.
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: executing client %d, %s.",
this.getName(),
client.clientId(),
(task.getClass()).getName()));
}
// 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 has not tasks or its
// quantum is used up.
}
}
}
// Point of no return.
} // end of run()
//
// end of Thread Method Overrides.
//-------------------------------------------------------
/**
* 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}.
*/
public 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();
}
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 yieldingPoll()
{
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();
}
return (retval);
} // end of yieldingPoll()
} // end of class Dispatcher
/**
* 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;
/**
* 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();
mRunQueue = EClient.runQueue(config);
mDispatchHandle =
(config.dispatchType()).dispatchHandle();
mMaxQuantum = config.quantum();
} // 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 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;
try
{
mTask.run();
}
catch (Exception jex)
{
sLogger.log(Level.WARNING,
"start-up/shutdown exception:",
jex);
}
mClient.mClientState = mFinalState;
}
return;
} // 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
} // end of class EClient