IDEA-CR-8423 followup: wait for two futures: "future, buildFuture" instead of just "future"

This commit is contained in:
Alexey Kudravtsev
2016-02-16 14:52:55 +03:00
parent 7b2214e2fa
commit d88a96aab8
2 changed files with 277 additions and 169 deletions
@@ -118,6 +118,7 @@ import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import static org.jetbrains.jps.api.CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage.TargetTypeBuildScope;
@@ -672,13 +673,12 @@ public class BuildManager implements Disposable {
return null;
}
final DelegateFuture<BuilderMessageHandler> _future = new DelegateFuture<>();
final DelegateFuture _future = new DelegateFuture();
// by using the same queue that processes events we ensure that
// the build will be aware of all events that have happened before this request
runCommand(new Runnable() {
@Override
public void run() {
final Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler> preloaded = takePreloadedProcess(projectPath);
final RequestFuture<PreloadedProcessMessageHandler> preloadedFuture = preloaded != null? preloaded.first : null;
final boolean usingPreloadedProcess = preloadedFuture != null;
@@ -693,160 +693,168 @@ public class BuildManager implements Disposable {
sessionId = UUID.randomUUID();
}
final RequestFuture<? extends BuilderMessageHandler> future = usingPreloadedProcess? preloadedFuture : new RequestFuture<>(handler, sessionId, new CancelBuildSessionAction<BuilderMessageHandler>());
_future.setDelegate(future);
final RequestFuture<? extends BuilderMessageHandler> future = usingPreloadedProcess? preloadedFuture : new RequestFuture<BuilderMessageHandler>(handler, sessionId, new CancelBuildSessionAction<BuilderMessageHandler>());
// futures we need to wait for: either just "future" or both "future" and "buildFuture" below
TaskFuture[] delegatesToWait = {future};
if (!usingPreloadedProcess && (future.isCancelled() || project.isDisposed())) {
// in case of preloaded process the process was already running, so the handler will be notified upon process termination
handler.sessionTerminated(sessionId);
((BasicFuture)future).setDone();
return;
}
final CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings globals =
CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings.newBuilder().setGlobalOptionsPath(PathManager.getOptionsPath()).build();
CmdlineRemoteProto.Message.ControllerMessage.FSEvent currentFSChanges;
final SequentialTaskExecutor projectTaskQueue;
final boolean needRescan;
synchronized (myProjectDataMap) {
final ProjectData data = getProjectData(projectPath);
if (isRebuild) {
data.dropChanges();
}
if (IS_UNIT_TEST_MODE) {
LOG.info("Scheduling build for " +
projectPath +
"; CHANGED: " +
new HashSet<String>(convertToStringPaths(data.myChanged)) +
"; DELETED: " +
new HashSet<String>(convertToStringPaths(data.myDeleted)));
}
needRescan = data.getAndResetRescanFlag();
currentFSChanges = needRescan ? null : data.createNextEvent();
projectTaskQueue = data.taskQueue;
}
final CmdlineRemoteProto.Message.ControllerMessage params;
if (isRebuild) {
params = CmdlineProtoUtil.createBuildRequest(projectPath, scopes, Collections.<String>emptyList(), userData, globals, null);
}
else if (onlyCheckUpToDate) {
params = CmdlineProtoUtil.createUpToDateCheckRequest(projectPath, scopes, paths, userData, globals, currentFSChanges);
}
else {
params = CmdlineProtoUtil.createBuildRequest(projectPath, scopes, isMake ? Collections.<String>emptyList() : paths, userData, globals, currentFSChanges);
}
if (!usingPreloadedProcess) {
myMessageDispatcher.registerBuildMessageHandler(future, params);
}
final CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings globals =
CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings.newBuilder().setGlobalOptionsPath(PathManager.getOptionsPath())
.build();
CmdlineRemoteProto.Message.ControllerMessage.FSEvent currentFSChanges;
final SequentialTaskExecutor projectTaskQueue;
final boolean needRescan;
synchronized (myProjectDataMap) {
final ProjectData data = getProjectData(projectPath);
if (isRebuild) {
data.dropChanges();
}
if (IS_UNIT_TEST_MODE) {
LOG.info("Scheduling build for " +
projectPath +
"; CHANGED: " +
new HashSet<String>(convertToStringPaths(data.myChanged)) +
"; DELETED: " +
new HashSet<String>(convertToStringPaths(data.myDeleted)));
}
needRescan = data.getAndResetRescanFlag();
currentFSChanges = needRescan ? null : data.createNextEvent();
projectTaskQueue = data.taskQueue;
}
try {
projectTaskQueue.submit(new Runnable() {
@Override
public void run() {
Throwable execFailure = null;
try {
if (project.isDisposed()) {
final CmdlineRemoteProto.Message.ControllerMessage params;
if (isRebuild) {
params = CmdlineProtoUtil.createBuildRequest(projectPath, scopes, Collections.<String>emptyList(), userData, globals, null);
}
else if (onlyCheckUpToDate) {
params = CmdlineProtoUtil.createUpToDateCheckRequest(projectPath, scopes, paths, userData, globals, currentFSChanges);
}
else {
params = CmdlineProtoUtil.createBuildRequest(projectPath, scopes, isMake ? Collections.<String>emptyList() : paths, userData, globals, currentFSChanges);
}
if (!usingPreloadedProcess) {
myMessageDispatcher.registerBuildMessageHandler(future, params);
}
try {
Future<?> buildFuture = projectTaskQueue.submit(new Runnable() {
@Override
public void run() {
Throwable execFailure = null;
try {
if (project.isDisposed()) {
if (usingPreloadedProcess) {
future.cancel(false);
}
else {
return;
}
}
myBuildsInProgress.put(projectPath, future);
final OSProcessHandler processHandler;
CharSequence errorsOnLaunch;
if (usingPreloadedProcess) {
future.cancel(false);
final boolean paramsSent = myMessageDispatcher.sendBuildParameters(future.getRequestID(), params);
if (!paramsSent) {
myMessageDispatcher.cancelSession(future.getRequestID());
}
processHandler = preloaded.second;
errorsOnLaunch = STDERR_OUTPUT.get(processHandler);
}
else {
return;
}
}
myBuildsInProgress.put(projectPath, future);
final OSProcessHandler processHandler;
CharSequence errorsOnLaunch;
if (usingPreloadedProcess) {
final boolean paramsSent = myMessageDispatcher.sendBuildParameters(future.getRequestID(), params);
if (!paramsSent) {
myMessageDispatcher.cancelSession(future.getRequestID());
}
processHandler = preloaded.second;
errorsOnLaunch = STDERR_OUTPUT.get(processHandler);
}
else {
if (isAutomake && needRescan) {
// if project state was cleared because of roots changed or this is the first compilation after project opening,
// ensure project model is saved on disk, so that automake sees the latest model state.
// For ordinary make all project, app settings and unsaved docs are always saved before build starts.
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
project.save();
}
});
}
catch(Throwable e) {
LOG.info(e);
}
}
processHandler = launchBuildProcess(project, myListenPort, sessionId, false);
errorsOnLaunch = new StringBuffer();
processHandler.addProcessListener(new StdOutputCollector((StringBuffer)errorsOnLaunch));
processHandler.startNotify();
}
Integer debugPort = processHandler.getUserData(COMPILER_PROCESS_DEBUG_PORT);
if (debugPort != null) {
String message = "Make: waiting for debugger connection on port " + debugPort;
messageHandler.handleCompileMessage(sessionId, CmdlineProtoUtil.createCompileProgressMessageResponse(message).getCompileMessage());
}
while (!processHandler.waitFor()) {
LOG.info("processHandler.waitFor() returned false for session " + sessionId + ", continue waiting");
}
final int exitValue = processHandler.getProcess().exitValue();
if (exitValue != 0) {
final StringBuilder msg = new StringBuilder();
msg.append("Abnormal build process termination: ");
if (errorsOnLaunch != null && errorsOnLaunch.length() > 0) {
msg.append("\n").append(errorsOnLaunch);
if (StringUtil.contains(errorsOnLaunch, "java.lang.NoSuchMethodError")) {
msg.append("\nThe error may be caused by JARs in Java Extensions directory which conflicts with libraries used by the external build process.")
.append("\nTry adding -Djava.ext.dirs=\"\" argument to 'Build process VM options' in File | Settings | Build, Execution, Deployment | Compiler to fix the problem.");
}
}
else {
msg.append("unknown error");
}
handler.handleFailure(sessionId, CmdlineProtoUtil.createFailure(msg.toString(), null));
}
}
catch (Throwable e) {
execFailure = e;
}
finally {
myBuildsInProgress.remove(projectPath);
notifySessionTerminationIfNeeded(sessionId, execFailure);
if (isProcessPreloadingEnabled(project)) {
runCommand(new Runnable() {
@Override
public void run() {
if (!myPreloadedBuilds.containsKey(projectPath)) {
try {
final Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>> preloadResult = launchPreloadedBuildProcess(project, projectTaskQueue);
myPreloadedBuilds.put(projectPath, preloadResult);
}
catch (Throwable e) {
LOG.info("Error pre-loading build process for project " + projectPath, e);
}
if (isAutomake && needRescan) {
// if project state was cleared because of roots changed or this is the first compilation after project opening,
// ensure project model is saved on disk, so that automake sees the latest model state.
// For ordinary make all project, app settings and unsaved docs are always saved before build starts.
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
project.save();
}
});
}
catch (Throwable e) {
LOG.info(e);
}
}
});
processHandler = launchBuildProcess(project, myListenPort, sessionId, false);
errorsOnLaunch = new StringBuffer();
processHandler.addProcessListener(new StdOutputCollector((StringBuffer)errorsOnLaunch));
processHandler.startNotify();
}
Integer debugPort = processHandler.getUserData(COMPILER_PROCESS_DEBUG_PORT);
if (debugPort != null) {
String message = "Make: waiting for debugger connection on port " + debugPort;
messageHandler
.handleCompileMessage(sessionId, CmdlineProtoUtil.createCompileProgressMessageResponse(message).getCompileMessage());
}
while (!processHandler.waitFor()) {
LOG.info("processHandler.waitFor() returned false for session " + sessionId + ", continue waiting");
}
final int exitValue = processHandler.getProcess().exitValue();
if (exitValue != 0) {
final StringBuilder msg = new StringBuilder();
msg.append("Abnormal build process termination: ");
if (errorsOnLaunch != null && errorsOnLaunch.length() > 0) {
msg.append("\n").append(errorsOnLaunch);
if (StringUtil.contains(errorsOnLaunch, "java.lang.NoSuchMethodError")) {
msg.append(
"\nThe error may be caused by JARs in Java Extensions directory which conflicts with libraries used by the external build process.")
.append(
"\nTry adding -Djava.ext.dirs=\"\" argument to 'Build process VM options' in File | Settings | Build, Execution, Deployment | Compiler to fix the problem.");
}
}
else {
msg.append("unknown error");
}
handler.handleFailure(sessionId, CmdlineProtoUtil.createFailure(msg.toString(), null));
}
}
catch (Throwable e) {
execFailure = e;
}
finally {
myBuildsInProgress.remove(projectPath);
notifySessionTerminationIfNeeded(sessionId, execFailure);
if (isProcessPreloadingEnabled(project)) {
runCommand(new Runnable() {
@Override
public void run() {
if (!myPreloadedBuilds.containsKey(projectPath)) {
try {
final Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>> preloadResult =
launchPreloadedBuildProcess(project, projectTaskQueue);
myPreloadedBuilds.put(projectPath, preloadResult);
}
catch (Throwable e) {
LOG.info("Error pre-loading build process for project " + projectPath, e);
}
}
}
});
}
}
}
}
});
}
catch (Throwable e) {
handleProcessExecutionFailure(sessionId, e);
});
delegatesToWait = new TaskFuture[]{future, new TaskFutureAdapter<>(buildFuture)};
}
catch (Throwable e) {
handleProcessExecutionFailure(sessionId, e);
}
}
boolean set = _future.setDelegates(delegatesToWait);
assert set;
}
});
@@ -1451,7 +1459,7 @@ public class BuildManager implements Disposable {
private static final class StdOutputCollector extends ProcessAdapter {
private final Appendable myOutput;
private int myStoredLength;
public StdOutputCollector(Appendable outputSink) {
StdOutputCollector(@NotNull Appendable outputSink) {
myOutput = outputSink;
}
@@ -1779,31 +1787,33 @@ public class BuildManager implements Disposable {
}
}
private static final class DelegateFuture<T> implements TaskFuture<T> {
private static final class DelegateFuture implements TaskFuture {
@Nullable
private TaskFuture<? extends T> myDelegate;
private TaskFuture[] myDelegates;
private Boolean myRequestedCancelState;
@NotNull
public synchronized TaskFuture<? extends T> getDelegate() {
TaskFuture<? extends T> delegate = myDelegate;
while (delegate == null) {
private synchronized TaskFuture[] getDelegates() {
TaskFuture[] delegates = myDelegates;
while (delegates == null) {
try {
wait();
}
catch (InterruptedException ignored) {
}
delegate = myDelegate;
delegates = myDelegates;
}
return delegate;
return delegates;
}
private synchronized boolean setDelegate(@NotNull TaskFuture<? extends T> delegate) {
if (myDelegate == null) {
private synchronized boolean setDelegates(@NotNull TaskFuture... delegates) {
if (myDelegates == null) {
try {
myDelegate = delegate;
myDelegates = delegates;
if (myRequestedCancelState != null) {
myDelegate.cancel(myRequestedCancelState);
for (TaskFuture delegate : delegates) {
delegate.cancel(myRequestedCancelState);
}
}
}
finally {
@@ -1816,56 +1826,64 @@ public class BuildManager implements Disposable {
@Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
final TaskFuture<? extends T> delegate = myDelegate;
if (delegate == null) {
Future[] delegates = myDelegates;
if (delegates == null) {
myRequestedCancelState = mayInterruptIfRunning;
return true;
}
return delegate.cancel(mayInterruptIfRunning);
Stream.of(delegates).forEach(delegate -> delegate.cancel(mayInterruptIfRunning));
return isDone();
}
@Override
public void waitFor() {
getDelegate().waitFor();
Stream.of(getDelegates()).forEach(TaskFuture::waitFor);
}
@Override
public boolean waitFor(long timeout, TimeUnit unit) {
return getDelegate().waitFor(timeout, unit);
Stream.of(getDelegates()).forEach(delegate -> delegate.waitFor(timeout, unit));
return isDone();
}
@Override
public boolean isCancelled() {
final TaskFuture<? extends T> delegate;
final Future[] delegates;
synchronized (this) {
delegate = myDelegate;
if (delegate == null) {
delegates = myDelegates;
if (delegates == null) {
return myRequestedCancelState != null;
}
}
return delegate.isCancelled();
return Stream.of(delegates).anyMatch(Future::isCancelled);
}
@Override
public boolean isDone() {
final TaskFuture<? extends T> delegate;
final Future[] delegates;
synchronized (this) {
delegate = myDelegate;
if (delegate == null) {
delegates = myDelegates;
if (delegates == null) {
return false;
}
}
return delegate.isDone();
return Stream.of(delegates).allMatch(Future::isDone);
}
@Override
public T get() throws InterruptedException, java.util.concurrent.ExecutionException {
return getDelegate().get();
public Object get() throws InterruptedException, java.util.concurrent.ExecutionException {
for (Future delegate : getDelegates()) {
delegate.get();
}
return null;
}
@Override
public T get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, java.util.concurrent.ExecutionException, TimeoutException {
return getDelegate().get(timeout, unit);
public Object get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, java.util.concurrent.ExecutionException, TimeoutException {
for (Future delegate : getDelegates()) {
delegate.get(timeout, unit);
}
return null;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* 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 org.jetbrains.jps.api;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.*;
/**
* Makes TaskFuture from the supplied Future
*/
public class TaskFutureAdapter<T> implements TaskFuture<T> {
@NotNull private final Future<T> myFuture;
public TaskFutureAdapter(@NotNull Future<T> future) {
myFuture = future;
}
@Override
public void waitFor() {
try {
get();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
catch (ExecutionException e) {
throw new RuntimeException(e);
}
catch (CancellationException ignored) {
}
}
@Override
public boolean waitFor(long timeout, TimeUnit unit) {
try {
get(timeout, unit);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
catch (ExecutionException e) {
throw new RuntimeException(e);
}
catch (TimeoutException ignored) {
}
catch (CancellationException ignored) {
}
return isDone();
}
// delegates
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return myFuture.cancel(mayInterruptIfRunning);
}
@Override
public boolean isCancelled() {
return myFuture.isCancelled();
}
@Override
public boolean isDone() {
return myFuture.isDone();
}
@Override
public T get() throws InterruptedException, ExecutionException {
return myFuture.get();
}
@Override
public T get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return myFuture.get(timeout, unit);
}
}