blob: 01e241f653cca86d814f202e2a23a5c670d3f5ba [file] [log] [blame]
package org.apache.s4.core;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import org.I0Itec.zkclient.IZkDataListener;
import org.apache.s4.base.util.ModulesLoader;
import org.apache.s4.base.util.S4RLoader;
import org.apache.s4.base.util.S4RLoaderFactory;
import org.apache.s4.comm.DefaultCommModule;
import org.apache.s4.comm.ModulesLoaderFactory;
import org.apache.s4.comm.topology.ZNRecord;
import org.apache.s4.comm.topology.ZkClient;
import org.apache.s4.comm.util.ArchiveFetchException;
import org.apache.s4.comm.util.ArchiveFetcher;
import org.apache.s4.core.util.AppConfig;
import org.apache.s4.core.util.ParametersInjectionModule;
import org.apache.s4.deploy.DeploymentFailedException;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.name.Named;
import com.google.inject.util.Modules;
import com.google.inject.util.Modules.OverriddenModuleBuilder;
/**
* This is the bootstrap for S4 nodes.
* <p>
* Its roles are to:
* <ul>
* <li>register within the S4 cluster (and acquire a partition).
* <li>wait for an application to be published on the S4 cluster
* </ul>
* <p>
* When an application is available, custom modules are fetched if necessary and a full-featured S4 node is started. The
* application code is then downloaded and the app started.
* <p>
* For testing purposes, it is also possible to start an application without packaging code, provided the application
* classes are available in the classpath.
*
*
*
*/
public class S4Bootstrap {
private static Logger logger = LoggerFactory.getLogger(S4Bootstrap.class);
public static final String MANIFEST_S4_APP_CLASS = "S4-App-Class";
public static final String S4R_URI = "s4r_uri";
private final ZkClient zkClient;
private final String appPath;
private final AtomicBoolean deployed = new AtomicBoolean(false);
private final ArchiveFetcher fetcher;
private Injector parentInjector;
CountDownLatch signalOneAppLoaded = new CountDownLatch(1);
@Inject
public S4Bootstrap(@Named("s4.cluster.name") String clusterName, ZkClient zkClient, ArchiveFetcher fetcher) {
this.fetcher = fetcher;
this.zkClient = zkClient;
String appDir = "/s4/clusters/" + clusterName + "/app";
if (!zkClient.exists(appDir)) {
zkClient.create(appDir, null, CreateMode.PERSISTENT);
}
appPath = appDir + "/s4App";
zkClient.subscribeDataChanges(appPath, new AppChangeListener());
}
public void start(Injector parentInjector) throws InterruptedException, ArchiveFetchException {
this.parentInjector = parentInjector;
if (zkClient.exists(appPath)) {
if (!deployed.get()) {
loadModulesAndStartApp(parentInjector);
}
}
signalOneAppLoaded.await();
}
private void loadModulesAndStartApp(final Injector parentInjector) throws ArchiveFetchException {
final ZNRecord appData = zkClient.readData(appPath);
// can be null
final AppConfig appConfig = new AppConfig(appData);
String appName = appConfig.getAppName();
List<File> modulesLocalCopies = new ArrayList<File>();
for (String uriString : appConfig.getCustomModulesURIs()) {
modulesLocalCopies.add(fetchModuleAndCopyToLocalFile(appName, uriString));
}
final ModulesLoader modulesLoader = new ModulesLoaderFactory().createModulesLoader(modulesLocalCopies);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// load app class through modules classloader and start it
startS4App(appConfig, parentInjector, modulesLoader);
signalOneAppLoaded.countDown();
}
}, "S4 platform loader");
t.start();
}
private void startS4App(AppConfig appConfig, Injector parentInjector, ClassLoader modulesLoader) {
try {
App app = loadApp(appConfig, modulesLoader);
app.init();
app.start();
} catch (Exception e) {
logger.error("Cannot start S4 node", e);
System.exit(1);
}
}
private App loadApp(AppConfig appConfig, ClassLoader modulesLoader) throws DeploymentFailedException {
Module combinedPlatformModule;
try {
combinedPlatformModule = loadPlatformModules(appConfig, modulesLoader);
} catch (Exception e) {
throw new DeploymentFailedException("Cannot load platform modules", e);
}
if (appConfig.getAppURI() == null) {
if (appConfig.getAppClassName() != null) {
try {
// In that case we won't be using an S4R classloader, app classes are available from the current
// classloader
// The app module provides bindings specific to the app class loader, in this case the current
// thread's
// class loader.
AppModule appModule = new AppModule(Thread.currentThread().getContextClassLoader());
// NOTE: because the app module can be overriden
Module combinedModule = Modules.override(appModule).with(combinedPlatformModule);
Injector injector = parentInjector.createChildInjector(combinedModule);
logger.info("Starting S4 app with application class [{}]", appConfig.getAppClassName());
return (App) injector.getInstance(Class.forName(appConfig.getAppClassName(), true, modulesLoader));
// server.startApp(app, "appName", clusterName);
} catch (Exception e) {
throw new DeploymentFailedException(String.format(
"Cannot start application: cannot instantiate app class %s due to: %s",
appConfig.getAppClassName(), e.getMessage()), e);
}
} else {
throw new DeploymentFailedException(
"Application class name must be specified when application URI omitted");
}
} else {
try {
URI uri = new URI(appConfig.getAppURI());
// fetch application
File localS4RFileCopy;
try {
localS4RFileCopy = File.createTempFile("tmp", "s4r");
} catch (IOException e1) {
logger.error(
"Cannot deploy app [{}] because a local copy of the S4R file could not be initialized due to [{}]",
appConfig.getAppName(), e1.getClass().getName() + "->" + e1.getMessage());
throw new DeploymentFailedException("Cannot deploy application [" + appConfig.getAppName() + "]",
e1);
}
localS4RFileCopy.deleteOnExit();
try {
if (ByteStreams.copy(fetcher.fetch(uri), Files.newOutputStreamSupplier(localS4RFileCopy)) == 0) {
throw new DeploymentFailedException("Cannot copy archive from [" + uri.toString() + "] to ["
+ localS4RFileCopy.getAbsolutePath() + "] (nothing was copied)");
}
} catch (Exception e) {
throw new DeploymentFailedException("Cannot deploy application [" + appConfig.getAppName()
+ "] from URI [" + uri.toString() + "] ", e);
}
// install locally
Injector injector = parentInjector.createChildInjector(combinedPlatformModule);
App loadedApp = loadS4R(injector, localS4RFileCopy, appConfig.getAppName());
if (loadedApp != null) {
return loadedApp;
} else {
throw new DeploymentFailedException("Cannot deploy application [" + appConfig.getAppName()
+ "] from URI [" + uri.toString() + "] : cannot start application");
}
} catch (URISyntaxException e) {
throw new DeploymentFailedException(String.format(
"Cannot deploy application [%s] : invalid URI for fetching S4R archive %s : %s", new Object[] {
appConfig.getAppName(), appConfig.getAppURI(), e.getMessage() }), e);
}
}
}
private File fetchModuleAndCopyToLocalFile(String appName, String uriString) throws ArchiveFetchException {
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException e2) {
throw new ArchiveFetchException("Invalid module URI : [" + uriString + "]", e2);
}
File localModuleFileCopy;
try {
localModuleFileCopy = File.createTempFile("tmp", "module");
} catch (IOException e1) {
logger.error(
"Cannot deploy app [{}] because a local copy of the module file could not be initialized due to [{}]",
appName, e1.getClass().getName() + "->" + e1.getMessage());
throw new ArchiveFetchException("Cannot deploy application [" + appName + "]", e1);
}
localModuleFileCopy.deleteOnExit();
try {
if (ByteStreams.copy(fetcher.fetch(uri), Files.newOutputStreamSupplier(localModuleFileCopy)) == 0) {
throw new ArchiveFetchException("Cannot copy archive from [" + uri.toString() + "] to ["
+ localModuleFileCopy.getAbsolutePath() + "] (nothing was copied)");
}
} catch (Exception e) {
throw new ArchiveFetchException("Cannot deploy application [" + appName + "] from URI [" + uri.toString()
+ "] ", e);
}
return localModuleFileCopy;
}
private static Module loadPlatformModules(AppConfig appConfig, ClassLoader modulesLoader) throws IOException,
InstantiationException, IllegalAccessException, ClassNotFoundException {
InputStream commConfigFileInputStream = Resources.getResource("default.s4.comm.properties").openStream();
InputStream coreConfigFileInputStream = Resources.getResource("default.s4.core.properties").openStream();
logger.info("Initializing S4 app with : {}", appConfig.toString());
AbstractModule commModule = new DefaultCommModule(commConfigFileInputStream);
AbstractModule coreModule = new DefaultCoreModule(coreConfigFileInputStream);
List<com.google.inject.Module> extraModules = new ArrayList<com.google.inject.Module>();
for (String moduleClass : appConfig.getCustomModulesNames()) {
extraModules.add((Module) Class.forName(moduleClass, true, modulesLoader).newInstance());
}
Module combinedModule = Modules.combine(commModule, coreModule);
if (extraModules.size() > 0) {
OverriddenModuleBuilder overridenModuleBuilder = Modules.override(combinedModule);
combinedModule = overridenModuleBuilder.with(extraModules);
}
if (appConfig.getNamedParameters() != null && !appConfig.getNamedParameters().isEmpty()) {
logger.debug("Adding named parameters for injection : {}", appConfig.getNamedParametersAsString());
Map<String, String> namedParameters = new HashMap<String, String>();
namedParameters.putAll(appConfig.getNamedParameters());
combinedModule = Modules.override(combinedModule).with(new ParametersInjectionModule(namedParameters));
}
return combinedModule;
}
private App loadS4R(Injector injector, File s4r, String appName) {
// TODO handle application upgrade
logger.info("Loading application [{}] from file [{}]", appName, s4r.getAbsolutePath());
S4RLoaderFactory loaderFactory = injector.getInstance(S4RLoaderFactory.class);
S4RLoader appClassLoader = loaderFactory.createS4RLoader(s4r.getAbsolutePath());
try {
JarFile s4rFile = new JarFile(s4r);
if (s4rFile.getManifest() == null) {
logger.warn("Cannot load s4r archive [{}] : missing manifest file");
return null;
}
if (!s4rFile.getManifest().getMainAttributes().containsKey(new Name(MANIFEST_S4_APP_CLASS))) {
logger.warn("Cannot load s4r archive [{}] : missing attribute [{}] in manifest", s4r.getAbsolutePath(),
MANIFEST_S4_APP_CLASS);
return null;
}
String appClassName = s4rFile.getManifest().getMainAttributes().getValue(MANIFEST_S4_APP_CLASS);
logger.info("App class name is: " + appClassName);
App app = null;
try {
Object o = (appClassLoader.loadClass(appClassName)).newInstance();
app = (App) o;
// we use the app module to provide bindings that depend upon a classloader savy of app classes, e.g.
// for serialization/deserialization
AppModule appModule = new AppModule(appClassLoader);
injector.createChildInjector(appModule).injectMembers(app);
} catch (Exception e) {
logger.error("Could not load s4 application form s4r file [{" + s4r.getAbsolutePath() + "}]", e);
return null;
}
logger.info("Loaded application from file {}", s4r.getAbsolutePath());
signalOneAppLoaded.countDown();
return app;
} catch (IOException e) {
logger.error("Could not load s4 application form s4r file [{" + s4r.getAbsolutePath() + "}]", e);
return null;
}
}
class AppChangeListener implements IZkDataListener {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
if (!deployed.get()) {
loadModulesAndStartApp(parentInjector);
deployed.set(true);
}
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
logger.error("Application undeployment is not supported yet");
}
}
}