/*
* Copyright 2012 astamuse company,Ltd.
*
* 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 com.astamuse.asta4d.web.util.timeout;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.StringUtils;
import com.astamuse.asta4d.util.IdGenerator;
import com.astamuse.asta4d.web.WebApplicationContext;
public class DefaultSessionAwareExpirableDataManager implements ExpirableDataManager {
private static final String SessionCheckIdKey = DefaultSessionAwareExpirableDataManager.class + "#SessionCheckIdKey";
private Map<String, DataHolder> dataMap = null;
private AtomicInteger dataCounter = null;
private ScheduledExecutorService service = null;
// 3 minutes
private long expirationCheckPeriodInMilliseconds = 3 * 60 * 1000;
private int maxDataSize = 10_000;
private boolean sessionAware = true;
private long spinTimeInMilliseconds = 1000;
// 5 times spinning
private long maxSpinTimeInMilliseconds = 1000 * 5;
private String checkThreadName = this.getClass().getSimpleName() + "-check-thread";
public DefaultSessionAwareExpirableDataManager() {
dataMap = createThreadSafeDataMap();
dataCounter = new AtomicInteger();
}
public void setExpirationCheckPeriodInMilliseconds(long expirationCheckPeriodInMilliseconds) {
this.expirationCheckPeriodInMilliseconds = expirationCheckPeriodInMilliseconds;
}
public void setMaxDataSize(int maxDataSize) {
this.maxDataSize = maxDataSize;
}
public void setSessionAware(boolean sessionAware) {
this.sessionAware = sessionAware;
}
public void setSpinTimeInMilliseconds(long spinTimeInMilliseconds) {
this.spinTimeInMilliseconds = spinTimeInMilliseconds;
}
public void setMaxSpinTimeInMilliseconds(long maxSpinTimeInMilliseconds) {
this.maxSpinTimeInMilliseconds = maxSpinTimeInMilliseconds;
}
protected Map<String, DataHolder> createThreadSafeDataMap() {
return new ConcurrentHashMap<>();
}
protected void decreaseCount() {
dataCounter.decrementAndGet();
}
protected void addCount(int delta) {
dataCounter.addAndGet(delta);
}
protected void increaseCount() {
dataCounter.incrementAndGet();
}
protected int getCurrentCount() {
return dataCounter.get();
}
@Override
public void start() {
service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, checkThreadName);
}
});
// start check thread
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
List<Entry<String, DataHolder>> entries = new ArrayList<>(dataMap.entrySet());
long currentTime = System.currentTimeMillis();
int removedCounter = 0;
Object existing;
for (Entry<String, DataHolder> entry : entries) {
if (entry.getValue().isExpired(currentTime)) {
existing = dataMap.remove(entry.getKey());
if (existing != null) {// we removed it successfully
removedCounter++;
}
}
}
if (removedCounter > 0) {
addCount(-removedCounter);
}
}
}, expirationCheckPeriodInMilliseconds, expirationCheckPeriodInMilliseconds, TimeUnit.MILLISECONDS);
}
@Override
public void stop() {
// release all resources
service.shutdownNow();
dataCounter = null;
dataMap = null;
}
@SuppressWarnings("unchecked")
public <T> T get(String dataId, boolean remove) {
DataHolder holder;
if (remove) {
holder = dataMap.remove(dataId);
if (holder != null) {
decreaseCount();
if (holder.isExpired(System.currentTimeMillis())) {
holder = null;
}
}
} else {
holder = dataMap.get(dataId);
if (holder != null) {
if (holder.isExpired(System.currentTimeMillis())) {
holder = dataMap.remove(dataId);
if (holder != null) {
decreaseCount();
holder = null;
}
}
}
}
if (holder == null) {
return null;
} else {
if (StringUtils.equals(retrieveSessionCheckId(false), holder.sessionId)) {
return (T) holder.getData();
} else {
return null;
}
}
}
public void put(String dataId, Object data, long expireMilliSeconds) {
checkSize();
Object existing = dataMap.put(dataId, new DataHolder(data, expireMilliSeconds, retrieveSessionCheckId(true)));
if (existing == null) {
increaseCount();
}
}
protected void checkSize() {
if (getCurrentCount() >= maxDataSize) {
try {
long spinTimeTotal = 0;
while (getCurrentCount() >= maxDataSize) {
if (spinTimeTotal >= maxSpinTimeInMilliseconds) {
String msg = "There are too many data in %s and we could not get empty space after waiting for %d milliseconds." +
" The configured max size is %d and perhaps you should increase the value.";
msg = String.format(msg, this.getClass().getName(), spinTimeTotal, maxDataSize);
throw new TooManyDataException(msg);
}
Thread.sleep(spinTimeInMilliseconds);
spinTimeTotal += spinTimeInMilliseconds;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* NOTE: Because there is no way to retrieve the session id via WebApplicationContext, thus we use a independent check id which is
* different from the http request session id to check whether current request client is the same client from previous request.
*
* @param create
* @return
*/
protected String retrieveSessionCheckId(boolean create) {
String sessionCheckId = null;
if (sessionAware) {
WebApplicationContext context = WebApplicationContext.getCurrentThreadWebApplicationContext();
sessionCheckId = context.getData(WebApplicationContext.SCOPE_SESSION, SessionCheckIdKey);
if (sessionCheckId == null && create) {
sessionCheckId = IdGenerator.createId();
context.setData(WebApplicationContext.SCOPE_SESSION, SessionCheckIdKey, sessionCheckId);
}
}
return sessionCheckId;
}
@Override
protected void finalize() throws Throwable {
stop();
super.finalize();
}
protected static class DataHolder implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private final Object data;
private final long creationTime;
private final long expireMilliSeconds;
private final String sessionId;
private DataHolder(Object data, long expireMilliSeconds, String sessionId) {
this.sessionId = sessionId;
this.data = data;
this.expireMilliSeconds = expireMilliSeconds;
this.creationTime = System.currentTimeMillis();
}
private Object getData() {
return data;
}
private boolean isExpired(long currentTime) {
return (currentTime - creationTime) > expireMilliSeconds;
}
}
}