/* * 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.job_exec; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; 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.Preconditions; import com.google.common.io.Closer; import com.google.common.util.concurrent.ExecutionList; import com.typesafe.config.ConfigFactory; import gobblin.broker.gobblin_scopes.GobblinScopeTypes; import gobblin.broker.SimpleScope; import gobblin.broker.SharedResourcesBrokerFactory; import gobblin.broker.SharedResourcesBrokerImpl; import gobblin.broker.iface.SharedResourcesBroker; import gobblin.configuration.ConfigurationKeys; import gobblin.instrumented.Instrumented; import gobblin.metrics.GobblinMetrics; import gobblin.metrics.MetricContext; import gobblin.metrics.Tag; import gobblin.runtime.JobContext; import gobblin.runtime.JobException; import gobblin.runtime.JobLauncher; import gobblin.runtime.JobLauncherFactory; import gobblin.runtime.JobLauncherFactory.JobLauncherType; import gobblin.runtime.JobState.RunningState; import gobblin.runtime.api.Configurable; import gobblin.runtime.api.GobblinInstanceEnvironment; import gobblin.runtime.api.JobExecution; import gobblin.runtime.api.JobExecutionDriver; import gobblin.runtime.api.JobExecutionLauncher; import gobblin.runtime.api.JobExecutionResult; import gobblin.runtime.api.JobExecutionState; import gobblin.runtime.api.JobExecutionStateListener; import gobblin.runtime.api.JobExecutionStatus; import gobblin.runtime.api.JobSpec; import gobblin.runtime.api.JobTemplate; import gobblin.runtime.api.SpecNotFoundException; import gobblin.runtime.instance.StandardGobblinInstanceLauncher; import gobblin.runtime.job_spec.ResolvedJobSpec; import gobblin.runtime.listeners.AbstractJobListener; import gobblin.runtime.std.DefaultConfigurableImpl; import gobblin.runtime.std.JobExecutionStateListeners; import gobblin.runtime.std.JobExecutionUpdatable; import gobblin.util.ExecutorsUtils; import lombok.AllArgsConstructor; import lombok.Getter; /** * An implementation of JobExecutionDriver which acts as an adapter to the legacy * {@link JobLauncher} API. */ public class JobLauncherExecutionDriver extends FutureTask<JobExecutionResult> implements JobExecutionDriver { private final Logger _log; private final JobSpec _jobSpec; private final JobExecutionUpdatable _jobExec; private final JobExecutionState _jobState; private final JobExecutionStateListeners _callbackDispatcher; private final ExecutionList _executionList; private final DriverRunnable _runnable; private final Closer _closer; private JobContext _jobContext; /** * Creates a new JobExecutionDriver which acts as an adapter to the legacy {@link JobLauncher} API. * @param sysConfig the system/environment config * @param jobSpec the JobSpec to be executed * @param jobLauncherType an optional jobLauncher type; the value follows the convention of * {@link JobLauncherFactory#newJobLauncher(java.util.Properties, java.util.Properties, String). * If absent, {@link JobLauncherFactory#newJobLauncher(java.util.Properties, java.util.Properties)} * will be used which looks for the {@link ConfigurationKeys#JOB_LAUNCHER_TYPE_KEY} * in the system configuration. * @param jobExecStateListener an optional listener to listen for state changes in the execution. * @param log an optional logger to be used; if none is specified, a default one * will be instantiated. */ public static JobLauncherExecutionDriver create(Configurable sysConfig, JobSpec jobSpec, Optional<JobLauncherFactory.JobLauncherType> jobLauncherType, Optional<Logger> log, boolean instrumentationEnabled, JobExecutionLauncher.StandardMetrics launcherMetrics, SharedResourcesBroker<GobblinScopeTypes> instanceBroker) { Logger actualLog = log.isPresent() ? log.get() : LoggerFactory.getLogger(JobLauncherExecutionDriver.class); JobExecutionStateListeners callbackDispatcher = new JobExecutionStateListeners(actualLog); JobExecutionUpdatable jobExec = JobExecutionUpdatable.createFromJobSpec(jobSpec); JobExecutionState jobState = new JobExecutionState(jobSpec, jobExec, Optional.<JobExecutionStateListener>of(callbackDispatcher)); JobLauncher jobLauncher = createLauncher(sysConfig, jobSpec, actualLog, jobLauncherType.isPresent() ? Optional.of(jobLauncherType.get().toString()) : Optional.<String>absent(), instanceBroker); JobListenerToJobStateBridge bridge = new JobListenerToJobStateBridge(actualLog, jobState, instrumentationEnabled, launcherMetrics); DriverRunnable runnable = new DriverRunnable(jobLauncher, bridge, jobState, callbackDispatcher, jobExec); return new JobLauncherExecutionDriver(jobSpec, actualLog, runnable); } protected JobLauncherExecutionDriver(JobSpec jobSpec, Logger log, DriverRunnable runnable) { super(runnable); _closer = Closer.create(); _closer.register(runnable.getJobLauncher()); _log = log; _jobSpec = jobSpec; _jobExec = runnable.getJobExec(); _callbackDispatcher = _closer.register(runnable.getCallbackDispatcher()); _jobState = runnable.getJobState(); _executionList = new ExecutionList(); _runnable = runnable; } /** * A runnable that actually executes the job. */ @AllArgsConstructor @Getter private static class DriverRunnable implements Callable<JobExecutionResult> { private final JobLauncher jobLauncher; private final JobListenerToJobStateBridge bridge; private final JobExecutionState jobState; private final JobExecutionStateListeners callbackDispatcher; private final JobExecutionUpdatable jobExec; @Override public JobExecutionResult call() throws JobException, InterruptedException, TimeoutException { jobLauncher.launchJob(bridge); jobState.awaitForDone(Long.MAX_VALUE); return JobExecutionResult.createFromState(jobState); } } private static JobLauncher createLauncher(Configurable _sysConfig, JobSpec _jobSpec, Logger _log, Optional<String> jobLauncherType, SharedResourcesBroker<GobblinScopeTypes> instanceBroker) { if (jobLauncherType.isPresent()) { return JobLauncherFactory.newJobLauncher(_sysConfig.getConfigAsProperties(), _jobSpec.getConfigAsProperties(), jobLauncherType.get(), instanceBroker); } else { _log.info("Creating auto jobLauncher for " + _jobSpec); try { return JobLauncherFactory.newJobLauncher(_sysConfig.getConfigAsProperties(), _jobSpec.getConfigAsProperties(), instanceBroker); } catch (Exception e) { throw new RuntimeException("JobLauncher creation failed: " + e, e); } } } @Override public JobExecution getJobExecution() { return _jobExec; } @Override public JobExecutionStatus getJobExecutionStatus() { return _jobState; } protected void startAsync() throws JobException { _log.info("Starting " + getClass().getSimpleName()); ExecutorsUtils.newThreadFactory(Optional.of(_log), Optional.of("job-launcher-execution-driver")).newThread(this).start(); } @Override protected void done() { _executionList.execute(); try { shutDown(); } catch (IOException ioe) { _log.error("Failed to close job launcher."); } } private void shutDown() throws IOException { _log.info("Shutting down " + getClass().getSimpleName()); if (null != _jobContext) { switch (_jobContext.getJobState().getState()) { case PENDING: case SUCCESSFUL: case RUNNING: { // We have to pass another listener instance as launcher does not store the listener used // in launchJob() cancel(false); break; } case FAILED: case COMMITTED: case CANCELLED: { // Nothing to do break; } } } _closer.close(); } @Override public void addListener(Runnable listener, Executor executor) { _executionList.add(listener, executor); } static class JobListenerToJobStateBridge extends AbstractJobListener { private final JobExecutionState _jobState; private final boolean _instrumentationEnabled; private final JobExecutionLauncher.StandardMetrics _launcherMetrics; private JobContext _jobContext; public JobListenerToJobStateBridge(Logger log, JobExecutionState jobState, boolean instrumentationEnabled, JobExecutionLauncher.StandardMetrics launcherMetrics) { super(Optional.of(log)); _jobState = jobState; _instrumentationEnabled = instrumentationEnabled; _launcherMetrics = launcherMetrics; } @Override public void onJobPrepare(JobContext jobContext) throws Exception { super.onJobPrepare(jobContext); _jobContext = jobContext; if (_jobState.getRunningState() == null) { _jobState.switchToPending(); } _jobState.switchToRunning(); if (_instrumentationEnabled && null != _launcherMetrics) { _launcherMetrics.getNumJobsLaunched().inc(); } } @Override public void onJobStart(JobContext jobContext) throws Exception { super.onJobStart(jobContext); } @Override public void onJobCompletion(JobContext jobContext) throws Exception { Preconditions.checkArgument(jobContext.getJobState().getState() == RunningState.SUCCESSFUL || jobContext.getJobState().getState() == RunningState.COMMITTED || jobContext.getJobState().getState() == RunningState.FAILED, "Unexpected state: " + jobContext.getJobState().getState() + " in " + jobContext); super.onJobCompletion(jobContext); if (_instrumentationEnabled && null != _launcherMetrics) { _launcherMetrics.getNumJobsCompleted().inc(); } if (jobContext.getJobState().getState() == RunningState.FAILED) { if (_instrumentationEnabled && null != _launcherMetrics) { _launcherMetrics.getNumJobsFailed().inc(); } _jobState.switchToFailed(); } else { // TODO Remove next line once the JobLauncher starts sending notifications for success _jobState.switchToSuccessful(); _jobState.switchToCommitted(); if (_instrumentationEnabled && null != _launcherMetrics) { _launcherMetrics.getNumJobsCommitted().inc(); } } } @Override public void onJobCancellation(JobContext jobContext) throws Exception { super.onJobCancellation(jobContext); _jobState.switchToCancelled(); if (_instrumentationEnabled && null != _launcherMetrics) { _launcherMetrics.getNumJobsCancelled().inc(); } } } @VisibleForTesting JobLauncher getLegacyLauncher() { return _runnable.getJobLauncher(); } /** {@inheritDoc} */ @Override public void registerStateListener(JobExecutionStateListener listener) { _callbackDispatcher.registerStateListener(listener); } /** {@inheritDoc} */ @Override public void unregisterStateListener(JobExecutionStateListener listener) { _callbackDispatcher.unregisterStateListener(listener); } /** {@inheritDoc} */ @Override public JobExecutionState getJobExecutionState() { return _jobState; } /** * Creates a new instance of {@link JobLauncherExecutionDriver}. * * <p>Conventions * <ul> * <li>If no jobLauncherType is specified, one will be determined by the JobSpec * (see {@link JobLauncherFactory). * <li> Convention for sysConfig: use the sysConfig of the gobblinInstance if specified, * otherwise use empty config. * <li> Convention for log: use gobblinInstance logger plus "." + jobSpec if specified, otherwise * use JobExecutionDriver class name plus "." + jobSpec * </ul> */ public static class Launcher implements JobExecutionLauncher, GobblinInstanceEnvironment { private Optional<JobLauncherType> _jobLauncherType = Optional.absent(); private Optional<Configurable> _sysConfig = Optional.absent(); private Optional<GobblinInstanceEnvironment> _gobblinEnv = Optional.absent(); private Optional<Logger> _log = Optional.absent(); private Optional<MetricContext> _metricContext = Optional.absent(); private Optional<Boolean> _instrumentationEnabled = Optional.absent(); private JobExecutionLauncher.StandardMetrics _metrics; private Optional<SharedResourcesBroker<GobblinScopeTypes>> _instanceBroker = Optional.absent(); public Launcher() { } /** Leave unchanged for */ public Launcher withJobLauncherType(JobLauncherType jobLauncherType) { Preconditions.checkNotNull(jobLauncherType); _jobLauncherType = Optional.of(jobLauncherType); return this; } public Optional<JobLauncherType> getJobLauncherType() { return _jobLauncherType; } /** System-wide settings */ public Configurable getDefaultSysConfig() { return _gobblinEnv.isPresent() ? _gobblinEnv.get().getSysConfig() : DefaultConfigurableImpl.createFromConfig(ConfigFactory.empty()); } @Override public Configurable getSysConfig() { if (!_sysConfig.isPresent()) { _sysConfig = Optional.of(getDefaultSysConfig()); } return _sysConfig.get(); } public Launcher withSysConfig(Configurable sysConfig) { _sysConfig = Optional.of(sysConfig); return this; } /** Parent Gobblin instance */ public Launcher withGobblinInstanceEnvironment(GobblinInstanceEnvironment gobblinInstance) { _gobblinEnv = Optional.of(gobblinInstance); return this; } public Optional<GobblinInstanceEnvironment> getGobblinInstanceEnvironment() { return _gobblinEnv; } public Logger getLog(JobSpec jobSpec) { return getJobLogger(getLog(), jobSpec); } public Launcher withInstrumentationEnabled(boolean enabled) { _instrumentationEnabled = Optional.of(enabled); return this; } public boolean getDefaultInstrumentationEnabled() { return _gobblinEnv.isPresent() ? _gobblinEnv.get().isInstrumentationEnabled() : GobblinMetrics.isEnabled(getSysConfig().getConfig()); } @Override public boolean isInstrumentationEnabled() { if (!_instrumentationEnabled.isPresent()) { _instrumentationEnabled = Optional.of(getDefaultInstrumentationEnabled()); } return _instrumentationEnabled.get(); } private static Logger getJobLogger(Logger parentLog, JobSpec jobSpec) { return LoggerFactory.getLogger(parentLog.getName() + "." + jobSpec.toShortString()); } public Launcher withMetricContext(MetricContext instanceMetricContext) { _metricContext = Optional.of(instanceMetricContext); return this; } @Override public MetricContext getMetricContext() { if (!_metricContext.isPresent()) { _metricContext = Optional.of(getDefaultMetricContext()); } return _metricContext.get(); } public MetricContext getDefaultMetricContext() { if (_gobblinEnv.isPresent()) { return _gobblinEnv.get().getMetricContext() .childBuilder(JobExecutionLauncher.class.getSimpleName()).build(); } gobblin.configuration.State fakeState = new gobblin.configuration.State(getSysConfig().getConfigAsProperties()); List<Tag<?>> tags = new ArrayList<>(); MetricContext res = Instrumented.getMetricContext(fakeState, Launcher.class, tags); return res; } @Override public JobExecutionDriver launchJob(JobSpec jobSpec) { Preconditions.checkNotNull(jobSpec); if (!(jobSpec instanceof ResolvedJobSpec)) { try { jobSpec = new ResolvedJobSpec(jobSpec); } catch (JobTemplate.TemplateException | SpecNotFoundException exc) { throw new RuntimeException("Can't launch job " + jobSpec.getUri(), exc); } } return JobLauncherExecutionDriver.create(getSysConfig(), jobSpec, _jobLauncherType, Optional.of(getLog(jobSpec)), isInstrumentationEnabled(), getMetrics(), getInstanceBroker()); } @Override public List<Tag<?>> generateTags(gobblin.configuration.State state) { return Collections.emptyList(); } @Override public void switchMetricContext(List<Tag<?>> tags) { throw new UnsupportedOperationException(); } @Override public void switchMetricContext(MetricContext context) { throw new UnsupportedOperationException(); } @Override public String getInstanceName() { return _gobblinEnv.isPresent() ? _gobblinEnv.get().getInstanceName() : getClass().getName(); } public Logger getDefaultLog() { return _gobblinEnv.isPresent() ? _gobblinEnv.get().getLog() : LoggerFactory.getLogger(getClass()); } @Override public Logger getLog() { if (! _log.isPresent()) { _log = Optional.of(getDefaultLog()); } return _log.get(); } public Launcher withLog(Logger log) { _log = Optional.of(log); return this; } @Override public StandardMetrics getMetrics() { if (_metrics == null) { _metrics = new JobExecutionLauncher.StandardMetrics(this); } return _metrics; } public Launcher withInstanceBroker(SharedResourcesBroker<GobblinScopeTypes> broker) { _instanceBroker = Optional.of(broker); return this; } public SharedResourcesBroker<GobblinScopeTypes> getInstanceBroker() { if (!_instanceBroker.isPresent()) { if (_gobblinEnv.isPresent()) { _instanceBroker = Optional.of(_gobblinEnv.get().getInstanceBroker()); } else { _instanceBroker = Optional.of(getDefaultInstanceBroker()); } } return _instanceBroker.get(); } public SharedResourcesBroker<GobblinScopeTypes> getDefaultInstanceBroker() { getLog().warn("Creating a default instance broker for job launcher. Objects may not be shared across all jobs in this instance."); SharedResourcesBrokerImpl<GobblinScopeTypes> globalBroker = SharedResourcesBrokerFactory.createDefaultTopLevelBroker(getSysConfig().getConfig(), GobblinScopeTypes.GLOBAL.defaultScopeInstance()); return globalBroker.newSubscopedBuilder(new SimpleScope<>(GobblinScopeTypes.INSTANCE, getInstanceName())).build(); } } @Override public void registerWeakStateListener(JobExecutionStateListener listener) { _callbackDispatcher.registerWeakStateListener(listener); } @Override public boolean isDone() { RunningState runState = getJobExecutionStatus().getRunningState(); return runState == null ? false : runState.isDone() ; } @Override public boolean cancel(boolean mayInterruptIfRunning) { // FIXME there is a race condition here as the job may complete successfully before we // call cancelJob() below. There isn't an easy way to fix that right now. RunningState runState = getJobExecutionStatus().getRunningState(); if (runState.isCancelled()) { return true; } else if (runState.isDone()) { return false; } try { // No special processing of callbacks necessary getLegacyLauncher().cancelJob(new AbstractJobListener(){}); } catch (JobException e) { throw new RuntimeException("Unable to cancel job " + _jobSpec + ": " + e, e); } return super.cancel(mayInterruptIfRunning); } @Override public boolean isCancelled() { return getJobExecutionStatus().getRunningState().isCancelled(); } @Override public JobExecutionResult get() throws InterruptedException { try { return super.get(); } catch (ExecutionException ee) { return JobExecutionResult.createFailureResult(ee.getCause()); } } @Override public JobExecutionResult get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { try { return super.get(timeout, unit); } catch (ExecutionException ee) { return JobExecutionResult.createFailureResult(ee.getCause()); } } }