/* * Copyright 2014 Google Inc. * * 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.google.gwt.dev.codeserver; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.dev.cfg.ModuleDef; import com.google.gwt.dev.codeserver.JobEvent.CompileStrategy; import com.google.gwt.dev.codeserver.JobEvent.Status; import com.google.gwt.dev.util.log.AbstractTreeLogger; import com.google.gwt.thirdparty.guava.common.base.Preconditions; import com.google.gwt.thirdparty.guava.common.collect.ImmutableList; import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap; import com.google.gwt.thirdparty.guava.common.collect.ImmutableSortedMap; import com.google.gwt.thirdparty.guava.common.util.concurrent.Futures; import com.google.gwt.thirdparty.guava.common.util.concurrent.ListenableFuture; import com.google.gwt.thirdparty.guava.common.util.concurrent.SettableFuture; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; /** * A request for Super Dev Mode to compile something. * * <p>Each job has a lifecycle where it goes through up to four states. See * {@link JobEvent.Status}. * * <p>Jobs are thread-safe. */ class Job { private static final ConcurrentMap<String, AtomicInteger> prefixToNextId = new ConcurrentHashMap<String, AtomicInteger>(); // Primary key private final String id; // Input private final String inputModuleName; private final ImmutableSortedMap<String, String> bindingProperties; // Output private final SettableFuture<Result> result = SettableFuture.create(); // Listeners private final Outbox outbox; private final RecompileListener recompileListener; private final JobChangeListener jobChangeListener; private final LogSupplier logSupplier; private JobEventTable table; // non-null when submitted // Miscellaneous private final ImmutableList<String> args; private final Set<String> tags; /** * The id to report to the recompile listener. */ private int compileId = -1; // non-negative after the compile has started private CompileDir compileDir; // non-null after the compile has started private CompileStrategy compileStrategy; // non-null after the compile has started private String outputModuleName; // non-null after successful compile private Exception listenerFailure; /** * Creates a job to update an outbox. * @param bindingProperties Properties that uniquely identify a permutation. * (Otherwise, more than one permutation will be compiled.) * @param parentLogger The parent of the logger that will be used for this job. */ Job(Outbox box, Map<String, String> bindingProperties, TreeLogger parentLogger, Options options) { this.id = chooseNextId(box); this.outbox = box; this.inputModuleName = box.getInputModuleName(); // TODO: we will use the binding properties to find or create the outbox, // then take binding properties from the outbox here. this.bindingProperties = ImmutableSortedMap.copyOf(bindingProperties); this.recompileListener = Preconditions.checkNotNull(options.getRecompileListener()); this.jobChangeListener = Preconditions.checkNotNull(options.getJobChangeListener()); this.args = Preconditions.checkNotNull(options.getArgs()); this.tags = Preconditions.checkNotNull(options.getTags()); this.logSupplier = new LogSupplier(parentLogger, id); } static boolean isValidJobId(String id) { return ModuleDef.isValidModuleName(id); } private static String chooseNextId(Outbox box) { String prefix = box.getId(); prefixToNextId.putIfAbsent(prefix, new AtomicInteger(0)); return prefix + "_" + prefixToNextId.get(prefix).getAndIncrement(); } /** * A string uniquely identifying this job (within this process). * * <p>Note that the number doesn't have any particular relationship * with the output directory's name since jobs can be submitted out of order. */ String getId() { return id; } /** * The module name that will be sent to the compiler. */ String getInputModuleName() { return inputModuleName; } /** * The binding properties to use for this recompile. */ ImmutableSortedMap<String, String> getBindingProperties() { return bindingProperties; } /** * The outbox that will serve the job's result (if successful). */ Outbox getOutbox() { return outbox; } /** * Returns the logger for this job. (Creates it on first use.) */ TreeLogger getLogger() { return logSupplier.get(); } /** * Blocks until we have the result of this recompile. */ Result waitForResult() { return Futures.getUnchecked(getFutureResult()); } /** * Returns a Future that will contain the result of this recompile. */ ListenableFuture<Result> getFutureResult() { return result; } Exception getListenerFailure() { return listenerFailure; } // === state transitions === /** * Returns true if this job has been submitted to the JobRunner. * (That is, if {@link #onSubmitted} has ever been called.) */ synchronized boolean wasSubmitted() { return table != null; } boolean isDone() { return result.isDone(); } /** * Reports that this job has been submitted to the JobRunner. * Starts sending updates to the JobTable. * @throws IllegalStateException if the job was already started. */ synchronized void onSubmitted(JobEventTable table) { if (wasSubmitted()) { throw new IllegalStateException("compile job has already started: " + id); } this.table = table; table.publish(makeEvent(Status.WAITING), getLogger()); } /** * Reports that we started to compile the job. */ synchronized void onStarted(int compileId, CompileDir compileDir) { if (table == null || !table.isActive(this)) { throw new IllegalStateException("compile job is not active: " + id); } this.compileId = compileId; this.compileDir = compileDir; try { recompileListener.startedCompile(inputModuleName, compileId, compileDir); } catch (Exception e) { getLogger().log(TreeLogger.Type.WARN, "recompile listener threw exception", e); listenerFailure = e; } publish(makeEvent(Status.COMPILING)); } /** * Reports that this job has made progress while compiling. * @throws IllegalStateException if the job is not running. */ synchronized void onProgress(String stepMessage) { checkIsCompiling("onProgress"); publish(makeEvent(Status.COMPILING, stepMessage)); } synchronized void setCompileStrategy(CompileStrategy strategy) { checkIsCompiling("setCompileStrategy"); if (compileStrategy != null) { throw new IllegalStateException("setCompileStrategy can only be set once per job"); } this.compileStrategy = strategy; // Not bothering to send an event just for this change, so it will be included // in the next event. } /** * Reports that this job has finished. * @throws IllegalStateException if the job is not running. */ synchronized void onFinished(Result newResult) { if (table == null || !table.isActive(this)) { throw new IllegalStateException("compile job is not active: " + id); } // Report that we finished unless the listener messed up already. if (listenerFailure == null) { try { recompileListener.finishedCompile(inputModuleName, compileId, newResult.isOk()); } catch (Exception e) { getLogger().log(TreeLogger.Type.WARN, "recompile listener threw exception", e); listenerFailure = e; } } result.set(newResult); outputModuleName = newResult.outputModuleName; if (newResult.isOk()) { publish(makeEvent(Status.SERVING)); } else { publish(makeEvent(Status.ERROR)); } } /** * Reports that this job's output is no longer available. */ synchronized void onGone() { if (table == null || !table.isActive(this)) { throw new IllegalStateException("compile job is not active: " + id); } publish(makeEvent(Status.GONE)); } private JobEvent makeEvent(Status status) { return makeEvent(status, null); } private JobEvent makeEvent(Status status, String message) { JobEvent.Builder out = new JobEvent.Builder(); out.setJobId(getId()); out.setInputModuleName(getInputModuleName()); out.setBindings(getBindingProperties()); out.setStatus(status); out.setMessage(message); out.setOutputModuleName(outputModuleName); out.setCompileDir(compileDir); out.setCompileStrategy(compileStrategy); out.setArguments(args); out.setTags(tags); out.setMetricMap(getMetricMapSnapshot()); return out.build(); } private Map<String, Long> getMetricMapSnapshot() { TreeLogger logger = getLogger(); if (logger instanceof AbstractTreeLogger) { return ((AbstractTreeLogger)logger).getMetricMap().getSnapshot(); } return ImmutableMap.of(); // not found } /** * Makes an event visible externally. */ private void publish(JobEvent event) { if (listenerFailure == null) { try { jobChangeListener.onJobChange(event); } catch (Exception e) { getLogger().log(Type.WARN, "JobChangeListener threw exception", e); listenerFailure = e; } } table.publish(event, getLogger()); } private void checkIsCompiling(String methodName) { if (table == null || table.getPublishedEvent(this).getStatus() != Status.COMPILING) { throw new IllegalStateException(methodName + " called for a job that isn't compiling: " + id); } } /** * Creates a child logger on first use. */ static class LogSupplier { private final TreeLogger parent; private final String jobId; private TreeLogger child; LogSupplier(TreeLogger parent, String jobId) { this.parent = parent; this.jobId = jobId; } synchronized TreeLogger get() { if (child == null) { child = parent.branch(Type.INFO, "Job " + jobId); if (child instanceof AbstractTreeLogger) { ((AbstractTreeLogger)child).resetMetricMap(); } } return child; } } /** * The result of a recompile. */ static class Result { /** * non-null if successful */ final CompileDir outputDir; /** * non-null if successful. */ final String outputModuleName; /** * non-null for an error */ final Throwable error; Result(CompileDir outputDir, String outputModuleName, Throwable error) { assert (outputDir == null) != (error == null); this.outputDir = outputDir; this.outputModuleName = outputModuleName; this.error = error; } boolean isOk() { return error == null; } } }