001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.dataAccess.db;
019
020import io.stallion.Context;
021import io.stallion.requests.JobRequest;
022import io.stallion.requests.TaskRequest;
023import net.sf.ehcache.Cache;
024import net.sf.ehcache.CacheManager;
025import net.sf.ehcache.Element;
026import net.sf.ehcache.config.CacheConfiguration;
027import net.sf.ehcache.config.PersistenceConfiguration;
028
029import javax.servlet.http.Cookie;
030
031import static io.stallion.utils.Literals.*;
032
033/**
034 * SmartQueryCache is used by the DB singleton to cache query results.
035 * Smart cache uses a bunch of heuristics to ensure that a user who has
036 * just made an update never sees stale data.
037*/
038public class SmartQueryCache {
039    private static CacheManager manager;
040
041
042    public static void load() {
043
044        manager = CacheManager.create();
045    }
046
047    public static void initCache(String bucket) {
048        if (manager == null) {
049            load();
050        }
051        if (manager.getCache(bucket) != null) {
052            return;
053        }
054        CacheConfiguration config = new CacheConfiguration(bucket, 10000);
055        //config.setDiskPersistent(false);
056        //config.setMaxElementsOnDisk(0);
057        //config.setMaxBytesLocalDisk(0L);
058        config.setOverflowToOffHeap(false);
059        PersistenceConfiguration persistenceConfiguration = new PersistenceConfiguration();
060        persistenceConfiguration.strategy(PersistenceConfiguration.Strategy.NONE);
061        config.persistence(persistenceConfiguration);
062        manager.addCache(new Cache(config));
063    }
064
065    public static void shutdown() {
066        if (manager != null) {
067            manager.shutdown();
068        }
069        manager = null;
070    }
071
072    /**
073     * Under certain conditions, will skip the cache lookup and always return null, thus prompting a cache refresh
074     * This is to prevent getting a stale object right after a user has made a POST request.
075     * Otherwise, will hit the cache as normal.
076     *
077     *  By default:
078     *
079     * * Results are cached for 15 seconds
080     * * caching is skipped when run via a task or a job
081     * * caching is skipped for non-GET requests
082     * * caching is only skipped once per request
083     * * caching is skipped if the user made a post request in the last 15 seconds.
084     *
085     * Thus altogether, if you are getting dozens of GET requests per second, due to a page
086     * getting heavy traffic, then the cache will almost always be hit. But if a user
087     * has just made some sort of update, they are guaranteed not to get stale data.
088     *
089     * @param bucket
090     * @param key
091     * @return
092     */
093    public static Object getSmart(String bucket, String key) {
094        if (checkShouldSkip(bucket, key)) {
095            return null;
096        }
097        return get(bucket, key);
098    }
099
100
101    public static boolean checkShouldSkip(String bucket, String key) {
102        // We only skip the cache at most once per lookup, per request
103        // So we set a request context item to mark that we've done this check.
104        Boolean seen = (Boolean)Context.getRequest().getItems().get("checked-should-skip-for-key-" + bucket + "---" + key);
105        if (seen != null && seen) {
106            return false;
107        }
108        Context.getRequest().getItems().put("checked-should-skip-for-key-" + bucket + "---" + key, true);
109
110        if (Context.getRequest() instanceof TaskRequest) {
111            return true;
112        }
113        if (Context.getRequest() instanceof JobRequest) {
114            return true;
115        }
116        if (!"GET".equals(Context.getRequest().getMethod())) {
117            return true;
118        }
119
120        // The current user
121        Cookie postBackCookie = Context.getRequest().getCookie("st-recent-postback");
122        if (postBackCookie != null && !empty(postBackCookie.getValue())) {
123            Long t = Long.parseLong(postBackCookie.getValue());
124            if (t != null && t > (mils() - 15000)) {
125                return true;
126            }
127        }
128        return false;
129    }
130
131    public static Object get(String bucket, String key) {
132        if (manager == null) {
133            return null;
134        }
135        Element element = manager.getCache(bucket).get(key);
136        if (element != null) {
137            return element.getObjectValue();
138        }
139        return null;
140    }
141
142    public static void set(String bucket, String key, Object value) {
143        if (manager == null) {
144            load();
145        }
146        if (!manager.cacheExists(bucket)) {
147            initCache(bucket);
148        }
149        Element element = new Element(key, value);
150        element.setTimeToLive(15);
151        manager.getCache(bucket).put(element);
152    }
153
154    public static void clearBucket(String bucket) {
155        if (manager.cacheExists(bucket) && manager.getCache(bucket).getSize() > 0) {
156            manager.getCache(bucket).removeAll();
157        }
158    }
159}