/**
* Copyright 2014 Comcast Cable Communications Management, LLC
*
* 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 com.comcast.viper.flume2storm.connection.receptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.joda.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.comcast.viper.flume2storm.KryoUtil;
import com.comcast.viper.flume2storm.connection.KryoNetParameters;
import com.comcast.viper.flume2storm.connection.MyKryoSerialization;
import com.comcast.viper.flume2storm.connection.parameters.KryoNetConnectionParameters;
import com.comcast.viper.flume2storm.connection.sender.KryoNetEventSender;
import com.comcast.viper.flume2storm.event.F2SEvent;
import com.esotericsoftware.kryonet.Client;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.FrameworkMessage.KeepAlive;
import com.esotericsoftware.kryonet.Listener;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
/**
* A Flume2Storm event receptor implementation using KryoNet framework. Note
* that the receptor, when started, will always try to reconnect to the KryoNet
* server. On disconnection, it will retry immediately, and then wait for a
* specified retry delay before retrying again.
*/
public class KryoNetEventReceptor implements EventReceptor<KryoNetConnectionParameters> {
protected static final Logger LOG = LoggerFactory.getLogger(KryoNetEventReceptor.class);
protected final KryoNetParameters kryoNetParameters;
protected final KryoNetConnectionParameters connectionParams;
protected final EventReceptorStats stats;
protected final AtomicReference<ImmutableList<EventReceptorListener>> listeners;
protected final Queue<F2SEvent> events;
protected final AtomicBoolean keepRunning;
protected final AtomicReference<Instant> nextConnectionTime;
protected MaintainConnection maintainConnectionThread;
protected Client client;
// TODO fix sleep time before reconnection attempt
/**
* Thread to maintain the connection to the KryoNet server open
*/
protected class MaintainConnection extends Thread {
/**
* @see java.lang.Thread#run()
*/
@Override
public void run() {
LOG.debug("Maintain connection thread started");
try {
while (keepRunning.get()) {
if (!isConnected() && Instant.now().isAfter(nextConnectionTime.get())) {
if (!connect()) {
setNextReconnectionTime();
LOG.trace("Connection failed. Set next connection attempt to {}", nextConnectionTime.get());
}
} else {
try {
Thread.sleep(100);
} catch (final InterruptedException e) {
// Nothing to do
}
}
}
} catch (Exception e) {
LOG.error("Exception in maintain thread: " + e.getMessage(), e);
}
cleanup();
LOG.debug("Maintain connection thread terminated");
}
}
/**
* @param connectionParams
* Connection parameters to the {@link KryoNetEventSender}
* @param kryoNetParams
* Generic parameters of the {@link KryoNetEventReceptor}
*/
public KryoNetEventReceptor(final KryoNetConnectionParameters connectionParams, KryoNetParameters kryoNetParams) {
Preconditions.checkNotNull(connectionParams);
this.kryoNetParameters = kryoNetParams;
this.connectionParams = connectionParams;
stats = new EventReceptorStats(connectionParams.getId());
listeners = new AtomicReference<>(ImmutableList.copyOf(new ArrayList<EventReceptorListener>()));
events = new ConcurrentLinkedQueue<F2SEvent>();
keepRunning = new AtomicBoolean(false);
nextConnectionTime = new AtomicReference<Instant>(new Instant(0));
}
/**
* @see com.comcast.viper.flume2storm.connection.receptor.EventReceptor#getConnectionParameters()
*/
public KryoNetConnectionParameters getConnectionParameters() {
return connectionParams;
}
/**
* @see com.comcast.viper.flume2storm.connection.receptor.EventReceptor#getStats()
*/
public EventReceptorStats getStats() {
return stats;
}
/**
* @see com.comcast.viper.flume2storm.connection.receptor.EventReceptor#getEvents(int)
*/
public List<F2SEvent> getEvents(final int maxEvents) {
final List<F2SEvent> result = new ArrayList<F2SEvent>();
for (int i = 0; i < maxEvents; i++) {
final F2SEvent e = events.poll();
if (e == null) {
break;
}
stats.decrEventsQueued();
result.add(e);
}
LOG.trace("Collected {} event(s) (max {})", result.size(), maxEvents);
return result;
}
/**
* @see com.comcast.viper.flume2storm.connection.receptor.EventReceptor#getEvents()
*/
public List<F2SEvent> getEvents() {
return getEvents(Integer.MAX_VALUE);
}
/**
* @see com.comcast.viper.flume2storm.connection.receptor.EventReceptor#start()
*/
public boolean start() {
if (keepRunning.get()) {
LOG.warn("KryoNet client of {} already started", connectionParams.getConnectionStr());
return true;
}
LOG.trace("Starting KryoNet client of {}", connectionParams.getConnectionStr());
keepRunning.set(true);
stats.reset();
nextConnectionTime.set(new Instant(0));
maintainConnectionThread = new MaintainConnection();
maintainConnectionThread.start();
LOG.debug("KryoNet client of {} started", connectionParams.getConnectionStr());
return true;
}
/**
* Creates the KryoNet client and attempts to connect to the KryoNet server
*
* @return True if connected successfully
*/
protected boolean connect() {
if (client != null)
cleanup();
LOG.debug("KryoNet client of {} connecting...", connectionParams.getConnectionStr());
client = new Client(connectionParams.getWriteBufferSize(), connectionParams.getObjectBufferSize(),
new MyKryoSerialization(connectionParams.getObjectBufferSize()));
KryoUtil.register(client.getKryo());
client.addListener(new KryoConnectionListener());
client.start();
try {
client.connect(kryoNetParameters.getConnectionTimeout(), connectionParams.getAddress(),
connectionParams.getPort());
return true;
} catch (final IOException e) {
LOG.error("Failed to connect to the KryoNet server " + connectionParams.getConnectionStr(), e);
return false;
}
}
/**
* @return True if connected, false otherwise
*/
public boolean isConnected() {
return client != null && client.isConnected();
}
/**
* Cleans up the connection so it can try and reconnect
*/
protected void cleanup() {
try {
LOG.debug("KryoNet client of {} closing...", connectionParams.getConnectionStr());
if (client != null) {
client.close();
LOG.info("KryoNet client of {} closed", connectionParams.getConnectionStr());
}
} catch (Exception e) {
LOG.error("Failed to close KryoNet client", e);
} finally {
client = null;
}
}
/**
* @see com.comcast.viper.flume2storm.connection.receptor.EventReceptor#stop()
*/
public boolean stop() {
if (!keepRunning.get()) {
LOG.warn("KryoNet client of {} already stopped", connectionParams.getConnectionStr());
return true;
}
keepRunning.set(false);
try {
if (maintainConnectionThread == null) {
maintainConnectionThread.join(kryoNetParameters.getTerminationTimeout());
}
return true;
} catch (final InterruptedException e) {
LOG.warn("Failed to terminate inner thread in {} ms", kryoNetParameters.getTerminationTimeout());
return false;
} finally {
maintainConnectionThread = null;
}
}
protected void setNextReconnectionTime() {
nextConnectionTime.set(Instant.now().plus(kryoNetParameters.getRetrySleepDelay()));
}
protected class KryoConnectionListener extends Listener {
public KryoConnectionListener() {
}
/**
* @see com.esotericsoftware.kryonet.Listener#connected(com.esotericsoftware.kryonet.Connection)
*/
@Override
public void connected(final Connection connection) {
stats.setConnected();
LOG.info("KryoNet client of {} connected", connectionParams.getConnectionStr());
for (EventReceptorListener erListener : listeners.get()) {
erListener.onConnection();
}
}
/**
* @see com.esotericsoftware.kryonet.Listener#disconnected(com.esotericsoftware.kryonet.Connection)
*/
@Override
public void disconnected(final Connection connection) {
stats.setDisconnected();
LOG.info("KryoNet client of {} disconnected", connectionParams.getConnectionStr());
cleanup();
for (EventReceptorListener erListener : listeners.get()) {
erListener.onDisconnection();
}
}
/**
* @see com.esotericsoftware.kryonet.Listener#received(com.esotericsoftware.kryonet.Connection,
* java.lang.Object)
*/
@Override
public void received(final Connection connection, final Object object) {
if (object instanceof F2SEvent) {
try {
final F2SEvent fe = (F2SEvent) object;
assert fe != null;
LOG.trace("KryoNet client of {} received event {}", connectionParams.getConnectionStr(), fe);
stats.incrEventsIn();
if (!events.add(fe)) {
LOG.warn("KryoNet client of {} failed to enqueue event {}", connectionParams.getConnectionStr(), fe);
} else {
stats.incrEventsQueued();
}
for (EventReceptorListener erListener : listeners.get()) {
erListener.onEvent(ImmutableList.of(fe));
}
} catch (final ClassCastException cce) {
LOG.error("KryoNet client of {} received a message of unexpected type: {}",
connectionParams.getConnectionStr(), object.getClass());
} catch (final Exception e) {
LOG.error("KryoNet client of {} failed to handle message: {}", object);
}
setNextReconnectionTime();
} else if (object instanceof KeepAlive) {
// ignoring keep alive
} else {
LOG.trace("KryoNet client of {} ignoring unknown message of type {}", connectionParams.getConnectionStr(),
object == null ? "null" : object.getClass());
}
}
}
public synchronized void addListener(EventReceptorListener listener) {
List<EventReceptorListener> newList = new ArrayList<>(listeners.get());
newList.add(listener);
listeners.set(ImmutableList.copyOf(newList));
}
public synchronized void removeListener(EventReceptorListener listener) {
List<EventReceptorListener> newList = new ArrayList<>(listeners.get());
newList.remove(listener);
listeners.set(ImmutableList.copyOf(newList));
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(connectionParams)
.append(kryoNetParameters).append(stats).append("nextConnectionTime", nextConnectionTime).toString();
}
}