/*
* Copyright Myrrix Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.myrrix.web;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.logging.Handler;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.common.net.HostAndPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.myrrix.common.ClassUtils;
import net.myrrix.common.ReloadingReference;
import net.myrrix.common.io.IOUtils;
import net.myrrix.common.MyrrixRecommender;
import net.myrrix.common.PartitionsUtils;
import net.myrrix.common.log.MemoryHandler;
import net.myrrix.online.AbstractRescorerProvider;
import net.myrrix.online.ClientThread;
import net.myrrix.online.RescorerProvider;
import net.myrrix.online.ServerRecommender;
import net.myrrix.online.io.ResourceRetriever;
import net.myrrix.online.partition.PartitionLoader;
import net.myrrix.web.servlets.AbstractMyrrixServlet;
/**
* <p>This servlet lifecycle listener makes sure that the shared {@link MyrrixRecommender} instance
* is initialized at startup, along with related objects, and shut down when the container is destroyed.</p>
*
* @author Sean Owen
* @since 1.0
*/
public final class InitListener implements ServletContextListener {
private static final Logger log = LoggerFactory.getLogger(InitListener.class);
private static final String KEY_PREFIX = InitListener.class.getName();
public static final String LOG_HANDLER = KEY_PREFIX + ".LOG_HANDLER";
public static final String LOCAL_INPUT_DIR_KEY = KEY_PREFIX + ".LOCAL_INPUT_DIR";
public static final String PORT_KEY = KEY_PREFIX + ".PORT";
public static final String READ_ONLY_KEY = KEY_PREFIX + ".READ_ONLY";
public static final String BUCKET_KEY = KEY_PREFIX + ".BUCKET";
public static final String INSTANCE_ID_KEY = KEY_PREFIX + ".INSTANCE_ID";
public static final String RESCORER_PROVIDER_CLASS_KEY = KEY_PREFIX + ".RESCORER_PROVIDER_CLASS";
public static final String CLIENT_THREAD_CLASS_KEY = KEY_PREFIX + ".CLIENT_THREAD_CLASS";
public static final String ALL_PARTITIONS_SPEC_KEY = KEY_PREFIX + ".ALL_PARTITIONS_SPEC";
public static final String PARTITION_KEY = KEY_PREFIX + ".PARTITION";
private static final Pattern COMMA = Pattern.compile(",");
private File tempDirToDelete;
private ClientThread clientThread;
@Override
public void contextInitialized(ServletContextEvent event) {
log.info("Initializing Myrrix in servlet context...");
ServletContext context = event.getServletContext();
configureLogging(context);
configureTempDir(context);
File localInputDir = configureLocalInputDir(context);
int partition = configurePartition(context);
String bucket = getAttributeOrParam(context, BUCKET_KEY);
String instanceID = getAttributeOrParam(context, INSTANCE_ID_KEY);
configureRescorerProvider(context, bucket, instanceID);
ReloadingReference<List<List<HostAndPort>>> allPartitionsReference =
configureAllPartitionsReference(context, bucket, instanceID);
MyrrixRecommender recommender =
new ServerRecommender(bucket, instanceID, localInputDir, partition, allPartitionsReference);
context.setAttribute(AbstractMyrrixServlet.RECOMMENDER_KEY, recommender);
configureClientThread(context, bucket, instanceID, recommender);
log.info("Myrrix is initialized");
}
private static void configureLogging(ServletContext context) {
MemoryHandler.setSensibleLogFormat();
Handler logHandler = null;
for (Handler handler : java.util.logging.Logger.getLogger("").getHandlers()) {
if (handler instanceof MemoryHandler) {
logHandler = handler;
break;
}
}
if (logHandler == null) {
// Not previously configured by command line, make a new one
logHandler = new MemoryHandler();
java.util.logging.Logger.getLogger("").addHandler(logHandler);
}
context.setAttribute(LOG_HANDLER, logHandler);
}
/**
* This is a possible workaround for Tomcat on Windows, not creating the temp dir it allocates?
*/
private static void configureTempDir(ServletContext context) {
File tempDir = (File) context.getAttribute(ServletContext.TEMPDIR);
Preconditions.checkNotNull(tempDir, "Servlet container didn't set %s", ServletContext.TEMPDIR);
if (!tempDir.exists()) {
log.warn("{} was set to {} but it did not exist", ServletContext.TEMPDIR, tempDir);
if (!tempDir.mkdirs()) {
log.warn("Failed to create dir {}", tempDir);
}
} else if (tempDir.isFile()) {
log.warn("{} is a file, not directory", tempDir);
}
}
private File configureLocalInputDir(ServletContext context) {
String localInputDirName = getAttributeOrParam(context, LOCAL_INPUT_DIR_KEY);
File localInputDir;
if (localInputDirName == null) {
localInputDir = Files.createTempDir();
localInputDir.deleteOnExit();
tempDirToDelete = localInputDir;
} else {
localInputDir = new File(localInputDirName);
if (!localInputDir.exists()) {
boolean madeDirs = localInputDir.mkdirs();
if (!madeDirs) {
log.warn("Failed to create local input dir {}", localInputDir);
}
}
tempDirToDelete = null;
}
context.setAttribute(AbstractMyrrixServlet.LOCAL_INPUT_DIR_KEY, localInputDir.getAbsolutePath());
return localInputDir;
}
private static int configurePartition(ServletContext context) {
int partition;
String partitionString = getAttributeOrParam(context, PARTITION_KEY);
if (partitionString == null) {
partition = 0;
log.info("No partition specified, so it is implicitly partition #{}", partition);
} else {
partition = Integer.parseInt(partitionString);
log.info("Running as partition #{}", partition);
}
context.setAttribute(AbstractMyrrixServlet.PARTITION_KEY, partition);
return partition;
}
private static void configureRescorerProvider(ServletContext context, String bucket, String instanceID) {
RescorerProvider rescorerProvider;
try {
rescorerProvider = loadRescorerProvider(context, bucket, instanceID);
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
} catch (ClassNotFoundException cnfe) {
throw new IllegalStateException(cnfe);
}
if (rescorerProvider != null) {
context.setAttribute(AbstractMyrrixServlet.RESCORER_PROVIDER_KEY, rescorerProvider);
}
}
private static ReloadingReference<List<List<HostAndPort>>> configureAllPartitionsReference(ServletContext context,
final String bucket,
final String instanceID) {
boolean readOnly = Boolean.parseBoolean(getAttributeOrParam(context, READ_ONLY_KEY));
context.setAttribute(AbstractMyrrixServlet.READ_ONLY_KEY, readOnly);
final String portString = getAttributeOrParam(context, PORT_KEY);
final CharSequence allPartitionsSpecString = getAttributeOrParam(context, ALL_PARTITIONS_SPEC_KEY);
ReloadingReference<List<List<HostAndPort>>> allPartitionsReference = null;
if (allPartitionsSpecString != null) {
allPartitionsReference =
new ReloadingReference<List<List<HostAndPort>>>(new Callable<List<List<HostAndPort>>>() {
@Override
public List<List<HostAndPort>> call() {
if (RunnerConfiguration.AUTO_PARTITION_SPEC.equals(allPartitionsSpecString)) {
int port = Integer.parseInt(portString);
PartitionLoader loader =
ClassUtils.loadInstanceOf("net.myrrix.online.partition.PartitionLoaderImpl",
PartitionLoader.class);
List<List<HostAndPort>> newPartitions = loader.loadPartitions(port, bucket, instanceID);
log.debug("Latest partitions: {}", newPartitions);
return newPartitions;
}
return PartitionsUtils.parseAllPartitions(allPartitionsSpecString);
}
}, 10, TimeUnit.MINUTES);
// "Tickle" it to pre-load and check for errors
allPartitionsReference.get();
context.setAttribute(AbstractMyrrixServlet.ALL_PARTITIONS_REF_KEY, allPartitionsReference);
}
return allPartitionsReference;
}
private void configureClientThread(ServletContext context,
String bucket,
String instanceID,
MyrrixRecommender recommender) {
ClientThread theThread;
try {
theThread = loadClientThreadClass(context, bucket, instanceID);
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
} catch (ClassNotFoundException cnfe) {
throw new IllegalStateException(cnfe);
}
if (theThread != null) {
theThread.setRecommender(recommender);
clientThread = theThread;
new Thread(theThread, "MyrrixClientThread").start();
}
}
private static String getAttributeOrParam(ServletContext context, String key) {
Object valueObject = context.getAttribute(key);
String valueString = valueObject == null ? null : valueObject.toString();
if (valueString == null) {
valueString = context.getInitParameter(key);
}
return valueString;
}
private static RescorerProvider loadRescorerProvider(ServletContext context, String bucket, String instanceID)
throws IOException, ClassNotFoundException {
String rescorerProviderClassNames = getAttributeOrParam(context, RESCORER_PROVIDER_CLASS_KEY);
if (rescorerProviderClassNames == null) {
return null;
}
log.info("Using RescorerProvider class(es) {}", rescorerProviderClassNames);
boolean allClassesFound = true;
for (String rescorerProviderClassName : COMMA.split(rescorerProviderClassNames)) {
if (!ClassUtils.classExists(rescorerProviderClassName)) {
allClassesFound = false;
break;
}
}
if (allClassesFound) {
log.info("Found class(es) in local classpath");
return AbstractRescorerProvider.loadRescorerProviders(rescorerProviderClassNames, null);
}
log.info("Class doesn't exist in local classpath");
ResourceRetriever resourceRetriever =
ClassUtils.loadInstanceOf("net.myrrix.online.io.DelegateResourceRetriever", ResourceRetriever.class);
resourceRetriever.init(bucket);
File tempResourceFile = resourceRetriever.getRescorerJar(instanceID);
if (tempResourceFile == null) {
log.info("No external rescorer JAR is available in this implementation");
throw new ClassNotFoundException(rescorerProviderClassNames);
}
log.info("Loading class(es) {} from {}, copied from remote JAR at key {}",
rescorerProviderClassNames, tempResourceFile, tempResourceFile);
RescorerProvider rescorerProvider =
AbstractRescorerProvider.loadRescorerProviders(rescorerProviderClassNames,
tempResourceFile.toURI().toURL());
if (!tempResourceFile.delete()) {
log.info("Could not delete {}", tempResourceFile);
}
return rescorerProvider;
}
private static ClientThread loadClientThreadClass(ServletContext context, String bucket, String instanceID)
throws IOException, ClassNotFoundException {
String clientThreadClassName = getAttributeOrParam(context, CLIENT_THREAD_CLASS_KEY);
if (clientThreadClassName == null) {
return null;
}
log.info("Using Runnable/Closeable client thread class {}", clientThreadClassName);
if (ClassUtils.classExists(clientThreadClassName)) {
log.info("Found class on local classpath");
return ClassUtils.loadInstanceOf(clientThreadClassName, ClientThread.class);
}
log.info("Class doesn't exist in local classpath");
ResourceRetriever resourceRetriever =
ClassUtils.loadInstanceOf("net.myrrix.online.io.DelegateResourceRetriever", ResourceRetriever.class);
resourceRetriever.init(bucket);
File tempResourceFile = resourceRetriever.getClientThreadJar(instanceID);
if (tempResourceFile == null) {
log.info("No external client thread JAR is available in this implementation");
throw new ClassNotFoundException(clientThreadClassName);
}
log.info("Loading class {} from {}, copied from remote JAR at key {}",
clientThreadClassName, tempResourceFile, tempResourceFile);
ClientThread clientThreadRunnable =
ClassUtils.loadFromRemote(clientThreadClassName, ClientThread.class, tempResourceFile.toURI().toURL());
if (!tempResourceFile.delete()) {
log.info("Could not delete {}", tempResourceFile);
}
return clientThreadRunnable;
}
@Override
public void contextDestroyed(ServletContextEvent event) {
log.info("Uninitializing Myrrix in servlet context...");
ClientThread theClientThread = clientThread;
if (theClientThread != null) {
try {
theClientThread.close();
} catch (IOException e) {
log.warn("Error while closing client thread", e);
}
}
ServletContext context = event.getServletContext();
Closeable recommender = (Closeable) context.getAttribute(AbstractMyrrixServlet.RECOMMENDER_KEY);
if (recommender != null) {
try {
recommender.close();
} catch (IOException e) {
log.warn("Unexpected error while closing", e);
}
}
IOUtils.deleteRecursively(tempDirToDelete);
log.info("Myrrix is uninitialized");
}
}