/* * 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.impl; import org.kaaproject.kaa.client.FailureListener; import org.kaaproject.kaa.client.bootstrap.BootstrapManager; import org.kaaproject.kaa.client.channel.ChannelDirection; import org.kaaproject.kaa.client.channel.KaaDataChannel; import org.kaaproject.kaa.client.channel.KaaDataDemultiplexer; import org.kaaproject.kaa.client.channel.KaaDataMultiplexer; import org.kaaproject.kaa.client.channel.KaaInternalChannelManager; import org.kaaproject.kaa.client.channel.KaaInvalidChannelException; import org.kaaproject.kaa.client.channel.ServerType; import org.kaaproject.kaa.client.channel.TransportConnectionInfo; import org.kaaproject.kaa.client.channel.TransportProtocolId; import org.kaaproject.kaa.client.channel.connectivity.ConnectivityChecker; import org.kaaproject.kaa.client.channel.failover.FailoverDecision; import org.kaaproject.kaa.client.channel.failover.FailoverManager; import org.kaaproject.kaa.client.channel.failover.FailoverStatus; import org.kaaproject.kaa.client.channel.impl.sync.SyncTask; import org.kaaproject.kaa.client.context.ExecutorContext; import org.kaaproject.kaa.common.TransportType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; public class DefaultChannelManager implements KaaInternalChannelManager { public static final Logger LOG = LoggerFactory // NOSONAR .getLogger(DefaultChannelManager.class); private final List<KaaDataChannel> channels = new LinkedList<>(); private final Map<TransportType, KaaDataChannel> upChannels = new HashMap<>(); private final BootstrapManager bootstrapManager; private final Map<TransportProtocolId, TransportConnectionInfo> lastServers = new HashMap<>(); private final Map<TransportProtocolId, List<TransportConnectionInfo>> bootststrapServers; private final Map<TransportProtocolId, TransportConnectionInfo> lastBsServers = new HashMap<>(); private final Map<String, BlockingQueue<SyncTask>> syncTaskQueueMap = new ConcurrentHashMap<>(); private final Map<String, SyncWorker> syncWorkers = new HashMap<>(); private FailureListener failureListener; private FailoverManager failoverManager; private ExecutorContext executorContext; private ConnectivityChecker connectivityChecker; private boolean isShutdown = false; private boolean isPaused = false; private KaaDataMultiplexer operationsMultiplexer; private KaaDataDemultiplexer operationsDemultiplexer; private KaaDataMultiplexer bootstrapMultiplexer; private KaaDataDemultiplexer bootstrapDemultiplexer; /** * All-args constructor. */ public DefaultChannelManager(BootstrapManager manager, Map<TransportProtocolId, List<TransportConnectionInfo>> bootststrapServers, ExecutorContext executorContext, FailureListener failureListener) { if (manager == null || bootststrapServers == null || bootststrapServers.isEmpty()) { throw new ChannelRuntimeException("Failed to create channel manager"); } this.bootstrapManager = manager; this.bootststrapServers = bootststrapServers; this.executorContext = executorContext; this.failureListener = failureListener; } private boolean useChannelForType(KaaDataChannel channel, TransportType type) { ChannelDirection direction = channel.getSupportedTransportTypes().get(type); if (direction != null && (direction.equals(ChannelDirection.BIDIRECTIONAL) || direction.equals(ChannelDirection.UP))) { upChannels.put(type, channel); return true; } return false; } private void useNewChannelForType(TransportType type) { for (KaaDataChannel channel : channels) { if (useChannelForType(channel, type)) { return; } } upChannels.put(type, null); } private void applyNewChannel(KaaDataChannel channel) { for (TransportType type : channel.getSupportedTransportTypes().keySet()) { useChannelForType(channel, type); } } private void replaceAndRemoveChannel(KaaDataChannel channel) { channels.remove(channel); for (Map.Entry<TransportType, KaaDataChannel> entry : upChannels.entrySet()) { if (entry.getValue() == channel) { useNewChannelForType(entry.getKey()); } } stopWorker(channel); channel.shutdown(); } private void addChannelToList(KaaDataChannel channel) { if (!channels.contains(channel)) { channel.setConnectivityChecker(connectivityChecker); channels.add(channel); startWorker(channel); TransportConnectionInfo server; if (channel.getServerType() == ServerType.BOOTSTRAP) { server = getCurrentBootstrapServer(channel.getTransportProtocolId()); } else { server = lastServers.get(channel.getTransportProtocolId()); } if (server != null) { LOG.debug("Applying server {} for channel [{}] type {}", server, channel.getId(), channel.getTransportProtocolId()); channel.setServer(server); if (failoverManager != null) { failoverManager.onServerChanged(server); } else { LOG.warn("Failover manager isn't set: null"); } } else { if (lastServers != null && lastServers.isEmpty()) { if (channel.getServerType() == ServerType.BOOTSTRAP) { LOG.warn("Failed to find bootstrap service for channel [{}] type {}", channel.getId(), channel.getTransportProtocolId()); } else { LOG.info("Failed to find operations service for channel [{}] type {}", channel.getId(), channel.getTransportProtocolId()); } } else { LOG.debug("list of services is empty for channel [{}] type {}", channel.getId(), channel.getTransportProtocolId()); } } } } @Override public synchronized void setChannel(TransportType transport, KaaDataChannel channel) throws KaaInvalidChannelException { if (isShutdown) { LOG.warn("Can't set a channel. Channel manager is down"); return; } if (channel != null) { if (!useChannelForType(channel, transport)) { throw new KaaInvalidChannelException("Unsupported transport type " + transport.toString() + " for channel \"" + channel.getId() + "\""); } if (isPaused) { channel.pause(); } addChannelToList(channel); } } @Override public synchronized void addChannel(KaaDataChannel channel) { if (isShutdown) { LOG.warn("Can't add a channel. Channel manager is down"); return; } if (channel != null) { if (ServerType.BOOTSTRAP == channel.getServerType()) { channel.setMultiplexer(bootstrapMultiplexer); channel.setDemultiplexer(bootstrapDemultiplexer); } else { channel.setMultiplexer(operationsMultiplexer); channel.setDemultiplexer(operationsDemultiplexer); } if (isPaused) { channel.pause(); } addChannelToList(channel); applyNewChannel(channel); } } @Override public synchronized void removeChannel(KaaDataChannel channel) { replaceAndRemoveChannel(channel); } @Override public synchronized void removeChannel(String id) { for (KaaDataChannel channel : channels) { if (channel.getId().equals(id)) { replaceAndRemoveChannel(channel); return; } } } @Override public synchronized List<KaaDataChannel> getChannels() { return new LinkedList<>(channels); } @Override public TransportConnectionInfo getActiveServer(TransportType type) { KaaDataChannel channel = upChannels.get(type); if (channel == null) { return null; } return channel.getServer(); } private KaaDataChannel getChannel(TransportType type) { KaaDataChannel result = upChannels.get(type); if (result == null) { LOG.error("Failed to find channel for transport {}", type); throw new ChannelRuntimeException("Failed to find channel for transport " + type.toString()); } return result; } @Override public synchronized KaaDataChannel getChannel(String id) { for (KaaDataChannel channel : channels) { if (channel.getId().equals(id)) { return channel; } } return null; } @Override public void sync(TransportType type) { sync(type, false, false); } private void sync(TransportType type, boolean ack, boolean all) { LOG.debug("Lookup channel by type {}", type); KaaDataChannel channel = getChannel(type); BlockingQueue<SyncTask> queue = syncTaskQueueMap.get(channel.getId()); if (queue != null) { queue.offer(new SyncTask(type, ack, all)); } else { LOG.warn("Can't find queue for channel [{}]", channel.getId()); } } @Override public void syncAck(TransportType type) { sync(type, true, false); } @Override public void syncAll(TransportType type) { sync(type, false, true); } @Override public synchronized void onTransportConnectionInfoUpdated(TransportConnectionInfo newServer) { LOG.debug("Transport connection info updated for server: {}", newServer); if (isShutdown) { LOG.warn("Can't process server update. Channel manager is down"); return; } if (newServer.getServerType() == ServerType.OPERATIONS) { LOG.info("Adding new operations service: {}", newServer); lastServers.put(newServer.getTransportId(), newServer); } for (KaaDataChannel channel : channels) { if (channel.getServerType() == newServer.getServerType() && channel.getTransportProtocolId().equals(newServer.getTransportId())) { LOG.debug("Applying server {} for channel [{}] type {}", newServer, channel.getId(), channel.getTransportProtocolId()); channel.setServer(newServer); if (failoverManager != null) { failoverManager.onServerChanged(newServer); } else { LOG.warn("Failover manager isn't set: null"); } } } } @Override public synchronized void onServerFailed(final TransportConnectionInfo server, FailoverStatus status) { if (isShutdown) { LOG.warn("Can't process server failure. Channel manager is down"); return; } if (server.getServerType() == ServerType.BOOTSTRAP) { final TransportConnectionInfo nextConnectionInfo = getNextBootstrapServer(server); if (nextConnectionInfo != null) { LOG.trace("Using next bootstrap service"); FailoverDecision decision = failoverManager.onFailover( FailoverStatus.CURRENT_BOOTSTRAP_SERVER_NA); switch (decision.getAction()) { case NOOP: LOG.warn("No operation is performed according to failover strategy decision"); break; case RETRY: long retryPeriod = decision.getRetryPeriod(); LOG.warn("Attempt to reconnect to the current bootstrap service will be made in {} ms, " + "according to failover strategy decision", retryPeriod); executorContext.getScheduledExecutor().schedule(new Runnable() { @Override public void run() { onTransportConnectionInfoUpdated(server); } }, retryPeriod, TimeUnit.MILLISECONDS); break; case USE_NEXT_BOOTSTRAP: retryPeriod = decision.getRetryPeriod(); LOG.warn("Attempt to connect to the next bootstrap service will be made in {} ms, " + "according to failover strategy decision", retryPeriod); executorContext.getScheduledExecutor().schedule(new Runnable() { @Override public void run() { onTransportConnectionInfoUpdated(nextConnectionInfo); } }, retryPeriod, TimeUnit.MILLISECONDS); break; case FAILURE: LOG.warn("Calling failure listener according to failover strategy decision!"); failureListener.onFailure(); break; default: break; } } else { LOG.trace("Can't find next bootstrap service"); FailoverDecision decision = failoverManager.onFailover(status); switch (decision.getAction()) { case NOOP: LOG.warn("No operation is performed according to failover strategy decision"); break; case RETRY: long retryPeriod = decision.getRetryPeriod(); LOG.warn("Attempt to reconnect to first bootstrap service will be made in {} ms, " + "according to failover strategy decision", retryPeriod); executorContext.getScheduledExecutor().schedule(new Runnable() { @Override public void run() { onTransportConnectionInfoUpdated(server); } }, retryPeriod, TimeUnit.MILLISECONDS); break; case FAILURE: LOG.warn("Calling failure listener according to failover strategy decision!"); failureListener.onFailure(); break; default: break; } } } else { bootstrapManager.useNextOperationsServer(server.getTransportId(), status); } } @Override public synchronized void clearChannelList() { channels.clear(); upChannels.clear(); } private TransportConnectionInfo getCurrentBootstrapServer(TransportProtocolId type) { TransportConnectionInfo bsi = lastBsServers.get(type); if (bsi == null) { List<TransportConnectionInfo> serverList = bootststrapServers.get(type); if (serverList != null && !serverList.isEmpty()) { bsi = serverList.get(0); lastBsServers.put(type, bsi); } } return bsi; } private TransportConnectionInfo getNextBootstrapServer(TransportConnectionInfo currentServer) { TransportConnectionInfo bsi = null; List<TransportConnectionInfo> serverList = bootststrapServers.get( currentServer.getTransportId()); int serverIndex = serverList.indexOf(currentServer); if (serverIndex >= 0) { if (++serverIndex == serverList.size()) { serverIndex = 0; } bsi = serverList.get(serverIndex); lastBsServers.put(currentServer.getTransportId(), bsi); } return bsi; } @Override public void setConnectivityChecker(ConnectivityChecker checker) { if (isShutdown) { LOG.warn("Can't set connectivity checker. Channel manager is down"); return; } connectivityChecker = checker; for (KaaDataChannel channel : channels) { channel.setConnectivityChecker(connectivityChecker); } } @Override public synchronized void shutdown() { if (!isShutdown) { isShutdown = true; for (KaaDataChannel channel : channels) { channel.shutdown(); } for (SyncWorker worker : syncWorkers.values()) { worker.shutdown(); } } } @Override public synchronized void pause() { if (isShutdown) { LOG.warn("Can't pause. Channel manager is down"); return; } if (!isPaused) { isPaused = true; for (KaaDataChannel channel : upChannels.values()) { channel.pause(); } } } @Override public synchronized void resume() { if (isShutdown) { LOG.warn("Can't resume. Channel manager is down"); return; } if (isPaused) { isPaused = false; for (KaaDataChannel channel : upChannels.values()) { channel.resume(); } } } @Override public void setOperationMultiplexer(KaaDataMultiplexer multiplexer) { this.operationsMultiplexer = multiplexer; } @Override public void setOperationDemultiplexer(KaaDataDemultiplexer demultiplexer) { this.operationsDemultiplexer = demultiplexer; } @Override public void setBootstrapMultiplexer(KaaDataMultiplexer multiplexer) { this.bootstrapMultiplexer = multiplexer; } @Override public void setBootstrapDemultiplexer(KaaDataDemultiplexer demultiplexer) { this.bootstrapDemultiplexer = demultiplexer; } private void startWorker(KaaDataChannel channel) { stopWorker(channel); SyncWorker worker = new SyncWorker(channel); syncTaskQueueMap.put(channel.getId(), new LinkedBlockingQueue<SyncTask>()); syncWorkers.put(channel.getId(), worker); worker.start(); } private void stopWorker(KaaDataChannel channel) { BlockingQueue<SyncTask> skippedTasks = syncTaskQueueMap.remove(channel.getId()); if (skippedTasks != null) { for (SyncTask task : skippedTasks) { LOG.info("Task skipped due to worker shutdown: {}", task); } } SyncWorker worker = syncWorkers.remove(channel.getId()); if (worker != null) { LOG.debug("[{}] stopping worker", channel.getId()); worker.shutdown(); } } public void setFailoverManager(FailoverManager failoverManager) { this.failoverManager = failoverManager; } private class SyncWorker extends Thread { private final KaaDataChannel channel; private volatile boolean stop; private SyncWorker(KaaDataChannel channel) { super(); this.channel = channel; } @Override public void run() { LOG.debug("[{}] Worker started", channel.getId()); while (!stop) { try { BlockingQueue<SyncTask> taskQueue = syncTaskQueueMap.get(channel.getId()); SyncTask task = taskQueue.take(); List<SyncTask> additionalTasks = new ArrayList<SyncTask>(); if (taskQueue.drainTo(additionalTasks) > 0) { LOG.debug("[{}] Merging task {} with {}", channel.getId(), task, additionalTasks); task = SyncTask.merge(task, additionalTasks); } if (task.isAll()) { LOG.debug("[{}] Going to invoke syncAll method for types {}", channel.getId(), task.getTypes()); channel.syncAll(); } else if (task.isAckOnly()) { LOG.debug("[{}] Going to invoke syncAck method for types {}", channel.getId(), task.getTypes()); channel.syncAck(task.getTypes()); } else { LOG.debug("[{}] Going to invoke sync method", channel.getId()); channel.sync(task.getTypes()); } } catch (InterruptedException ex) { if (stop) { LOG.debug("[{}] Worker is interrupted.", channel.getId()); } else { LOG.warn("[{}] Worker is interrupted.", channel.getId(), ex); } } } LOG.debug("[{}] Worker stopped", channel.getId()); } public void shutdown() { this.stop = true; this.interrupt(); } } }