Skip to content

Commit f1e40d5

Browse files
committed
Fixes Handling of InterruptedExceptions
Before we treated InterruptedExceptions just like every other type of exception. This is bad because it means that the thread that had been asked to exit was having to go through the process of displaying a UI component which was causing deadlocks. Closes #236
1 parent d6b5545 commit f1e40d5

File tree

9 files changed

+88
-37
lines changed

9 files changed

+88
-37
lines changed

core/src/main/java/edu/wpi/grip/core/GRIPCoreModule.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
import edu.wpi.grip.core.sources.MultiImageFileSource;
1717
import edu.wpi.grip.core.util.ExceptionWitness;
1818

19+
import java.util.logging.Level;
20+
import java.util.logging.Logger;
21+
1922
/**
2023
* A Guice {@link com.google.inject.Module} for GRIP's core package. This is where instances of {@link Pipeline},
2124
* {@link Palette}, {@link Project}, etc... are created.
2225
*/
2326
public class GRIPCoreModule extends AbstractModule {
27+
private final Logger logger = Logger.getLogger(GRIPCoreModule.class.getName());
2428

2529
private final EventBus eventBus = new EventBus(this::onSubscriberException);
2630

@@ -60,10 +64,20 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
6064
}
6165

6266
private void onSubscriberException(Throwable exception, SubscriberExceptionContext context) {
63-
eventBus.post(new UnexpectedThrowableEvent(exception, "An event subscriber threw an exception"));
67+
if (exception instanceof InterruptedException) {
68+
logger.log(Level.FINE, "EventBus Subscriber threw InterruptedException", exception);
69+
Thread.currentThread().interrupt();
70+
} else {
71+
eventBus.post(new UnexpectedThrowableEvent(exception, "An event subscriber threw an exception"));
72+
}
6473
}
6574

6675
private void onThreadException(Thread thread, Throwable exception) {
67-
eventBus.post(new UnexpectedThrowableEvent(exception, thread + " threw an exception"));
76+
if (exception instanceof InterruptedException) {
77+
logger.log(Level.FINE, "InterruptedException from thread " + thread, exception);
78+
Thread.currentThread().interrupt();
79+
} else {
80+
eventBus.post(new UnexpectedThrowableEvent(exception, thread + " threw an exception"));
81+
}
6882
}
6983
}

