/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.svc.cache;
import password.pwm.PwmApplication;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.localdb.LocalDB;
import password.pwm.util.localdb.LocalDBException;
import password.pwm.util.logging.PwmLogger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
public class LocalDBCacheStore implements CacheStore {
private static final PwmLogger LOGGER = PwmLogger.forClass(LocalDBCacheStore.class);
private static final LocalDB.DB DB = LocalDB.DB.CACHE;
private static final int MAX_REMOVALS_PER_CYCLE = 10 * 1000;
private static final int TICKS_BETWEEN_PURGE_CYCLES = 1000;
private final LocalDB localDB;
private final ExecutorService timer;
private final AtomicInteger ticks = new AtomicInteger(0);
private final CacheStoreInfo cacheStoreInfo = new CacheStoreInfo();
LocalDBCacheStore(final PwmApplication pwmApplication) {
this.localDB = pwmApplication.getLocalDB();
try {
localDB.truncate(DB);
} catch (LocalDBException e) {
LOGGER.error("error while clearing LocalDB CACHE DB during init: " + e.getMessage());
}
timer = JavaHelper.makeSingleThreadExecutorService(pwmApplication, LocalDBCacheStore.class);
}
@Override
public void store(final CacheKey cacheKey, final Instant expirationDate, final String data)
throws PwmUnrecoverableException
{
ticks.incrementAndGet();
cacheStoreInfo.incrementStoreCount();
try {
localDB.put(DB,cacheKey.getHash(),JsonUtil.serialize(new CacheValueWrapper(cacheKey, expirationDate, data)));
} catch (LocalDBException e) {
LOGGER.error("error while writing cache: " + e.getMessage());
}
if (ticks.get() > TICKS_BETWEEN_PURGE_CYCLES) {
ticks.set(0);
timer.execute(new PurgerTask());
}
}
@Override
public String read(final CacheKey cacheKey)
throws PwmUnrecoverableException
{
cacheStoreInfo.incrementReadCount();
final String hashKey = cacheKey.getHash();
final String storedValue;
try {
storedValue = localDB.get(DB,hashKey);
} catch (LocalDBException e) {
LOGGER.error("error while reading cache: " + e.getMessage());
return null;
}
if (storedValue != null) {
try {
final CacheValueWrapper valueWrapper = JsonUtil.deserialize(storedValue, CacheValueWrapper.class);
if (cacheKey.equals(valueWrapper.getCacheKey())) {
if (valueWrapper.getExpirationDate().isAfter(Instant.now())) {
cacheStoreInfo.getHitCount();
return valueWrapper.getPayload();
}
}
} catch (Exception e) {
LOGGER.error("error reading from cache: " + e.getMessage());
}
try {
localDB.remove(DB,hashKey);
} catch (LocalDBException e) {
LOGGER.error("error while purging record from cache: " + e.getMessage());
}
}
cacheStoreInfo.incrementMissCount();
return null;
}
@Override
public CacheStoreInfo getCacheStoreInfo() {
return cacheStoreInfo;
}
private boolean purgeExpiredRecords() throws LocalDBException {
final List<String> removalKeys = new ArrayList<>();
final LocalDB.LocalDBIterator<String> localDBIterator = localDB.iterator(DB);
int counter = 0;
try {
while (localDBIterator.hasNext() && removalKeys.size() < MAX_REMOVALS_PER_CYCLE) {
final String key = localDBIterator.next();
counter++;
boolean keep = false;
try {
if (key != null) {
final String strValue = localDB.get(DB, key);
if (strValue != null) {
final CacheValueWrapper valueWrapper = JsonUtil.deserialize(strValue, CacheValueWrapper.class);
if (valueWrapper.getExpirationDate().isBefore(Instant.now())) {
keep = true;
}
}
}
} catch (Exception e) {
LOGGER.error("error reading from cache: " + e.getMessage());
}
if (!keep) {
removalKeys.add(key);
}
}
} finally {
if (localDBIterator != null) {
localDBIterator.close();
}
}
if (!removalKeys.isEmpty()) {
LOGGER.debug("purging " + removalKeys.size() + " expired cache records");
localDB.removeAll(DB, removalKeys);
} else {
LOGGER.trace("purger examined " + counter + " records and did not discover any expired cache records");
}
return removalKeys.size() >= MAX_REMOVALS_PER_CYCLE;
}
private class PurgerTask extends TimerTask {
@Override
public void run() {
try {
purgeExpiredRecords();
} catch (LocalDBException e) {
LOGGER.error("error while running purger task: " + e.getMessage(),e);
}
}
}
@Override
public int itemCount()
{
try {
return localDB.size(DB);
} catch (LocalDBException e) {
LOGGER.error("unexpected error reading size from localDB: " + e.getMessage(), e);
}
return 0;
}
}