/**
* 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.sink;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationUtils;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.flume.Channel;
import org.apache.flume.ChannelException;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.EventDeliveryException;
import org.apache.flume.Transaction;
import org.apache.flume.conf.Configurable;
import org.apache.flume.instrumentation.SinkCounter;
import org.apache.flume.sink.AbstractSink;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.comcast.viper.flume2storm.F2SConfigurationException;
import com.comcast.viper.flume2storm.connection.parameters.ConnectionParameters;
import com.comcast.viper.flume2storm.connection.parameters.ConnectionParametersFactory;
import com.comcast.viper.flume2storm.connection.sender.EventSender;
import com.comcast.viper.flume2storm.connection.sender.EventSenderFactory;
import com.comcast.viper.flume2storm.connection.sender.EventSenderStats;
import com.comcast.viper.flume2storm.connection.sender.EventSenderStatsMBean;
import com.comcast.viper.flume2storm.event.F2SEvent;
import com.comcast.viper.flume2storm.location.LocationService;
import com.comcast.viper.flume2storm.location.LocationServiceFactory;
import com.comcast.viper.flume2storm.location.ServiceProvider;
import com.comcast.viper.flume2storm.location.ServiceProviderSerialization;
import com.google.common.base.Preconditions;
/**
* This Flume sink sends the Flume events picked up from the channel to the
* clients connected to it, load-balancing the data between them.
*
* @param <CP>
* The Connection Parameters class
* @param <SP>
* The Service Provider class
*/
public class StormSink<CP extends ConnectionParameters, SP extends ServiceProvider<CP>> extends AbstractSink implements
Configurable {
private static final Logger LOG = LoggerFactory.getLogger(StormSink.class);
protected SinkCounter sinkCounter;
protected StormSinkConfiguration configuration;
protected LocationServiceFactory<SP> locationServiceFactory;
protected LocationService<SP> locationService;
protected ServiceProviderSerialization<SP> serviceProviderSerialization;
protected EventSenderFactory<CP> eventSenderFactory;
protected ConnectionParametersFactory<CP> connectionParametersFactory;
protected EventSender<CP> eventSender;
protected EventConverter eventConverter;
/**
* @see org.apache.flume.conf.Configurable#configure(org.apache.flume.Context)
*/
@Override
public void configure(final Context context) {
Preconditions.checkNotNull(context);
if (sinkCounter == null) {
sinkCounter = new SinkCounter(getName());
}
try {
Configuration mapConfiguration = new MapConfiguration(context.getParameters());
LOG.debug("Storm-sink configuration:\n{}", ConfigurationUtils.toString(mapConfiguration));
configuration = StormSinkConfiguration.from(mapConfiguration);
Class<? extends ConnectionParametersFactory<CP>> connectionParametersFactoryClass = (Class<? extends ConnectionParametersFactory<CP>>) Class
.forName(configuration.getConnectionParametersFactoryClassName());
this.connectionParametersFactory = connectionParametersFactoryClass.newInstance();
Class<? extends EventSenderFactory<CP>> eventSenderFactoryClass = (Class<? extends EventSenderFactory<CP>>) Class
.forName(configuration.getEventSenderFactoryClassName());
this.eventSenderFactory = eventSenderFactoryClass.newInstance();
Class<? extends LocationServiceFactory<SP>> locationServiceFactoryClass = (Class<? extends LocationServiceFactory<SP>>) Class
.forName(configuration.getLocationServiceFactoryClassName());
this.locationServiceFactory = locationServiceFactoryClass.newInstance();
Class<? extends ServiceProviderSerialization<SP>> serviceProviderSerializationClass = (Class<? extends ServiceProviderSerialization<SP>>) Class
.forName(configuration.getServiceProviderSerializationClassName());
this.serviceProviderSerialization = serviceProviderSerializationClass.newInstance();
} catch (Exception e) {
LOG.error("Failed to configure Storm sink: " + e.getMessage(), e);
}
}
/**
* @see org.apache.flume.sink.AbstractSink#start()
*/
@Override
public synchronized void start() {
try {
LOG.debug("Opening...");
sinkCounter.start();
CP connectionParameters = connectionParametersFactory.create(configuration.get());
eventSender = eventSenderFactory.create(connectionParameters, configuration.get());
eventSender.start();
locationService = locationServiceFactory.create(configuration.get(), serviceProviderSerialization);
locationService.start();
locationService.register((SP) eventSender);
eventConverter = new EventConverter();
LOG.info("Opened");
super.start();
} catch (F2SConfigurationException e) {
LOG.error(e.getLocalizedMessage(), e);
}
}
/**
* @see org.apache.flume.sink.AbstractSink#stop()
*/
@Override
public synchronized void stop() {
LOG.debug("Closing...");
locationService.unregister((SP) eventSender);
locationService.stop();
eventSender.stop();
sinkCounter.stop();
LOG.info("Closed");
super.stop();
}
/**
* @see org.apache.flume.Sink#process()
*/
@Override
public Status process() throws EventDeliveryException {
if (eventSender.getStats().getNbClients() == 0) {
// No receptor connected
return Status.BACKOFF;
}
Status result = Status.READY;
final Channel channel = getChannel();
final Transaction transaction = channel.getTransaction();
try {
transaction.begin();
List<F2SEvent> batch = new ArrayList<F2SEvent>(configuration.getBatchSize());
for (int i = 0; i < configuration.getBatchSize(); i++) {
final Event event = channel.take();
if (event == null) {
break;
}
batch.add(eventConverter.convert(event));
}
if (batch.isEmpty()) {
sinkCounter.incrementBatchEmptyCount();
result = Status.BACKOFF;
} else {
sinkCounter.addToEventDrainAttemptCount(batch.size());
if (eventSender.send(batch) == 0) {
throw new EventDeliveryException("No event sent to receptor");
}
sinkCounter.addToEventDrainSuccessCount(batch.size());
if (batch.size() == configuration.getBatchSize()) {
sinkCounter.incrementBatchCompleteCount();
} else {
sinkCounter.incrementBatchUnderflowCount();
}
}
transaction.commit();
} catch (final Throwable t) {
transaction.rollback();
if (t instanceof Error) {
throw (Error) t;
} else if (t instanceof ChannelException) {
if (!(t.getCause() instanceof InterruptedException)) {
LOG.error("Storm Sink " + getName() + ": Unable to get event from channel " + channel.getName(), t);
}
result = Status.BACKOFF;
} else {
throw new EventDeliveryException("Failed to accept events", t);
}
} finally {
transaction.close();
}
return result;
}
/**
* @return The stats associated with teh {@link EventSender}
*/
public EventSenderStatsMBean getEventSernderStats() {
return eventSender == null ? new EventSenderStats("Unknown") : eventSender.getStats();
}
/**
* @return The sink counter
*/
public SinkCounter getSinkCounter() {
return sinkCounter;
}
}