core/src/main/java/edu/wpi/grip/core/StartStoppable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public interface StartStoppable {
2626
* @throws TimeoutException If the thread fails to stop in a timely manner
2727
* @throws IOException If cleaning up some system resource fails.
2828
*/
29-
void stop() throws TimeoutException, IOException;
29+
void stop() throws InterruptedException, TimeoutException, IOException;
3030

3131
/**
3232
* Used to indicate if the source is running or stopped

core/src/main/java/edu/wpi/grip/core/Step.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ private synchronized void runPerformIfPossible() {
143143

144144
try {
145145
this.operation.perform(inputSockets, outputSockets, data);
146-
} catch (Exception e) {
146+
} catch (RuntimeException e) {
147+
// We do not want to catch all exceptions, only runtime exceptions.
148+
// This is especially important when it comes to InterruptedExceptions
147149
final String operationFailedMessage = "The " + operation.getName() + " operation did not perform correctly.";
148150
logger.log(Level.WARNING, operationFailedMessage, e);
149151
witness.flagException(e, operationFailedMessage);

core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ public void start() throws IOException, IllegalStateException {
208208
} catch (TimeoutException | IOException e) {
209209
// TODO: This should use the ExceptionWitness once that has a UI component added for it
210210
eventBus.post(new UnexpectedThrowableEvent(e, "Camera Frame Grabber could not be stopped!"));
211+
} catch (InterruptedException e) {
212+
Thread.currentThread().interrupt();
211213
}
212214
}
213215
);
@@ -228,13 +230,13 @@ public void start() throws IOException, IllegalStateException {
228230
* @throws IOException If there is a problem stopping the Source
229231
* @throws IllegalStateException If the camera is already stopped.
230232
*/
231-
public void stop() throws TimeoutException, IOException {
233+
public void stop() throws InterruptedException, TimeoutException, IOException {
232234
synchronized (this) {
233235
if (frameThread.isPresent()) {
234236
final Thread ex = frameThread.get();
235237
ex.interrupt();
236238
try {
237-
ex.join(TimeUnit.SECONDS.toMillis(500));
239+
ex.join(TimeUnit.SECONDS.toMillis(10));
238240
if (ex.isAlive()) {
239241
throw new TimeoutException("Unable to terminate video feed from Web Camera");
240242
}
@@ -243,6 +245,7 @@ public void stop() throws TimeoutException, IOException {
243245
} catch (InterruptedException e) {
244246
Thread.currentThread().interrupt();
245247
logger.log(Level.WARNING, e.getMessage(), e);
248+
throw e;
246249
} finally {
247250
// This will always run even if a timeout exception occurs
248251
try {
@@ -266,7 +269,7 @@ public synchronized boolean isStarted() {
266269
}
267270

268271
@Subscribe
269-
public void onSourceRemovedEvent(SourceRemovedEvent event) throws TimeoutException, IOException {
272+
public void onSourceRemovedEvent(SourceRemovedEvent event) throws InterruptedException, TimeoutException, IOException {
270273
if (event.getSource() == this) {
271274
try {
272275
if (this.isStarted()) this.stop();

core/src/main/java/edu/wpi/grip/core/util/ExceptionWitness.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,16 @@ public interface Factory {
4848
/**
4949
* Indicates to the witness that an exception has occurred. This will also post an {@link ExceptionEvent} to the {@link EventBus}
5050
*
51-
* @param exception The exception that this is reporting
51+
* @param exception The exception that this is reporting.
52+
* If the Exception is an InterruptedException then this will not post an exception, instead,
53+
* it will set the threads interrupted state and return.
5254
* @param message Any additional details that should be associated with this message.
5355
*/
5456
public final void flagException(final Exception exception, final String message) {
57+
if (exception instanceof InterruptedException) {
58+
Thread.currentThread().interrupt();
59+
return;
60+
}
5561
isExceptionState.set(true);
5662
this.eventBus.post(new ExceptionEvent(origin, exception, message));
5763
}

ui/src/main/java/edu/wpi/grip/ui/Main.java

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,28 +92,35 @@ public void start(Stage stage) throws Exception {
9292
public final void onUnexpectedThrowableEvent(UnexpectedThrowableEvent event) {
9393
// Print throwable before showing the exception so that errors are in order in the console
9494
event.getThrowable().printStackTrace();
95-
// This should still use PlatformImpl
96-
PlatformImpl.runAndWait(() -> {
97-
// WARNING! Do not post any events from within this! It could result in a deadlock!
98-
synchronized (this.dialogLock) {
99-
try {
100-
// Don't create more than one exception dialog at the same time
101-
final ExceptionAlert exceptionAlert = new ExceptionAlert(root, event.getThrowable(), event.getMessage(), event.isFatal(), getHostServices());
102-
exceptionAlert.setInitialFocus();
103-
exceptionAlert.showAndWait();
104-
} catch (Throwable e) {
105-
// Well in this case something has gone very, very wrong
106-
// We don't want to create a feedback loop either.
107-
e.printStackTrace();
108-
System.exit(1); // Ensure we shut down the application if we get an exception
109-
}
110-
}
111-
});
11295

113-
if (event.isFatal()) {
114-
System.err.println("Original fatal exception");
115-
event.getThrowable().printStackTrace();
116-
System.exit(1);
96+
try {
97+
// If this is an interrupted exception then don't show an error.
98+
// We should exit as fast as possible.
99+
if (!(event.getThrowable() instanceof InterruptedException)) {
100+
// This should still use PlatformImpl
101+
PlatformImpl.runAndWait(() -> {
102+
// WARNING! Do not post any events from within this! It could result in a deadlock!
103+
synchronized (this.dialogLock) {
104+
try {
105+
// Don't create more than one exception dialog at the same time
106+
final ExceptionAlert exceptionAlert = new ExceptionAlert(root, event.getThrowable(), event.getMessage(), event.isFatal(), getHostServices());
107+
exceptionAlert.setInitialFocus();
108+
exceptionAlert.showAndWait();
109+
} catch (Throwable e) {
110+
// Well in this case something has gone very, very wrong
111+
// We don't want to create a feedback loop either.
112+
e.printStackTrace();
113+
System.exit(1); // Ensure we shut down the application if we get an exception
114+
}
115+
}
116+
});
117+
}
118+
} finally {
119+
if (event.isFatal()) {
120+
System.err.println("Original fatal exception");
121+
event.getThrowable().printStackTrace();
122+
System.exit(1);
123+
}
117124
}
118125
}
119126
}

ui/src/main/java/edu/wpi/grip/ui/components/StartStoppableButton.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public interface Factory {
6464
// If this fails then an StartedStoppedEvent will not be posted
6565
} catch (TimeoutException | IOException e) {
6666
eventBus.post(new UnexpectedThrowableEvent(e, "Failed to stop"));
67+
} catch (InterruptedException e) {
68+
// Unfortunately we have to eat the exception here because we can't rethrow it
69+
Thread.currentThread().interrupt();
6770
}
6871
});
6972

ui/src/main/java/edu/wpi/grip/ui/util/GRIPPlatform.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public Runnable getAction() {
4949
* JavaFX application thread. If we are already on the JavaFX application thread
5050
* then this will be run immediately. Otherwise, it will be run as an event
5151
* inside of the event bus.
52+
* If
5253
* If {@link #runAsSoonAsPossible(Runnable)} is called within itself it will always run
5354
* immediately because the runnable will always be run in the JavaFX thread.
5455
*
@@ -69,25 +70,28 @@ public void runAsSoonAsPossible(Runnable action) {
6970
}
7071

7172
@Subscribe
72-
public void onJavaFXRunnerEvent(JavaFXRunnerEvent event) {
73+
public void onJavaFXRunnerEvent(JavaFXRunnerEvent event) throws InterruptedException {
7374
assert !Platform.isFxApplicationThread() : "This should never be run on the application thread. This can cause a deadlock!";
75+
final Thread callingThread = Thread.currentThread();
76+
if (Thread.interrupted()) {
77+
throw new InterruptedException("Interrupted in onJavaFXRunnerEvent");
78+
}
7479

7580
// queue on JavaFX thread and wait for completion
7681
final CountDownLatch doneLatch = new CountDownLatch(1);
7782
Platform.runLater(() -> {
7883
try {
79-
event.getAction().run();
84+
// If the calling thread was interrupted then don't run this event.
85+
if(!callingThread.isInterrupted()) {
86+
event.getAction().run();
87+
}
8088
} finally {
8189
doneLatch.countDown();
8290
}
8391
});
8492

85-
try {
86-
while (!doneLatch.await(500, TimeUnit.MILLISECONDS)) {
87-
logger.log(Level.WARNING, "POTENTIAL DEADLOCK!");
88-
}
89-
} catch (InterruptedException e) {
90-
// ignore exception
93+
while (!doneLatch.await(500, TimeUnit.MILLISECONDS)) {
94+
logger.log(Level.WARNING, "POTENTIAL DEADLOCK!");
9195
}
9296
}
9397

ui/src/test/java/edu/wpi/grip/ui/util/GRIPPlatformTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.util.logging.Logger;
1616

17+
import static org.junit.Assert.assertFalse;
1718
import static org.junit.Assert.assertTrue;
1819

1920
public class GRIPPlatformTest extends ApplicationTest {
@@ -76,4 +77,15 @@ public void testRunAsSoonAsPossibleDoesNotDeadlockWhenRunInsideItself() throws E
7677
waiter.await();
7778
}
7879

80+
@Test
81+
public void testRunAsSoonAsPossibleWillNotCallIfInterrupted() throws Exception {
82+
final boolean hasRun [] = {false};
83+
84+
Thread.currentThread().interrupt();
85+
platform.runAsSoonAsPossible(() -> {
86+
hasRun[0] = true;
87+
});
88+
assertFalse("runAsSoonAsPossible ran when interrupted", hasRun[0]);
89+
}
90+
7991
}

0 commit comments

Comments
 (0)