/*
* 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.server.thrift;
import com.google.common.base.Function;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Time;
import com.twitter.common.thrift.Thrift;
import com.twitter.common.thrift.ThriftFactory;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TMultiplexedProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TTransport;
import org.kaaproject.kaa.server.common.thrift.KaaThriftService;
import org.kaaproject.kaa.server.common.thrift.gen.operations.OperationsThriftService;
import org.kaaproject.kaa.server.common.thrift.gen.operations.OperationsThriftService.Iface;
import org.kaaproject.kaa.server.common.zk.gen.ConnectionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Neighbor Connection Class. Hold thrift connection pool to specific operations
* server. Provides sendEventMessage() for send messages to neighbor Operations
* Server
*
* @author Andrey Panasenko
* @author Andrew Shvayka
*/
public final class NeighborConnection<T extends NeighborTemplate<V>, V> {
private static final Logger LOG = LoggerFactory.getLogger(NeighborConnection.class);
/**
* SOCKET_TIMEOUT on opened connection in seconds.
*/
private static final long DEFAULT_SOCKET_TIMEOUT_CONNECTION_TO_NEIGHBOR = 20;
/**
* Default maximum number of event messages queue.
*/
private static final int DEFAULT_EVENT_MESSAGE_QUEUE_LENGTH = 1024 * 1024;
/**
* ID of connection in thriftHost:thriftPort format.
*/
private final String id;
/**
* ConnectionInfo of neighbor Operations server.
*/
private final ConnectionInfo connectionInfo;
private final int maxNumberConnection;
private final T template;
/**
* Real SOCKET_TIMEOUT on opened connection, if not set used default.
*/
private final long socketTimeout;
/**
* Real maximum number of event messages queue.
*/
private final int messageQueueLength = DEFAULT_EVENT_MESSAGE_QUEUE_LENGTH;
private ThriftFactory<OperationsThriftService.Iface> clientFactory;
private Thrift<OperationsThriftService.Iface> thrift;
private LinkedBlockingQueue<V> messageQueue;
/**
* Fixed Thread pool to run event workers.
*/
private ExecutorService executor;
/**
* Future list of event workers.
*/
private List<Future<?>> workers;
private boolean started;
/**
* Create new instance of <code>NeighborConnection</code>.
*
* @param connectionInfo is connection info
* @param maxNumberConnection os max number connection
* @param socketTimeout is socket timeout
* @param template is template
*/
public NeighborConnection(ConnectionInfo connectionInfo, int maxNumberConnection,
long socketTimeout, T template) {
this.connectionInfo = connectionInfo;
this.maxNumberConnection = maxNumberConnection;
this.socketTimeout = socketTimeout;
this.template = template;
this.id = Neighbors.getServerId(connectionInfo);
}
public NeighborConnection(ConnectionInfo connectionInfo,
int maxNumberNeighborConnections, T template) {
this(connectionInfo, maxNumberNeighborConnections,
DEFAULT_SOCKET_TIMEOUT_CONNECTION_TO_NEIGHBOR, template);
}
/**
* Cancel event workers.
*/
private void cancelWorkers() {
for (Future<?> f : workers) {
f.cancel(true);
}
workers.clear();
}
/**
* Return Thrift service client interface.
*
* @return OperationsThriftService.Iface
*/
public OperationsThriftService.Iface getClient() {
return thrift.builder()
.disableStats()
.withRequestTimeout(Amount.of(socketTimeout, Time.SECONDS))
.create();
}
/**
* Start neighbor connection if it not started yet.
*/
public synchronized void start() {
if (!started) {
executor = Executors.newFixedThreadPool(maxNumberConnection);
messageQueue = new LinkedBlockingQueue<>(messageQueueLength);
workers = new LinkedList<>();
clientFactory = ThriftFactory.create(OperationsThriftService.Iface.class);
InetSocketAddress address = new InetSocketAddress(
connectionInfo.getThriftHost().toString(), connectionInfo.getThriftPort()
);
Set<InetSocketAddress> backends = new HashSet<>();
backends.add(address);
thrift = clientFactory.withMaxConnectionsPerEndpoint(maxNumberConnection)
.withSocketTimeout(Amount.of(socketTimeout, Time.SECONDS))
.withClientFactory(new Function<TTransport, OperationsThriftService.Iface>() {
@Override
public Iface apply(TTransport transport) {
TProtocol protocol = new TBinaryProtocol(transport);
TMultiplexedProtocol mprotocol = new TMultiplexedProtocol(
protocol, KaaThriftService.OPERATIONS_SERVICE.getServiceName()
);
return new OperationsThriftService.Client(mprotocol);
}
}).build(backends);
for (int i = 0; i < maxNumberConnection; i++) {
EventWorker worker = new EventWorker(template);
workers.add(executor.submit(worker));
}
started = true;
} else {
LOG.debug("Neighbor Connection {} is already started", getId());
}
}
/**
* Stops neighbor Operations server connections.
*/
public synchronized void shutdown() {
if (started) {
cancelWorkers();
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
LOG.error("Neighbor Connection {} error terminates ExecutorService", getId(), ex);
}
thrift.close();
started = false;
} else {
LOG.debug("Neighbor Connection {} is already stopped or was not started yet", getId());
}
}
/**
* Send list of event message to the neighbor operations server.
*
* @param messages a list of messages that will be sent to to the neighbor server
* @throws InterruptedException in case of queuing error occurred.
*/
public void sendMessages(Collection<V> messages) throws InterruptedException {
for (V e : messages) {
if (!messageQueue.offer(e, 1, TimeUnit.MINUTES)) {
LOG.error("NeighborConnection [{}] event messages queue is full "
+ "more than 1 minute. Operation impossible.", getId());
throw new InterruptedException("Event messages queue is full more than 10 minutes");
}
}
}
/**
* Neighbor Operations Server ID getter.
*
* @return the id
*/
public String getId() {
return id;
}
/**
* Return Neighbor Operations Server ConnectionInfo.
*
* @return the connectionInfo
*/
public ConnectionInfo getConnectionInfo() {
return connectionInfo;
}
/**
* SOCKET_TIMEOUT on opened connection getter.
*
* @return the socketTimeout
*/
public long getSocketTimeout() {
return socketTimeout;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
@SuppressWarnings("rawtypes")
NeighborConnection other = (NeighborConnection) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
@Override
public String toString() {
return "NeighborConnection [Id=" + id + "]";
}
/**
* EventWorker Class. Provides sending EventMessages asynchronously.
* EventWorker blocks if messageQueue is empty in poll() operation.
*/
public class EventWorker implements Runnable {
private final T template;
private final UUID uniqueId = UUID.randomUUID();
private OperationsThriftService.Iface client = getClient();
private boolean operate = true;
public EventWorker(T template) {
super();
this.template = template;
}
@Override
public void run() {
LinkedList<V> messages = new LinkedList<>(); // NOSONAR
while (operate) {
try {
V event = messageQueue.poll(1, TimeUnit.HOURS);
if (event != null) {
messages.push(event);
messageQueue.drainTo(messages);
template.process(client, messages);
LOG.debug("EventWorker [{}:<{}>] {} messages sent", id, uniqueId, messages.size());
messages.clear();
}
} catch (TException te) {
LOG.error("EventWorker [{}:{}] error sending event messages pack. ", id, uniqueId, te);
template.onServerError(id, te);
} catch (InterruptedException ex) {
LOG.info("EventWorker [{}<{}>] terminated: ", id, uniqueId, ex);
operate = false;
}
}
}
}
}