/** * Copyright 2011-2017 Asakusa Framework Team. * * 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 com.asakusafw.yaess.basic; import java.io.IOException; import java.text.MessageFormat; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.asakusafw.yaess.basic.JobExecutor.Executing; import com.asakusafw.yaess.core.ExecutionContext; import com.asakusafw.yaess.core.Job; import com.asakusafw.yaess.core.JobScheduler; import com.asakusafw.yaess.core.PhaseMonitor; import com.asakusafw.yaess.core.PhaseMonitor.JobStatus; import com.asakusafw.yaess.core.YaessLogger; /** * An abstract implementation of {@link JobScheduler}. * @since 0.2.3 */ public abstract class AbstractJobScheduler extends JobScheduler { static final YaessLogger YSLOG = new YaessBasicLogger(AbstractJobScheduler.class); static final Logger LOG = LoggerFactory.getLogger(AbstractJobScheduler.class); /** * Returns a {@link JobExecutor} for this scheduler. * @return the {@link JobExecutor} */ protected abstract JobExecutor getJobExecutor(); @Override public final void execute( PhaseMonitor monitor, ExecutionContext context, List<? extends Job> jobs, ErrorHandler errorHandler) throws InterruptedException, IOException { if (monitor == null) { throw new IllegalArgumentException("monitor must not be null"); //$NON-NLS-1$ } if (context == null) { throw new IllegalArgumentException("context must not be null"); //$NON-NLS-1$ } if (jobs == null) { throw new IllegalArgumentException("jobs must not be null"); //$NON-NLS-1$ } if (errorHandler == null) { throw new IllegalArgumentException("errorHandler must not be null"); //$NON-NLS-1$ } monitor.open(jobs.size()); try { monitor.checkCancelled(); Engine engine = new Engine(getJobExecutor(), monitor, context, errorHandler, jobs); engine.run(); } finally { monitor.close(); } } private static final class Engine { private final JobExecutor executor; final PhaseMonitor monitor; final ExecutionContext context; private final ErrorHandler handler; private final LinkedList<Job> waiting; private final Map<String, Executing> executing; private final BlockingQueue<Executing> doneQueue; private final Set<String> blockers; private boolean sawError; Engine( JobExecutor executor, PhaseMonitor monitor, ExecutionContext context, ErrorHandler handler, List<? extends Job> waiting) { assert executor != null; assert monitor != null; assert context != null; assert handler != null; assert waiting != null; this.executor = executor; this.monitor = monitor; this.context = context; this.handler = handler; this.waiting = new LinkedList<>(waiting); this.executing = new HashMap<>(); this.doneQueue = new LinkedBlockingQueue<>(); this.blockers = new TreeSet<>(); for (Job job : waiting) { blockers.add(job.getId()); } this.sawError = false; } void run() throws IOException, InterruptedException { while (waiting.isEmpty() == false) { boolean submitted = submitAllWaiting(); if (submitted == false && executing.isEmpty()) { assert waiting.isEmpty() == false; if (sawError) { waiting.clear(); } else { throw new IOException(MessageFormat.format( "Jobnet execution was deadlocked: {4} (batch={0}, flow={1}, phase={2}, execution={3})", context.getBatchId(), context.getFlowId(), context.getPhase(), context.getExecutionId(), blockers)); } } else { waitForDone(); } } while (executing.isEmpty() == false) { waitForDone(); } if (sawError) { throw new IOException(MessageFormat.format( "Jobnet was failed by preceding errors (batch={0}, flow={1}, phase={2}, execution={3})", context.getBatchId(), context.getFlowId(), context.getPhase(), context.getExecutionId())); } } private boolean submitAllWaiting() throws IOException, InterruptedException { boolean sawSubmit = false; for (Iterator<Job> iter = waiting.iterator(); iter.hasNext();) { Job next = iter.next(); LOG.debug("Attemps to submit job: {}", next.getId()); if (isBlocked(next)) { LOG.debug("Job is currently blocked: {}", next.getId()); continue; } iter.remove(); if (submit(next)) { sawSubmit = true; } } return sawSubmit; } private void waitForDone() throws InterruptedException, IOException { assert executing.isEmpty() == false; monitor.checkCancelled(); Executing done = doneQueue.take(); assert done.isDone(); handleDone(done); while (true) { monitor.checkCancelled(); Executing rest = doneQueue.poll(); if (rest == null) { break; } assert rest.isDone(); handleDone(rest); } } private void handleDone(Executing done) throws InterruptedException, IOException { assert done != null; assert done.isDone(); Executing removed = executing.remove(done.getJob().getId()); assert removed != null; try { done.get(); done(done.getJob()); monitor.reportJobStatus(done.getJob().getId(), JobStatus.SUCCESS, null); } catch (CancellationException e) { sawError = true; monitor.reportJobStatus(done.getJob().getId(), JobStatus.CANCELLED, e); } catch (ExecutionException e) { sawError = true; Throwable cause = e.getCause(); if (cause instanceof InterruptedException) { monitor.reportJobStatus(done.getJob().getId(), JobStatus.CANCELLED, cause); cancelExecution(); throw (InterruptedException) cause; } else if (cause instanceof IOException) { monitor.reportJobStatus(done.getJob().getId(), JobStatus.FAILED, cause); handleException(done.getJob(), (IOException) cause); } else if (cause instanceof Error) { monitor.reportJobStatus(done.getJob().getId(), JobStatus.FAILED, cause); cancelExecution(); throw (Error) cause; } else if (cause instanceof RuntimeException) { monitor.reportJobStatus(done.getJob().getId(), JobStatus.FAILED, cause); cancelExecution(); throw (RuntimeException) cause; } else { monitor.reportJobStatus(done.getJob().getId(), JobStatus.FAILED, cause); cancelExecution(); throw new AssertionError(cause); } } } private boolean submit(Job job) throws InterruptedException, IOException { assert job != null; monitor.checkCancelled(); try { Executing execution = executor.submit( monitor.createJobMonitor(job.getId(), 1), context, job, doneQueue); executing.put(execution.getJob().getId(), execution); return true; } catch (IOException e) { sawError = true; handleException(job, e); return false; } } private void handleException(Job job, IOException exception) throws IOException { assert job != null; assert exception != null; if (handler.handle(context, exception) == false) { cancelExecution(); throw exception; } } private void cancelExecution() { for (Executing exec : executing.values()) { exec.cancel(true); } } private boolean isBlocked(Job job) { assert job != null; for (String blocker : job.getBlockerIds()) { if (blockers.contains(blocker)) { return true; } } return false; } private void done(Job job) { assert job != null; blockers.remove(job.getId()); } } }