/*------------------------------------------------------------------------------
Name: DispatchManager.java
Project: xmlBlaster.org
Copyright: xmlBlaster.org, see xmlBlaster-LICENSE file
------------------------------------------------------------------------------*/
package org.xmlBlaster.util.dispatch;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jacorb.security.sas.ISASContext;
import org.xmlBlaster.authentication.plugins.I_MsgSecurityInterceptor;
import org.xmlBlaster.client.I_XmlBlasterAccess;
import org.xmlBlaster.client.queuemsg.MsgQueueGetEntry;
import org.xmlBlaster.util.Global;
import org.xmlBlaster.util.I_Timeout;
import org.xmlBlaster.util.MsgUnit;
import org.xmlBlaster.util.SessionName;
import org.xmlBlaster.util.Timestamp;
import org.xmlBlaster.util.XmlBlasterException;
import org.xmlBlaster.util.def.Constants;
import org.xmlBlaster.util.def.ErrorCode;
import org.xmlBlaster.util.def.MethodName;
import org.xmlBlaster.util.dispatch.plugins.I_MsgDispatchInterceptor;
import org.xmlBlaster.util.error.I_MsgErrorHandler;
import org.xmlBlaster.util.error.MsgErrorInfo;
import org.xmlBlaster.util.plugin.PluginManagerBase;
import org.xmlBlaster.util.property.PropString;
import org.xmlBlaster.util.qos.address.AddressBase;
import org.xmlBlaster.util.qos.address.CallbackAddress;
import org.xmlBlaster.util.queue.I_Entry;
import org.xmlBlaster.util.queue.I_Queue;
import org.xmlBlaster.util.queue.I_QueueEntry;
import org.xmlBlaster.util.queue.I_QueuePutListener;
import org.xmlBlaster.util.queuemsg.MsgQueueEntry;
/**
* Manages the sending of messages and commands and does error recovery
* further we communicate with the dispatcher plugin if one is configured.
* <p />
* There is one instance of this class per queue and remote connection.
* @author xmlBlaster@marcelruff.info
*/
public final class DispatchManager implements I_Timeout, I_QueuePutListener
{
public final String ME;
private final Global glob;
private static Logger log = Logger.getLogger(DispatchManager.class.getName());
private final I_Queue msgQueue;
private final DispatchConnectionsHandler dispatchConnectionsHandler;
private final I_MsgErrorHandler failureListener;
private final I_MsgSecurityInterceptor securityInterceptor;
private final I_MsgDispatchInterceptor msgInterceptor;
private HashSet connectionStatusListeners;
private final String typeVersion;
/** If > 0 does burst mode */
private long collectTime = -1L;
private long toAliveTime = 0;
private long toPollingTime = 0;
private boolean dispatchWorkerIsActive = false;
/** The worker for synchronous invocations */
private DispatchWorker syncDispatchWorker;
private Timestamp timerKey = null;
private int notifyCounter = 0;
private boolean isShutdown = false;
private boolean isSyncMode = false;
private boolean trySyncMode = false; // true: client side queue embedding, false: server side callback queue
private boolean inAliveTransition = false;
private final Object ALIVE_TRANSITION_MONITOR = new Object();
private int burstModeMaxEntries = -1;
private long burstModeMaxBytes = -1L;
/** async delivery is activated only when this flag is 'true'. Used to temporarly inhibit dispatch of messages */
private boolean dispatcherActive = true;
private boolean shallCallToAliveSync;
private boolean inDispatchManagerCtor;
private SessionName sessionName;
/**
* @param msgQueue The message queue which i use (!!! TODO: this changes, we should pass it on every method where needed)
* @param connectionStatusListener The implementation which listens on connectionState events (e.g. XmlBlasterAccess.java), or null
* @param addrArr The addresses i shall connect to
*/
public DispatchManager(Global glob, I_MsgErrorHandler failureListener,
I_MsgSecurityInterceptor securityInterceptor,
I_Queue msgQueue, I_ConnectionStatusListener connectionStatusListener,
AddressBase[] addrArr, SessionName sessionName) throws XmlBlasterException {
if (failureListener == null || msgQueue == null)
throw new IllegalArgumentException("DispatchManager failureListener=" + failureListener + " msgQueue=" + msgQueue);
this.inDispatchManagerCtor = true;
this.ME = msgQueue.getStorageId().getId();
this.glob = glob;
this.sessionName = sessionName;
if (log.isLoggable(Level.FINE)) log.fine(ME+": Loading DispatchManager ...");
this.msgQueue = msgQueue;
this.failureListener = failureListener;
this.securityInterceptor = securityInterceptor;
this.dispatchConnectionsHandler = this.glob.createDispatchConnectionsHandler(this);
this.connectionStatusListeners = new HashSet();
if (connectionStatusListener != null) this.connectionStatusListeners.add(connectionStatusListener);
initDispatcherActive(addrArr);
/*
* Check i a plugin is configured ("DispatchPlugin/defaultPlugin")
* If configured, the plugin instance is searched in the Global scope
* and if none is found one is created (see DispatcherPluginManager)
* Default server setting is to use no dispatcher plugin
*/
PropString propString = new PropString(PluginManagerBase.NO_PLUGIN_TYPE); // "undef";
if (addrArr != null && addrArr.length > 0) // Check if client wishes a specific plugin
propString.setValue(addrArr[0].getDispatchPlugin());
this.typeVersion = propString.getValue();
this.msgInterceptor = glob.getDispatchPluginManager().getPlugin(this.typeVersion); // usually from cache
if (log.isLoggable(Level.FINE)) log.fine(ME+": DispatchPlugin/defaultPlugin=" + propString.getValue() + " this.msgInterceptor=" + this.msgInterceptor);
if (this.msgInterceptor != null) {
this.msgInterceptor.addDispatchManager(this);
if (log.isLoggable(Level.FINE)) log.fine(ME+": Activated dispatcher plugin '" + this.typeVersion + "'");
}
this.msgQueue.addPutListener(this); // to get putPre() and putPost() events
this.dispatchConnectionsHandler.initialize(addrArr);
this.inDispatchManagerCtor = false;
}
/**
* @return Never null
*/
public SessionName getSessionName() {
return this.sessionName;
}
public boolean isSyncMode() {
return this.isSyncMode;
}
/**
* Set behavior of dispatch framework.
* @param trySyncMode true: client side queue embedding, false: server side callback queue
* defaults to false
*/
public void trySyncMode(boolean trySyncMode) {
this.trySyncMode = trySyncMode;
switchToSyncMode();
}
/**
* Reconfigure dispatcher with given properties.
*
* Note that only a limited re-configuration is supported
* @param addressArr The new configuration
*/
public final void updateProperty(CallbackAddress[] addressArr) throws XmlBlasterException {
initDispatcherActive(addressArr);
this.dispatchConnectionsHandler.initialize(addressArr);
}
public void finalize() {
try {
removeBurstModeTimer();
//if (log.isLoggable(Level.FINE)) log.fine(ME+": finalize - garbage collected");
}
catch (Throwable e) {
e.printStackTrace();
}
try {
super.finalize();
}
catch (Throwable e) {
e.printStackTrace();
}
}
public I_Queue getQueue() {
return this.msgQueue;
}
/*
* Register yourself if you want to be informed about the remote connection status.
* @param connectionStatusListener The implementation which listens on connectionState events (e.g. XmlBlasterAccess.java)
* @return true if we did not already contain the specified element.
*/
public synchronized boolean addConnectionStatusListener(I_ConnectionStatusListener connectionStatusListener) {
return this.connectionStatusListeners.add(connectionStatusListener);
}
public synchronized boolean addConnectionStatusListener(I_ConnectionStatusListener connectionStatusListener, boolean fireInitial) {
if (connectionStatusListener == null) return true;
boolean ret = this.connectionStatusListeners.add(connectionStatusListener);
if (fireInitial) {
if (isDead())
connectionStatusListener.toDead(this, ConnectionStateEnum.DEAD, null/*"Initial call"*/);
else if (isPolling())
connectionStatusListener.toPolling(this, ConnectionStateEnum.POLLING);
else
connectionStatusListener.toAlive(this, ConnectionStateEnum.ALIVE);
}
return ret;
}
/**
* Remove the given listener
* @param connectionStatusListener
* @return true if it was removed
*/
public synchronized boolean removeConnectionStatusListener(I_ConnectionStatusListener connectionStatusListener) {
return this.connectionStatusListeners.remove(connectionStatusListener);
}
public synchronized I_ConnectionStatusListener[] getConnectionStatusListeners() {
if (this.connectionStatusListeners.size() == 0)
return new I_ConnectionStatusListener[0];
return (I_ConnectionStatusListener[])this.connectionStatusListeners.toArray(new I_ConnectionStatusListener[this.connectionStatusListeners.size()]);
}
/**
* The name in the configuration file for the plugin
* @return e.g. "Priority,1.0"
*/
public String getTypeVersion() {
return this.typeVersion;
}
/**
* @return The import/export encrypt handle or null if created by a SubjectInfo (no session info available)
*/
public I_MsgSecurityInterceptor getMsgSecurityInterceptor() {
return this.securityInterceptor;
}
/**
* @return The handler of all callback plugins, is never null
*/
public final DispatchConnectionsHandler getDispatchConnectionsHandler() {
return this.dispatchConnectionsHandler;
}
/**
* How many messages maximum shall the callback thread take in one bulk out of the
* callback queue and deliver in one bulk.
*/
public final int getBurstModeMaxEntries() {
return this.burstModeMaxEntries;
}
/**
* How many bytes maximum shall the callback thread take in one bulk out of the
* callback queue and deliver in one bulk.
*/
public final long getBurstModeMaxBytes() {
return this.burstModeMaxBytes;
}
/**
* Get timestamp when we went to ALIVE state.
* @return millis timestamp
*/
public final long getAliveSinceTime() {
return this.toAliveTime;
}
/**
* Get timestamp when we went to POLLING state.
* @return millis timestamp
*/
public final long getPollingSinceTime() {
return this.toPollingTime;
}
/**
* Call by DispatchConnectionsHandler on state transition
* NOTE: toAlive is called initially when a protocol plugin is successfully loaded
* but we don't know yet if it ever is able to connect
*/
void toAlive(ConnectionStateEnum oldState) {
if (log.isLoggable(Level.FINER)) log.finer(ME+": Switch from " + oldState + " to ALIVE");
// Remember the current collectTime
AddressBase addr = this.dispatchConnectionsHandler.getAliveAddress();
if (addr == null) {
log.severe(ME+": toAlive action has no alive address");
return;
}
try {
this.inAliveTransition = true;
if (this.toAliveTime <= this.toPollingTime) {
this.toAliveTime = System.currentTimeMillis();
}
this.burstModeMaxEntries = addr.getBurstModeMaxEntries();
this.burstModeMaxBytes = addr.getBurstModeMaxBytes();
synchronized (this.ALIVE_TRANSITION_MONITOR) {
// 1. We allow a client to intercept and for example destroy all entries in the queue
I_ConnectionStatusListener[] listeners = getConnectionStatusListeners();
for (int i=0; i<listeners.length; i++) {
listeners[i].toAlive(this, oldState);
}
// 2. If a dispatch plugin is registered it may do its work
if (this.msgInterceptor != null)
this.msgInterceptor.toAlive(this, oldState);
}
}
finally {
this.inAliveTransition = false;
}
collectTime = addr.getCollectTime(); // burst mode if > 0L
// 3. Deliver. Will be delayed if burst mode timer is activated, will switch to sync mode if necessary
activateDispatchWorker();
if (this.shallCallToAliveSync && !this.inDispatchManagerCtor && this.isSyncMode)
callToAliveSync();
}
public void reachedAliveSync(ConnectionStateEnum oldState, I_XmlBlasterAccess connection) {
I_ConnectionStatusListener[] listeners = getConnectionStatusListeners();
for (int i=0; i<listeners.length; i++) {
listeners[i].toAlive(this, oldState);
}
}
/** Call by DispatchConnectionsHandler on state transition */
void toPolling(ConnectionStateEnum oldState) {
if (isDead()) {
return;
}
if (log.isLoggable(Level.FINER)) log.finer(ME+": Switch from " + oldState + " to POLLING");
if (this.toPollingTime <= this.toAliveTime) {
this.toPollingTime = System.currentTimeMillis();
}
switchToASyncMode();
// 1. We allow a client to intercept and for example destroy all entries in the queue
I_ConnectionStatusListener[] listeners = getConnectionStatusListeners();
for (int i=0; i<listeners.length; i++) {
listeners[i].toPolling(this, oldState);
}
// 2. If a dispatch plugin is registered it may do its work
if (this.msgInterceptor != null)
this.msgInterceptor.toPolling(this, oldState);
}
/**
*
* @param ex
*/
public void toDead(XmlBlasterException ex) {
shutdownFomAnyState(ConnectionStateEnum.UNDEF, ex);
}
/** Call by DispatchConnectionsHandler on state transition */
void shutdownFomAnyState(ConnectionStateEnum oldState, XmlBlasterException ex) {
if (log.isLoggable(Level.FINER)) log.finer(ME+": Switch from " + oldState + " to DEAD");
if (oldState == ConnectionStateEnum.DEAD) return;
if (this.isShutdown) return;
if (ex != null) { // Very dangerous code! The caller ends up with changed Exception type
ex.changeErrorCode(ErrorCode.COMMUNICATION_NOCONNECTION_DEAD);
}
else {
ex = new XmlBlasterException(glob, ErrorCode.COMMUNICATION_NOCONNECTION_DEAD, ME,
"Switch from " + oldState + " to DEAD, reason is not known");
}
// 1. We allow a client to intercept and for example destroy all entries in the queue
I_ConnectionStatusListener[] listeners = getConnectionStatusListeners();
for (int i=0; i<listeners.length; i++) {
try {
// Only pass original ex.getMessage() - not the changed errorCode
listeners[i].toDead(this, oldState, ex);
}
catch (Throwable e) {
e.printStackTrace();
}
}
// 2. If a dispatch plugin is registered it may do its work
if (this.msgInterceptor != null)
this.msgInterceptor.toDead(this, oldState, ex);
if (oldState != ConnectionStateEnum.UNDEF)
givingUpDelivery(ex);
}
private void givingUpDelivery(XmlBlasterException ex) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Entering givingUpDelivery(), state is " + this.dispatchConnectionsHandler.getState());
removeBurstModeTimer();
boolean userThread = this.dispatchConnectionsHandler.isUserThread();
if (!userThread) { // If the client user thread it will receive the exception and handle it self
// The error handler flushed the queue and does error handling with them
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, (MsgQueueEntry)null, this, ex));
}
shutdown();
}
public void postSendNotification(MsgQueueEntry entry) {
MsgQueueEntry[] entries = new MsgQueueEntry[] { entry };
postSendNotification(entries);
}
public void postSendNotification(MsgQueueEntry[] entries) {
I_PostSendListener postSendListener = this.dispatchConnectionsHandler.getPostSendListener();
if (postSendListener != null) {
try {
postSendListener.postSend(entries);
}
catch (Throwable e) {
e.printStackTrace();
log.warning("postSendListener.postSend() exception: " + e.toString());
}
}
}
/**
* Notify I_PostSendListener about problem.
* <p>
* Typically XmlBlasterAccess is notified when message came asynchronously from queue
*
* @param entryList
* @param ex
* @return true if processed
* @see I_PostSendListener#postSend(MsgQueueEntry) for explanation
*/
public boolean sendingFailedNotification(MsgQueueEntry[] entries, XmlBlasterException ex) {
I_PostSendListener postSendListener = this.dispatchConnectionsHandler.getPostSendListener();
if (postSendListener == null)
return false;
try {
return postSendListener.sendingFailed(entries, ex);
}
catch (Throwable e) {
e.printStackTrace();
log.warning("postSendListener.sendingFailed() exception: " + e.toString());
return false;
}
}
/**
* Called by DispatchWorker if an Exception occured in sync mode
* Only on client side
*/
void handleSyncWorkerException(List<I_Entry> entryList, Throwable throwable) throws XmlBlasterException {
if (log.isLoggable(Level.FINER)) log.finer(ME+": Sync delivery failed connection state is " + this.dispatchConnectionsHandler.getState().toString() + ": " + throwable.toString());
XmlBlasterException xmlBlasterException = XmlBlasterException.convert(glob,ME,null,throwable);
if (isDead()) throw xmlBlasterException;
if (xmlBlasterException.isUser()) {
// Exception from remote client from update(), pass it to error handler and carry on ...?
// A PublishPlugin could throw it
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
getMsgErrorHandler().handleErrorSync(new MsgErrorInfo(glob, entries, this, xmlBlasterException));
return;
}
else if (xmlBlasterException.isCommunication()) {
if (this.msgInterceptor != null && isPolling()) { // If we have a plugin it shall handle it
try {
entryList = this.msgInterceptor.handleNextMessages(this, entryList);
if (entryList != null && entryList.size() > 0) {
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, entries, this, xmlBlasterException));
}
}
catch (XmlBlasterException xmlBlasterException2) {
internalError(xmlBlasterException2);
}
if (entryList != null && entryList.size() > 0) {
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, entries, this, xmlBlasterException));
}
return;
}
// Exception from connection to remote client (e.g. from Corba layer)
// DispatchManager handles this
// Error handling in sync mode
// 1. throwExceptionBackToPusher
// 2. Switch to async mode and collect message (wait on better times)
// 3. If we have serious problems (programming exceptions or isDead()) throw exception back
// 4. Pass exception to an error handler plugin
switchToASyncMode();
// Simulate return values, and manipulate missing informations into entries ...
I_QueueEntry[] entries = (I_QueueEntry[])entryList.toArray(new I_QueueEntry[entryList.size()]);
getDispatchConnectionsHandler().createFakedReturnObjects(entries, Constants.STATE_OK, Constants.INFO_QUEUED);
msgQueue.put(entries, I_Queue.IGNORE_PUT_INTERCEPTOR);
if (log.isLoggable(Level.FINE)) log.fine(ME+": Delivery failed, pushed " + entries.length + " entries into tail back queue");
}
else {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Invocation failed: " + xmlBlasterException.getMessage());
throw xmlBlasterException;
}
}
/**
* Messages are successfully sent, remove them now from queue (sort of a commit()):
* We remove filtered/destroyed messages as well (which doen't show up in entryListChecked)
* @param postSendNotify TODO
*/
public void removeFromQueue(MsgQueueEntry[] entries, boolean postSendNotify) throws XmlBlasterException {
I_MsgDispatchInterceptor msgInterceptor = getMsgDispatchInterceptor();
MsgUnit[] msgUnits = null;
if (msgInterceptor != null) { // we need to do this before removal since the msgUnits are weak references and would be deleted by gc
msgUnits = new MsgUnit[entries.length];
for (int i=0; i < msgUnits.length; i++) {
msgUnits[i] = entries[i].getMsgUnit();
}
}
this.msgQueue.removeRandom(entries);
/*(currently only done in sync invocation)
ArrayList defaultEntries = sendAsyncResponseEvent(entryList);
if (defaultEntries.size() > 0) {
MsgQueueEntry[] entries = (MsgQueueEntry[])defaultEntries.toArray(new MsgQueueEntry[defaultEntries.size()]);
this.msgQueue.removeRandom(entries);
}
*/
if (postSendNotify)
postSendNotification(entries);
if (msgInterceptor != null) {
msgInterceptor.postHandleNextMessages(this, msgUnits);
}
if (log.isLoggable(Level.FINE)) log.fine("Commit of successful sending of " +
entries.length + " messages done, current queue size is " +
this.msgQueue.getNumOfEntries() + " '" + entries[0].getLogId() + "'");
}
/**
* Called by DispatchWorker if an Exception occurred in async mode.
* @throws XmlBlasterException should never happen but is possible during removing entries from queue
*/
void handleWorkerException(List<I_Entry> entryList, Throwable throwable) throws XmlBlasterException {
// Note: The DispatchManager is notified about connection problems directly by its DispatchConnectionsHandler
// we don't need to take care of ErrorCode.COMMUNICATION*
if (log.isLoggable(Level.FINER)) log.finer(ME+": Async delivery failed connection state is " + this.dispatchConnectionsHandler.getState().toString() + ": " + throwable.toString());
//Thread.currentThread().dumpStack();
if (entryList == null) {
if (!this.isShutdown)
log.warning(ME+": Didn't expect null entryList in handleWorkerException() for throwable " + throwable.getMessage() + toXml(""));
return;
}
getDispatchStatistic().setLastDeliveryException(throwable.toString());
getDispatchStatistic().incrNumDeliveryExceptions(1);
if (throwable instanceof XmlBlasterException) {
XmlBlasterException ex = (XmlBlasterException)throwable;
if (log.isLoggable(Level.FINE)) log.fine(ME+": Invocation or callback failed: " + ex.getMessage());
if (ex.isUser()) {
// Exception from remote client from update(), pass it to error handler and carry on ...
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
boolean isHandled = sendingFailedNotification(entries, ex);
if (isHandled)
removeFromQueue(entries, false);
else
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, entries, this, ex));
}
else if (ex.isCommunication()) {
if (this.msgInterceptor != null) { // If we have a plugin it shall handle it
if (isPolling()) { // is this code really invoked ? Note of Michele Laghi on 2007-12-19
try {
entryList = this.msgInterceptor.handleNextMessages(this, entryList);
if (entryList != null && entryList.size() > 0) {
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, entries, this, ex));
}
}
catch (XmlBlasterException ex2) {
internalError(ex2);
}
if (entryList != null && entryList.size() > 0) {
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, entries, this, ex));
}
}
if (msgInterceptor != null) { // we want the exception notification at least
msgInterceptor.onDispatchWorkerException(this, ex);
}
}
// Exception from connection to remote client (e.g. from Corba layer)
// DispatchManager handles this
}
else {
//log.severe(ME+": Callback failed: " + ex.toString());
//ex.printStackTrace();
MsgQueueEntry[] entries = (MsgQueueEntry[])entryList.toArray(new MsgQueueEntry[entryList.size()]);
boolean isHandled = sendingFailedNotification(entries, ex);
if (isHandled)
removeFromQueue(entries, false);
else
internalError(ex);
}
}
else {
//log.severe(ME+": Callback failed: " + throwable.toString());
//throwable.printStackTrace();
XmlBlasterException ex = new XmlBlasterException(glob, ErrorCode.COMMUNICATION_NOCONNECTION_DEAD, ME, "", throwable);
// sendingFailedNotification() not called as the msgs remain in queue until problem is resolved by admin
internalError(ex);
}
}
public I_MsgErrorHandler getMsgErrorHandler() {
return this.failureListener;
}
/**
* We register a QueuePutListener and all put() into the queue are
* intercepted - our put() is called instead.
* We then deliver this QueueEntry directly to the remote
* connection and return synchronously the returned value or the
* Exception if one is thrown.
*/
public void switchToSyncMode() {
if (this.isSyncMode) return;
synchronized (this) {
if (this.isSyncMode) return;
if (this.syncDispatchWorker == null) this.syncDispatchWorker = new DispatchWorker(glob, this);
this.isSyncMode = true;
if (this.timerKey != null)
log.severe(ME+": Burst mode timer was activated and we switched to synchronous delivery" +
" - handling of this situation is not coded yet");
removeBurstModeTimer();
boolean isAlive = isAlive();
log.info(ME+": Switched to synchronous message delivery, inAliveTransition=" + this.inAliveTransition + " isAlive=" + isAlive + " trySyncMode=" + this.trySyncMode);
if (isAlive) { // For FailSafePing
if (this.inAliveTransition) { // For FailSafeAsync
this.shallCallToAliveSync = true;
}
else {
callToAliveSync();
}
}
}
}
private void callToAliveSync() {
this.shallCallToAliveSync = false;
I_ConnectionStatusListener[] listeners = getConnectionStatusListeners();
for (int i=0; i<listeners.length; i++)
listeners[i].toAliveSync(this, ConnectionStateEnum.ALIVE);
}
/**
* Switch back to asynchronous mode.
* Our thread pool will take the messages out of the queue
* and deliver them in asynchronous mode.
*/
public void switchToASyncMode() {
if (!this.isSyncMode) return;
synchronized (this) {
if (!this.isSyncMode) return;
//this.msgQueue.removePutListener(this);
this.isSyncMode = false;
activateDispatchWorker(); // just in case there are some messages pending in the queue
log.info(ME+": Switched to asynchronous message delivery");
}
}
/**
* @see I_QueuePutListener#putPre(I_QueueEntry)
*/
public boolean putPre(I_QueueEntry queueEntry) throws XmlBlasterException {
//I_QueueEntry[] queueEntries = new I_QueueEntry[1];
//queueEntries[0] = queueEntry;
return putPre(new I_QueueEntry[] { queueEntry });
}
/**
* @see #putPre(I_QueueEntry)
* @see I_QueuePutListener#putPre(I_QueueEntry[])
*/
public boolean putPre(I_QueueEntry[] queueEntries) throws XmlBlasterException {
if (!this.isSyncMode) {
/*
for (int i=0; i < queueEntries.length; i++) {
if (queueEntries[i] instanceof MsgQueueEntry) {
MsgQueueEntry msgQueueEntry = (MsgQueueEntry)queueEntries[i];
if (MethodName.SUBSCRIBE == msgQueueEntry.getMethodName()) {
if (getSessionName().getPublicSessionId() < 1) {
// we should never allow a subscription without a positive sessionId if the
// server is not accessible
throw new XmlBlasterException(glob, ErrorCode.RESOURCE_TEMPORARY_UNAVAILABLE, ME,
"Manager: The Subscription for '" + getSessionName().toString() + "' failed since the server is currently not available");
}
}
}
}
*/
if (this.inAliveTransition) {
// Do not allow other threads to put messages to queue during transition to alive
synchronized (ALIVE_TRANSITION_MONITOR) {
// don't allow
}
}
return true; // Add entry to queue
}
if (log.isLoggable(Level.FINE)) log.fine(ME+": putPre() - Got " + queueEntries.length + " QueueEntries to deliver synchronously ...");
ArrayList entryList = new ArrayList(queueEntries.length);
for (int ii=0; ii<queueEntries.length; ii++) {
if (this.trySyncMode && !this.isSyncMode && queueEntries[ii] instanceof MsgQueueGetEntry) { // this.trySyncMode === isClientSide
throw new XmlBlasterException(this.glob, ErrorCode.RESOURCE_UNAVAILABLE, ME, "You can't call get() in asynchronous mode (gets can't be queued because we don't know its return value)");
}
entryList.add(queueEntries[ii]);
}
this.syncDispatchWorker.run(entryList);
return false;
}
/**
* @see I_QueuePutListener#putPost(I_QueueEntry)
*/
public void putPost(I_QueueEntry queueEntry) throws XmlBlasterException {
if (!this.isSyncMode) {
if (this.dispatcherActive) notifyAboutNewEntry();
if (((MsgQueueEntry)queueEntry).wantReturnObj()) {
// Simulate return values, and manipulate missing informations into entries ...
I_QueueEntry[] entries = new I_QueueEntry[] { queueEntry };
getDispatchConnectionsHandler().createFakedReturnObjects(entries, Constants.STATE_OK, Constants.INFO_QUEUED);
}
}
}
/**
* @see #putPost(I_QueueEntry)
* @see I_QueuePutListener#putPost(I_QueueEntry[])
*/
public void putPost(I_QueueEntry[] queueEntries) throws XmlBlasterException {
if (!this.isSyncMode && !this.inAliveTransition) {
if (this.dispatcherActive) notifyAboutNewEntry();
if (queueEntries.length > 0 && ((MsgQueueEntry)queueEntries[0]).wantReturnObj()) {
// Simulate return values, and manipulate missing informations into entries ...
getDispatchConnectionsHandler().createFakedReturnObjects(queueEntries, Constants.STATE_OK, Constants.INFO_QUEUED);
}
}
}
/**
* Here we prepare messages which are coming directly from the queue.
* <ol>
* <li>We eliminate destroyed messages</li>
* <li>We make a shallow copy of the message.
* We need to do this, out messages are references directly into the queue.
* The delivery framework is later changing the QoS
* and plugins may change the content - and this should not modify the queue entries</li>
* </ol>
*/
public ArrayList prepareMsgsFromQueue(List<I_Entry> entryList) {
if (entryList == null || entryList.size() < 1) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Got zero messages from queue, expected at least one, can happen if client disconnected in the mean time: " + toXml(""));
return null;
}
return prepareMsgsFromQueue(ME, log, this.msgQueue, entryList);
}
public static ArrayList prepareMsgsFromQueue(String logId, Logger log, I_Queue queue, List<I_Entry> entryList) {
// Remove all expired messages and do a shallow copy
int size = entryList.size();
ArrayList result = new ArrayList(size);
for (int ii=0; ii<size; ii++) {
MsgQueueEntry entry = (MsgQueueEntry)entryList.get(ii);
// Take care to remove the filtered away messages from the queue as well
if (entry.isDestroyed()) {
log.info(logId+": Message " + entry.getLogId() + " is destroyed, ignoring it");
if (log.isLoggable(Level.FINE)) log.fine("Message " + entry.getLogId() + " is destroyed, ignoring it: " + entry.toXml());
try {
queue.removeRandom(entry); // Probably change to use [] for better performance
}
catch (Throwable e) {
log.severe(logId+": Internal error when removing expired message " + entry.getLogId() + " from queue, no recovery implemented, we continue: " + e.toString());
}
continue;
}
result.add(entry.clone()); // expired messages are sent as well
}
return result;
}
/**
* When somebody puts a new entry into the queue, we want to be
* notified about this after the entry is fed.
* <p>
* Called by I_Queue.putPost()
*/
public void notifyAboutNewEntry() {
if (log.isLoggable(Level.FINER)) log.finer(ME+": Entering notifyAboutNewEntry("+this.notifyCounter+")");
this.notifyCounter++;
//activateDispatchWorker();
if (checkSending(true) == false)
return;
if (useBurstModeTimer() == true)
return;
startWorkerThread(false);
}
/**
* Counts how often a new entry was added since the current worker thread was started.
*/
public int getNotifyCounter() {
return this.notifyCounter;
}
/**
* Give the callback worker thread a kick to deliver the messages.
* Throws no exception.
*/
private void activateDispatchWorker() {
if (checkSending(false) == false)
return;
if (useBurstModeTimer() == true)
return;
startWorkerThread(false);
}
/**
* @return true if a burst mode timer was activated
*/
private boolean useBurstModeTimer() {
if (collectTime <= 0L) return false;
// Messages are sent delayed on timeout (burst mode)
if (log.isLoggable(Level.FINE)) log.fine(ME+": Executing useBurstModeTimer() collectTime=" + collectTime + " dispatchWorkerIsActive=" + dispatchWorkerIsActive);
synchronized (this) {
if (this.isShutdown) return false;
if (this.timerKey == null) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Starting burstMode timer with " + collectTime + " msec");
this.timerKey = this.glob.getBurstModeTimer().addTimeoutListener(this, collectTime, null);
}
}
return true;
}
/**
* Remove the burst mode timer
*/
private void removeBurstModeTimer() {
synchronized (this) {
if (this.timerKey != null) {
this.glob.getBurstModeTimer().removeTimeoutListener(timerKey);
this.timerKey = null;
}
}
}
/**
* @param fromTimeout for logging only
*/
private void startWorkerThread(boolean fromTimeout) {
if (this.dispatchWorkerIsActive == false) {
synchronized (this) {
if (this.isShutdown) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": startWorkerThread() failed, we are shutdown: " + toXml(""));
return;
}
if (this.dispatchWorkerIsActive == false) { // send message directly
this.dispatchWorkerIsActive = true;
this.notifyCounter = 0;
try {
this.glob.getDispatchWorkerPool().execute(new DispatchWorker(glob, this));
}
catch (Throwable e) {
this.dispatchWorkerIsActive = false;
log.severe(ME+": Unexpected error occurred: " + e.toString());
e.printStackTrace();
}
}
}
return;
}
if (fromTimeout) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Burst mode timeout occurred, last callback worker thread is not finished - we do nothing (the worker thread will give us a kick)");
}
else {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Last callback worker thread is not finished - we do nothing (the worker thread will give us a kick)");
}
}
public boolean isDead() {
return this.dispatchConnectionsHandler.isDead();
}
public boolean isPolling() {
return this.dispatchConnectionsHandler.isPolling();
}
public boolean isAlive() {
return this.dispatchConnectionsHandler.isAlive();
}
/**
* Can be called when client connection is lost (NOT the callback connection).
* Currently only detected by the SOCKET protocol plugin.
* Others can only detect lost clients with their callback protocol pings
*/
public void lostClientConnection() {
log.warning(ME+": Lost client connection");
// If SOCKET: the cb connection is lost as well and we can go to polling mode
pingCallbackServer(false);
}
public boolean pingCallbackServer(boolean sync) {
DispatchConnection dispatchConnection = this.dispatchConnectionsHandler.getCurrentDispatchConnection();
if (dispatchConnection != null) {
if (sync) {
dispatchConnection.timeout(null); // force a ping
}
else {
// force a ping via another thread
this.glob.getPingTimer().addTimeoutListener(dispatchConnection, 0L, null);
}
return true;
}
return false;
}
/**
* @param isPublisherThread We take care that the publisher thread, coming through putPost()
* does never too much work to return fast enough and avoid possible dead locks.
* @return true is status is OK and we can try to send a message
*/
private boolean checkSending(boolean isPublisherThread) {
if (this.isShutdown) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": The dispatcher is shutdown, can't activate callback worker thread" + toXml(""));
return false; // assert
}
if (this.isSyncMode) {
return false;
}
if (!this.dispatcherActive) {
return false;
}
if (msgQueue.isShutdown() && !isPublisherThread) { // assert
if (log.isLoggable(Level.FINE)) log.fine(ME+": The queue is shutdown, can't activate callback worker thread.");
// e.g. client has disconnected on the mean time.
//Thread.currentThread().dumpStack();
shutdown();
return false;
}
if (this.dispatchConnectionsHandler.isUndef()) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Not connected yet, state is UNDEF");
return false;
}
if (this.dispatchConnectionsHandler.isDead() && !isPublisherThread) {
String text = "No recoverable remote connection available, giving up queue " + msgQueue.getStorageId() + ".";
if (log.isLoggable(Level.FINE)) log.fine(ME+": "+text);
givingUpDelivery(new XmlBlasterException(glob,ErrorCode.COMMUNICATION_NOCONNECTION_DEAD, ME, text));
return false;
}
if (msgQueue.getNumOfEntries() == 0L) {
return false;
}
if (this.msgInterceptor != null) {
if (this.msgInterceptor.doActivate(this) == false) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": this.msgInterceptor.doActivate==false");
return false; // A plugin told us to suppress sending the message
}
return true;
}
/*
* The msgInterceptor plugin needs to have a chance to take care of this even in polling mode
*/
if (this.dispatchConnectionsHandler.isPolling()) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Can't send message as connection is lost and we are polling");
return false;
}
return true;
}
/**
* We are notified about the burst mode timeout through this method.
* @param userData You get bounced back your userData which you passed
* with Timeout.addTimeoutListener()
*/
public void timeout(Object userData) {
this.timerKey = null;
if (log.isLoggable(Level.FINE)) log.fine(ME+": Burst mode timeout occurred, queue entries=" + msgQueue.getNumOfEntries() + ", starting callback worker thread ...");
startWorkerThread(true);
}
/**
* @return The interceptor plugin if available, otherwise null
*/
public I_MsgDispatchInterceptor getMsgDispatchInterceptor() {
return this.msgInterceptor;
}
/**
* Set new callback addresses, typically after a session login/logout
*/
public void setAddresses(AddressBase[] addr) throws XmlBlasterException {
this.dispatchConnectionsHandler.initialize(addr);
}
/**
* Switch on/off the sending of messages.
*/
private void initDispatcherActive(AddressBase[] addrArr) {
if (addrArr != null) {
for (int ii=0; ii<addrArr.length; ii++) { // TODO: How to handle setting of multiple addresses??
this.dispatcherActive = addrArr[ii].isDispatcherActive();
}
}
}
/**
* The worker notifies us that it is finished, if messages are available
* it is triggered again.
*/
void setDispatchWorkerIsActive(boolean val) {
this.dispatchWorkerIsActive = val;
if (val == false) {
if (this.isShutdown) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": setDispatchWorkerIsActive(" + val + ") failed, we are shutdown: " + toXml(""));
return;
}
if (msgQueue.getNumOfEntries() > 0) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Finished callback job. Giving a kick to send the remaining " + msgQueue.getNumOfEntries() + " messages.");
try {
activateDispatchWorker();
}
catch(Throwable e) {
log.severe(ME+": "+e.toString()); e.printStackTrace(); // Assure the queue is flushed with another worker
}
}
else {
if (this.trySyncMode && !this.isSyncMode) {
switchToSyncMode();
}
}
}
}
/**
* Called locally and from TopicHandler when internal error (Throwable) occurred to avoid infinite looping
*/
public void internalError(Throwable throwable) {
givingUpDelivery((throwable instanceof XmlBlasterException) ? (XmlBlasterException)throwable :
new XmlBlasterException(glob, ErrorCode.COMMUNICATION_NOCONNECTION_DEAD, ME, "", throwable));
log.severe(ME+": PANIC: Internal error, doing shutdown: " + throwable.getMessage());
shutdown();
}
/**
* @return A container holding some statistical delivery information
*/
public DispatchStatistic getDispatchStatistic() {
return this.dispatchConnectionsHandler.getDispatchStatistic();
}
public boolean isShutdown() {
return this.isShutdown;
}
/**
* Stop all callback drivers of this client.
* Possibly invoked twice (givingUpDelivery() calls it indirectly as well)
* We don't shutdown the corresponding queue.
*/
public void shutdown() {
if (log.isLoggable(Level.FINER)) log.finer(ME+": Entering shutdown ...");
if (this.isShutdown) return;
synchronized (this) {
if (this.isShutdown) return;
this.isShutdown = true;
this.msgQueue.removePutListener(this);
// remove all ConnectionStatusListeners
this.connectionStatusListeners.clear();
removeBurstModeTimer();
// NOTE: We would need to remove the 'final' qualifier to be able to set to null
if (this.msgInterceptor != null) {
try {
this.msgInterceptor.shutdown(this);
}
catch (XmlBlasterException e) {
log.warning(ME+": Ignoring problems during shutdown of plugin: " + e.getMessage());
}
//this.msgInterceptor = null;
}
if (this.dispatchConnectionsHandler != null) {
this.dispatchConnectionsHandler.shutdown();
//this.dispatchConnectionsHandler = null;
}
//this.msgQueue = null;
//this.failureListener = null;
//this.securityInterceptor = null;
//if (this.dispatchWorkerPool != null) {
// this.dispatchWorkerPool.shutdown(); NO: not here, is the scope and duty of Global
// this.dispatchWorkerPool = null;
//}
if (this.syncDispatchWorker != null)
this.syncDispatchWorker.shutdown();
}
}
/**
* For logging
*/
public String getId() {
return this.msgQueue.getStorageId().getId();
}
/**
* Dump state of this object into a XML ASCII string.
* <br>
* @param extraOffset indenting of tags for nice output
* @return internal state as a XML ASCII string
*/
public String toXml(String extraOffset) {
StringBuffer sb = new StringBuffer(2000);
if (extraOffset == null) extraOffset = "";
String offset = Constants.OFFSET + extraOffset;
sb.append(offset).append("<DispatchManager id='").append(getId());
if (this.msgQueue != null)
sb.append("' numEntries='").append(this.msgQueue.getNumOfEntries());
sb.append("' isShutdown='").append(this.isShutdown).append("'>");
sb.append(this.dispatchConnectionsHandler.toXml(extraOffset+Constants.INDENT));
sb.append(offset).append(" <dispatchWorkerIsActive>").append(dispatchWorkerIsActive).append("</dispatchWorkerIsActive>");
sb.append(offset).append("</DispatchManager>");
return sb.toString();
}
/**
* Inhibits/activates the delivery of asynchronous dispatches of messages.
* @param dispatcherActive
*/
public void setDispatcherActive(boolean dispatcherActive) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Changed dispatcherActive from " + this.dispatcherActive + " to " + dispatcherActive);
this.dispatcherActive = dispatcherActive;
if (this.dispatcherActive) notifyAboutNewEntry();
}
/**
*
* @return true if the dispacher is currently activated, i.e. if it is
* able to deliver asynchronousy messages from the callback queue.
*/
public boolean isDispatcherActive() {
return this.dispatcherActive;
}
public ArrayList filterDistributorEntries(ArrayList entries, Throwable ex) {
return this.dispatchConnectionsHandler.filterDistributorEntries(entries, ex);
}
}