diff options
4 files changed, 635 insertions, 160 deletions
diff --git a/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerTest.java b/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerTest.java index 4ed251a9a..d83ce6ec6 100644 --- a/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerTest.java +++ b/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerTest.java @@ -24,8 +24,10 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.IComputation; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler; +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ShutdownStatus; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -33,9 +35,9 @@ import org.junit.Test; @SuppressWarnings("nls") public class ResourceComputationSchedulerTest { - private ResourceComputationScheduler<String> scheduler; + protected ResourceComputationScheduler<String> scheduler; - private boolean flag; + protected boolean flag; @Test public void testInitializeCanBeCalledSeveralTimes() { @@ -79,6 +81,26 @@ public class ResourceComputationSchedulerTest { assertTrue(flag); } + @Test(expected = OperationCanceledException.class) + public void testInterruptedExceptionInCallCausesOperationCanceledException() throws Exception { + scheduler.initialize(); + scheduler.call(new Callable<String>() { + public String call() throws Exception { + throw new InterruptedException(); + } + }, null); + } + + @Test(expected = OperationCanceledException.class) + public void testOperationCanceledExceptionCall() throws Exception { + scheduler.initialize(); + scheduler.call(new Callable<String>() { + public String call() throws Exception { + throw new OperationCanceledException(); + } + }, null); + } + @Test public void testPostTreatmentIsCalledWhenExceptionInTreatment() throws Exception { scheduler.initialize(); @@ -265,76 +287,6 @@ public class ResourceComputationSchedulerTest { scheduler.scheduleComputation(new TestSuccessfulComputation(desc, "comp")); } - @Test - public void testDemandShutdownWithLongRunningTaskThatInterruptsImproperly() throws Exception { - scheduler.initialize(); - final CompStatus cs = new CompStatus(); - Integer result = scheduler.call(new Callable<Integer>() { - - public Integer call() throws Exception { - scheduler.scheduleComputation(new TimedComputation(cs, "long1", 1000, false)); - // We ask for shutdown before the task can complete - // The scheduler is configured to wait only 100ms - scheduler.demandShutdown(); - - return Integer.valueOf(42); - } - }, null); - // desc has not been updated - assertFalse(cs.isFailed()); - assertFalse(cs.isSuccess()); - assertEquals(Integer.valueOf(42), result); - assertTrue(scheduler.getComputedElements().isEmpty()); - Thread.sleep(1000); - checkInterruptedAndSuccess(cs); - } - - @Test - public void testDemandShutdownWithLongRunningTaskThatInterruptsGracefully() throws Exception { - scheduler.initialize(); - final CompStatus cs = new CompStatus(); - Integer result = scheduler.call(new Callable<Integer>() { - - public Integer call() throws Exception { - scheduler.scheduleComputation(new TimedComputation(cs, "long1", 1000, true)); - // We ask for shutdown before the task can complete - // The scheduler is configured to wait only 100ms - scheduler.demandShutdown(); - - return Integer.valueOf(42); - } - }, null); - // desc has not been updated - assertFalse(cs.isFailed()); - assertFalse(cs.isSuccess()); - assertEquals(Integer.valueOf(42), result); - assertTrue(scheduler.getComputedElements().isEmpty()); - Thread.sleep(1000); - checkInterruptedAndFailure(cs); - } - - @Test - public void testDemandShutdownWithRunningTaskThatTerminatesGracefully() throws Exception { - scheduler = new ResourceComputationScheduler<String>(1, TimeUnit.SECONDS); - scheduler.initialize(); - final CompStatus desc = new CompStatus(); - Integer result = scheduler.call(new Callable<Integer>() { - - public Integer call() throws Exception { - scheduler.scheduleComputation(new TimedComputation(desc, "long1", 500, false)); - // We ask for shutdown before the task can complete - // The scheduler is configured to wait 1s while the task only takes 500ms - scheduler.demandShutdown(); - - return Integer.valueOf(42); - } - }, null); - assertEquals(Integer.valueOf(42), result); - Thread.sleep(1000); - checkSuccess(desc); - assertTrue(scheduler.getComputedElements().isEmpty()); - } - protected void checkSuccess(CompStatus state) { assertEquals(1, state.getCallCount()); assertFalse(state.isInterrupted()); @@ -373,7 +325,7 @@ public class ResourceComputationSchedulerTest { @Before public void setUp() { - scheduler = new ResourceComputationScheduler<String>(100, TimeUnit.MILLISECONDS); + scheduler = new ResourceComputationScheduler<String>(10, TimeUnit.MILLISECONDS, null); } @After @@ -392,15 +344,15 @@ public class ResourceComputationSchedulerTest { private final String name; - private TestSuccessfulComputation(CompStatus desc, String name) { - this.cs = desc; + private TestSuccessfulComputation(CompStatus cs, String name) { + this.cs = cs; this.name = name; } public void run() { cs.addCall(); try { - Thread.sleep(100); + Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); cs.interrupt(); @@ -483,86 +435,11 @@ public class ResourceComputationSchedulerTest { } /** - * A test computation that systematically throws an exception when run, and updates its {@link CompStatus} - * accordingly if onFailure() is called on its post-treatment. - * - * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> - */ - private static class InterruptibleRunnable implements Runnable { - protected final CompStatus cs; - - protected final long duration; - - protected final boolean throwOnInterrupt; - - private InterruptibleRunnable(CompStatus desc, long duration, boolean throwOnInterrupt) { - this.cs = desc; - this.duration = duration; - this.throwOnInterrupt = throwOnInterrupt; - } - - public void run() { - cs.addCall(); - try { - Thread.sleep(duration); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - cs.interrupt(); - if (throwOnInterrupt) { - throw new RuntimeException("Interrupted"); - } - } - } - } - - /** - * A test computation lasts for a given time, and updates its {@link CompStatus} accordingly if - * onFailure() is called on its post-treatment. - * - * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> - */ - private static final class TimedComputation extends InterruptibleRunnable implements IComputation<String> { - - protected final String name; - - private TimedComputation(CompStatus desc, String name, long duration, boolean throwOnInterrupt) { - super(desc, duration, throwOnInterrupt); - this.name = name; - } - - public FutureCallback<Object> getPostTreatment() { - return new FutureCallback<Object>() { - public void onFailure(Throwable t) { - if (cs.isInterrupted() && throwOnInterrupt) { - cs.fail("as expected"); - } else { - cs.fail("onFailure() called on TimedComputation " + name - + ", should have been onSuccess()."); - } - } - - public void onSuccess(Object r) { - if (cs.isInterrupted() && throwOnInterrupt) { - cs.fail("onSuccess() called on TimedComputation" + name - + ", should have been onFailure()."); - } else { - cs.success("as expected"); - } - } - }; - } - - public String getKey() { - return name; - } - } - - /** * Computation Status. * * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> */ - private static class CompStatus { + protected static class CompStatus { private boolean success = false; private boolean failed = false; @@ -571,6 +448,8 @@ public class ResourceComputationSchedulerTest { private boolean interrupted; + private ShutdownStatus shutdownStatus; + private String message; public String getMessage() { @@ -612,5 +491,13 @@ public class ResourceComputationSchedulerTest { public boolean isInterrupted() { return interrupted; } + + public void setShutdownStatus(ShutdownStatus shutdownStatus) { + this.shutdownStatus = shutdownStatus; + } + + public ShutdownStatus getShutdownStatus() { + return shutdownStatus; + } } } diff --git a/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerWithEventBusTest.java b/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerWithEventBusTest.java new file mode 100644 index 000000000..af9d2e1bc --- /dev/null +++ b/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/logical/resolver/ResourceComputationSchedulerWithEventBusTest.java @@ -0,0 +1,447 @@ +/******************************************************************************* + * Copyright (c) 2015 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.compare.ide.ui.tests.logical.resolver; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.common.util.concurrent.FutureCallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.IComputation; +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler; +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.CallStatus; +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ComputationState; +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ShutdownState; +import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ShutdownStatus; +import org.junit.Before; +import org.junit.Test; + +/** + * These tests are the same as in the extend class, except they use a {@link ResourceComputationScheduler} + * initialized with a non-null {@link EventBus}. It also adds tests that can only be performed when an + * EventBus is set on the scheduler. + * + * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> + */ +@SuppressWarnings("nls") +public class ResourceComputationSchedulerWithEventBusTest extends ResourceComputationSchedulerTest { + + private EventBus bus; + + @Test + public void testEventsLaunchedInStandardCase() throws Exception { + scheduler.initialize(); + final List<CallStatus> receivedEvents = new ArrayList<CallStatus>(); + bus.register(new Object() { + @Subscribe + public void callStatusChanged(CallStatus cs) { + receivedEvents.add(cs); + } + }); + scheduler.call(new Callable<String>() { + public String call() throws Exception { + return ""; + } + }, null); + assertEquals(4, receivedEvents.size()); + assertEquals(ComputationState.SETTING_UP, receivedEvents.get(0).getState()); + assertEquals(ComputationState.SCHEDULED, receivedEvents.get(1).getState()); + assertEquals(ComputationState.FINISHING, receivedEvents.get(2).getState()); + assertEquals(ComputationState.FINISHED, receivedEvents.get(3).getState()); + } + + @Test + public void testEventsLaunchedWhenCallThrowsException() throws Exception { + scheduler.initialize(); + final List<CallStatus> receivedEvents = new ArrayList<CallStatus>(); + bus.register(new Object() { + @Subscribe + public void callStatusChanged(CallStatus cs) { + receivedEvents.add(cs); + } + }); + Exception exceptionReceived = null; + try { + scheduler.call(new Callable<String>() { + public String call() throws Exception { + throw new Exception("Test"); + } + }, null); + } catch (Exception e) { + exceptionReceived = e; + } + assertEquals(5, receivedEvents.size()); + assertEquals(ComputationState.SETTING_UP, receivedEvents.get(0).getState()); + assertEquals(ComputationState.SCHEDULED, receivedEvents.get(1).getState()); + assertEquals(ComputationState.FAILED, receivedEvents.get(2).getState()); + // Check the root exception has been well passed in the event too + assertNotNull(exceptionReceived); + assertSame(exceptionReceived.getCause(), receivedEvents.get(2).getCause()); + assertEquals(ComputationState.FINISHING, receivedEvents.get(3).getState()); + assertEquals(ComputationState.FINISHED, receivedEvents.get(4).getState()); + } + + @Test + public void testEventsLaunchedWhenPostTreamentThrowsException() throws Exception { + scheduler.initialize(); + final List<CallStatus> receivedEvents = new ArrayList<CallStatus>(); + bus.register(new Object() { + @Subscribe + public void callStatusChanged(CallStatus cs) { + receivedEvents.add(cs); + } + }); + try { + scheduler.call(new Callable<String>() { + public String call() throws Exception { + return ""; + } + }, new Runnable() { + public void run() { + throw new RuntimeException(); + } + }); + fail("There should have been a RuntimeException"); + } catch (RuntimeException e) { + // As expected + } + // Flag will only be true if the CallStatus "FINISH event has been received + assertEquals(4, receivedEvents.size()); + assertEquals(ComputationState.SETTING_UP, receivedEvents.get(0).getState()); + assertEquals(ComputationState.SCHEDULED, receivedEvents.get(1).getState()); + assertEquals(ComputationState.FINISHING, receivedEvents.get(2).getState()); + assertEquals(ComputationState.FINISHED, receivedEvents.get(3).getState()); + } + + /** + * This test checks that when a scheduler executes a long-runnning task that does not handle interrupts + * "gracefully", that is by taking care to stop its treatment ASAP, then the scheduler can nevertheless + * shutdown correctly. + * + * @throws Exception + */ + @Test + public void testDemandShutdownWithLongRunningTaskThatInterruptsImproperly() throws Exception { + scheduler.initialize(); + final CompStatus cs = new CompStatus(); + final AtomicBoolean readyForFinalChecks = new AtomicBoolean(false); + + // The following computation does not handle properly interruption + // so we expect its treatment to terminate properly with succeeded instead of failed + // The computation used here will set cs to "interrupted" when it is interrupted, but it will go on + // until cs.trigger() is called, which is done after all the checks are made in the test. + final TriggerableComputation tc = new TriggerableComputation(cs, "long1", false) { + @Override + protected void succeeded(Object r) { + cs.success("As expected"); + } + + @Override + protected void failed(Throwable t) { + cs.fail("failed() called, should have been succeeded()."); + } + }; + + bus.register(new Object() { + @Subscribe + public void check(ShutdownStatus status) { + synchronized(scheduler) { + switch (status.getState()) { + case FINISH_SUCCESS: + cs.setShutdownStatus(status); + readyForFinalChecks.compareAndSet(false, true); + // This will trigger the execution of the final tests + scheduler.notifyAll(); + break; + case FINISH_FAILED: + cs.setShutdownStatus(status); + readyForFinalChecks.compareAndSet(false, true); + scheduler.notifyAll(); + } + } + } + }); + try { + Integer result = scheduler.call(new Callable<Integer>() { + + public Integer call() throws Exception { + scheduler.scheduleComputation(tc); + // We ask for shutdown before the task can complete + // The scheduler is configured to wait only 10ms + scheduler.demandShutdown(); + + return Integer.valueOf(42); + } + }, null); + assertEquals(Integer.valueOf(42), result); + assertTrue(scheduler.getComputedElements().isEmpty()); + + // This will wait for the shutdown to be over + // But the computation is still running + synchronized(scheduler) { + while (!readyForFinalChecks.get()) { + scheduler.wait(); + } + assertEquals(1, cs.getCallCount()); + assertTrue(cs.isInterrupted()); + assertFalse(cs.isFailed()); + // 2 next lines make sure the computation is still running + assertFalse(cs.isSuccess()); + assertNull(cs.getMessage()); + // Make sure the shutdown is ok + assertEquals(ShutdownState.FINISH_SUCCESS, cs.getShutdownStatus().getState()); + } + } finally { + // We finally allow the tested computation, that is still running, to terminate + tc.trigger(); + } + } + + @Test + public void testDemandShutdownWithLongRunningTaskThatInterruptsGracefully() throws Exception { + scheduler.initialize(); + final CompStatus cs = new CompStatus(); + final AtomicBoolean readyForFinalChecks = new AtomicBoolean(false); + + // The following computation handles interruption properly + // so we expect its treatment to terminate with failed + final TriggerableComputation tc = new TriggerableComputation(cs, "long1", true) { + @Override + protected void failed(Throwable t) { + cs.fail("As expected"); + } + + @Override + protected void succeeded(Object r) { + cs.success("Computation ends successfully when it should have failed."); + } + }; + bus.register(new Object() { + @Subscribe + public void check(ShutdownStatus status) { + synchronized(scheduler) { + switch (status.getState()) { + case FINISH_SUCCESS: + cs.setShutdownStatus(status); + readyForFinalChecks.compareAndSet(false, true); + // This will trigger the execution of the final tests + scheduler.notifyAll(); + break; + case FINISH_FAILED: + cs.setShutdownStatus(status); + readyForFinalChecks.compareAndSet(false, true); + scheduler.notifyAll(); + } + } + } + }); + Integer result = scheduler.call(new Callable<Integer>() { + + public Integer call() throws Exception { + scheduler.scheduleComputation(tc); + // We ask for shutdown before the task can complete + // The scheduler is configured to wait only 100ms + scheduler.demandShutdown(); + + return Integer.valueOf(42); + } + }, null); + + assertEquals(Integer.valueOf(42), result); + assertTrue(scheduler.getComputedElements().isEmpty()); + + synchronized(scheduler) { + // This will wait until the computation has finished to perform the final tests + while (!readyForFinalChecks.get()) { + scheduler.wait(); + } + assertEquals(1, cs.getCallCount()); + assertTrue(cs.isInterrupted()); + assertTrue(cs.isFailed()); + assertFalse(cs.isSuccess()); + assertEquals("As expected", cs.getMessage()); + } + } + + @Test + public void testDemandShutdownWithRunningTaskThatTerminatesGracefully() throws Exception { + scheduler = new ResourceComputationScheduler<String>(1, TimeUnit.SECONDS, bus); + scheduler.initialize(); + final CompStatus cs = new CompStatus(); + final AtomicBoolean readyForFinalChecks = new AtomicBoolean(false); + + // The following computation does not handle interruption properly + // but it has the time to terminate before the pools shut down + // so we expect to have it succeed + final TriggerableComputation tc = new TriggerableComputation(cs, "long1", false) { + @Override + protected void succeeded(Object r) { + getStatus().success("As expected"); + } + + @Override + protected void failed(Throwable t) { + getStatus().fail("failed() should not have been called."); + } + }; + bus.register(new Object() { + @Subscribe + public void check(ShutdownStatus status) { + synchronized(scheduler) { + switch (status.getState()) { + case FINISH_SUCCESS: + cs.setShutdownStatus(status); + readyForFinalChecks.compareAndSet(false, true); + // This will trigger the execution of the final tests + scheduler.notifyAll(); + break; + case FINISH_FAILED: + cs.setShutdownStatus(status); + readyForFinalChecks.compareAndSet(false, true); + scheduler.notifyAll(); + } + } + } + }); + Integer result = scheduler.call(new Callable<Integer>() { + + public Integer call() throws Exception { + scheduler.scheduleComputation(tc); + // We ask for shutdown before the task can complete + // The scheduler is configured to wait 1s while the task only takes 500ms + scheduler.demandShutdown(); + // This allows the test to make sure that the shutdown has been demanded before + // the treatment is over + tc.trigger(); + return Integer.valueOf(42); + } + }, null); + + assertEquals(Integer.valueOf(42), result); + assertTrue(scheduler.getComputedElements().isEmpty()); + + synchronized(scheduler) { + // This will wait until the computation has finished to perform the final tests + while (!readyForFinalChecks.get()) { + scheduler.wait(); + } + assertEquals(1, cs.getCallCount()); + assertFalse(cs.isInterrupted()); + assertTrue(cs.isSuccess()); + assertFalse(cs.isFailed()); + assertEquals("As expected", cs.getMessage()); + assertEquals(ShutdownState.FINISH_SUCCESS, cs.getShutdownStatus().getState()); + } + } + + @Override + @Before + public void setUp() { + bus = new EventBus(); + scheduler = new ResourceComputationScheduler<String>(10, TimeUnit.MILLISECONDS, bus); + } + + /** + * A test computation that sleeps until it's either triggered by a call to {@link #trigger()} or + * interrupted. + * + * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> + */ + private static class TriggerableComputation implements IComputation<String> { + private volatile boolean start = false; + + private final CompStatus cs; + + private final String name; + + private final boolean throwOnInterrupt; + + public TriggerableComputation(CompStatus cs, String name, boolean throwOnInterrupt) { + this.cs = cs; + this.name = name; + this.throwOnInterrupt = throwOnInterrupt; + } + + public void trigger() { + start = true; + } + + public void run() { + cs.addCall(); + while (!start) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + cs.interrupt(); + if (throwOnInterrupt) { + throw new RuntimeException("Interrupted"); + } + } + } + } + + public FutureCallback<Object> getPostTreatment() { + return new FutureCallback<Object>() { + public void onFailure(Throwable t) { + failed(t); + } + + public void onSuccess(Object r) { + succeeded(r); + } + }; + } + + public CompStatus getStatus() { + return cs; + } + + public String getKey() { + return name; + } + + /** + * Override this method to specify specific expected behaviour in tests. Update variable cs to convey + * information about succes or failure, since using junit methods (fail, assertTrue, etc.) here will + * not cause a test to fail. + * + * @param t + */ + protected void failed(Throwable t) { + cs.fail("As expected"); + } + + /** + * Override this method to specify specific expected behaviour in tests. Update variable cs to convey + * information about succes or failure, since using junit methods (fail, assertTrue, etc.) here will + * not cause a test to fail. + * + * @param t + */ + protected void succeeded(@SuppressWarnings("unused") Object r) { + cs.success("As expected"); + } + } +} diff --git a/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/suite/AllTests.java b/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/suite/AllTests.java index 560fdc18a..26969f1e9 100644 --- a/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/suite/AllTests.java +++ b/plugins/org.eclipse.emf.compare.ide.ui.tests/src/org/eclipse/emf/compare/ide/ui/tests/suite/AllTests.java @@ -27,6 +27,7 @@ import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.LocalMonitoredProxy import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.RemoteMonitoredProxyCreationListenerTest; import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.ResolutionEventsTest; import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.ResourceComputationSchedulerTest; +import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.ResourceComputationSchedulerWithEventBusTest; import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.ThreadedModelResolverGraphTest; import org.eclipse.emf.compare.ide.ui.tests.logical.resolver.ThreadedModelResolverWithCustomDependencyProviderTest; import org.eclipse.emf.compare.ide.ui.tests.structuremergeviewer.NavigatableTest; @@ -47,10 +48,10 @@ import org.junit.runners.Suite.SuiteClasses; @SuiteClasses({EMFCompareConfigurationTest.class, DependenciesTest.class, MergeActionTest.class, PseudoConflictsMergeActionTest.class, BugsTestSuite.class, NavigatableTest.class, NotLoadedFragmentNodeTest.class, NotLoadedFragmentItemTest.class, ResolutionEventsTest.class, - ResourceComputationSchedulerTest.class, ThreadedModelResolverGraphTest.class, - ThreadedModelResolverWithCustomDependencyProviderTest.class, DependencyGraphUpdaterTest.class, - GraphResolutionTest.class, EMFModelProviderTest.class, MergeAllCommandTests.class, - LocalMonitoredProxyCreationListenerTest.class, + ResourceComputationSchedulerTest.class, ResourceComputationSchedulerWithEventBusTest.class, + ThreadedModelResolverGraphTest.class, ThreadedModelResolverWithCustomDependencyProviderTest.class, + DependencyGraphUpdaterTest.class, GraphResolutionTest.class, EMFModelProviderTest.class, + MergeAllCommandTests.class, LocalMonitoredProxyCreationListenerTest.class, RemoteMonitoredProxyCreationListenerTest.class }) public class AllTests { /** diff --git a/plugins/org.eclipse.emf.compare.ide.ui/src/org/eclipse/emf/compare/ide/ui/internal/logical/resolver/ResourceComputationScheduler.java b/plugins/org.eclipse.emf.compare.ide.ui/src/org/eclipse/emf/compare/ide/ui/internal/logical/resolver/ResourceComputationScheduler.java index d922e7523..20804a0ff 100644 --- a/plugins/org.eclipse.emf.compare.ide.ui/src/org/eclipse/emf/compare/ide/ui/internal/logical/resolver/ResourceComputationScheduler.java +++ b/plugins/org.eclipse.emf.compare.ide.ui/src/org/eclipse/emf/compare/ide/ui/internal/logical/resolver/ResourceComputationScheduler.java @@ -14,6 +14,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import com.google.common.eventbus.EventBus; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -93,6 +94,9 @@ public class ResourceComputationScheduler<T> { /** Unit of the above duration. */ private final TimeUnit shutdownWaitUnit; + /** Event bus used to send state change events */ + private final EventBus eventBus; + /** * Constructor, configured to wait for tasks completion for 5 seconds (will wait at most 10 seconds). */ @@ -110,12 +114,28 @@ public class ResourceComputationScheduler<T> { * Unit to use to interpret the other parameter. */ public ResourceComputationScheduler(int shutdownWaitDuration, TimeUnit shutdownWaitUnit) { + this(shutdownWaitDuration, shutdownWaitUnit, null); + } + + /** + * Constructor. + * + * @param shutdownWaitDuration + * Time to wait for current tasks completion when shutting down the pools (will wait at most + * twice this amount of time). + * @param shutdownWaitUnit + * Unit to use to interpret the other parameter. + * @param eventBus + * The {@link EventBus} used to post events (shutdown events), can be {@code null} + */ + public ResourceComputationScheduler(int shutdownWaitDuration, TimeUnit shutdownWaitUnit, EventBus eventBus) { this.lock = new ReentrantLock(true); this.endOfTasks = lock.newCondition(); this.currentlyComputing = new HashSet<T>(); this.shutdownInProgress = new AtomicBoolean(false); this.shutdownWaitDuration = shutdownWaitDuration; this.shutdownWaitUnit = shutdownWaitUnit; + this.eventBus = eventBus; } /** @@ -171,11 +191,20 @@ public class ResourceComputationScheduler<T> { /** * If {@link #shutdownInProgress shutdown has not been requested before}, it submits a new task to * {@link #shutdownPools() shut down} {@link #computingPool} and {@link #unloadingPool}. Do nothing if - * current thread already is interrupted. + * current thread already is interrupted. If a shutdown is actually started, events will be posted to the + * scheduler's eventBus if there is one. The events will be: + * <ol> + * <li>STARTED</li> + * <li>SUCCESS if shutdown has succeeded or FAILURE if shutdown has failed</li> + * </ol> + * <b>Note</b> that these events will be sent in the calling Thread. */ public void demandShutdown() { if (!Thread.currentThread().isInterrupted()) { if (shutdownInProgress.compareAndSet(false, true)) { + if (eventBus != null) { + eventBus.post(ShutdownStatus.STARTED); + } Runnable runnable = new Runnable() { public void run() { shutdownPools(); @@ -186,10 +215,16 @@ public class ResourceComputationScheduler<T> { Futures.addCallback(listenableFuture, new FutureCallback<Object>() { public void onSuccess(Object result) { shutdownInProgress.set(false); + if (eventBus != null) { + eventBus.post(ShutdownStatus.SUCCESS); + } } public void onFailure(Throwable t) { shutdownInProgress.set(false); + if (eventBus != null) { + eventBus.post(new ShutdownStatus(t)); + } EMFCompareIDEUIPlugin.getDefault().log(t); } }); @@ -257,6 +292,16 @@ public class ResourceComputationScheduler<T> { * <li>{@link #dispose()} has not been called</li> * </ul> * </p> + * If the scheduler has an eventBus, it will post the following events: + * <ol> + * <li>SETTING_UP</li> + * <li>SCHEDULED as soon as the set-up is finished</li> + * <li>FAILURE if and only if the given callable throws an exception</li> + * <li>FINISHING as soon as the given callable has finished running (successfully or not)</li> + * <li>FINISHED as soon as the tear-down is finished</li> + * </ol> + * <b>Note</b> that these events will be sent in the Thread that ran the computation, NOT in the calling + * Thread. * * @param callable * will be executed as soon as this instance is no longer computing anything. Must not be @@ -271,9 +316,18 @@ public class ResourceComputationScheduler<T> { public synchronized <U> U call(Callable<U> callable, Runnable postTreatment) { checkNotNull(callable); try { + if (eventBus != null) { + eventBus.post(CallStatus.SETTING_UP); + } setUpComputation(); + if (eventBus != null) { + eventBus.post(CallStatus.SCHEDULED); + } return callable.call(); } catch (Exception e) { + if (eventBus != null) { + eventBus.post(new CallStatus(e)); + } if (e instanceof InterruptedException) { throw new OperationCanceledException(); } @@ -282,9 +336,18 @@ public class ResourceComputationScheduler<T> { } throw new RuntimeException(e); } finally { - tearDownComputation(); - if (postTreatment != null) { - postTreatment.run(); + if (eventBus != null) { + eventBus.post(CallStatus.FINISHING); + } + try { + tearDownComputation(); + if (postTreatment != null) { + postTreatment.run(); + } + } finally { + if (eventBus != null) { + eventBus.post(CallStatus.FINISHED); + } } } } @@ -548,4 +611,81 @@ public class ResourceComputationScheduler<T> { } } } + + public static enum ComputationState { + /** Computation is setting-up (preparing pools and so on). */ + SETTING_UP, + /** Computation is set-up and scheduled. */ + SCHEDULED, + /** Computation is over, tear-down and post-treatments are starting. */ + FINISHING, + /** Computation is over and has failed. */ + FAILED, + /** Computation is over and tear-down + post-treatments are finished. */ + FINISHED; + } + + public static class CallStatus { + public static final CallStatus SETTING_UP = new CallStatus(ComputationState.SETTING_UP); + + public static final CallStatus SCHEDULED = new CallStatus(ComputationState.SCHEDULED); + + public static final CallStatus FINISHING = new CallStatus(ComputationState.FINISHING); + + public static final CallStatus FINISHED = new CallStatus(ComputationState.FINISHED); + + private final Throwable cause; + + private final ComputationState state; + + private CallStatus(ComputationState state) { + this.state = state; + this.cause = null; + } + + private CallStatus(Throwable cause) { + this.state = ComputationState.FAILED; + this.cause = cause; + } + + public Throwable getCause() { + return cause; + } + + public ComputationState getState() { + return state; + } + } + + public static enum ShutdownState { + STARTED, FINISH_FAILED, FINISH_SUCCESS; + } + + public static class ShutdownStatus { + public static final ShutdownStatus STARTED = new ShutdownStatus(ShutdownState.STARTED); + + public static final ShutdownStatus SUCCESS = new ShutdownStatus(ShutdownState.FINISH_SUCCESS); + + private final Throwable cause; + + private final ShutdownState state; + + private ShutdownStatus(ShutdownState state) { + this.state = state; + this.cause = null; + } + + private ShutdownStatus(Throwable cause) { + this.state = ShutdownState.FINISH_FAILED; + this.cause = cause; + } + + public Throwable getCause() { + return cause; + } + + public ShutdownState getState() { + return state; + } + } } |