/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.runtime.app; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.commons.lang3.reflect.ConstructorUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.util.concurrent.Service; import com.google.common.util.concurrent.ServiceManager; import gobblin.annotation.Alpha; import gobblin.admin.AdminWebServer; import gobblin.configuration.ConfigurationKeys; import gobblin.configuration.State; import gobblin.metrics.GobblinMetrics; import gobblin.rest.JobExecutionInfoServer; import gobblin.runtime.services.JMXReportingService; import gobblin.runtime.services.MetricsReportingService; import gobblin.util.ApplicationLauncherUtils; /** * An implementation of {@link ApplicationLauncher} that defines an application as a set of {@link Service}s that should * be started and stopped. The class will run a set of core services, some of which are optional, some of which are * mandatory. These {@link Service}s are as follows: * * <ul> * <li>{@link MetricsReportingService} is optional and controlled by {@link ConfigurationKeys#METRICS_ENABLED_KEY}</li> * <li>{@link JobExecutionInfoServer} is optional and controlled by {@link ConfigurationKeys#JOB_EXECINFO_SERVER_ENABLED_KEY}</li> * <li>{@link AdminWebServer} is optional and controlled by {@link ConfigurationKeys#ADMIN_SERVER_ENABLED_KEY}</li> * <li>{@link JMXReportingService} is mandatory</li> * </ul> * * <p> * Additional {@link Service}s can be added via the {@link #addService(Service)} method. A {@link Service} cannot be * added after the application has started. Additional {@link Service}s can also be specified via the configuration * key {@link #APP_ADDITIONAL_SERVICES}. * </p> * * <p> * An {@link ServiceBasedAppLauncher} cannot be restarted. * </p> */ @Alpha public class ServiceBasedAppLauncher implements ApplicationLauncher { /** * The name of the application. Not applicable for YARN jobs, which uses a separate key for the application name. */ public static final String APP_NAME = "app.name"; /** * The number of seconds to wait for the application to stop, the default value is {@link #DEFAULT_APP_STOP_TIME_SECONDS} */ public static final String APP_STOP_TIME_SECONDS = "app.stop.time.seconds"; private static final String DEFAULT_APP_STOP_TIME_SECONDS = Long.toString(60); /** * A comma separated list of fully qualified classes that implement the {@link Service} interface. These * {@link Service}s will be run in addition to the core services. */ public static final String APP_ADDITIONAL_SERVICES = "app.additional.services"; private static final Logger LOG = LoggerFactory.getLogger(ServiceBasedAppLauncher.class); private final int stopTime; private final String appId; private final List<Service> services; private volatile boolean hasStarted = false; private volatile boolean hasStopped = false; private ServiceManager serviceManager; public ServiceBasedAppLauncher(Properties properties, String appName) throws Exception { this.stopTime = Integer.parseInt(properties.getProperty(APP_STOP_TIME_SECONDS, DEFAULT_APP_STOP_TIME_SECONDS)); this.appId = ApplicationLauncherUtils.newAppId(appName); this.services = new ArrayList<>(); // Add core Services needed for any application addJobExecutionServerAndAdminUI(properties); addMetricsService(properties); addJMXReportingService(); // Add any additional Services specified via configuration keys addServicesFromProperties(properties); // Add a shutdown hook that interrupts the main thread addInterruptedShutdownHook(); } /** * Starts the {@link ApplicationLauncher} by starting all associated services. This method also adds a shutdown hook * that invokes {@link #stop()} and the {@link #close()} methods. So {@link #stop()} and {@link #close()} need not be * called explicitly; they can be triggered during the JVM shutdown. */ @Override public synchronized void start() { if (this.hasStarted) { LOG.warn("ApplicationLauncher has already started"); return; } this.hasStarted = true; this.serviceManager = new ServiceManager(this.services); // A listener that shutdowns the application if any service fails. this.serviceManager.addListener(new ServiceManager.Listener() { @Override public void failure(Service service) { super.failure(service); LOG.error(String.format("Service %s has failed.", service.getClass().getSimpleName()), service.failureCause()); try { service.stopAsync(); ServiceBasedAppLauncher.this.stop(); } catch (ApplicationException ae) { LOG.error("Could not shutdown services gracefully. This may cause the application to hang."); } } }); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { try { ServiceBasedAppLauncher.this.stop(); } catch (ApplicationException e) { LOG.error("Failed to shutdown application", e); } finally { try { ServiceBasedAppLauncher.this.close(); } catch (IOException e) { LOG.error("Failed to close application", e); } } } }); LOG.info("Starting the Gobblin application and all its associated Services"); // Start the application this.serviceManager.startAsync().awaitHealthy(); } /** * Stops the {@link ApplicationLauncher} by stopping all associated services. */ @Override public synchronized void stop() throws ApplicationException { if (!this.hasStarted) { LOG.warn("ApplicationLauncher was never started"); return; } if (this.hasStopped) { LOG.warn("ApplicationLauncher has already stopped"); return; } this.hasStopped = true; LOG.info("Shutting down the application"); try { this.serviceManager.stopAsync().awaitStopped(this.stopTime, TimeUnit.SECONDS); } catch (TimeoutException te) { LOG.error("Timeout in stopping the service manager", te); } } @Override public void close() throws IOException { // Do nothing } /** * Add a {@link Service} to be run by this {@link ApplicationLauncher}. * * <p> * This method is public because there are certain classes launchers (such as Azkaban) that require the * {@link ApplicationLauncher} to extend a pre-defined class. Since Java classes cannot extend multiple classes, * composition needs to be used. In which case this method needs to be public. * </p> */ public void addService(Service service) { if (this.hasStarted) { throw new IllegalArgumentException("Cannot add a service while the application is running!"); } this.services.add(service); } private void addJobExecutionServerAndAdminUI(Properties properties) { boolean jobExecInfoServerEnabled = Boolean .valueOf(properties.getProperty(ConfigurationKeys.JOB_EXECINFO_SERVER_ENABLED_KEY, Boolean.FALSE.toString())); boolean adminUiServerEnabled = Boolean.valueOf(properties.getProperty(ConfigurationKeys.ADMIN_SERVER_ENABLED_KEY, Boolean.FALSE.toString())); if (jobExecInfoServerEnabled) { LOG.info("Will launch the job execution info server"); JobExecutionInfoServer executionInfoServer = new JobExecutionInfoServer(properties); addService(executionInfoServer); if (adminUiServerEnabled) { LOG.info("Will launch the admin UI server"); addService(new AdminWebServer(properties, executionInfoServer.getAdvertisedServerUri())); } } else if (adminUiServerEnabled) { LOG.warn("Not launching the admin UI because the job execution info server is not enabled"); } } private void addMetricsService(Properties properties) { if (GobblinMetrics.isEnabled(properties)) { addService(new MetricsReportingService(properties, this.appId)); } } private void addJMXReportingService() { addService(new JMXReportingService()); } private void addServicesFromProperties(Properties properties) throws IllegalAccessException, InstantiationException, ClassNotFoundException, InvocationTargetException { if (properties.containsKey(APP_ADDITIONAL_SERVICES)) { for (String serviceClassName : new State(properties).getPropAsSet(APP_ADDITIONAL_SERVICES)) { Class<?> serviceClass = Class.forName(serviceClassName); if (Service.class.isAssignableFrom(serviceClass)) { Service service; Constructor<?> constructor = ConstructorUtils.getMatchingAccessibleConstructor(serviceClass, Properties.class); if (constructor != null) { service = (Service) constructor.newInstance(properties); } else { service = (Service) serviceClass.newInstance(); } addService(service); } else { throw new IllegalArgumentException(String.format("Class %s specified by %s does not implement %s", serviceClassName, APP_ADDITIONAL_SERVICES, Service.class.getSimpleName())); } } } } private void addInterruptedShutdownHook() { final Thread mainThread = Thread.currentThread(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { mainThread.interrupt(); } }); } }