/************************************************************************************** * Copyright (C) 2008 EsperTech, Inc. All rights reserved. * * http://esper.codehaus.org * * 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.ExtensionServicesContext; import com.espertech.esper.epl.expression.ExprEvaluator; import com.espertech.esper.epl.expression.ExprNode; import com.espertech.esper.schedule.ScheduleHandleCallback; import com.espertech.esper.schedule.ScheduleSlot; import com.espertech.esper.util.CollectionUtil; import com.espertech.esper.util.StopCallback; import com.espertech.esper.view.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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 TimeOrderViewFactory timeOrderViewFactory; private final ExprNode timestampExpression; private final ExprEvaluator timestampEvaluator; protected final long intervalSize; protected final IStreamSortRankRandomAccess optionalSortedRandomAccess; protected final ScheduleSlot scheduleSlot; protected final EPStatementHandleCallback handle; private EventBean[] eventsPerStream = new EventBean[1]; protected TreeMap<Object, Object> sortedEvents; protected boolean isCallbackScheduled; protected int eventCount; /** * Ctor. * @param optionalSortedRandomAccess is the friend class handling the random access, if required by * expressions * @param timeOrderViewFactory for copying this view in a group-by * @param timestampExpr the property name of the event supplying timestamp values * @param intervalSize the interval time length */ public TimeOrderView( AgentInstanceViewFactoryChainContext agentInstanceContext, TimeOrderViewFactory timeOrderViewFactory, ExprNode timestampExpr, ExprEvaluator timestampEvaluator, long intervalSize, IStreamSortRankRandomAccess optionalSortedRandomAccess) { this.agentInstanceContext = agentInstanceContext; this.timeOrderViewFactory = timeOrderViewFactory; this.timestampExpression = timestampExpr; this.timestampEvaluator = timestampEvaluator; this.intervalSize = intervalSize; this.optionalSortedRandomAccess = optionalSortedRandomAccess; this.scheduleSlot = agentInstanceContext.getStatementContext().getScheduleBucket().allocateSlot(); sortedEvents = new TreeMap<Object, Object>(); ScheduleHandleCallback callback = new ScheduleHandleCallback() { public void scheduledTrigger(ExtensionServicesContext extensionServicesContext) { TimeOrderView.this.expire(); } }; handle = new EPStatementHandleCallback(agentInstanceContext.getEpStatementAgentInstanceHandle(), callback); agentInstanceContext.getTerminationCallbacks().add(this); } /** * Returns the timestamp property name. * @return property name supplying timestamp values */ public ExprNode getTimestampExpression() { return timestampExpression; } /** * Returns the time interval size. * @return interval size */ public long getIntervalSize() { return intervalSize; } public View cloneView() { return timeOrderViewFactory.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) { 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 - intervalSize + 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()) { updateChildren(newData, postOldEventsArray); } } 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 + " intervalSize=" + intervalSize; } /** * 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 expireBeforeTimestamp = agentInstanceContext.getStatementContext().getSchedulingService().getTime() - intervalSize + 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()]); updateChildren(null, oldEvents); } } // 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.getTerminationCallbacks().remove(this); } public void stop() { stopSchedule(); } public void stopSchedule() { if (handle != null) { agentInstanceContext.getStatementContext().getSchedulingService().remove(handle, scheduleSlot); } } private static final Log log = LogFactory.getLog(TimeOrderView.class); }