/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.communication.connection.impl;
import static de.rcenvironment.core.communication.channel.MessageChannelState.MARKED_AS_BROKEN;
import static de.rcenvironment.core.communication.connection.api.ConnectionSetupState.CONNECTED;
import static de.rcenvironment.core.communication.connection.api.ConnectionSetupState.CONNECTING;
import static de.rcenvironment.core.communication.connection.api.ConnectionSetupState.DISCONNECTED;
import static de.rcenvironment.core.communication.connection.api.ConnectionSetupState.DISCONNECTING;
import static de.rcenvironment.core.communication.connection.api.ConnectionSetupState.WAITING_TO_RECONNECT;
import static de.rcenvironment.core.communication.connection.api.DisconnectReason.ACTIVE_SHUTDOWN;
import static de.rcenvironment.core.communication.connection.api.DisconnectReason.ERROR;
import static de.rcenvironment.core.communication.connection.api.DisconnectReason.FAILED_TO_AUTO_RECONNECT;
import static de.rcenvironment.core.communication.connection.api.DisconnectReason.FAILED_TO_CONNECT;
import static de.rcenvironment.core.communication.connection.api.DisconnectReason.REMOTE_SHUTDOWN;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.AUTO_RETRY_DELAY_EXPIRED;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.CHANNEL_BROKEN;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.CHANNEL_CLOSED_BY_OWN_REQUEST;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.CHANNEL_CLOSED_BY_REMOTE;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.CONNECT_ATTEMPT_FAILED;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.CONNECT_ATTEMPT_SUCCESSFUL;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.START_REQUESTED;
import static de.rcenvironment.core.communication.connection.impl.ConnectionSetupImpl.StateMachineEventType.STOP_REQUESTED;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.communication.channel.MessageChannelService;
import de.rcenvironment.core.communication.common.CommunicationException;
import de.rcenvironment.core.communication.connection.api.ConnectionSetup;
import de.rcenvironment.core.communication.connection.api.ConnectionSetupListener;
import de.rcenvironment.core.communication.connection.api.ConnectionSetupState;
import de.rcenvironment.core.communication.connection.api.DisconnectReason;
import de.rcenvironment.core.communication.model.NetworkContactPoint;
import de.rcenvironment.core.communication.transport.spi.MessageChannel;
import de.rcenvironment.core.communication.utils.NetworkContactPointUtils;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.AbstractFixedTransitionsStateMachine;
import de.rcenvironment.core.utils.incubator.StateChangeException;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* Default {@link ConnectionSetup} implementation.
*
* @author Robert Mischke
*/
// TODO >5.0.0: improve threading/locking model after reworking state machine base class - misc_ro
public class ConnectionSetupImpl implements ConnectionSetup {
private static final int MINIMUM_INITIAL_DELAY_MSEC = 5000;
private static final int SEC_TO_MSEC_FACTOR = 1000;
private static final int NO_MAXIMUM_AUTO_RETRY_DELAY = 0; // marker value
private static final ConnectionSetupState[][] VALID_STATE_TRANSITIONS = new ConnectionSetupState[][] {
// standard lifecycle
{ DISCONNECTED, CONNECTING },
{ CONNECTING, CONNECTED },
{ CONNECTED, DISCONNECTING },
{ DISCONNECTING, DISCONNECTED },
// connection failure
{ CONNECTING, DISCONNECTED },
// follow-up to connection failure with automatic reconnect enabled
{ DISCONNECTED, WAITING_TO_RECONNECT },
// automatic reconnect delay has passed, reconnecting
{ WAITING_TO_RECONNECT, CONNECTING },
// actively disconnected/stopped by user while waiting for automatic reconnect
{ WAITING_TO_RECONNECT, DISCONNECTED },
// message channel closed without active disconnect
{ CONNECTED, DISCONNECTED }
};
private static final int STATE_WAITING_POLLING_INTERVAL = 25;
private NetworkContactPoint ncp;
private String displayName;
private MessageChannelService channelService;
private final StateMachine stateMachine = new StateMachine();
private final AsyncTaskService threadPool = ConcurrencyUtils.getAsyncTaskService();
private final Log log = LogFactory.getLog(getClass());
private ConnectionSetupListener listener;
private long id;
private boolean connnectOnStartup;
private boolean autoRetryEnabled;
private int autoRetryInitialDelayMsec;
private int autoRetryMaximumDelayMsec;
private float autoRetryDelayMultiplier;
/**
* The event types posted to the connection setup's state machine.
*
* @author Robert Mischke
*/
protected enum StateMachineEventType {
START_REQUESTED,
STOP_REQUESTED,
CONNECT_ATTEMPT_SUCCESSFUL, // context: attempt id, message channel
CONNECT_ATTEMPT_FAILED, // context: attempt id
AUTO_RETRY_DELAY_EXPIRED,
CHANNEL_CLOSED_BY_OWN_REQUEST, // context: message channel
CHANNEL_BROKEN, // context: message channel
CHANNEL_CLOSED_BY_REMOTE // context: message channel
}
/**
* The event objects posted to the connection setup's state machine, encapsulating an event type and an optional channel id this event
* relates to.
*
* The reason why this is necessary is that otherwise, delayed asynchronous events could be misinterpreted, for example a delayed
* "connection error" event that was caused by a channel different from the one that is currently active. Without the channel id, this
* could lead to the current channel being wrongly closed; with the id, this can be prevented.
*
* @author Robert Mischke
*/
private static final class StateMachineEvent {
private final StateMachineEventType type;
private final MessageChannel relatedChannel;
private final long taskId;
StateMachineEvent(StateMachineEventType type) {
this(type, null, 0);
}
StateMachineEvent(StateMachineEventType type, MessageChannel relatedChannel) {
this(type, relatedChannel, 0);
}
StateMachineEvent(StateMachineEventType type, MessageChannel relatedChannel, long taskId) {
if (taskId < 0) {
throw new IllegalArgumentException();
}
this.type = type;
this.relatedChannel = relatedChannel;
this.taskId = taskId;
}
public StateMachineEventType getType() {
return type;
}
public MessageChannel getRelatedChannel() {
return relatedChannel;
}
public long getTaskId() {
return taskId;
}
@Override
public String toString() {
return StringUtils.format("%s (#%d, %s)", type.name(), taskId, relatedChannel);
}
}
/**
* Internal state machine for {@link ConnectionSetupImpl} instances.
*
* @author Robert Mischke
*/
private final class StateMachine extends AbstractFixedTransitionsStateMachine<ConnectionSetupState, StateMachineEvent> {
/**
* A {@link Runnable} that performs a connect attempt. Posts either a {@link StateMachineEventType#CONNECT_ATTEMPT_SUCCESSFUL} or a
* {@link StateMachineEventType#CONNECT_ATTEMPT_FAILED} event on completion.
*
* @author Robert Mischke
*/
private final class AsyncConnectTask implements Runnable {
private final long taskId;
private final boolean isAutoRetry;
private volatile Future<MessageChannel> future;
AsyncConnectTask(long taskId, boolean isAutoRetry) {
this.taskId = taskId;
this.isAutoRetry = isAutoRetry;
}
@Override
@TaskDescription("Communication Layer: ConnectionSetup connecting")
public void run() {
try {
future = channelService.connect(ncp, true);
try {
MessageChannel newMessageChannel = future.get();
log.debug("Message channel " + newMessageChannel.getChannelId() + " established for connection setup " + id);
channelService.registerNewOutgoingChannel(newMessageChannel);
postEvent(new StateMachineEvent(CONNECT_ATTEMPT_SUCCESSFUL, newMessageChannel, taskId));
} catch (InterruptedException e) {
throw new CommunicationException("The connection attempt was interrupted");
} catch (ExecutionException e) {
// unwrap exception as far as possible for shorter stacktrace
throw unwrapFailedToConnectException(e);
}
} catch (CommunicationException e) {
// TODO reduce number of stacktrace layers by unwrapping or changing source behaviour - misc_ro
final String exceptionString;
if (e.getCause() == null) {
// typical case: only use message
exceptionString = e.getMessage();
} else {
// rare/unexpected case: add whole cause chain
exceptionString = e.toString();
}
if (isAutoRetry) {
log.info(StringUtils.format("Failed to auto-reconnect to \"%s\": %s (Connection details: %s)",
displayName, exceptionString, getNetworkContactPointString()));
} else {
log.warn(StringUtils.format("Failed to connect to \"%s\": %s (Connection details: %s)",
displayName, exceptionString, getNetworkContactPointString()));
}
postEvent(new StateMachineEvent(CONNECT_ATTEMPT_FAILED, null, taskId));
} catch (CancellationException e) {
log.info(StringUtils.format("The connect attempt to \"%s\" was cancelled", displayName));
}
}
public void attemptToCancel() {
final Future<MessageChannel> copyOfFuture = future;
if (copyOfFuture != null) {
copyOfFuture.cancel(true);
}
}
private CommunicationException unwrapFailedToConnectException(ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof CommunicationException) {
return (CommunicationException) cause;
} else if (cause != null) {
return new CommunicationException(cause);
} else {
return new CommunicationException(e);
}
}
}
/**
* A {@link Runnable} that closes the given {@link MessageChannel}. No {@link StateMachineEvent} is posted on completion; instead,
* the closing channel will be detected by an external call to the {@link ConnectionSetupImpl#onMessageChannelClosed()} method.
*
* @author Robert Mischke
*/
private final class AsyncDisconnectTask implements Runnable {
private final MessageChannel channel;
private AsyncDisconnectTask(MessageChannel channel) {
this.channel = channel;
}
@Override
@TaskDescription("Communication Layer: ConnectionSetup disconnecting")
public void run() {
channelService.closeOutgoingChannel(channel);
}
}
/**
* A simple callback {@link Runnable} to signal when the auto-retry wait time has expired.
*
* @author Robert Mischke
*/
private final class AsyncAutoRetryTrigger implements Runnable {
private final long taskId;
private AsyncAutoRetryTrigger(long taskId) {
this.taskId = taskId;
}
@Override
@TaskDescription("Communication Layer: ConnectionSetup auto-reconnect timer")
public void run() {
postEvent(new StateMachineEvent(AUTO_RETRY_DELAY_EXPIRED, null, taskId));
}
}
private boolean isConnectionIntended;
// cleared on disconnect
private MessageChannel connectedMessageChannel;
// not cleared on disconnect (for fetching information after a disconnect)
private MessageChannel lastConnectedMessageChannel;
private DisconnectReason lastDisconnectReason;
private int consecutiveConnectionFailures;
private boolean currentConnectAttemptIsAutoRetry = false; // set on connection failure, if applicable
private AsyncConnectTask currentConnectTask = null;
private long currentConnectTaskId;
private long currentAutoRetryWaitExpiredTaskId;
StateMachine() {
super(ConnectionSetupState.DISCONNECTED, VALID_STATE_TRANSITIONS);
}
public synchronized MessageChannel getConnectedMessageChannel() {
return connectedMessageChannel;
}
public synchronized MessageChannel getLastConnectedMessageChannel() {
return lastConnectedMessageChannel;
}
public synchronized DisconnectReason getLastDisconnectReason() {
return lastDisconnectReason;
}
@Override
protected ConnectionSetupState processEvent(ConnectionSetupState currentState, StateMachineEvent event)
throws StateChangeException {
log.debug(StringUtils.format("Processing event %s while in state %s", event, currentState));
switch (event.getType()) {
case START_REQUESTED:
isConnectionIntended = true;
switch (currentState) {
case DISCONNECTED:
// connect
connectAsync(false); // false = no auto-retry
return CONNECTING;
case WAITING_TO_RECONNECT:
connectAsync(false); // false = no auto-retry (reset failure count etc.)
return CONNECTING;
default:
// TODO add reconnect wait time shortening
log.debug("Ignoring connection START request while in state " + currentState);
return null;
}
case STOP_REQUESTED:
isConnectionIntended = false;
switch (currentState) {
case CONNECTED:
// this state is only used on active shutdown; external events switch to DISCONNECTED immediately
lastDisconnectReason = ACTIVE_SHUTDOWN;
if (connectedMessageChannel != null) {
disconnectAsync(connectedMessageChannel);
return DISCONNECTING;
} else {
log.warn("Undefined active channel when processing event " + event);
return null;
}
case CONNECTING:
// invalidate the current connect attempt
currentConnectTaskId++;
if (currentConnectTask != null) {
log.debug("Cancelling connect attempt");
currentConnectTask.attemptToCancel();
} else {
log.warn("Unexpected state: Connection is " + CONNECTING + ", but there is no associated task");
}
return DISCONNECTED;
case WAITING_TO_RECONNECT:
lastDisconnectReason = ACTIVE_SHUTDOWN;
return DISCONNECTED;
default:
// FIXME implement: connect cancelling, ...
log.warn("Ignoring STOP request as state " + currentState + " is not supported yet");
return null;
}
case CONNECT_ATTEMPT_SUCCESSFUL:
MessageChannel newChannel = event.getRelatedChannel();
if (!checkForCurrentAttemptId(event, currentConnectTaskId)) {
log.warn("Connection established, but it belongs to an outdated connect request; triggering disconnect of channel "
+ newChannel);
disconnectAsync(newChannel);
return null;
}
currentConnectTask = null;
if (!isConnectionIntended) {
log.warn("Connection established, but no connection is intended anymore; triggering disconnect of channel "
+ newChannel);
disconnectAsync(newChannel);
return null;
}
if (currentState != CONNECTING) {
log.debug("Discarding event " + event + " as the current state is not " + CONNECTING);
return null;
}
connectedMessageChannel = newChannel;
lastConnectedMessageChannel = newChannel;
if (consecutiveConnectionFailures == 0) {
log.info(StringUtils.format("Network connection established: \"%s\"", displayName));
} else {
// TODO text is not quite correct if a connection broke down and is reestablished on the first attempt - misc_ro
log.info(StringUtils.format("Network connection \"%s\" was successfully established after %d failed attempts",
displayName, consecutiveConnectionFailures));
}
return CONNECTED;
case CONNECT_ATTEMPT_FAILED:
if (!checkForCurrentAttemptId(event, currentConnectTaskId)) {
return null;
}
currentConnectTask = null;
consecutiveConnectionFailures++;
final boolean wasAutoRetryAttempt = currentConnectAttemptIsAutoRetry;
final boolean triggerAutoRetry;
if (wasAutoRetryAttempt) {
lastDisconnectReason = FAILED_TO_AUTO_RECONNECT;
triggerAutoRetry = true;
} else {
lastDisconnectReason = FAILED_TO_CONNECT;
triggerAutoRetry = isConnectionIntended && autoRetryEnabled;
}
listener.onConnectionAttemptFailed(ConnectionSetupImpl.this, consecutiveConnectionFailures == 1, triggerAutoRetry);
if (triggerAutoRetry) {
return WAITING_TO_RECONNECT;
} else {
return DISCONNECTED;
}
case CHANNEL_CLOSED_BY_OWN_REQUEST:
case CHANNEL_BROKEN:
case CHANNEL_CLOSED_BY_REMOTE:
return handleDisconnectEvent(event);
case AUTO_RETRY_DELAY_EXPIRED:
// if auto-retry mode was cancelled in the meantime, ignore this timer call
if (currentState != WAITING_TO_RECONNECT) {
return null;
}
if (currentAutoRetryWaitExpiredTaskId != event.getTaskId()) {
log.debug("Ignoring an outdated auto-retry timer callback");
return null;
}
log.debug("Reconnect delay expired, auto-retrying connection " + displayName);
connectAsync(true); // true = auto-retry
return CONNECTING;
default:
log.warn("Unprocessed event: " + event);
break;
}
return null; // do not change state
}
private boolean checkForCurrentAttemptId(StateMachineEvent event, long currentAttemptId) {
// sanity check
if (currentAttemptId <= 0) {
throw new IllegalStateException();
}
if (currentAttemptId == event.getTaskId()) {
return true;
} else {
log.debug(StringUtils.format("Ignoring event of type %s as it refers to attempt #%d while the current attempt is #%d",
event.getType(), event.getTaskId(), currentAttemptId));
return false;
}
}
private ConnectionSetupState handleDisconnectEvent(StateMachineEvent event) {
if (event.getRelatedChannel() != connectedMessageChannel) {
log.debug("Ignoring " + CHANNEL_BROKEN + " event as it refers to message channel " + event.getRelatedChannel()
+ ", while the current channel is " + connectedMessageChannel);
return null;
}
boolean triggerAutoRetry = false;
switch (event.getType()) {
case CHANNEL_CLOSED_BY_OWN_REQUEST:
lastDisconnectReason = ACTIVE_SHUTDOWN;
// never auto-retry on active disconnect, regardless of setting
break;
case CHANNEL_BROKEN:
triggerAutoRetry = autoRetryEnabled;
lastDisconnectReason = ERROR;
break;
case CHANNEL_CLOSED_BY_REMOTE:
triggerAutoRetry = autoRetryEnabled;
lastDisconnectReason = REMOTE_SHUTDOWN;
break;
default:
throw new RuntimeException("Should not be reached with event type " + event.getType());
}
listener.onConnectionClosed(ConnectionSetupImpl.this, lastDisconnectReason, triggerAutoRetry);
log.info(StringUtils.format("Network connection closed (%s): \"%s\"", lastDisconnectReason.getDisplayText(), displayName));
if (triggerAutoRetry) {
// count the initial connection breakdown towards the number of connection failures; this prevents redundant user feedback
// on the first attempt to reconnect
// TODO move to state change code? would require additional "enter reconnect" state, though
consecutiveConnectionFailures = 1;
return WAITING_TO_RECONNECT;
} else {
return DISCONNECTED;
}
}
private void connectAsync(boolean isAutoRetry) {
currentConnectAttemptIsAutoRetry = isAutoRetry;
if (!isAutoRetry) {
consecutiveConnectionFailures = 0;
}
currentConnectTaskId++;
currentConnectTask = new AsyncConnectTask(currentConnectTaskId, isAutoRetry);
threadPool.execute(currentConnectTask);
}
private void disconnectAsync(final MessageChannel channel) {
threadPool.execute(new AsyncDisconnectTask(channel));
}
@Override
protected void onStateChanged(ConnectionSetupState oldState, ConnectionSetupState newState) {
log.debug("Connection setup " + displayName + " changed state from " + oldState + " to " + newState);
switch (newState) {
case CONNECTING:
if (connectedMessageChannel != null) {
log.error("Internal error: Current message channel was not 'null' when transitioning from " + oldState + " to "
+ newState);
connectedMessageChannel = null;
}
lastDisconnectReason = null;
break;
case DISCONNECTING:
break;
case DISCONNECTED:
connectedMessageChannel = null;
break;
case WAITING_TO_RECONNECT:
connectedMessageChannel = null;
long targetDelay = calculateNextAutoRetryDelay();
log.debug(StringUtils.format("Scheduling auto-retry of connection %s in %d msec "
+ "(failure count: %d, delay multiplier: %s, maximum: %d)", displayName,
targetDelay, consecutiveConnectionFailures, autoRetryDelayMultiplier, autoRetryMaximumDelayMsec));
final long taskId = ++currentAutoRetryWaitExpiredTaskId;
threadPool.scheduleAfterDelay(new AsyncAutoRetryTrigger(taskId), targetDelay);
break;
default:
break;
}
listener.onStateChanged(ConnectionSetupImpl.this, oldState, newState);
}
@Override
protected void onStateChangeException(StateMachineEvent event, StateChangeException e) {
log.error("Invalid state change attempt, cause by event " + event, e);
}
private long calculateNextAutoRetryDelay() {
long targetDelay =
Math.round(autoRetryInitialDelayMsec * Math.pow(autoRetryDelayMultiplier, consecutiveConnectionFailures - 1));
if (autoRetryMaximumDelayMsec != NO_MAXIMUM_AUTO_RETRY_DELAY) {
// apply upper limit, if set
targetDelay = Math.min(targetDelay, autoRetryMaximumDelayMsec);
}
return targetDelay;
}
}
public ConnectionSetupImpl(NetworkContactPoint ncp, String displayName, long id, boolean connnectOnStartup,
MessageChannelService channelService, ConnectionSetupListener listener) {
this.ncp = ncp;
this.displayName = displayName;
this.id = id;
this.connnectOnStartup = connnectOnStartup;
this.channelService = channelService;
this.listener = listener;
Map<String, String> attributes = ncp.getAttributes();
parseAutoRetryConfiguration(attributes);
}
@Override
public void connectSync() throws CommunicationException {
// FIXME implement
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public void signalStartIntent() {
stateMachine.postEvent(new StateMachineEvent(START_REQUESTED));
}
@Override
public void signalStopIntent() {
stateMachine.postEvent(new StateMachineEvent(STOP_REQUESTED));
}
@Override
public void awaitState(ConnectionSetupState targetState, int timeoutMsec) throws TimeoutException, InterruptedException {
if (stateMachine.getState() == targetState) {
return;
}
int timeRemaining = timeoutMsec;
while (timeRemaining > 0) {
int waitTime = Math.min(STATE_WAITING_POLLING_INTERVAL, timeRemaining);
Thread.sleep(waitTime);
if (stateMachine.getState() == targetState) {
return;
}
timeRemaining -= STATE_WAITING_POLLING_INTERVAL;
}
throw new TimeoutException();
}
@Override
public ConnectionSetupState getState() {
return stateMachine.getState();
}
@Override
public DisconnectReason getDisconnectReason() {
return stateMachine.getLastDisconnectReason();
}
@Override
public long getId() {
return id;
}
@Override
public boolean getConnnectOnStartup() {
return connnectOnStartup;
}
@Override
public String getDisplayName() {
return displayName;
}
@Override
public String getNetworkContactPointString() {
return NetworkContactPointUtils.toDefinitionString(ncp);
}
/**
* Callback to notify this setup that its associated {@link MessageChannel} was closed.
*
* @param messageChannel the associated {@link MessageChannel}
*/
public void onMessageChannelClosed(MessageChannel messageChannel) {
log.debug("Message channel closed, adapting state of connection setup " + id + "; channel state: " + messageChannel.getState());
if (messageChannel.getState() == MARKED_AS_BROKEN) {
stateMachine.postEvent(new StateMachineEvent(CHANNEL_BROKEN, messageChannel));
} else if (messageChannel.isClosedBecauseMirrorChannelClosed()) {
// this assumes that remote channels only close on an orderly remote shutdown for now
stateMachine.postEvent(new StateMachineEvent(CHANNEL_CLOSED_BY_REMOTE, messageChannel));
} else {
stateMachine.postEvent(new StateMachineEvent(CHANNEL_CLOSED_BY_OWN_REQUEST, messageChannel));
}
}
@Override
public MessageChannel getCurrentChannel() {
return stateMachine.getConnectedMessageChannel();
}
@Override
public String getCurrentChannelId() {
MessageChannel channel = stateMachine.getConnectedMessageChannel();
if (channel != null) {
return channel.getChannelId();
} else {
return null;
}
}
@Override
public String getLastChannelId() {
MessageChannel channel = stateMachine.getLastConnectedMessageChannel();
if (channel != null) {
return channel.getChannelId();
} else {
return null;
}
}
@Override
public boolean equals(Object obj) {
if (obj.getClass() != getClass()) {
return false;
}
return ((ConnectionSetupImpl) obj).id == id;
}
@Override
public int hashCode() {
return ConnectionSetupImpl.class.hashCode() ^ (int) id;
}
private void parseAutoRetryConfiguration(Map<String, String> attributes) {
// set defaults
this.autoRetryEnabled = false;
this.autoRetryDelayMultiplier = 1.0f;
this.autoRetryMaximumDelayMsec = NO_MAXIMUM_AUTO_RETRY_DELAY;
// parse
String attrInitialDelay = attributes.get("autoRetryInitialDelay");
if (attrInitialDelay != null) {
try {
this.autoRetryInitialDelayMsec = SEC_TO_MSEC_FACTOR * Integer.parseInt(attrInitialDelay);
if (autoRetryInitialDelayMsec < MINIMUM_INITIAL_DELAY_MSEC) {
log.warn("Initial auto-retry delay cannot be less than " + MINIMUM_INITIAL_DELAY_MSEC + "; disabling for connection "
+ getDisplayName());
return; // disable auto-retry
}
String attrMultiplier = attributes.get("autoRetryDelayMultiplier");
if (attrMultiplier == null || attrMultiplier.isEmpty()) {
autoRetryDelayMultiplier = 1.0f;
} else {
// Note: always expects dot-separated float (as intended), regardless of locale
autoRetryDelayMultiplier = Float.parseFloat(attrMultiplier);
}
if (autoRetryDelayMultiplier < 1.0f) {
log.warn("Auto-retry backoff multiplier cannot be less than 1; setting to 1");
autoRetryDelayMultiplier = 1.0f;
}
String attrMaxDelay = attributes.get("autoRetryMaximumDelay");
if (attrMaxDelay != null) {
this.autoRetryMaximumDelayMsec = SEC_TO_MSEC_FACTOR * Integer.parseInt(attrMaxDelay);
if (autoRetryMaximumDelayMsec < autoRetryInitialDelayMsec) {
log.warn("Maximum auto-retry delay cannot be less than initial delay; disabling maximum delay for connection "
+ getDisplayName());
autoRetryMaximumDelayMsec = NO_MAXIMUM_AUTO_RETRY_DELAY;
}
}
// no parse exceptions -> enable
this.autoRetryEnabled = true;
log.debug(StringUtils.format(
"Parsed auto-retry settings for connection \"%s\": Initial delay=%d msec, maximum=%d msec, multiplier=%s",
getDisplayName(), autoRetryInitialDelayMsec, autoRetryMaximumDelayMsec, autoRetryDelayMultiplier));
} catch (NumberFormatException e) {
log.warn("Failed to parse auto-retry settings for connection setup " + getNetworkContactPointString());
}
}
}
}