package nl.tno.sensorstorm.storm; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import nl.tno.sensorstorm.api.annotation.FetcherDeclaration; import nl.tno.sensorstorm.api.particles.DataParticle; import nl.tno.sensorstorm.api.particles.MetaParticle; import nl.tno.sensorstorm.api.particles.Particle; import nl.tno.sensorstorm.api.processing.Fetcher; import nl.tno.sensorstorm.api.processing.Operation; import nl.tno.sensorstorm.config.EmptyStormConfiguration; import nl.tno.sensorstorm.impl.MetaParticleUtil; import nl.tno.sensorstorm.particlemapper.ParticleMapper; import nl.tno.sensorstorm.timer.TimerTickParticle; import nl.tno.storm.configuration.api.ExternalStormConfiguration; import nl.tno.storm.configuration.api.StormConfigurationException; import nl.tno.storm.configuration.impl.ZookeeperStormConfigurationFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import backtype.storm.Config; import backtype.storm.spout.SpoutOutputCollector; import backtype.storm.task.TopologyContext; import backtype.storm.topology.IRichSpout; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.tuple.Fields; /** * This is a generic Spout for the SensorStorm library. The logic for retrieving * data is implemented in a {@link Fetcher}, which runs on top of this spout. * <p> * This Spout injects {@link TimerTickParticle}s into the stream. This way, * {@link Operation}s in the topology are able to perform scheduled tasks and * recurring, even if there is no data to trigger processing. There are two ways * of doing this: Using the timestamps as they are set in the {@link Particle} * produced by the {@link Fetcher}, of using the system time. When the system * time is used, the timestamps in the particles will be overwritten before the * are sent to the topology. In both cases measures must be taken to make sure * that the timestamps in Particles are synchronized over all instances of * {@link SensorStormSpout}s. * <p> * In order to implement specific behavior (usually sending out more types of * {@link MetaParticle}s) this class can be extended. When a subclass introduces * new types of {@link MetaParticle}s, the subclass must call the * registerMetaParticle method in the constructor, so the topology will be able * to process the new type of {@link MetaParticle}. */ public class SensorStormSpout implements IRichSpout { private static final long serialVersionUID = -3199538353837853899L; protected Logger logger = LoggerFactory.getLogger(SensorStormSpout.class); protected ExternalStormConfiguration zookeeperStormConfiguration; protected SpoutOutputCollector collector; protected Fetcher fetcher; protected int nrOfOutputFields; private final long mainTimerTickFreqMs; private final boolean useParticleTime; private long lastKnownNow; private final List<Class<? extends MetaParticle>> registeredMetaParticles; private String originId; /** * Construct a SensorStormSpout with a {@link Fetcher} and no Timer * functionality. * * Default is that the main timer will be synced to the incoming particles, * but the mainTimerTickFreq is set to 0 which means no TimerTickParticles * will be produced. * * @param config * Reference to the native Storm config. * @param fetcher * Reference to the fetcher instance to be used. * @throws IllegalArgumentException * when the {@link Fetcher} does not have a * {@link FetcherDeclaration} annotation */ public SensorStormSpout(Config config, Fetcher fetcher) { this(config, fetcher, true, 0); } /** * Construct a SensorStormSpout with a {@link Fetcher} and Timer * functionality. * * @param config * Reference to the Storm config. * @param fetcher * Reference to the fetcher instance to be used. * @param useParticleTime * Parameter to indicate how to synchronize the main timer. True * means it is synchronized to the time in the * {@link DataParticle} coming from the {@link Fetcher}. False * means it is synchronized to the system clock of the server * this spout is running on AND that the timestap in the * {@link Particle}s will be overwritten with the system time * @param mainTimerTickFreqMs * The frequency the main timer must run on in milliseconds. * @throws IllegalArgumentException * when the {@link Fetcher} does not have a * {@link FetcherDeclaration} annotation */ public SensorStormSpout(Config config, Fetcher fetcher, boolean useParticleTime, long mainTimerTickFreqMs) { this.fetcher = fetcher; if (fetcher.getClass().getAnnotation(FetcherDeclaration.class) == null) { throw new IllegalArgumentException("The fetcher " + fetcher.getClass().getName() + " is missing the FetcherDeclaration annotation"); } this.mainTimerTickFreqMs = mainTimerTickFreqMs; this.useParticleTime = useParticleTime; lastKnownNow = -1; registeredMetaParticles = new ArrayList<Class<? extends MetaParticle>>(); registerMetaParticle(config, TimerTickParticle.class); } /** * Register a new type of {@link MetaParticle} so it can be processed by the * Storm topology. Subclasses of {@link SensorStormSpout} should call this * method in the constructor if they introduced new types of * {@link MetaParticle}s. * * @param config * The native storm configuration * @param metaParticleClass * Class of the {@link MetaParticle} that is produces by this * spout */ protected void registerMetaParticle(Config config, Class<? extends MetaParticle> metaParticleClass) { MetaParticleUtil.registerMetaParticleFieldsFromMetaParticleClass( config, TimerTickParticle.class); registeredMetaParticles.add(metaParticleClass); } @Override public void open(@SuppressWarnings("rawtypes") Map stormNativeConfig, TopologyContext context, SpoutOutputCollector collector) { this.collector = collector; originId = this.getClass().getName() + "." + context.getThisTaskIndex(); try { zookeeperStormConfiguration = ZookeeperStormConfigurationFactory .getInstance().getStormConfiguration(stormNativeConfig); } catch (StormConfigurationException e) { logger.error("Can not connect to zookeeper for get Storm configuration. Reason: " + e.getMessage()); zookeeperStormConfiguration = new EmptyStormConfiguration(); } try { fetcher.prepare(stormNativeConfig, zookeeperStormConfiguration, context); } catch (Exception e) { logger.warn("Unable to configure SensorStormSpout " + this.getClass().getName() + " due to ", e); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { Fields fields = null; FetcherDeclaration fetcherDeclaration = fetcher.getClass() .getAnnotation(FetcherDeclaration.class); for (Class<? extends DataParticle> outputParticleClass : fetcherDeclaration .outputs()) { if (fields == null) { fields = ParticleMapper.getFields(outputParticleClass); } else { fields = ParticleMapper.mergeFields(fields, ParticleMapper.getFields(outputParticleClass)); } } // Add fields from MetaParticles for (Class<? extends MetaParticle> c : registeredMetaParticles) { fields = ParticleMapper.mergeFields(fields, ParticleMapper.getFields(c)); } nrOfOutputFields = fields.size(); declarer.declare(fields); } /** * Fetches a new DataParticle from the fetcher. Syncs the main timer. Emits * zero or more TimerTickParticles. Emits the dataParticle. */ @Override public void nextTuple() { // get next particle DataParticle particle = fetcher.fetchParticle(); // emit particle together with optional leading timerTicks if (particle != null) { Long now; if (useParticleTime) { now = particle.getTimestamp(); } else { now = System.currentTimeMillis(); particle.setTimestamp(now); } emitTimerTicks(now); emitParticle(particle); } // emit always realtime timerTicks if necessary, also if there is no // particle to emit if (!useParticleTime) { emitTimerTicks(System.currentTimeMillis()); } } /** * Emit timerTicks from lastKnowNow up to now. Emit timerTicks up to and * including now * * @param now * Current time */ private void emitTimerTicks(long now) { // Do we have to emit timerTicks? if (mainTimerTickFreqMs != 0) { // firstTime? start from now if (lastKnownNow == -1) { lastKnownNow = now; // emit first timerTick at the same time as the particle emitParticle(new TimerTickParticle(now)); } else { // emit zero or more timerTicks up to now while ((now - lastKnownNow) >= mainTimerTickFreqMs) { lastKnownNow = lastKnownNow + mainTimerTickFreqMs; emitParticle(new TimerTickParticle(lastKnownNow)); } } } } /** * Emit a {@link Particle}, both {@link DataParticle} and * {@link MetaParticle} are possible. * * @param particle * {@link Particle} to emit */ public void emitParticle(Particle particle) { if (particle != null) { if (particle instanceof MetaParticle) { ((MetaParticle) particle).setOriginId(originId); } collector .emit(ParticleMapper.particleToValues(particle, nrOfOutputFields), UUID.randomUUID().toString()); } } @Override public void activate() { fetcher.activate(); } @Override public void close() { fetcher.deactivate(); } @Override public void deactivate() { fetcher.deactivate(); } @Override public Map<String, Object> getComponentConfiguration() { return null; } @Override public void ack(Object msgId) { // no retransmit is supported, only throttling } @Override public void fail(Object msgId) { // no retransmit is supported, only throttling } }