package storm.applications.bolt;
import backtype.storm.Config;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import storm.applications.constants.AdsAnalyticsConstants.Conf;
import storm.applications.constants.AdsAnalyticsConstants.Field;
import storm.applications.model.ads.AdEvent;
import storm.applications.tools.NthLastModifiedTimeTracker;
import storm.applications.tools.SlidingWindowCounter;
import storm.applications.util.stream.TupleUtils;
public class RollingCtrBolt extends AbstractBolt {
private static final Logger LOG = LoggerFactory.getLogger(RollingCtrBolt.class);
private static final String WINDOW_LENGTH_WARNING_TEMPLATE =
"Actual window length is %d seconds when it should be %d seconds"
+ " (you can safely ignore this warning during the startup phase)";
protected SlidingWindowCounter<String> clickCounter;
protected SlidingWindowCounter<String> impressionCounter;
protected int windowLengthInSeconds;
protected int emitFrequencyInSeconds;
protected NthLastModifiedTimeTracker lastModifiedTracker;
public RollingCtrBolt() {
this(60);
}
public RollingCtrBolt(int emitFrequencyInSeconds) {
this.emitFrequencyInSeconds = emitFrequencyInSeconds;
}
@Override
public void initialize() {
windowLengthInSeconds = config.getInt(Conf.CTR_WINDOW_LENGTH, 300);
int windowLenghtInSlots = windowLengthInSeconds / emitFrequencyInSeconds;
clickCounter = new SlidingWindowCounter<>(windowLenghtInSlots);
impressionCounter = new SlidingWindowCounter<>(windowLenghtInSlots);
lastModifiedTracker = new NthLastModifiedTimeTracker(windowLenghtInSlots);
}
@Override
public void execute(Tuple tuple) {
if (TupleUtils.isTickTuple(tuple)) {
LOG.debug("Received tick tuple, triggering emit of current window counts");
emitCurrentWindowCounts();
} else {
countObjAndAck(tuple);
}
}
private void emitCurrentWindowCounts() {
Map<String, Long> clickCounts = clickCounter.getCountsThenAdvanceWindow();
Map<String, Long> impressionCounts = impressionCounter.getCountsThenAdvanceWindow();
int actualWindowLengthInSeconds = lastModifiedTracker.secondsSinceOldestModification();
lastModifiedTracker.markAsModified();
if (actualWindowLengthInSeconds != windowLengthInSeconds) {
LOG.warn(String.format(WINDOW_LENGTH_WARNING_TEMPLATE, actualWindowLengthInSeconds, windowLengthInSeconds));
}
emit(clickCounts, impressionCounts, actualWindowLengthInSeconds);
}
private void emit(Map<String, Long> clickCounts, Map<String, Long> impressionCounts, int actualWindowLengthInSeconds) {
for (Entry<String, Long> entry : clickCounts.entrySet()) {
String key = entry.getKey();
String[] ids = key.split(":");
long clicks = entry.getValue();
long impressions = impressionCounts.get(key);
double ctr = (double)clicks / (double)impressions;
collector.emit(new Values(ids[0], ids[1], ctr, impressions, clicks, actualWindowLengthInSeconds));
}
}
protected void countObjAndAck(Tuple tuple) {
AdEvent event = (AdEvent) tuple.getValueByField(Field.EVENT);
String key = String.format("%d:%d", event.getQueryId(), event.getAdID());
if (event.getType() == AdEvent.Type.Click) {
clickCounter.incrementCount(key);
} else if (event.getType() == AdEvent.Type.Impression) {
impressionCounter.incrementCount(key);
}
collector.ack(tuple);
}
@Override
public Map<String, Object> getComponentConfiguration() {
Map<String, Object> conf = new HashMap<>();
conf.put(Config.TOPOLOGY_TICK_TUPLE_FREQ_SECS, emitFrequencyInSeconds);
return conf;
}
@Override
public Fields getDefaultFields() {
return new Fields(Field.QUERY_ID, Field.AD_ID, Field.CTR, Field.IMPRESSIONS,
Field.CLICKS, Field.WINDOW_LENGTH);
}
}