blob: 8272d351d6955283e7b2e242d64abd7265487579 [file] [log] [blame]
/*
* 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;
}
}
}