package net.contextfw.web.commons.cloud.storage; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import net.contextfw.web.application.PageHandle; import net.contextfw.web.application.WebApplication; import net.contextfw.web.application.WebApplicationException; import net.contextfw.web.application.configuration.Configuration; import net.contextfw.web.application.configuration.SettableProperty; import net.contextfw.web.application.scope.ScopedWebApplicationExecution; import net.contextfw.web.application.scope.WebApplicationStorage; import net.contextfw.web.commons.cloud.binding.CloudDatabase; import net.contextfw.web.commons.cloud.internal.mongo.ExceptionSafeExecution; import net.contextfw.web.commons.cloud.internal.mongo.MongoBase; import net.contextfw.web.commons.cloud.internal.serializer.Serializer; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.Singleton; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBObject; @Singleton public class MongoWebApplicationStorage extends MongoBase implements WebApplicationStorage { private static final double INITIAL_CURVE = 1.3; private static final int TRY_OUTS = 100; private static final int INITIAL_TRESHOLD = 100; private static final int SLEEP_PERIOD = 100; private static final Logger LOG = LoggerFactory .getLogger(MongoWebApplicationStorage.class); /** * Collection to hold pages */ public static final SettableProperty<String> COLLECTION_NAME = Configuration.createProperty(String.class, MongoWebApplicationStorage.class + ".collection"); /** * Informs whether throttling should be used */ public static final SettableProperty<Boolean> THROTTLE = Configuration.createProperty(Boolean.class, MongoWebApplicationStorage.class + "throttle"); /** * Informs how many concurrent page scopes must exists for certain IP-address * before throttling is used. */ public static final SettableProperty<Integer> THROTTLE_TRESHOLD = Configuration.createProperty(Integer.class, MongoWebApplicationStorage.class + "throttleTreshold"); /** * Informs whether throttling should be logged */ public static final SettableProperty<Boolean> THROTTLE_LOG = Configuration.createProperty(Boolean.class, MongoWebApplicationStorage.class + "throttleLog"); /** * Informs how fast the throttling will increase when page scopes increases. * * <p>The algorithm is following:</p> * <pre> * Math.pow(getPageCount(), THROTTLE_CURVE); * </pre> */ public static final SettableProperty<Double> THROTTLE_CURVE = Configuration.createProperty(Double.class, MongoWebApplicationStorage.class + "throttleCurve"); private static final String KEY_HANDLE = "handle"; private static final String KEY_REMOTE_ADDR = "remoteAddr"; private static final String KEY_VALID_THROUGH = "validThrough"; private static final String KEY_LOCKED = "locked"; private static final String KEY_APPLICATION = "application"; private static final DBObject APPLICATION_FIELDS = new BasicDBObject(KEY_APPLICATION, 1); private final boolean throttle; private final int throttleTreshold; private final boolean logThrottle; private final double throttleCurve; private final String collection; private final Serializer serializer; @Inject public MongoWebApplicationStorage(@CloudDatabase DB db, Configuration configuration, Serializer serializer) { super(db, configuration.get(Configuration.REMOVAL_SCHEDULE_PERIOD)); throttle = configuration.getOrElse(THROTTLE, false); throttleTreshold = configuration.getOrElse(THROTTLE_TRESHOLD, INITIAL_TRESHOLD); logThrottle = configuration.getOrElse(THROTTLE_LOG, false); throttleCurve = configuration.getOrElse(THROTTLE_CURVE, INITIAL_CURVE); collection = configuration.getOrElse(COLLECTION_NAME, "pages"); this.serializer = serializer; setIndexes(getCollection()); } @edu.umd.cs.findbugs.annotations.SuppressWarnings( value="SWL_SLEEP_WITH_LOCK_HELD", justification="Throttle is meant to be slow") private void throttle(String remoteAddr) { if (throttle) { long count = getCollection().count(o(KEY_REMOTE_ADDR, remoteAddr)); if (count > throttleTreshold) { synchronized (this) { try { long sleep = (long) Math.pow(getPageCount(), throttleCurve); if (logThrottle) { LOG.info("Throttling {} for {} ms", remoteAddr, sleep); } Thread.sleep(sleep); } catch (InterruptedException e) { } } } } } private long getPageCount() { return getCollection().count(); } private void create(PageHandle handle, String remoteAddr, WebApplication application, long validThrough) { application.setHandle(handle); BasicDBObject doc = new BasicDBObject(); doc.put(KEY_HANDLE, handle.toString()); doc.put(KEY_REMOTE_ADDR, remoteAddr); doc.put(KEY_VALID_THROUGH, validThrough); doc.put(KEY_LOCKED, true); getCollection().insert(doc); } private WebApplication load(DBObject obj) { if (obj != null) { return (WebApplication) serializer.unserialize((byte[]) obj.get(KEY_APPLICATION)); } else { return null; } } @Override protected DBCollection getCollection() { return getDb().getCollection(collection); } private void removeExpiredPages() { removeExpiredObjects(); } private PageHandle createHandle() { return new PageHandle(UUID.randomUUID().toString()); } @Override public void initialize(final WebApplication application, HttpServletRequest request, long validThrough, final ScopedWebApplicationExecution execution) { String remoteAddr = request.getRemoteAddr(); removeExpiredPages(); throttle(remoteAddr); final PageHandle handle = createHandle(); create(handle, remoteAddr, application, validThrough); executeExclusive(handle.toString(), remoteAddr, validThrough, application, execution); } @Override public void update(final PageHandle handle, HttpServletRequest request, long validThrough, final ScopedWebApplicationExecution execution) { String remoteAddr = request.getRemoteAddr(); removeExpiredPages(); throttle(remoteAddr); executeExclusive(handle.toString(), remoteAddr, validThrough, null, execution); } @Override public void execute(final PageHandle handle, final ScopedWebApplicationExecution execution) { executeExclusive(handle.toString(), null, null, null, execution); } @Override public void refresh(PageHandle handle, HttpServletRequest request, long validThrough) { DBObject query = b() .add(KEY_HANDLE, handle.toString()) .add(KEY_REMOTE_ADDR, request.getRemoteAddr()) .add(KEY_VALID_THROUGH, o("$gte", System.currentTimeMillis())).get(); getCollection().update(query, o("$set", o(KEY_VALID_THROUGH, validThrough))); } @Override public void remove(PageHandle handle, HttpServletRequest request) { DBObject query = b() .add(KEY_HANDLE, handle.toString()) .add(KEY_REMOTE_ADDR, request.getRemoteAddr()).get(); getCollection().remove(query); } private DBObject openExclusive(String handle) { BasicDBObjectBuilder queryBuilder = b() .add(KEY_HANDLE, handle) .add(KEY_LOCKED, false); DBObject update = o("$set", o(KEY_LOCKED, true)); DBObject query = queryBuilder.get(); for (int i = 0; i < TRY_OUTS; i++) { DBObject rv = getCollection().findAndModify( query, APPLICATION_FIELDS, null, false, update, true, false); if (rv == null) { try { Thread.sleep(SLEEP_PERIOD); } catch (InterruptedException e) { } } else { return rv; } } return getCollection().findOne( o(KEY_HANDLE, handle), APPLICATION_FIELDS); } private void closeExclusive(final String handle, final Long newValidThrough, final WebApplication application) { executeAsync(new ExceptionSafeExecution() { public void execute() throws Exception { DBObject query = o(KEY_HANDLE, handle); BasicDBObjectBuilder updateBuilder = b(); updateBuilder.push("$set"); updateBuilder.add(KEY_LOCKED, false); if (newValidThrough != null) { updateBuilder.add(KEY_VALID_THROUGH, newValidThrough); } if (application != null) { updateBuilder.add(KEY_APPLICATION, serializer.serialize(application)); } updateBuilder.pop(); getCollection().update(query, updateBuilder.get()); } }); } private void executeExclusive(String handle, String remoteAddr, Long newValidthrough, WebApplication givenApplication, final ScopedWebApplicationExecution execution) { WebApplication application = givenApplication != null ? givenApplication : loadExclusive(handle, remoteAddr); try { execution.execute(application); } finally { closeExclusive(handle, newValidthrough, application); } } private WebApplication loadExclusive(String handle, String remoteAddr) { DBCollection collection = getCollection(); BasicDBObjectBuilder queryBuilder = b().add(KEY_HANDLE, handle); if (remoteAddr != null) { queryBuilder.add(KEY_REMOTE_ADDR, remoteAddr); } queryBuilder .push(KEY_VALID_THROUGH) .add("$gte", System.currentTimeMillis()) .pop(); DBObject query = queryBuilder.get(); boolean exists = collection.count(query) == 1; if (exists) { return load(openExclusive(handle)); } else { return null; } } @Override public void storeLarge(PageHandle handle, String key, Object obj) { if (handle == null) { throw new IllegalArgumentException("Handle cannot be null"); } else if (StringUtils.isBlank(key)) { throw new IllegalArgumentException("Key cannot be null or blank!"); } DBObject query = b() .add(KEY_HANDLE, handle.toString()) .add(KEY_VALID_THROUGH, o("$gte", System.currentTimeMillis())).get(); DBObject update; if (obj == null) { update = o("$unset", o("large_" + key, 1)); } else { update = o("$set", o("large_" + key, serializer.serialize(obj))); } if (getCollection().update(query, update).getN() != 1) { throw new WebApplicationException("Page scope does not exist"); } } @SuppressWarnings("unchecked") @Override public <T> T loadLarge(PageHandle handle, String key, Class<T> type) { if (handle == null) { throw new IllegalArgumentException("Handle cannot be null"); } else if (StringUtils.isBlank(key)) { throw new IllegalArgumentException("Key cannot be null or blank!"); } DBObject query = b() .add(KEY_HANDLE, handle.toString()) .add(KEY_VALID_THROUGH, o("$gte", System.currentTimeMillis())).get(); DBObject field = o("large_" + key, 1); DBObject obj = getCollection().findOne(query, field); if (obj == null) { throw new WebApplicationException("Page scope does not exist"); } byte[] data = (byte[]) obj.get("large_" + key); return data == null ? null : (T) serializer.unserialize(data); } }