/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 Wolfgang M. Meier
* wolfgang@exist-db.org
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.storage;
import java.text.NumberFormat;
import java.util.*;
import net.jcip.annotations.ThreadSafe;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.config.annotation.ConfigurationClass;
import org.exist.config.annotation.ConfigurationFieldAsAttribute;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.source.Source;
import org.exist.util.Configuration;
import org.exist.util.hashtable.Object2ObjectHashMap;
import org.exist.xquery.*;
/**
* Global pool for pre-compiled XQuery expressions. Expressions are stored and
* retrieved from the pool by comparing the {@link org.exist.source.Source}
* objects from which they were created. For each XQuery, a maximum of
* {@link #MAX_STACK_SIZE} compiled expressions are kept in the pool. An XQuery
* expression will be removed from the pool if it has not been used for a
* pre-defined timeout. These settings can be configured in conf.xml.
*
* @author wolf
*/
@ConfigurationClass("query-pool")
@ThreadSafe
public class XQueryPool extends Object2ObjectHashMap implements BrokerPoolService {
private final static int MAX_POOL_SIZE = 128;
private final static int MAX_STACK_SIZE = 5;
private final static long TIMEOUT = 120000L;
private final static long TIMEOUT_CHECK_INTERVAL = 30000L;
private final static Logger LOG = LogManager.getLogger(XQueryPool.class);
private long lastTimeOutCheck;
private long lastTimeOfCleanup;
@ConfigurationFieldAsAttribute("size")
private int maxPoolSize;
@ConfigurationFieldAsAttribute("max-stack-size")
private int maxStackSize;
@ConfigurationFieldAsAttribute("timeout")
private long timeout;
@ConfigurationFieldAsAttribute("timeout-check-interval")
private long timeoutCheckInterval;
public static final String CONFIGURATION_ELEMENT_NAME = "query-pool";
public static final String MAX_STACK_SIZE_ATTRIBUTE = "max-stack-size";
public static final String POOL_SIZE_ATTTRIBUTE = "size";
public static final String TIMEOUT_ATTRIBUTE = "timeout";
public static final String TIMEOUT_CHECK_INTERVAL_ATTRIBUTE = "timeout-check-interval";
public static final String PROPERTY_MAX_STACK_SIZE = "db-connection.query-pool.max-stack-size";
public static final String PROPERTY_POOL_SIZE = "db-connection.query-pool.size";
public static final String PROPERTY_TIMEOUT = "db-connection.query-pool.timeout";
public static final String PROPERTY_TIMEOUT_CHECK_INTERVAL = "db-connection.query-pool.timeout-check-interval";
private final static int DEFAULT_SIZE = 27;
public XQueryPool() {
super(DEFAULT_SIZE);
this.lastTimeOutCheck = lastTimeOfCleanup = System.currentTimeMillis();
}
@Override
public void configure(final Configuration configuration) {
final Integer maxStSz = (Integer) configuration.getProperty(PROPERTY_MAX_STACK_SIZE);
final Integer maxPoolSz = (Integer) configuration.getProperty(PROPERTY_POOL_SIZE);
final Long t = (Long) configuration.getProperty(PROPERTY_TIMEOUT);
final Long tci = (Long) configuration.getProperty(PROPERTY_TIMEOUT_CHECK_INTERVAL);
final NumberFormat nf = NumberFormat.getNumberInstance();
if (maxPoolSz != null) {
maxPoolSize = maxPoolSz;
} else {
maxPoolSize = MAX_POOL_SIZE;
}
if (maxStSz != null) {
maxStackSize = maxStSz;
} else {
maxStackSize = MAX_STACK_SIZE;
}
if (t != null) {
timeout = t;
} else {
timeout = TIMEOUT;
}
if (tci != null) {
timeoutCheckInterval = tci;
} else {
timeoutCheckInterval = TIMEOUT_CHECK_INTERVAL;
}
LOG.info("QueryPool: " +
"size = " + nf.format(maxPoolSize) + "; " +
"maxStackSize = " + nf.format(maxStackSize) + "; " +
"timeout = " + nf.format(timeout) + "; " +
"timeoutCheckInterval = " + nf.format(timeoutCheckInterval));
}
public void returnCompiledXQuery(final Source source, final CompiledXQuery xquery) {
returnObject(source, xquery);
}
private synchronized void returnObject(final Source source, final CompiledXQuery xquery) {
final long ts = source.getCacheTimestamp();
if (ts == 0 || ts > lastTimeOfCleanup) {
if (size() >= maxPoolSize) {
timeoutCheck();
}
if (size() < maxPoolSize) {
Deque<CompiledXQuery> stack = (Deque<CompiledXQuery>)get(source);
if (stack == null) {
stack = new ArrayDeque<>();
source.setCacheTimestamp(System.currentTimeMillis());
put(source, stack);
}
if (stack.size() < maxStackSize) {
// check if the query is already in pool before adding,
// may happen for modules, don't add it a second time!
if(!stack.contains(xquery)) {
stack.push(xquery);
}
}
}
}
}
private Object borrowObject(final DBBroker broker, final Source source) {
final Source key;
final CompiledXQuery query;
synchronized (this) {
final int idx = getIndex(source);
if (idx < 0) {
return null;
}
key = (Source) keys[idx];
int validity = key.isValid(broker);
if (validity == Source.UNKNOWN) {
validity = key.isValid(source);
}
if (validity == Source.INVALID || validity == Source.UNKNOWN) {
keys[idx] = REMOVED;
values[idx] = null;
LOG.debug(source.getKey() + " is invalid");
return null;
}
final Deque<CompiledXQuery> stack = (Deque<CompiledXQuery>)values[idx];
if (stack == null || stack.isEmpty()) {
return null;
}
// now check if the compiled expression is valid
// it might become invalid if an imported module has changed.
query = stack.pop();
final XQueryContext context = query.getContext();
//context.setBroker(broker);
}
// query.isValid() may open collections which in turn tries to acquire
// org.exist.storage.lock.ReentrantReadWriteLock. In order to avoid
// deadlocks with concurrent queries holding that lock while borrowing
// we must not hold onto the XQueryPool while calling isValid().
if (!query.isValid()) {
synchronized (this) {
// the compiled query is no longer valid: one of the imported
// modules may have changed
remove(key);
return null;
}
} else {
return query;
}
}
public synchronized CompiledXQuery borrowCompiledXQuery(final DBBroker broker, final Source source) throws PermissionDeniedException {
final CompiledXQuery query = (CompiledXQuery) borrowObject(broker, source);
if (query == null) {
return null;
}
//check execution permission
source.validate(broker.getCurrentSubject(), Permission.EXECUTE);
// now check if the compiled expression is valid
// it might become invalid if an imported module has changed.
//final XQueryContext context = query.getContext();
//context.setBroker(broker);
return query;
}
public synchronized void clear() {
lastTimeOfCleanup = System.currentTimeMillis();
for (final Iterator i = iterator(); i.hasNext(); ) {
final Source next = (Source) i.next();
remove(next);
}
}
private void timeoutCheck() {
if (timeoutCheckInterval < 0L) {
return;
}
final long currentTime = System.currentTimeMillis();
if (currentTime - lastTimeOutCheck < timeoutCheckInterval) {
return;
}
for (final Iterator i = iterator(); i.hasNext(); ) {
final Source next = (Source) i.next();
if (currentTime - next.getCacheTimestamp() > timeout) {
remove(next);
}
}
lastTimeOutCheck = currentTime;
}
}