/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com 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; version 2 of the License. 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 */ /* * Created on Apr 13, 2011 */ package com.bigdata.rdf.sail.webapp; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Date; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.apache.log4j.Logger; import com.bigdata.Banner; import com.bigdata.bop.engine.QueryEngine; import com.bigdata.bop.fed.QueryEngineFactory; import com.bigdata.btree.BaseIndexStats; import com.bigdata.cache.SynchronizedHardReferenceQueueWithTimeout; import com.bigdata.counters.CounterSet; import com.bigdata.counters.ICounterSetAccess; import com.bigdata.counters.IProcessCounters; import com.bigdata.io.DirectBufferPool; import com.bigdata.journal.IIndexManager; import com.bigdata.journal.ITransactionService; import com.bigdata.journal.ITx; import com.bigdata.journal.Journal; import com.bigdata.journal.WarmUpTask; import com.bigdata.rdf.ServiceProviderHook; import com.bigdata.rdf.sail.BigdataSail; import com.bigdata.rdf.sail.CreateKBTask; import com.bigdata.rdf.sparql.ast.service.ServiceRegistry; import com.bigdata.rdf.task.AbstractApiTask; import com.bigdata.service.AbstractDistributedFederation; import com.bigdata.service.AbstractScaleOutClient; import com.bigdata.service.DefaultClientDelegate; import com.bigdata.service.ScaleOutClientFactory; import com.bigdata.service.IBigdataClient; import com.bigdata.service.IBigdataFederation; import com.bigdata.util.httpd.AbstractHTTPD; /** * Listener provides life cycle management of the {@link IIndexManager} by * interpreting the configuration parameters in the {@link ServletContext}. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ public class BigdataRDFServletContextListener implements ServletContextListener { private static final transient Logger log = Logger .getLogger(BigdataRDFServletContextListener.class); private Journal jnl = null; private AbstractScaleOutClient<?> jiniClient = null; private ITransactionService txs = null; private Long readLock = null; private long readLockTx; private BigdataRDFContext rdfContext; // private SparqlCache sparqlCache; /** * The set of init parameters from the <code>web.xml</code> file after we * have applied any overrides specified by the * {@link BigdataRDFServletContextListener#INIT_PARAM_OVERRIDES} attributes. */ private Map<String,String> effectiveInitParams; /** * <code>true</code> iff this class opened the {@link IIndexManager}, in * which case it will close it at the appropriate life cycle event. */ private boolean closeIndexManager; /** * The name of the {@link ServletContext} attribute under which we store * any overrides for the init parameters of the {@link ServletContext}. Note * that it is NOT possible to actual modify the init parameters specified in * the <code>web.xml</code> file. Therefore, we attach the overrides as an * attribute and then consult them from within * {@link BigdataRDFServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)} * . */ public static final String INIT_PARAM_OVERRIDES = "INIT_PARAMS_OVERRIDES"; /** * Return the effective value of the given init parameter, respecting any * overrides that were specified to the {@link NanoSparqlServer} when it * initialized the server. * * @param key * The name of the desired init parameter. * * @return The effective value of that init parameter. */ protected String getInitParameter(final String key) { return effectiveInitParams.get(key); } public BigdataRDFServletContextListener() { super(); } protected BigdataRDFContext getBigdataRDFContext() { return rdfContext; } @Override public void contextInitialized(final ServletContextEvent e) { if(log.isInfoEnabled()) log.info(""); Banner.banner(); final ServletContext context = e.getServletContext(); /* * Figure out the effective init parameters that we want to use * for this initialization procedure. */ { effectiveInitParams = new LinkedHashMap<String, String>(); /* * Copy the init params from web.xml into a local map. */ final Enumeration<String> names = context.getInitParameterNames(); while(names.hasMoreElements()) { final String name = names.nextElement(); final String value = context.getInitParameter(name); effectiveInitParams.put(name, value); } /* * Look for init parameter overrides that have been attached to the * WebAppContext by the NanoSparqlServer. If found, then apply them * before doing anything else. This is how we apply overrides to the * init parameters that were specified in "web.xml". */ { @SuppressWarnings("unchecked") final Map<String, String> initParamOverrides = (Map<String, String>) context .getAttribute(BigdataRDFServletContextListener.INIT_PARAM_OVERRIDES); if (initParamOverrides != null) { effectiveInitParams.putAll(initParamOverrides); } } } final String namespace; { String s = getInitParameter(ConfigParams.NAMESPACE); if (s == null) s = ConfigParams.DEFAULT_NAMESPACE; namespace = s; if (log.isInfoEnabled()) log.info(ConfigParams.NAMESPACE + "=" + namespace); } final boolean create; { final String s = getInitParameter(ConfigParams.CREATE); if (s != null) create = Boolean.valueOf(s); else create = ConfigParams.DEFAULT_CREATE; if (log.isInfoEnabled()) log.info(ConfigParams.CREATE + "=" + create); } final IIndexManager indexManager; if (context.getAttribute(IIndexManager.class.getName()) != null) { /* * The index manager object was directly set by the caller. */ indexManager = (IIndexManager) context .getAttribute(IIndexManager.class.getName()); // the caller is responsible for the life cycle. closeIndexManager = false; } else { /** * The index manager will be open based on the specified property * file or config file. * * Note: You may override the location of the property file using * <pre> * -Dcom.bigdata.rdf.sail.webapp.ConfigParams.propertyFile=FILE * </pre> */ // The fully qualified name of the environment variable. final String FQN_PROPERTY_FILE = ConfigParams.class.getName() + "." + ConfigParams.PROPERTY_FILE; // The default value is taken from the web.xml file. final String defaultValue = getInitParameter( ConfigParams.PROPERTY_FILE); // The effective location of the property file. final String propertyFile = System.getProperty(// FQN_PROPERTY_FILE,// defaultValue// ); if (propertyFile == null) throw new RuntimeException("Required config-param: " + ConfigParams.PROPERTY_FILE); if (log.isInfoEnabled()) log.info(ConfigParams.PROPERTY_FILE + "=" + propertyFile); indexManager = openIndexManager(propertyFile); // we are responsible for the life cycle. closeIndexManager = true; } if (create) { /* * Note: Nobody is watching this future. The task will log any errors. */ // indexManager.getExecutorService().submit( // new CreateKBTask(indexManager, namespace)); final Properties properties; if (indexManager instanceof IBigdataFederation) { properties = ((IBigdataFederation<?>) indexManager).getClient() .getProperties(); } else { properties = ((Journal) indexManager).getProperties(); } AbstractApiTask.submitApiTask(indexManager, new CreateKBTask( namespace, properties)); } // if( create ) txs = (indexManager instanceof Journal ? ((Journal) indexManager) .getTransactionManager().getTransactionService() : ((IBigdataFederation<?>) indexManager) .getTransactionService()); final long timestamp; { final String s = getInitParameter( ConfigParams.READ_LOCK); readLock = s == null ? null : Long.valueOf(s); if (readLock != null) { /* * Obtain a read-only transaction which will assert a read lock * for the specified commit time. The database WILL NOT release * storage associated with the specified commit point while this * server is running. Queries will read against the specified * commit time by default, but this may be overridden on a query * by query basis. */ try { timestamp = txs.newTx(readLock); } catch (IOException ex) { throw new RuntimeException(ex); } log.warn("Holding read lock: readLock=" + readLock + ", tx: " + timestamp); } else { /* * The default for queries is to read against then most recent * commit time as of the moment when the request is accepted. */ timestamp = ITx.READ_COMMITTED; } } final int queryThreadPoolSize; { final String s = getInitParameter( ConfigParams.QUERY_THREAD_POOL_SIZE); queryThreadPoolSize = s == null ? ConfigParams.DEFAULT_QUERY_THREAD_POOL_SIZE : Integer.valueOf(s); if (queryThreadPoolSize < 0) { throw new RuntimeException(ConfigParams.QUERY_THREAD_POOL_SIZE + " : Must be non-negative, not: " + s); } if (log.isInfoEnabled()) log.info(ConfigParams.QUERY_THREAD_POOL_SIZE + "=" + queryThreadPoolSize); } final boolean describeEachNamedGraph; { final String s = getInitParameter( ConfigParams.DESCRIBE_EACH_NAMED_GRAPH); describeEachNamedGraph = s == null ? ConfigParams.DEFAULT_DESCRIBE_EACH_NAMED_GRAPH : Boolean.valueOf(s); if (log.isInfoEnabled()) log.info(ConfigParams.DESCRIBE_EACH_NAMED_GRAPH + "=" + describeEachNamedGraph); } final boolean readOnly; { final String s = getInitParameter( ConfigParams.READ_ONLY); readOnly = s == null ? ConfigParams.DEFAULT_READ_ONLY : Boolean .valueOf(s); if (log.isInfoEnabled()) log.info(ConfigParams.READ_ONLY + "=" + readOnly); } final long queryTimeout; { final String s = getInitParameter( ConfigParams.QUERY_TIMEOUT); queryTimeout = s == null ? ConfigParams.DEFAULT_QUERY_TIMEOUT : Long.valueOf(s); if (queryTimeout < 0) { throw new RuntimeException(ConfigParams.QUERY_TIMEOUT + " : Must be non-negative, not: " + s); } if (log.isInfoEnabled()) log.info(ConfigParams.QUERY_TIMEOUT + "=" + queryTimeout); } final long warmupTimeoutMillis; { final String s = getInitParameter( ConfigParams.WARMUP_TIMEOUT); warmupTimeoutMillis = s == null ? ConfigParams.DEFAULT_WARMUP_TIMEOUT : Long.valueOf(s); if (warmupTimeoutMillis < 0) { throw new RuntimeException(ConfigParams.WARMUP_TIMEOUT + " : Must be non-negative, not: " + s); } if (log.isInfoEnabled()) log.info(ConfigParams.WARMUP_TIMEOUT + "=" + warmupTimeoutMillis); } final SparqlEndpointConfig config = new SparqlEndpointConfig(namespace, timestamp, queryThreadPoolSize, describeEachNamedGraph, readOnly, queryTimeout); rdfContext = new BigdataRDFContext(config, indexManager); // Used by BigdataBaseServlet context.setAttribute(BigdataServlet.ATTRIBUTE_INDEX_MANAGER, indexManager); // Used by BigdataRDFBaseServlet context.setAttribute(BigdataRDFServlet.ATTRIBUTE_RDF_CONTEXT, rdfContext); if (indexManager instanceof Journal && warmupTimeoutMillis > 0L) { // Note: null -or- empty => ALL namespaces. final List<String> warmupNamespaceList = new LinkedList<String>(); { String s = getInitParameter(ConfigParams.WARMUP_NAMESPACE_LIST); if (s == null) { s = ConfigParams.DEFAULT_WARMUP_NAMESPACE_LIST; } if (s != null) { final String[] a = s.split(","); for (String t : a) { t = t.trim(); if (t.isEmpty()) continue; warmupNamespaceList.add(t); } } if (log.isInfoEnabled()) log.info(ConfigParams.WARMUP_NAMESPACE_LIST + "=" + warmupNamespaceList); } final int warmupThreadPoolSize; { final String s = getInitParameter(ConfigParams.WARMUP_THREAD_POOL_SIZE); warmupThreadPoolSize = s == null ? ConfigParams.DEFAULT_WARMUP_THREAD_POOL_SIZE : Integer.valueOf(s); if (warmupThreadPoolSize <= 0) { throw new RuntimeException(ConfigParams.WARMUP_THREAD_POOL_SIZE + " : Must be positive, not: " + s); } if (log.isInfoEnabled()) log.info(ConfigParams.WARMUP_THREAD_POOL_SIZE + "=" + warmupThreadPoolSize); } /* * Note: The [timestamp] will be READ_COMMITTED or the read lock as * specified above. */ // Parameters that should not be messed with. final boolean visitLeaves = false; // Only materialize non-leaf pages. log.warn("Warming up the journal: namespaces=" + (warmupNamespaceList == null || warmupNamespaceList.isEmpty() ? "ALL" : warmupNamespaceList) + ", warmupTheads=" + warmupThreadPoolSize + ", timeout=" + TimeUnit.MILLISECONDS.toSeconds(warmupTimeoutMillis) + "s"); // Submit the warmup procedure. final Future<Map<String, BaseIndexStats>> ft = indexManager .getExecutorService().submit( new WarmUpTask(((Journal) indexManager), warmupNamespaceList, timestamp, warmupThreadPoolSize, visitLeaves)); try { // Await the warmup procedure termination. final Map<String, BaseIndexStats> statsMap = ft.get( warmupTimeoutMillis, TimeUnit.MILLISECONDS); } catch (ExecutionException e1) { /* * Abnormal termination. */ throw new RuntimeException("Warmup failure: " + e1, e1); } catch (InterruptedException e1) { /* * This thread was interrupted. This probably indicates shutdown of * the container. */ throw new RuntimeException(e1); } catch (TimeoutException e1) { /* * Ignore expected exception. The warmup procedure has reached its * timeout. This is fine. */ if (log.isInfoEnabled()) log.info("Warmup terminated by timeout."); // fall through. } } // // Initialize the SPARQL cache. // context.setAttribute(BigdataServlet.ATTRIBUTE_SPARQL_CACHE, // new SparqlCache(new MemoryManager(DirectBufferPool.INSTANCE))); { final boolean forceOverflow = Boolean .valueOf(getInitParameter(ConfigParams.FORCE_OVERFLOW)); if (forceOverflow && indexManager instanceof IBigdataFederation<?>) { log.warn("Forcing compacting merge of all data services: " + new Date()); ((AbstractDistributedFederation<?>) indexManager) .forceOverflow(true/* compactingMerge */, false/* truncateJournal */); log.warn("Did compacting merge of all data services: " + new Date()); } } { final String serviceWhitelist = getInitParameter(ConfigParams.SERVICE_WHITELIST); if (serviceWhitelist != null) { log.info("Service whitelist: " + serviceWhitelist); ServiceRegistry reg = ServiceRegistry.getInstance(); reg.setWhitelistEnabled(true); for(String url: serviceWhitelist.split("\\s*,\\s*")) { reg.addWhitelistURL(url); } } } /* * Force service/format registration. * * @see <a href="https://sourceforge.net/apps/trac/bigdata/ticket/439"> * Class loader problems </a> */ ServiceProviderHook.forceLoad(); if (log.isInfoEnabled()) log.info("done"); } @Override public void contextDestroyed(final ServletContextEvent e) { if(log.isInfoEnabled()) log.info(""); if (rdfContext != null) { rdfContext.shutdownNow(); rdfContext = null; } if (txs != null && readLock != null && readLock != -1L) { try { txs.abort(readLockTx); } catch (IOException ex) { log .error("Could not release transaction: tx=" + readLockTx, ex); } txs = null; readLock = null; } if (jnl != null) { if (closeIndexManager) jnl.close(); jnl = null; } if (jiniClient != null) { if (closeIndexManager) jiniClient.disconnect(true/* immediateShutdown */); jiniClient = null; } // // Clear the SPARQL cache. // if (sparqlCache != null) { // // sparqlCache.close(); // // sparqlCache = null; // // } effectiveInitParams = null; /* * Terminate various threads which should no longer be executing once we * have destroyed the servlet context. If you do not do this then * servlet containers such as tomcat will complain that we did not stop * some threads. */ { SynchronizedHardReferenceQueueWithTimeout.stopStaleReferenceCleaner(); } } /** * Open the {@link IIndexManager} identified by the property file. * * @param propertyFile * The property file (for a standalone bigdata instance) or the * jini configuration file (for a bigdata federation). The file * must end with either ".properties" or ".config". * * @return The {@link IIndexManager}. */ private IIndexManager openIndexManager(final String propertyFile) { // Locate the named .properties or .config file. final URL propertyFileUrl; if (new File(propertyFile).exists()) { // Check the file system. try { propertyFileUrl = new URL("file:" + propertyFile); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } } else { // Check the classpath. propertyFileUrl = BigdataRDFServletContextListener.class .getClassLoader().getResource(propertyFile); } if (log.isInfoEnabled()) log.info("bigdata configuration: propertyFile=" + propertyFile + ", propertyFileUrl=" + propertyFileUrl); if (propertyFileUrl == null) { throw new RuntimeException("Could not find file: file=" + propertyFile + ", user.dir=" + System.getProperty("user.dir")); } boolean isJini = false; if (propertyFile.endsWith(".config")) { // scale-out. isJini = true; } else if (propertyFile.endsWith(".properties")) { // local journal. isJini = false; } else { /* * Note: This is a hack, but we are recognizing the jini * configuration file with a .config extension and the journal * properties file with a .properties extension. */ throw new RuntimeException( "File must have '.config' or '.properties' extension: " + propertyFile); } final IIndexManager indexManager; try { if (isJini) { /* * A bigdata federation. * * Note: The Apache River configuration mechanism will search * both the file system and the classpath, much as we have done * above. * * TODO This will use the ClassLoader associated with the * JiniClient if that is different from the ClassLoader used * above, then it could be possible for one ClassLoader to find * the propertyFile resource and the other to not find that * resource. */ jiniClient = ScaleOutClientFactory.getJiniClient(new String[] { propertyFile }); jiniClient.setDelegate(new NanoSparqlServerFederationDelegate( jiniClient, this)); indexManager = jiniClient.connect(); } else { /* * Note: we only need to specify the FILE when re-opening a * journal containing a pre-existing KB. */ final Properties properties = new Properties(); { // Read the properties from the file. final InputStream is = new BufferedInputStream( propertyFileUrl.openStream()); try { properties.load(is); } finally { is.close(); } if (System.getProperty(BigdataSail.Options.FILE) != null) { // Override/set from the environment. properties.setProperty(BigdataSail.Options.FILE, System .getProperty(BigdataSail.Options.FILE)); } } indexManager = jnl = new Journal(properties); } } catch (Exception ex) { throw new RuntimeException(ex); } return indexManager; } /** * Hooked to report the query engine performance counters to the federation. * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @param <T> */ private static class NanoSparqlServerFederationDelegate<T> extends DefaultClientDelegate<T> { final private IBigdataClient<?> client; final private BigdataRDFServletContextListener servletContextListener; /** * The path component under which the query engine performance counters * will be reported. */ static private final String QUERY_ENGINE = "Query Engine"; public NanoSparqlServerFederationDelegate(final IBigdataClient<?> client, final BigdataRDFServletContextListener servletContextListener) { super(client, null/* clientOrService */); this.client = client; if (servletContextListener == null) throw new IllegalArgumentException(); this.servletContextListener = servletContextListener; } /** * Overridden to attach the counters reporting on the things which are * either dynamic or not otherwise part of the reported counter set for * the client. */ @Override public void reattachDynamicCounters() { // final BigdataRDFContext rdfContext = servletContextListener.rdfContext; final IBigdataFederation<?> fed; try { fed = client.getFederation(); assert fed != null; } catch (IllegalStateException ex) { log.warn("Closed: " + ex); return; } // The service's counter set hierarchy. final CounterSet serviceRoot = fed .getServiceCounterSet(); /* * DirectBufferPool counters. */ { // Ensure path exists. final CounterSet tmp = serviceRoot .makePath(IProcessCounters.Memory); synchronized (tmp) { // detach the old counters (if any). tmp.detach("DirectBufferPool"); // attach the current counters. tmp.makePath("DirectBufferPool").attach( DirectBufferPool.getCounters()); } } /* * QueryEngine counters. */ { /* * TODO It would be better to have this on the BigdataRDFContext * so we are not creating it lazily here if the NSS has not yet * been issued a query. */ final QueryEngine queryEngine = QueryEngineFactory.getInstance() .getQueryController(fed); final CounterSet tmp = serviceRoot; synchronized (tmp) { tmp.detach(QUERY_ENGINE); // attach the current counters. tmp.makePath(QUERY_ENGINE) .attach(queryEngine.getCounters()); } } } /** * {@inheritDoc} * <p> * Overridden to NOT start an embedded performance counter reporting * httpd instance. The {@link NanoSparqlServer} already provides a * {@link CountersServlet} through which this stuff gets reported to the * UI. */ @Override public AbstractHTTPD newHttpd(final int httpdPort, final ICounterSetAccess access) throws IOException { return null; } } }