/*
* Copyright 2014-2016 CyberVision, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kaaproject.kaa.client.channel.failover;
import org.kaaproject.kaa.client.channel.KaaChannelManager;
import org.kaaproject.kaa.client.channel.ServerType;
import org.kaaproject.kaa.client.channel.TransportConnectionInfo;
import org.kaaproject.kaa.client.channel.failover.strategies.DefaultFailoverStrategy;
import org.kaaproject.kaa.client.channel.failover.strategies.FailoverStrategy;
import org.kaaproject.kaa.client.context.ExecutorContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class DefaultFailoverManager implements FailoverManager {
private static final Logger LOG = LoggerFactory.getLogger(DefaultFailoverManager.class);
private static final long DEFAULT_FAILURE_RESOLUTION_TIMEOUT = 10;
private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;
private final KaaChannelManager channelManager;
private final ExecutorContext context;
private long failureResolutionTimeout;
private TimeUnit timeUnit = DEFAULT_TIME_UNIT;
private FailoverStrategy failoverStrategy;
private Map<ServerType, AccessPointIdResolution> resolutionProgressMap = new HashMap<>();
public DefaultFailoverManager(KaaChannelManager channelManager, ExecutorContext context) {
this(channelManager, context, new DefaultFailoverStrategy(),
DEFAULT_FAILURE_RESOLUTION_TIMEOUT, DEFAULT_TIME_UNIT);
}
/**
* All-args constructor.
*/
public DefaultFailoverManager(KaaChannelManager channelManager,
ExecutorContext context,
FailoverStrategy failoverStrategy,
long failureResolutionTimeout,
TimeUnit timeUnit) {
this.channelManager = channelManager;
this.context = context;
this.failoverStrategy = failoverStrategy;
this.failureResolutionTimeout = failureResolutionTimeout;
this.timeUnit = timeUnit;
}
@Override
public synchronized void onServerFailed(final TransportConnectionInfo connectionInfo,
FailoverStatus status) {
if (connectionInfo == null) {
LOG.warn("Server failed, but connection info is null, can't resolve");
return;
} else {
LOG.info("Server [{}, {}] failed", connectionInfo.getServerType(),
connectionInfo.getAccessPointId());
}
long currentResolutionTime = -1;
AccessPointIdResolution currentAccessPointIdResolution = resolutionProgressMap.get(
connectionInfo.getServerType());
if (currentAccessPointIdResolution != null) {
currentResolutionTime = currentAccessPointIdResolution.getResolutionTime();
if (currentAccessPointIdResolution.getAccessPointId() == connectionInfo.getAccessPointId()
&& currentAccessPointIdResolution.getCurResolution() != null
&& System.currentTimeMillis() < currentResolutionTime) {
LOG.debug("Resolution is in progress for {} server", connectionInfo);
return;
} else {
if (currentAccessPointIdResolution.getCurResolution() != null) {
LOG.trace("Cancelling old resolution: {}",
currentAccessPointIdResolution.getCurResolution());
cancelCurrentFailResolution(currentAccessPointIdResolution);
}
}
}
LOG.trace("Next fail resolution will be available in {} {}",
failureResolutionTimeout, timeUnit.toString());
Future<?> currentResolution = context.getScheduledExecutor().schedule(new Runnable() {
@Override
public void run() {
LOG.debug("Removing server {} from resolution map for type: {}",
connectionInfo, connectionInfo.getServerType());
resolutionProgressMap.remove(connectionInfo.getServerType());
}
}, failureResolutionTimeout, timeUnit);
channelManager.onServerFailed(connectionInfo, status);
long updatedResolutionTime = currentAccessPointIdResolution
!= null ? currentAccessPointIdResolution.getResolutionTime() : currentResolutionTime;
AccessPointIdResolution newAccessPointIdResolution =
new AccessPointIdResolution(connectionInfo.getAccessPointId(), currentResolution);
if (updatedResolutionTime != currentResolutionTime) {
newAccessPointIdResolution.setResolutionTime(updatedResolutionTime);
}
resolutionProgressMap.put(connectionInfo.getServerType(), newAccessPointIdResolution);
}
@Override
public synchronized void onServerChanged(TransportConnectionInfo connectionInfo) {
if (connectionInfo == null) {
LOG.warn("Server has changed, but its connection info is null, can't resolve");
return;
} else {
LOG.trace("Server [{}, {}] has changed",
connectionInfo.getServerType(), connectionInfo.getAccessPointId());
}
AccessPointIdResolution currentAccessPointIdResolution = resolutionProgressMap.get(
connectionInfo.getServerType());
if (currentAccessPointIdResolution == null) {
AccessPointIdResolution newResolution = new AccessPointIdResolution(
connectionInfo.getAccessPointId(), null);
resolutionProgressMap.put(connectionInfo.getServerType(), newResolution);
} else if (currentAccessPointIdResolution.getAccessPointId()
!= connectionInfo.getAccessPointId()) {
if (currentAccessPointIdResolution.getCurResolution() != null) {
LOG.trace("Cancelling fail resolution, as server [{}] has changed: {}",
connectionInfo, currentAccessPointIdResolution);
cancelCurrentFailResolution(currentAccessPointIdResolution);
}
AccessPointIdResolution newResolution = new AccessPointIdResolution(
connectionInfo.getAccessPointId(), null);
resolutionProgressMap.put(connectionInfo.getServerType(), newResolution);
} else {
LOG.debug("Same server [{}] is used, nothing has changed", connectionInfo);
}
}
@Override
public synchronized void onServerConnected(TransportConnectionInfo connectionInfo) {
LOG.trace("Server {} has connected", connectionInfo);
if (connectionInfo == null) {
LOG.warn("Server connection info is null, can't resolve");
return;
}
failoverStrategy.onRecover(connectionInfo);
AccessPointIdResolution accessPointIdResolution = resolutionProgressMap.get(
connectionInfo.getServerType());
if (accessPointIdResolution == null) {
LOG.trace("Server hasn't been set yet (failover resolution has happened), so a new server: "
+ "{} can't be connected", connectionInfo);
} else if (accessPointIdResolution.getCurResolution() != null
&& connectionInfo.getAccessPointId() == accessPointIdResolution.getAccessPointId()) {
LOG.trace("Cancelling fail resolution: {}", accessPointIdResolution);
cancelCurrentFailResolution(accessPointIdResolution);
} else if (accessPointIdResolution.getCurResolution() != null) {
LOG.debug("Connection for outdated accessPointId: {} was received, ignoring. "
+ "The new accessPointId is: {}",
connectionInfo.getAccessPointId(), accessPointIdResolution.getAccessPointId());
} else {
LOG.trace("There is no current resolution in progress, connected to the same server: {}",
connectionInfo);
}
}
@Override
public void setFailoverStrategy(FailoverStrategy failoverStrategy) {
if (failoverStrategy == null) {
throw new IllegalArgumentException("Failover strategy can't be null");
}
this.failoverStrategy = failoverStrategy;
}
@Override
public synchronized FailoverDecision onFailover(FailoverStatus failoverStatus) {
AccessPointIdResolution accessPointIdResolution = null;
long resolutionTime = System.currentTimeMillis();
switch (failoverStatus) {
case BOOTSTRAP_SERVERS_NA:
case CURRENT_BOOTSTRAP_SERVER_NA:
accessPointIdResolution = resolutionProgressMap.get(ServerType.BOOTSTRAP);
resolutionTime += failoverStrategy.getTimeUnit().toMillis(
failoverStrategy.getBootstrapServersRetryPeriod());
break;
case NO_OPERATION_SERVERS_RECEIVED:
accessPointIdResolution = resolutionProgressMap.get(ServerType.BOOTSTRAP);
break;
case OPERATION_SERVERS_NA:
accessPointIdResolution = resolutionProgressMap.get(ServerType.OPERATIONS);
resolutionTime += failoverStrategy.getTimeUnit().toMillis(
failoverStrategy.getOperationServersRetryPeriod());
break;
default:
break;
}
if (accessPointIdResolution != null) {
accessPointIdResolution.setResolutionTime(resolutionTime);
}
return failoverStrategy.onFailover(failoverStatus);
}
private void cancelCurrentFailResolution(AccessPointIdResolution accessPointIdResolution) {
if (accessPointIdResolution.getCurResolution() != null) {
accessPointIdResolution.getCurResolution().cancel(true);
accessPointIdResolution.setCurResolution(null);
} else {
LOG.trace("Current resolution is null, can't cancel");
}
}
static class AccessPointIdResolution {
private int accessPointId;
private long resolutionTimeMillis;
private Future<?> curResolution;
public AccessPointIdResolution(int accessPointId, Future<?> curResolution) {
this.accessPointId = accessPointId;
this.curResolution = curResolution;
this.resolutionTimeMillis = Long.MAX_VALUE;
}
public int getAccessPointId() {
return accessPointId;
}
public Future<?> getCurResolution() {
return curResolution;
}
public void setCurResolution(Future<?> curResolution) {
this.curResolution = curResolution;
}
public long getResolutionTime() {
return resolutionTimeMillis;
}
public void setResolutionTime(long resolutionTimeMillis) {
this.resolutionTimeMillis = resolutionTimeMillis;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AccessPointIdResolution that = (AccessPointIdResolution) obj;
if (accessPointId != that.accessPointId) {
return false;
}
if (curResolution != null ? !curResolution.equals(that.curResolution) : that.curResolution
!= null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = accessPointId;
result = 31 * result + (curResolution != null ? curResolution.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "AccessPointIdResolution{"
+ "accessPointId=" + accessPointId
+ ", resolutionTime=" + resolutionTimeMillis
+ ", curResolution=" + curResolution
+ '}';
}
}
}