All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy