diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/admin/ShowBackgroundThreadsController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/admin/ShowBackgroundThreadsController.java new file mode 100644 index 000000000..f8241ca6b --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/admin/ShowBackgroundThreadsController.java @@ -0,0 +1,97 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.admin; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import com.ibm.icu.text.SimpleDateFormat; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.utils.threads.VitroBackgroundThread; + +/** + * Show the list of living background threads (instances of + * VitroBackgroundThread), and their status. + */ +public class ShowBackgroundThreadsController extends FreemarkerHttpServlet { + + private static final String TEMPLATE_NAME = "admin-showThreads.ftl"; + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + + @Override + protected ResponseValues processRequest(VitroRequest vreq) throws Exception { + + SortedMap threadMap = new TreeMap(); + + for (VitroBackgroundThread thread : VitroBackgroundThread + .getLivingThreads()) { + ThreadInfo threadInfo = getThreadInfo(thread); + threadMap.put(threadInfo.getName(), threadInfo); + } + + Map bodyMap = new HashMap(); + bodyMap.put("threads", new ArrayList(threadMap.values())); + + return new TemplateResponseValues(TEMPLATE_NAME, bodyMap); + + } + + private ThreadInfo getThreadInfo(VitroBackgroundThread thread) { + try { + String name = thread.getName(); + String workLevel = String.valueOf(thread.getWorkLevel().getLevel()); + String since = formatDate(thread.getWorkLevel().getSince()); + String flags = String.valueOf(thread.getWorkLevel().getFlags()); + return new ThreadInfo(name, workLevel, since, flags); + } catch (Exception e) { + return new ThreadInfo("UNKNOWN THREAD", "UNKNOWN", "UNKNOWN", + e.toString()); + } + } + + private String formatDate(Date since) { + return new SimpleDateFormat(DATE_FORMAT).format(since); + } + + + public static class ThreadInfo { + private final String name; + private final String workLevel; + private final String since; + private final String flags; + + public ThreadInfo(String name, String workLevel, String since, + String flags) { + this.name = name; + this.workLevel = workLevel; + this.since = since; + this.flags = flags; + } + + public String getName() { + return name; + } + + public String getWorkLevel() { + return workLevel; + } + + public String getSince() { + return since; + } + + public String getFlags() { + return flags; + } + + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/admin/WaitForBackgroundThreadsController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/admin/WaitForBackgroundThreadsController.java new file mode 100644 index 000000000..a01131550 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/admin/WaitForBackgroundThreadsController.java @@ -0,0 +1,186 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.admin; + +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; +import static javax.servlet.http.HttpServletResponse.SC_TEMPORARY_REDIRECT; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet; +import edu.cornell.mannlib.vitro.webapp.utils.threads.VitroBackgroundThread; +import edu.cornell.mannlib.vitro.webapp.utils.threads.VitroBackgroundThread.WorkLevel; +import edu.cornell.mannlib.vitro.webapp.utils.threads.VitroBackgroundThread.WorkLevelStamp; + +/** + * Wait for background threads to complete. Used in Selenium testing. + * + * This servlet will poll background threads (instances of + * VitroBackgroundThread) until all living threads are idle, or until a maximum + * wait time has been met. The wait time can be specified with a "waitLimit" + * parameter on the request (in seconds), or the default value will be used. + * + * If the maximum time expires before all threads become idle, the result will + * be 503 (Service Unavailable) + * + * Else if a "return" parameter exists, and a "referer" header exists, the + * result will be a 307 (Temporary Redirect) back to the referer URL. + * + * Otherwise, the result will be 200 (OK), with a brief message. + */ +public class WaitForBackgroundThreadsController extends VitroHttpServlet { + private static final Log log = LogFactory + .getLog(WaitForBackgroundThreadsController.class); + + private static final String PARAMETER_WAIT_LIMIT = "waitLimit"; + private static final String PARAMETER_RETURN = "return"; + private static final String HEADER_REFERER = "Referer"; + private static final String HEADER_LOCATION = "Location"; + private static final int DEFAULT_WAIT_LIMIT_VALUE = 30; + private static final int POLLING_INTERVAL = 3; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + + int maximumWait = figureMaximumWait(req); + String redirect = figureRedirect(req); + + Collection remainingThreads = waitForThreads(maximumWait); + if (remainingThreads.isEmpty()) { + if (redirect == null) { + sendOK(resp); + } else { + sendRedirect(resp, redirect); + } + } else { + sendFailure(resp, maximumWait, remainingThreads); + } + } + + /** + * If the "waitLimit " parameter is present and is set to a non-negative + * integer, use that as the maximum number of seconds. Otherwise, use the + * default limit. + */ + private int figureMaximumWait(HttpServletRequest req) { + String valueString = req.getParameter(PARAMETER_WAIT_LIMIT); + if (valueString == null) { + return DEFAULT_WAIT_LIMIT_VALUE; + } + + int value; + try { + value = Integer.parseInt(valueString); + } catch (NumberFormatException e) { + return DEFAULT_WAIT_LIMIT_VALUE; + } + + if (value <= 0) { + return DEFAULT_WAIT_LIMIT_VALUE; + } + + log.debug("Maximum wait time (seconds): " + value); + return value; + } + + /** + * If there is a "return" parameter and a "referer" header, return the + * referer URL. Otherwise, return null. + */ + private String figureRedirect(HttpServletRequest req) { + if (!req.getParameterMap().containsKey(PARAMETER_RETURN)) { + return null; + } + + Enumeration referers = req.getHeaders(HEADER_REFERER); + if ((referers == null) || (!referers.hasMoreElements())) { + return null; + } + + String redirect = (String) referers.nextElement(); + log.debug("Redirect is to '" + redirect + "'"); + return redirect; + } + + /** + * Wait until all background threads have become idle, or until the time + * limit is passed. Return the names of any that are still active; hopefully + * an empty list. + */ + private Collection waitForThreads(int maximumWait) { + int elapsedSeconds = 0; + + while (true) { + Collection threadNames = getNamesOfBusyThreads(); + if (threadNames.isEmpty()) { + return Collections.emptySet(); + } + + try { + log.debug("Waiting for " + POLLING_INTERVAL + " seconds."); + Thread.sleep(POLLING_INTERVAL * 1000); + } catch (InterruptedException e) { + // Why would this happen? Anyway, stop waiting. + return Collections.singleton("Polling was interrupted"); + } + + elapsedSeconds += POLLING_INTERVAL; + if (elapsedSeconds >= maximumWait) { + return threadNames; + } + } + } + + private Collection getNamesOfBusyThreads() { + List names = new ArrayList(); + for (VitroBackgroundThread thread : VitroBackgroundThread.getThreads()) { + if (thread.isAlive()) { + WorkLevelStamp stamp = thread.getWorkLevel(); + if ((stamp != null) && (stamp.getLevel() == WorkLevel.WORKING)) { + names.add(thread.getName()); + } + } + } + log.debug("Busy threads: " + names); + return names; + } + + private void sendOK(HttpServletResponse resp) throws IOException { + log.debug("All threads are idle"); + resp.setStatus(SC_OK); + resp.getWriter().println( + "All threads are idle."); + } + + private void sendRedirect(HttpServletResponse resp, String redirect) { + log.debug("All threads are idle. Redirecting to '" + redirect + "'"); + resp.setStatus(SC_TEMPORARY_REDIRECT); + resp.setHeader(HEADER_LOCATION, redirect); + } + + private void sendFailure(HttpServletResponse resp, int maximumWait, + Collection namesOfBusyThreads) throws IOException { + log.debug("Timeout after " + maximumWait + + " seconds with busy threads: " + namesOfBusyThreads); + resp.setStatus(SC_SERVICE_UNAVAILABLE); + resp.getWriter().println( + "After " + maximumWait + " seconds, " + + namesOfBusyThreads.size() + + " threads are still busy: " + namesOfBusyThreads + + ""); + } +} diff --git a/webapp/web/WEB-INF/web.xml b/webapp/web/WEB-INF/web.xml index 430ac32f2..0c31fc78f 100644 --- a/webapp/web/WEB-INF/web.xml +++ b/webapp/web/WEB-INF/web.xml @@ -741,6 +741,24 @@ /admin/restrictLogins + + ShowBackgroundThreads + edu.cornell.mannlib.vitro.webapp.controller.admin.ShowBackgroundThreadsController + + + ShowBackgroundThreads + /admin/showThreads + + + + WaitForBackgroundThreads + edu.cornell.mannlib.vitro.webapp.controller.admin.WaitForBackgroundThreadsController + + + WaitForBackgroundThreads + /admin/wait + + StatementChangeListingController edu.cornell.mannlib.vitro.webapp.controller.edit.listing.jena.StatementChangeListingController diff --git a/webapp/web/templates/freemarker/body/admin/admin-showThreads.ftl b/webapp/web/templates/freemarker/body/admin/admin-showThreads.ftl new file mode 100644 index 000000000..6b2b9a127 --- /dev/null +++ b/webapp/web/templates/freemarker/body/admin/admin-showThreads.ftl @@ -0,0 +1,31 @@ +<#-- $This file is distributed under the terms of the license in /doc/license.txt$ --> + +<#-- Template viewing the authorization mechanisms: current identifiers, factories, policies, etc. --> + + + + +

Background Threads

+ +
+ <#list threads as threadInfo> + + + + + +
Name${threadInfo.name}
WorkLevel${threadInfo.workLevel}
Since${threadInfo.since}
Flags${threadInfo.flags}
+ +
\ No newline at end of file