/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 Wolfgang M. Meier
* wolfgang@exist-db.org
* http://exist.sourceforge.net
*
* 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: ProcessMonitor.java 8235 2008-10-17 16:03:27Z chaeron $
*/
package org.exist.storage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.http.servlets.RequestWrapper;
import org.exist.http.urlrewrite.XQueryURLRewrite;
import org.exist.source.Source;
import org.exist.util.Configuration;
import org.exist.xquery.Variable;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryWatchDog;
import org.exist.xquery.functions.request.RequestModule;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.JavaObjectValue;
import org.exist.xquery.value.Type;
import java.util.*;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.stream.StreamSupport;
/**
* Class to keep track of all running queries in a database instance. The main
* purpose of this class is to signal running queries that the database is going to
* shut down. This is done through the {@link org.exist.xquery.XQueryWatchDog}
* registered by each query. It is up to the query to check the watchdog's state.
* If it simply ignores the terminate signal, it will be killed after the shutdown
* timeout is reached.
*
* @author wolf
*/
public class ProcessMonitor implements BrokerPoolService {
public final static String ACTION_UNSPECIFIED = "unspecified";
public final static String ACTION_VALIDATE_DOC = "validating document";
public final static String ACTION_STORE_DOC = "storing document";
public final static String ACTION_STORE_BINARY = "storing binary resource";
public final static String ACTION_REMOVE_XML = "remove XML resource";
public final static String ACTION_REMOVE_BINARY = "remove binary resource";
public final static String ACTION_REMOVE_COLLECTION = "remove collection";
public final static String ACTION_REINDEX_COLLECTION = "reindex collection";
public final static String ACTION_COPY_COLLECTION = "copy collection";
public final static String ACTION_MOVE_COLLECTION = "move collection";
public final static String ACTION_BACKUP = "backup";
private final static Logger LOG = LogManager.getLogger(ProcessMonitor.class);
public final static long QUERY_HISTORY_TIMEOUT = 2 * 60 * 1000; // 2 minutes
public final static long MIN_TIME = 100;
private final Set<XQueryWatchDog> runningQueries = new HashSet<XQueryWatchDog>();
private final DelayQueue<QueryHistory> history = new DelayQueue<>();
private Map<Thread, JobInfo> processes = new HashMap<Thread, JobInfo>();
private long maxShutdownWait;
private long historyTimespan = QUERY_HISTORY_TIMEOUT;
private long minTime = MIN_TIME;
private boolean trackRequests = false;
@Override
public void configure(final Configuration configuration) {
this.maxShutdownWait = configuration.getProperty(BrokerPool.PROPERTY_SHUTDOWN_DELAY, BrokerPool.DEFAULT_MAX_SHUTDOWN_WAIT);
}
public void startJob(String action) {
startJob(action, null);
}
public void startJob(String action, Object addInfo) {
startJob(action, addInfo, null);
}
//TODO: addInfo = XmldbURI ? -shabanovd
public void startJob(String action, Object addInfo, Monitor monitor) {
final JobInfo info = new JobInfo(action, monitor);
info.setAddInfo(addInfo);
synchronized (this) {
processes.put(info.getThread(), info);
}
}
public synchronized void endJob() {
processes.remove(Thread.currentThread());
notifyAll();
}
public JobInfo[] runningJobs() {
synchronized (this) {
final JobInfo jobs[] = new JobInfo[processes.size()];
int j = 0;
for (final Iterator<JobInfo> i = processes.values().iterator(); i.hasNext(); j++) {
//BUG: addInfo = XmldbURI ? -shabanovd
jobs[j] = i.next();
}
return jobs;
}
}
public void stopRunningJobs() {
final long waitStart = System.currentTimeMillis();
synchronized (this) {
if (maxShutdownWait > -1) {
while (processes.size() > 0) {
try {
//Wait until they become inactive...
this.wait(1000);
} catch (final InterruptedException e) {
}
//...or force the shutdown
if(maxShutdownWait > -1 && System.currentTimeMillis() - waitStart > maxShutdownWait){
break;
}
}
}
for (final JobInfo job : processes.values()) {
job.stop();
}
}
}
public void queryStarted(XQueryWatchDog watchdog) {
synchronized (runningQueries) {
watchdog.setRunningThread(Thread.currentThread().getName());
runningQueries.add(watchdog);
}
}
public void queryCompleted(XQueryWatchDog watchdog) {
boolean found;
synchronized (runningQueries) {
found = runningQueries.remove(watchdog);
}
// add to query history if elapsed time > minTime
final long elapsed = System.currentTimeMillis() - watchdog.getStartTime();
if (found && elapsed > minTime) {
synchronized (history) {
final Source source = watchdog.getContext().getSource();
final String sourceKey = source == null ? "unknown" : source.path();
QueryHistory qh = new QueryHistory(sourceKey, historyTimespan);
qh.setMostRecentExecutionTime(watchdog.getStartTime());
qh.setMostRecentExecutionDuration(elapsed);
qh.incrementInvocationCount();
if (trackRequests) {
qh.setRequestURI(getRequestURI(watchdog));
}
history.add(qh);
cleanHistory();
}
}
}
private void cleanHistory() {
// remove timed out entries
while (history.poll() != null);
}
/**
* The max duration (in milliseconds) for which queries are tracked in the query history. Older queries
* will be removed (default is {@link #QUERY_HISTORY_TIMEOUT}).
*
* @param time max duration in ms
*/
public void setHistoryTimespan(long time) {
historyTimespan = time;
}
public long getHistoryTimespan() {
return historyTimespan;
}
/**
* The minimum duration of a query (in milliseconds) to be added to the query history. Use this to filter out
* very short-running queries (default is {@link #MIN_TIME}).
*
* @param time min duration in ms
*/
public void setMinTime(long time) {
this.minTime = time;
}
public long getMinTime() {
return minTime;
}
/**
* Set to true if the class should attempt to determine the HTTP URI through which the query was triggered.
* This is an important piece of information for diagnosis, but gathering it might be expensive, so request
* URI tracking is disabled by default.
*
* @param track attempt to track URIs if true
*/
public void setTrackRequestURI(boolean track) {
trackRequests = track;
}
public boolean getTrackRequestURI() {
return trackRequests;
}
public static class QueryHistory implements Delayed {
private final String source;
private String requestURI = null;
private long mostRecentExecutionTime;
private long mostRecentExecutionDuration;
private int invocationCount = 0;
private long expires;
public QueryHistory(String source, long delay) {
this.source = source;
this.expires = System.currentTimeMillis() + delay;
}
public String getSource() {
return source;
}
public void incrementInvocationCount() {
invocationCount++;
}
public int getInvocationCount() {
return invocationCount;
}
public long getMostRecentExecutionTime() {
return mostRecentExecutionTime;
}
public void setMostRecentExecutionTime(long mostRecentExecutionTime) {
this.mostRecentExecutionTime = mostRecentExecutionTime;
}
public long getMostRecentExecutionDuration() {
return mostRecentExecutionDuration;
}
public void setMostRecentExecutionDuration(long mostRecentExecutionDuration) {
this.mostRecentExecutionDuration = mostRecentExecutionDuration;
}
public String getRequestURI() {
return requestURI;
}
public void setRequestURI(String uri) {
requestURI = uri;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expires - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
if (expires < ((QueryHistory) o).expires) {
return -1;
}
if (expires > ((QueryHistory) o).expires) {
return 1;
}
return 0;
}
}
public QueryHistory[] getRecentQueryHistory() {
synchronized (history) {
cleanHistory();
return
history.stream()
.sorted((o1, o2) -> o1.expires > o2.expires ? -1 : (o1.expires < o2.expires ? 1 : 0))
.toArray(QueryHistory[]::new);
}
}
public void killAll(long waitTime) {
// directly called from BrokerPool itself. no need to synchronize.
for(final XQueryWatchDog watchdog : runningQueries) {
LOG.debug("Killing query: " +
ExpressionDumper.dump(watchdog.getContext().getRootExpression()));
watchdog.kill(waitTime);
}
}
public XQueryWatchDog[] getRunningXQueries()
{
synchronized (runningQueries) {
return runningQueries.stream().toArray(XQueryWatchDog[]::new);
}
}
public final static class Monitor {
boolean stop = false;
public boolean proceed() {
return !stop;
}
public void stop() {
LOG.debug("Terminating job");
this.stop = true;
}
}
public final static class JobInfo {
private Thread thread;
private String action;
private long startTime;
private Object addInfo = null;
private Monitor monitor = null;
public JobInfo(String action, Monitor monitor) {
this.thread = Thread.currentThread();
this.action = action;
this.monitor = monitor;
this.startTime = System.currentTimeMillis();
}
public String getAction() {
return action;
}
public Thread getThread() {
return thread;
}
public long getStartTime() {
return startTime;
}
public void setAddInfo(Object info) {
this.addInfo = info;
}
public Object getAddInfo() {
return addInfo;
}
public void stop() {
if (monitor != null) {
monitor.stop();
}
}
}
/**
* Try to figure out the HTTP request URI by which a query was called.
* Request tracking is not enabled unless {@link #setTrackRequestURI(boolean)}
* is called.
*
* @param watchdog
* @return
*/
public static String getRequestURI(XQueryWatchDog watchdog) {
final RequestModule reqModule = (RequestModule)watchdog.getContext().getModule(RequestModule.NAMESPACE_URI);
if (reqModule == null) {
return null;
}
try {
final Variable var = reqModule.resolveVariable(RequestModule.REQUEST_VAR);
if(var == null || var.getValue() == null) {
return null;
}
if (var.getValue().getItemType() != Type.JAVA_OBJECT) {
return null;
}
final JavaObjectValue value = (JavaObjectValue) var.getValue().itemAt(0);
if (value.getObject() instanceof RequestWrapper) {
final RequestWrapper wrapper = (RequestWrapper) value.getObject();
final Object attr = wrapper.getAttribute(XQueryURLRewrite.RQ_ATTR_REQUEST_URI);
String uri;
if (attr == null) {
uri = wrapper.getRequestURI();
} else {
uri = attr.toString();
}
String queryString = wrapper.getQueryString();
if (queryString != null) {
uri += "?" + queryString;
}
return uri;
}
} catch (XPathException e) {
// ignore and return null
}
return null;
}
}