package com.janrain.backplane2.server.dao;
import com.janrain.commons.supersimpledb.SimpleDBException;
import com.janrain.commons.supersimpledb.message.Message;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* Cache for (Backplane) Messages
*
* @author Johnny Bufu
*/
public class MessageCache<T extends Message> {
// - PUBLIC
/**
* @param maxCacheSizeBytes max cache size in bytes; 0 or negative values effectively disable the cache
*/
public MessageCache(long maxCacheSizeBytes) {
this.maxCacheSizeBytes = maxCacheSizeBytes;
}
public synchronized long getMaxCacheSizeBytes() {
return maxCacheSizeBytes;
}
public synchronized void setMaxCacheSizeBytes(long maxCacheSizeBytes) {
this.maxCacheSizeBytes = maxCacheSizeBytes;
}
public synchronized T get(String messageId) {
return cache.get(messageId);
}
public synchronized T getFirstMessage() {
//noinspection LoopStatementThatDoesntLoop
for (T t : cache.values()) {
return t;
}
return null;
}
public synchronized T getLastMessage() {
T last = null;
for (T t : cache.values()) {
last = t;
}
return last;
}
/**
* Adds new Messages to the Cache.
*
* All new Messages MUST compare greater than any existing message in the cache, otherwise the operation will fail.
*
* @param messages
* @throws SimpleDBException if any of the provided messages compares smaller than any existing message in the cache.
*/
public synchronized void add(List<T> messages) throws SimpleDBException {
lastUpdated.set(System.currentTimeMillis());
if (messages == null || messages.isEmpty()) return;
Collections.sort(messages);
T first = messages.get(0);
T lastCached = getLastMessage();
if (lastCached != null && first.compareTo(lastCached) < 0) {
throw new SimpleDBException("Cache update rejected, newer messages exists: " + lastCached.getIdValue());
}
for(T message : messages) {
cache.put(message.getIdValue(), message);
size.addAndGet(message.sizeBytes());
}
logger.info("Added " + messages.size() + " " + first.getClass().getSimpleName() + " items to cache");
}
public synchronized @NotNull List<T> getMessagesSince(String sinceIso8601timestamp, long acceptableStaleMillis) {
return System.currentTimeMillis() - lastUpdated.get() < acceptableStaleMillis ? getMessagesSince(sinceIso8601timestamp) :
new ArrayList<T>();
}
/**
* If the provided sinceIso8601timestamp is within the timestamps/IDs of the currently cached Messages,
* all messages on or after the provided sinceIso8601timestamp are returned.
* Otherwise an empty list is returned.
*/
public synchronized @NotNull List<T> getMessagesSince(String sinceIso8601timestamp) {
List<T> result = new ArrayList<T>();
T first = getFirstMessage();
if (first != null && first.getIdValue().compareTo(sinceIso8601timestamp) <= 0) {
for(Map.Entry<String,T> entry : cache.entrySet()) {
if (entry.getValue().getIdValue().compareTo(sinceIso8601timestamp) > 0) {
result.add(entry.getValue());
}
}
}
return result;
}
public long getLastUpdated() {
return lastUpdated.get();
}
// - PRIVATE
private static final Logger logger = Logger.getLogger(MessageCache.class);
private final LinkedHashMap<String,T> cache = new LinkedHashMap<String, T>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, T> eldest) {
int removed = 0;
Iterator<Map.Entry<String, T>> entries = entrySet().iterator();
while ( size.get() > maxCacheSizeBytes && entries.hasNext()) {
Map.Entry<String, T> next = entries.next();
entries.remove();
size.addAndGet( -1 * next.getValue().sizeBytes());
removed++;
}
if (removed > 0) {
logger.info("Removed " + removed + " " + eldest.getClass().getSimpleName() + " items from cache, new size is: " + size() + " items / " + size.get() + " bytes");
}
return false;
}
};
private final AtomicLong size = new AtomicLong(0);
private final AtomicLong lastUpdated = new AtomicLong(0);
private long maxCacheSizeBytes;
}