/* * 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.service.modules.core; import gobblin.service.FlowId; import gobblin.service.Schedule; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import lombok.Getter; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.reflect.ConstructorUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.helix.ControllerChangeListener; import org.apache.helix.HelixManager; import org.apache.helix.NotificationContext; import org.apache.helix.messaging.handling.HelixTaskResult; import org.apache.helix.messaging.handling.MessageHandler; import org.apache.helix.messaging.handling.MessageHandlerFactory; import org.apache.helix.model.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.base.Throwables; import com.google.common.eventbus.EventBus; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.name.Names; import com.linkedin.data.template.StringMap; import com.linkedin.r2.RemoteInvocationException; import com.linkedin.restli.server.resources.BaseResource; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import gobblin.annotation.Alpha; import gobblin.configuration.ConfigurationKeys; import gobblin.restli.EmbeddedRestliServer; import gobblin.runtime.api.TopologySpec; import gobblin.runtime.app.ApplicationException; import gobblin.runtime.app.ApplicationLauncher; import gobblin.runtime.app.ServiceBasedAppLauncher; import gobblin.runtime.api.Spec; import gobblin.runtime.api.SpecNotFoundException; import gobblin.runtime.spec_catalog.FlowCatalog; import gobblin.scheduler.SchedulerService; import gobblin.service.FlowConfig; import gobblin.service.FlowConfigClient; import gobblin.service.FlowConfigsResource; import gobblin.service.HelixUtils; import gobblin.service.ServiceConfigKeys; import gobblin.service.modules.orchestration.Orchestrator; import gobblin.service.modules.scheduler.GobblinServiceJobScheduler; import gobblin.service.modules.topology.TopologySpecFactory; import gobblin.runtime.spec_catalog.TopologyCatalog; import gobblin.util.ClassAliasResolver; import gobblin.util.ConfigUtils; @Alpha public class GobblinServiceManager implements ApplicationLauncher { private static final Logger LOGGER = LoggerFactory.getLogger(GobblinServiceManager.class); protected final ServiceBasedAppLauncher serviceLauncher; private volatile boolean stopInProgress = false; // An EventBus used for communications between services running in the ApplicationMaster protected final EventBus eventBus = new EventBus(GobblinServiceManager.class.getSimpleName()); protected final FileSystem fs; protected final Path serviceWorkDir; protected final String serviceId; protected final boolean isTopologyCatalogEnabled; protected final boolean isFlowCatalogEnabled; protected final boolean isSchedulerEnabled; protected final boolean isRestLIServerEnabled; protected final boolean isTopologySpecFactoryEnabled; protected TopologyCatalog topologyCatalog; @Getter protected FlowCatalog flowCatalog; @Getter protected GobblinServiceJobScheduler scheduler; protected Orchestrator orchestrator; protected EmbeddedRestliServer restliServer; protected TopologySpecFactory topologySpecFactory; protected Optional<HelixManager> helixManager; protected ClassAliasResolver<TopologySpecFactory> aliasResolver; @Getter protected Config config; public GobblinServiceManager(String serviceName, String serviceId, Config config, Optional<Path> serviceWorkDirOptional) throws Exception { // Done to preserve backwards compatibility with the previously hard-coded timeout of 5 minutes Properties properties = ConfigUtils.configToProperties(config); if (!properties.contains(ServiceBasedAppLauncher.APP_STOP_TIME_SECONDS)) { properties.setProperty(ServiceBasedAppLauncher.APP_STOP_TIME_SECONDS, Long.toString(300)); } this.config = config; this.serviceId = serviceId; this.serviceLauncher = new ServiceBasedAppLauncher(properties, serviceName); this.fs = buildFileSystem(config); this.serviceWorkDir = serviceWorkDirOptional.isPresent() ? serviceWorkDirOptional.get() : getServiceWorkDirPath(this.fs, serviceName, serviceId); // Initialize TopologyCatalog this.isTopologyCatalogEnabled = ConfigUtils.getBoolean(config, ServiceConfigKeys.GOBBLIN_SERVICE_TOPOLOGY_CATALOG_ENABLED_KEY, true); if (isTopologyCatalogEnabled) { this.topologyCatalog = new TopologyCatalog(config, Optional.of(LOGGER)); this.serviceLauncher.addService(topologyCatalog); } // Initialize FlowCatalog this.isFlowCatalogEnabled = ConfigUtils.getBoolean(config, ServiceConfigKeys.GOBBLIN_SERVICE_FLOW_CATALOG_ENABLED_KEY, true); if (isFlowCatalogEnabled) { this.flowCatalog = new FlowCatalog(config, Optional.of(LOGGER)); this.serviceLauncher.addService(flowCatalog); } // Initialize Helix Optional<String> zkConnectionString = Optional.fromNullable(ConfigUtils.getString(config, ServiceConfigKeys.ZK_CONNECTION_STRING_KEY, null)); if (zkConnectionString.isPresent()) { LOGGER.info("Using ZooKeeper connection string: " + zkConnectionString); // This will create and register a Helix controller in ZooKeeper this.helixManager = Optional.fromNullable(buildHelixManager(config, zkConnectionString.get())); } else { LOGGER.info("No ZooKeeper connection string. Running in single instance mode."); this.helixManager = Optional.absent(); } // Initialize ServiceScheduler this.isSchedulerEnabled = ConfigUtils.getBoolean(config, ServiceConfigKeys.GOBBLIN_SERVICE_SCHEDULER_ENABLED_KEY, true); if (isSchedulerEnabled) { this.orchestrator = new Orchestrator(config, Optional.of(this.topologyCatalog), Optional.of(LOGGER)); SchedulerService schedulerService = new SchedulerService(properties); this.scheduler = new GobblinServiceJobScheduler(config, this.helixManager, Optional.of(this.flowCatalog), Optional.of(this.topologyCatalog), this.orchestrator, schedulerService, Optional.of(LOGGER)); } // Initialize RestLI this.isRestLIServerEnabled = ConfigUtils.getBoolean(config, ServiceConfigKeys.GOBBLIN_SERVICE_RESTLI_SERVER_ENABLED_KEY, true); if (isRestLIServerEnabled) { Injector injector = Guice.createInjector(new Module() { @Override public void configure(Binder binder) { binder.bind(FlowCatalog.class).annotatedWith(Names.named("flowCatalog")).toInstance(flowCatalog); binder.bindConstant().annotatedWith(Names.named("readyToUse")).to(Boolean.TRUE); } }); this.restliServer = EmbeddedRestliServer.builder() .resources(Lists.<Class<? extends BaseResource>>newArrayList(FlowConfigsResource.class)) .injector(injector) .build(); this.serviceLauncher.addService(restliServer); } // Register Scheduler to listen to changes in Flows if (isSchedulerEnabled) { this.flowCatalog.addListener(this.scheduler); this.topologyCatalog.addListener(this.orchestrator); } // Initialize TopologySpecFactory this.isTopologySpecFactoryEnabled = ConfigUtils.getBoolean(config, ServiceConfigKeys.GOBBLIN_SERVICE_TOPOLOGY_SPEC_FACTORY_ENABLED_KEY, true); if (this.isTopologySpecFactoryEnabled) { this.aliasResolver = new ClassAliasResolver<>(TopologySpecFactory.class); String topologySpecFactoryClass = ServiceConfigKeys.DEFAULT_TOPOLOGY_SPEC_FACTORY; if (config.hasPath(ServiceConfigKeys.TOPOLOGYSPEC_FACTORY_KEY)) { topologySpecFactoryClass = config.getString(ServiceConfigKeys.TOPOLOGYSPEC_FACTORY_KEY); } try { LOGGER.info("Using TopologySpecFactory class name/alias " + topologySpecFactoryClass); this.topologySpecFactory = (TopologySpecFactory) ConstructorUtils .invokeConstructor(Class.forName(this.aliasResolver.resolve(topologySpecFactoryClass)), config); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException | ClassNotFoundException e) { throw new RuntimeException(e); } } } public boolean isLeader() { // If helix manager is absent, then this standalone instance hence leader // .. else check if this master of cluster return !helixManager.isPresent() || helixManager.get().isLeader(); } /** * Build the {@link HelixManager} for the Service Master. */ private HelixManager buildHelixManager(Config config, String zkConnectionString) { String helixClusterName = config.getString(ServiceConfigKeys.HELIX_CLUSTER_NAME_KEY); String helixInstanceName = ConfigUtils.getString(config, ServiceConfigKeys.HELIX_INSTANCE_NAME_KEY, GobblinServiceManager.class.getSimpleName()); LOGGER.info("Creating Helix cluster if not already present [overwrite = false]: " + zkConnectionString); HelixUtils.createGobblinHelixCluster(zkConnectionString, helixClusterName, false); return HelixUtils.buildHelixManager(helixInstanceName, helixClusterName, zkConnectionString); } private FileSystem buildFileSystem(Config config) throws IOException { return config.hasPath(ConfigurationKeys.FS_URI_KEY) ? FileSystem .get(URI.create(config.getString(ConfigurationKeys.FS_URI_KEY)), new Configuration()) : FileSystem.get(new Configuration()); } private Path getServiceWorkDirPath(FileSystem fs, String serviceName, String serviceId) { return new Path(fs.getHomeDirectory(), serviceName + Path.SEPARATOR + serviceId); } /** * Handle leadership change. * @param changeContext notification context */ private void handleLeadershipChange(NotificationContext changeContext) { if (this.helixManager.isPresent() && this.helixManager.get().isLeader()) { LOGGER.info("Leader notification for {} HM.isLeader {}", this.helixManager.get().getInstanceName(), this.helixManager.get().isLeader()); if (this.isSchedulerEnabled) { LOGGER.info("Gobblin Service is now running in master instance mode, enabling Scheduler."); this.scheduler.setActive(true); } } else if (this.helixManager.isPresent()) { LOGGER.info("Leader lost notification for {} HM.isLeader {}", this.helixManager.get().getInstanceName(), this.helixManager.get().isLeader()); if (this.isSchedulerEnabled) { LOGGER.info("Gobblin Service is now running in slave instance mode, disabling Scheduler."); this.scheduler.setActive(false); } } } @Override public void start() throws ApplicationException { LOGGER.info("[Init] Starting the Gobblin Service Manager"); if (this.helixManager.isPresent()) { connectHelixManager(); } this.eventBus.register(this); this.serviceLauncher.start(); if (this.helixManager.isPresent()) { // Subscribe to leadership changes this.helixManager.get().addControllerListener(new ControllerChangeListener() { @Override public void onControllerChange(NotificationContext changeContext) { handleLeadershipChange(changeContext); } }); // Update for first time since there might be no notification if (helixManager.get().isLeader()) { if (this.isSchedulerEnabled) { LOGGER.info("[Init] Gobblin Service is running in master instance mode, enabling Scheduler."); this.scheduler.setActive(true); } } else { if (this.isSchedulerEnabled) { LOGGER.info("[Init] Gobblin Service is running in slave instance mode, not enabling Scheduler."); } } } else { // No Helix manager, hence standalone service instance // .. designate scheduler to itself LOGGER.info("[Init] Gobblin Service is running in single instance mode, enabling Scheduler."); this.scheduler.setActive(true); } // Populate TopologyCatalog with all Topologies generated by TopologySpecFactory if (this.isTopologySpecFactoryEnabled) { Collection<TopologySpec> topologySpecs = this.topologySpecFactory.getTopologies(); for (TopologySpec topologySpec : topologySpecs) { this.topologyCatalog.put(topologySpec); } } } @Override public void stop() throws ApplicationException { if (this.stopInProgress) { return; } LOGGER.info("Stopping the Gobblin Service Manager"); this.stopInProgress = true; try { this.serviceLauncher.stop(); } catch (ApplicationException ae) { LOGGER.error("Error while stopping Gobblin Service Manager", ae); } finally { disconnectHelixManager(); } } @VisibleForTesting void connectHelixManager() { try { if (this.helixManager.isPresent()) { this.helixManager.get().connect(); this.helixManager.get() .getMessagingService() .registerMessageHandlerFactory(Message.MessageType.USER_DEFINE_MSG.toString(), getUserDefinedMessageHandlerFactory()); } } catch (Exception e) { LOGGER.error("HelixManager failed to connect", e); throw Throwables.propagate(e); } } /** * Creates and returns a {@link MessageHandlerFactory} for handling of Helix * {@link org.apache.helix.model.Message.MessageType#USER_DEFINE_MSG}s. * * @returns a {@link MessageHandlerFactory}. */ protected MessageHandlerFactory getUserDefinedMessageHandlerFactory() { return new ControllerUserDefinedMessageHandlerFactory(this.flowCatalog, this.scheduler); } @VisibleForTesting void disconnectHelixManager() { if (isHelixManagerConnected()) { if (this.helixManager.isPresent()) { this.helixManager.get().disconnect(); } } } @VisibleForTesting boolean isHelixManagerConnected() { return this.helixManager.isPresent() && this.helixManager.get().isConnected(); } @Override public void close() throws IOException { this.serviceLauncher.close(); } /** * A custom {@link MessageHandlerFactory} for {@link ControllerUserDefinedMessageHandler}s that * handle messages of type {@link org.apache.helix.model.Message.MessageType#USER_DEFINE_MSG}. */ private static class ControllerUserDefinedMessageHandlerFactory implements MessageHandlerFactory { private FlowCatalog flowCatalog; private GobblinServiceJobScheduler jobScheduler; public ControllerUserDefinedMessageHandlerFactory(FlowCatalog flowCatalog, GobblinServiceJobScheduler jobScheduler) { this.flowCatalog = flowCatalog; this.jobScheduler = jobScheduler; } @Override public MessageHandler createHandler(Message message, NotificationContext context) { return new ControllerUserDefinedMessageHandler(flowCatalog, jobScheduler, message, context); } @Override public String getMessageType() { return Message.MessageType.USER_DEFINE_MSG.toString(); } public List<String> getMessageTypes() { return Collections.singletonList(getMessageType()); } @Override public void reset() { } /** * A custom {@link MessageHandler} for handling user-defined messages to the controller. */ private static class ControllerUserDefinedMessageHandler extends MessageHandler { private FlowCatalog flowCatalog; private GobblinServiceJobScheduler jobScheduler; public ControllerUserDefinedMessageHandler(FlowCatalog flowCatalog, GobblinServiceJobScheduler jobScheduler, Message message, NotificationContext context) { super(message, context); this.flowCatalog = flowCatalog; this.jobScheduler = jobScheduler; } @Override public HelixTaskResult handleMessage() throws InterruptedException { if (jobScheduler.isActive()) { String flowSpecUri = _message.getAttribute(Message.Attributes.INNER_MESSAGE); try { if (_message.getMsgSubType().equals(ServiceConfigKeys.HELIX_FLOWSPEC_ADD)) { Spec spec = flowCatalog.getSpec(new URI(flowSpecUri)); this.jobScheduler.onAddSpec(spec); } else if (_message.getMsgSubType().equals(ServiceConfigKeys.HELIX_FLOWSPEC_REMOVE)) { List<String> flowSpecUriParts = Splitter.on(":").omitEmptyStrings().trimResults().splitToList(flowSpecUri); this.jobScheduler.onDeleteSpec(new URI(flowSpecUriParts.get(0)), flowSpecUriParts.get(1)); } else if (_message.getMsgSubType().equals(ServiceConfigKeys.HELIX_FLOWSPEC_UPDATE)) { Spec spec = flowCatalog.getSpec(new URI(flowSpecUri)); this.jobScheduler.onUpdateSpec(spec); } } catch (SpecNotFoundException | URISyntaxException e) { LOGGER.error("Cannot process Helix message for flowSpecUri: " + flowSpecUri, e); } } HelixTaskResult helixTaskResult = new HelixTaskResult(); helixTaskResult.setSuccess(true); return helixTaskResult; } @Override public void onError(Exception e, ErrorCode code, ErrorType type) { LOGGER.error( String.format("Failed to handle message with exception %s, error code %s, error type %s", e, code, type)); } } } private static String getServiceId() { return "1"; } private static Options buildOptions() { Options options = new Options(); options.addOption("a", ServiceConfigKeys.SERVICE_NAME_OPTION_NAME, true, "Gobblin application name"); return options; } private static void printUsage(Options options) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp(GobblinServiceManager.class.getSimpleName(), options); } public static void main(String[] args) throws Exception { Options options = buildOptions(); try { CommandLine cmd = new DefaultParser().parse(options, args); if (!cmd.hasOption(ServiceConfigKeys.SERVICE_NAME_OPTION_NAME)) { printUsage(options); System.exit(1); } boolean isTestMode = false; if (cmd.hasOption("test_mode")) { isTestMode = Boolean.parseBoolean(cmd.getOptionValue("test_mode", "false")); } Config config = ConfigFactory.load(); try (GobblinServiceManager gobblinServiceManager = new GobblinServiceManager( cmd.getOptionValue(ServiceConfigKeys.SERVICE_NAME_OPTION_NAME), getServiceId(), config, Optional.<Path>absent())) { gobblinServiceManager.start(); if (isTestMode) { testGobblinService(gobblinServiceManager); } } } catch (ParseException pe) { printUsage(options); System.exit(1); } } // TODO: Remove after adding test cases @SuppressWarnings("DLS_DEAD_LOCAL_STORE") private static void testGobblinService(GobblinServiceManager gobblinServiceManager) { FlowConfigClient client = new FlowConfigClient(String.format("http://localhost:%s/", gobblinServiceManager.restliServer.getPort())); Map<String, String> flowProperties = Maps.newHashMap(); flowProperties.put("param1", "value1"); final String TEST_GROUP_NAME = "testGroup1"; final String TEST_FLOW_NAME = "testFlow1"; final String TEST_SCHEDULE = "0 1/0 * ? * *"; final String TEST_TEMPLATE_URI = "FS:///templates/test.template"; FlowConfig flowConfig = new FlowConfig().setId(new FlowId().setFlowGroup(TEST_GROUP_NAME).setFlowName(TEST_FLOW_NAME)) .setTemplateUris(TEST_TEMPLATE_URI).setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE). setRunImmediately(true)) .setProperties(new StringMap(flowProperties)); try { client.createFlowConfig(flowConfig); } catch (RemoteInvocationException e) { throw new RuntimeException(e); } } }