/*
***************************************************************************************
* Copyright (C) 2006 EsperTech, Inc. All rights reserved. *
* http://www.espertech.com/esper *
* http://www.espertech.com *
* ---------------------------------------------------------------------------------- *
* The software in this package is published under the terms of the GPL license *
* a copy of which has been included with this distribution in the license.txt file. *
***************************************************************************************
*/
package com.espertech.esper.view.ext;
import com.espertech.esper.client.EventBean;
import com.espertech.esper.client.EventType;
import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext;
import com.espertech.esper.core.service.EPStatementHandleCallback;
import com.espertech.esper.core.service.EngineLevelExtensionServicesContext;
import com.espertech.esper.epl.expression.core.ExprEvaluator;
import com.espertech.esper.epl.expression.core.ExprNode;
import com.espertech.esper.epl.expression.time.ExprTimePeriodEvalDeltaConst;
import com.espertech.esper.metrics.instrumentation.InstrumentationHelper;
import com.espertech.esper.schedule.ScheduleHandleCallback;
import com.espertech.esper.util.CollectionUtil;
import com.espertech.esper.util.StopCallback;
import com.espertech.esper.view.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
/**
* Window retaining timestamped events up to a given number of seconds such that
* older events that arrive later are sorted into the window and released in timestamp order.
* <p>
* The insert stream consists of all arriving events. The remove stream consists of events in
* order of timestamp value as supplied by each event.
* <p>
* Timestamp values on events should match engine time. The window compares engine time to timestamp value
* and releases events when the event's timestamp is less then engine time minus interval size (the
* event is older then the window tail).
* <p>
* The view accepts 2 parameters. The first parameter is the field name to get the event timestamp value from,
* the second parameter defines the interval size.
*/
public class TimeOrderView extends ViewSupport implements DataWindowView, CloneableView, StoppableView, StopCallback {
protected final AgentInstanceViewFactoryChainContext agentInstanceContext;
private final ViewFactory viewFactory;
private final ExprNode timestampExpression;
private final ExprEvaluator timestampEvaluator;
protected final ExprTimePeriodEvalDeltaConst timeDeltaComputation;
protected final IStreamSortRankRandomAccess optionalSortedRandomAccess;
protected final long scheduleSlot;
protected final EPStatementHandleCallback handle;
private EventBean[] eventsPerStream = new EventBean[1];
protected TreeMap<Object, Object> sortedEvents;
protected boolean isCallbackScheduled;
protected int eventCount;
public TimeOrderView(AgentInstanceViewFactoryChainContext agentInstanceContext,
ViewFactory viewFactory,
ExprNode timestampExpr,
ExprEvaluator timestampEvaluator,
ExprTimePeriodEvalDeltaConst timeDeltaComputation,
IStreamSortRankRandomAccess optionalSortedRandomAccess) {
this.agentInstanceContext = agentInstanceContext;
this.viewFactory = viewFactory;
this.timestampExpression = timestampExpr;
this.timestampEvaluator = timestampEvaluator;
this.timeDeltaComputation = timeDeltaComputation;
this.optionalSortedRandomAccess = optionalSortedRandomAccess;
this.scheduleSlot = agentInstanceContext.getStatementContext().getScheduleBucket().allocateSlot();
sortedEvents = new TreeMap<Object, Object>();
ScheduleHandleCallback callback = new ScheduleHandleCallback() {
public void scheduledTrigger(EngineLevelExtensionServicesContext extensionServicesContext) {
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().qViewScheduledEval(TimeOrderView.this, TimeOrderView.this.viewFactory.getViewName());
}
TimeOrderView.this.expire();
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().aViewScheduledEval();
}
}
};
handle = new EPStatementHandleCallback(agentInstanceContext.getEpStatementAgentInstanceHandle(), callback);
agentInstanceContext.addTerminationCallback(this);
}
/**
* Returns the timestamp property name.
*
* @return property name supplying timestamp values
*/
public ExprNode getTimestampExpression() {
return timestampExpression;
}
public ExprTimePeriodEvalDeltaConst getTimeDeltaComputation() {
return timeDeltaComputation;
}
public View cloneView() {
return viewFactory.makeView(agentInstanceContext);
}
public final EventType getEventType() {
// The schema is the parent view's schema
return parent.getEventType();
}
public final void update(EventBean[] newData, EventBean[] oldData) {
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().qViewProcessIRStream(this, viewFactory.getViewName(), newData, oldData);
}
EventBean[] postOldEventsArray = null;
// Remove old data
if (oldData != null) {
for (int i = 0; i < oldData.length; i++) {
EventBean oldDataItem = oldData[i];
Object sortValues = getTimestamp(oldDataItem);
boolean result = CollectionUtil.removeEventByKeyLazyListMap(sortValues, oldDataItem, sortedEvents);
if (result) {
eventCount--;
if (postOldEventsArray == null) {
postOldEventsArray = oldData;
} else {
postOldEventsArray = CollectionUtil.addArrayWithSetSemantics(postOldEventsArray, oldData);
}
internalHandleRemoved(sortValues, oldDataItem);
}
}
}
if ((newData != null) && (newData.length > 0)) {
// figure out the current tail time
long engineTime = agentInstanceContext.getStatementContext().getSchedulingService().getTime();
long windowTailTime = engineTime - timeDeltaComputation.deltaAdd(engineTime) + 1;
long oldestEvent = Long.MAX_VALUE;
if (!sortedEvents.isEmpty()) {
oldestEvent = (Long) sortedEvents.firstKey();
}
boolean addedOlderEvent = false;
// add events or post events as remove stream if already older then tail time
ArrayList<EventBean> postOldEvents = null;
for (int i = 0; i < newData.length; i++) {
// get timestamp of event
EventBean newEvent = newData[i];
Long timestamp = getTimestamp(newEvent);
// if the event timestamp indicates its older then the tail of the window, release it
if (timestamp < windowTailTime) {
if (postOldEvents == null) {
postOldEvents = new ArrayList<EventBean>(2);
}
postOldEvents.add(newEvent);
} else {
if (timestamp < oldestEvent) {
addedOlderEvent = true;
oldestEvent = timestamp;
}
// add to list
CollectionUtil.addEventByKeyLazyListMapBack(timestamp, newEvent, sortedEvents);
eventCount++;
internalHandleAdd(timestamp, newEvent);
}
}
// If we do have data, check the callback
if (!sortedEvents.isEmpty()) {
// If we haven't scheduled a callback yet, schedule it now
if (!isCallbackScheduled) {
long callbackWait = oldestEvent - windowTailTime + 1;
agentInstanceContext.getStatementContext().getSchedulingService().add(callbackWait, handle, scheduleSlot);
isCallbackScheduled = true;
} else {
// We may need to reschedule, and older event may have been added
if (addedOlderEvent) {
oldestEvent = (Long) sortedEvents.firstKey();
long callbackWait = oldestEvent - windowTailTime + 1;
agentInstanceContext.getStatementContext().getSchedulingService().remove(handle, scheduleSlot);
agentInstanceContext.getStatementContext().getSchedulingService().add(callbackWait, handle, scheduleSlot);
isCallbackScheduled = true;
}
}
}
if (postOldEvents != null) {
postOldEventsArray = postOldEvents.toArray(new EventBean[postOldEvents.size()]);
}
if (optionalSortedRandomAccess != null) {
optionalSortedRandomAccess.refresh(sortedEvents, eventCount, eventCount);
}
}
// update child views
if (this.hasViews()) {
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().qViewIndicate(this, viewFactory.getViewName(), newData, postOldEventsArray);
}
updateChildren(newData, postOldEventsArray);
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().aViewIndicate();
}
}
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().aViewProcessIRStream();
}
}
public void internalHandleAdd(Object sortValues, EventBean newDataItem) {
// no action required
}
public void internalHandleRemoved(Object sortValues, EventBean oldDataItem) {
// no action required
}
public void internalHandleExpired(Object sortValues, EventBean oldDataItem) {
// no action required
}
public void internalHandleExpired(Object sortValues, List<EventBean> oldDataItems) {
// no action required
}
protected Long getTimestamp(EventBean newEvent) {
eventsPerStream[0] = newEvent;
return (Long) timestampEvaluator.evaluate(eventsPerStream, true, agentInstanceContext);
}
/**
* True to indicate the sort window is empty, or false if not empty.
*
* @return true if empty sort window
*/
public boolean isEmpty() {
return sortedEvents.isEmpty();
}
public final Iterator<EventBean> iterator() {
return new SortWindowIterator(sortedEvents);
}
public final String toString() {
return this.getClass().getName() +
" timestampExpression=" + timestampExpression;
}
public void visitView(ViewDataVisitor viewDataVisitor) {
viewDataVisitor.visitPrimary(sortedEvents, false, viewFactory.getViewName(), eventCount, null);
}
/**
* This method removes (expires) objects from the window and schedules a new callback for the
* time when the next oldest message would expire from the window.
*/
protected final void expire() {
long currentTime = agentInstanceContext.getStatementContext().getSchedulingService().getTime();
long expireBeforeTimestamp = currentTime - timeDeltaComputation.deltaSubtract(currentTime) + 1;
isCallbackScheduled = false;
List<EventBean> releaseEvents = null;
Long oldestKey;
while (true) {
if (sortedEvents.isEmpty()) {
oldestKey = null;
break;
}
oldestKey = (Long) sortedEvents.firstKey();
if (oldestKey >= expireBeforeTimestamp) {
break;
}
Object released = sortedEvents.remove(oldestKey);
if (released != null) {
if (released instanceof List) {
List<EventBean> releasedEventList = (List<EventBean>) released;
if (releaseEvents == null) {
releaseEvents = releasedEventList;
} else {
releaseEvents.addAll(releasedEventList);
}
eventCount -= releasedEventList.size();
internalHandleExpired(oldestKey, releasedEventList);
} else {
EventBean releasedEvent = (EventBean) released;
if (releaseEvents == null) {
releaseEvents = new ArrayList<EventBean>(4);
}
releaseEvents.add(releasedEvent);
eventCount--;
internalHandleExpired(oldestKey, releasedEvent);
}
}
}
if (optionalSortedRandomAccess != null) {
optionalSortedRandomAccess.refresh(sortedEvents, eventCount, eventCount);
}
// If there are child views, do the update method
if (this.hasViews()) {
if ((releaseEvents != null) && (!releaseEvents.isEmpty())) {
EventBean[] oldEvents = releaseEvents.toArray(new EventBean[releaseEvents.size()]);
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().qViewIndicate(this, viewFactory.getViewName(), null, oldEvents);
}
updateChildren(null, oldEvents);
if (InstrumentationHelper.ENABLED) {
InstrumentationHelper.get().aViewIndicate();
}
}
}
// If we still have events in the window, schedule new callback
if (oldestKey == null) {
return;
}
// Next callback
long callbackWait = oldestKey - expireBeforeTimestamp + 1;
agentInstanceContext.getStatementContext().getSchedulingService().add(callbackWait, handle, scheduleSlot);
isCallbackScheduled = true;
}
public void stopView() {
stopSchedule();
agentInstanceContext.removeTerminationCallback(this);
}
public void stop() {
stopSchedule();
}
public void stopSchedule() {
if (handle != null) {
agentInstanceContext.getStatementContext().getSchedulingService().remove(handle, scheduleSlot);
}
}
public ViewFactory getViewFactory() {
return viewFactory;
}
private static final Logger log = LoggerFactory.getLogger(TimeOrderView.class);
}