/*
* Copyright 2014 Eediom Inc.
*
* 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 org.araqne.logdb.cep;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EventClock<T extends EventClockItem> {
private final Logger slog = LoggerFactory.getLogger(EventClock.class);
private final TimeoutComparator timeoutComparator = new TimeoutComparator();
private final ExpireComparator expireComparator = new ExpireComparator();
private final EventClockCallback callback;
private final String host;
private final PriorityQueue<Expirable<T>> timeoutQueue;
private final HashSet<T> timeoutSet;
private final PriorityQueue<T> expireQueue;
private AtomicLong lastTime = new AtomicLong();
public EventClock(EventClockCallback callback, String host, long lastTime, int initialCapacity) {
this.callback = callback;
this.host = host;
this.lastTime = new AtomicLong(lastTime);
this.timeoutQueue = new PriorityQueue<Expirable<T>>(initialCapacity);
this.expireQueue = new PriorityQueue<T>(initialCapacity, expireComparator);
this.timeoutSet = new HashSet<T>(initialCapacity);
}
public String getHost() {
return host;
}
public Date getTime() {
return new Date(lastTime.get());
}
public List<T> getTimeoutContexts() {
List<T> l = new ArrayList<T>(timeoutQueue.size());
for (Expirable<T> e : timeoutQueue) {
l.add(e.item);
}
Collections.sort(l, timeoutComparator);
return l;
}
public List<T> getExpireContexts() {
List<T> l = new ArrayList<T>(expireQueue);
Collections.sort(l, expireComparator);
return l;
}
public int getTimeoutQueueLength() {
return timeoutQueue.size();
}
public int getExpireQueueLength() {
return expireQueue.size();
}
public void setTime(long now, boolean force) {
if (force) {
lastTime.set(now);
} else {
while (now > lastTime.get()) {
long l = lastTime.get();
if (lastTime.compareAndSet(l, now)) {
evictContext(now);
break;
}
}
}
}
public void add(T item) {
synchronized (expireQueue) {
if (item.getExpireTime() != 0)
expireQueue.add(item);
}
synchronized (timeoutQueue) {
if (item.getTimeoutTime() != 0)
addTimeout(item);
}
}
public void updateTimeout(T item) {
if (slog.isDebugEnabled()) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
slog.debug("araqne logdb cep: update timeout [{}] of context [{}]", df.format(new Date(item.getTimeoutTime())),
item.getKey());
}
synchronized (timeoutQueue) {
if (item.getTimeoutTime() != 0 && !timeoutSet.contains(item)) {
addTimeout(item);
}
}
}
// The ctx object will be added into the timeoutQueue again when ORIGINAL
// timeout has met;
// so following O(n) operation (remove) can be avoided.
// synchronized (timeoutQueue) {
// // reorder
// timeoutQueue.remove(ctx);
// timeoutQueue.add(ctx);
// }
private void addTimeout(T item) {
timeoutQueue.add(new Expirable<T>(item, item.getTimeoutTime()));
timeoutSet.add(item);
}
// This class caches ORIGINAL timeout time of EventClockItem.
// It helps timeoutQueue always to be sorted by timeout time.
// If the timeout time of an EventClockItem has updated,
// EventClock attempts once to evict the EventClockItem by ORIGINAL timeout
// time,
// but add it again into the queue.
private static class Expirable<T> implements Comparable<Expirable<T>> {
private long expireTime;
private T item;
public Expirable(T item, long timeoutTime) {
this.expireTime = timeoutTime;
this.item = item;
}
public Expirable(T item) {
this(item, -1);
}
public long getExpireTime() {
return expireTime;
}
@Override
public int compareTo(Expirable<T> t) {
long d = this.expireTime - t.expireTime;
return d < 0 ? -1 : (d > 0 ? +1 : 0);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((item == null) ? 0 : item.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
@SuppressWarnings("unchecked")
Expirable<T> other = (Expirable<T>) obj;
if (item == null) {
if (other.item != null)
return false;
} else if (item != other.item)
return false;
return true;
}
}
public void remove(T item) {
synchronized (expireQueue) {
expireQueue.remove(item);
}
synchronized (timeoutQueue) {
timeoutQueue.remove(new Expirable<T>(item));
timeoutSet.remove(item);
}
}
private void evictContext(long now) {
HashMap<EventKey, T> expiredEvictees = new HashMap<EventKey, T>();
synchronized (expireQueue) {
while (true) {
T item = expireQueue.peek();
if (item == null)
break;
if (item.getExpireTime() <= now) {
expireQueue.poll();
expiredEvictees.put(item.getKey(), item);
} else
break;
}
}
for (T e : expiredEvictees.values()) {
try {
callback.onRemove(e, EventCause.EXPIRE);
} catch (Throwable t) {
slog.error("aranqe logdb cep: event clock callback should not throw any exception", t);
}
}
expiredEvictees = null;
HashMap<EventKey, T> timeoutEvictees = new HashMap<EventKey, T>();
synchronized (timeoutQueue) {
while (true) {
Expirable<T> e = timeoutQueue.peek();
if (e == null)
break;
if (e.getExpireTime() <= now) {
timeoutQueue.poll();
timeoutSet.remove(e.item);
// if timeout time has updated, don't evict and add again;
if (e.item.getTimeoutTime() != 0 && e.item.getTimeoutTime() > e.getExpireTime()) {
addTimeout(e.item);
} else {
timeoutEvictees.put(e.item.getKey(), e.item);
}
} else
break;
}
}
for (T e : timeoutEvictees.values()) {
try {
callback.onRemove(e, EventCause.TIMEOUT);
} catch (Throwable t) {
slog.error("aranqe logdb cep: event clock callback should not throw any exception", t);
}
}
}
@Override
public String toString() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return host + " (timeout: " + timeoutQueue.size() + ", expire: " + expireQueue.size() + ") => "
+ df.format(new Date(lastTime.get()));
}
private class TimeoutComparator implements Comparator<T> {
@Override
public int compare(T o1, T o2) {
long t1 = o1.getTimeoutTime();
long t2 = o2.getTimeoutTime();
if (t1 == t2)
return 0;
return t1 < t2 ? -1 : 1;
}
}
private class ExpireComparator implements Comparator<T> {
@Override
public int compare(T o1, T o2) {
long t1 = o1.getExpireTime();
long t2 = o2.getExpireTime();
if (t1 == t2)
return 0;
return t1 < t2 ? -1 : 1;
}
}
}