Bug 532273 - [DataBinding] Race condition between Binding.doUpdate and
Observable.dispose with multiple realms

Change-Id: I4136f4077267c28e8345df082f68d1589cdfba92
Signed-off-by: Conrad Groth <info@conrad-groth.de>
diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/Binding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/Binding.java
index b8eaaef..37268ca 100644
--- a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/Binding.java
+++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/Binding.java
@@ -181,4 +181,18 @@
 		return Observables.staticObservableList(context.getValidationRealm(),
 				Collections.singletonList(model));
 	}
+
+	/**
+	 * @param observable the observable that is checked for disposal before the
+	 * runnable gets executed.
+	 * @param r the Runnable to execute in the observable's realm.
+	 * @since 1.6
+	 */
+	protected final void execAfterDisposalCheck(final IObservable observable, final Runnable r) {
+		observable.getRealm().exec(() -> {
+			if (!observable.isDisposed()) {
+				r.run();
+			}
+		});
+	}
 }
diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ListBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ListBinding.java
index a0fac10..2326763 100644
--- a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ListBinding.java
+++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ListBinding.java
@@ -85,7 +85,7 @@
 	@Override
 	protected void postInit() {
 		if (modelToTarget.getUpdatePolicy() == UpdateListStrategy.POLICY_UPDATE) {
-			getModel().getRealm().exec(() -> {
+			execAfterDisposalCheck(getModel(), () -> {
 				((IObservableList) getModel()).addListChangeListener(modelChangeListener);
 				updateModelToTarget();
 			});
@@ -94,7 +94,7 @@
 		}
 
 		if (targetToModel.getUpdatePolicy() == UpdateListStrategy.POLICY_UPDATE) {
-			getTarget().getRealm().exec(() -> {
+			execAfterDisposalCheck(getTarget(), () -> {
 				((IObservableList) getTarget()).addListChangeListener(targetChangeListener);
 				if (modelToTarget.getUpdatePolicy() == UpdateListStrategy.POLICY_NEVER) {
 					// we have to sync from target to model, if the other
@@ -113,7 +113,7 @@
 	@Override
 	public void updateModelToTarget() {
 		final IObservableList modelList = (IObservableList) getModel();
-		modelList.getRealm().exec(() -> {
+		execAfterDisposalCheck(modelList, () -> {
 			ListDiff diff = Diffs.computeListDiff(Collections.EMPTY_LIST, modelList);
 			doUpdate(modelList, (IObservableList) getTarget(), diff, modelToTarget, true, true);
 		});
@@ -122,7 +122,7 @@
 	@Override
 	public void updateTargetToModel() {
 		final IObservableList targetList = (IObservableList) getTarget();
-		targetList.getRealm().exec(() -> {
+		execAfterDisposalCheck(targetList, () -> {
 			ListDiff diff = Diffs.computeListDiff(Collections.EMPTY_LIST, targetList);
 			doUpdate(targetList, (IObservableList) getModel(), diff, targetToModel, true, true);
 		});
@@ -156,7 +156,7 @@
 					 */
 					diff.getDifferences();
 				}
-				destination.getRealm().exec(() -> {
+				execAfterDisposalCheck(destination, () -> {
 					if (destination == getTarget()) {
 						updatingTarget = true;
 					} else {
diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/SetBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/SetBinding.java
index dc6279e..30d65f0 100644
--- a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/SetBinding.java
+++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/SetBinding.java
@@ -87,7 +87,7 @@
 	@Override
 	protected void postInit() {
 		if (modelToTarget.getUpdatePolicy() == UpdateSetStrategy.POLICY_UPDATE) {
-			getModel().getRealm().exec(() -> {
+			execAfterDisposalCheck(getModel(), () -> {
 				((IObservableSet) getModel()).addSetChangeListener(modelChangeListener);
 				updateModelToTarget();
 			});
@@ -96,7 +96,7 @@
 		}
 
 		if (targetToModel.getUpdatePolicy() == UpdateSetStrategy.POLICY_UPDATE) {
-			getTarget().getRealm().exec(() -> {
+			execAfterDisposalCheck(getTarget(), () -> {
 				((IObservableSet) getTarget()).addSetChangeListener(targetChangeListener);
 				if (modelToTarget.getUpdatePolicy() == UpdateSetStrategy.POLICY_NEVER) {
 					// we have to sync from target to model, if the other
@@ -114,7 +114,7 @@
 	@Override
 	public void updateModelToTarget() {
 		final IObservableSet modelSet = (IObservableSet) getModel();
-		modelSet.getRealm().exec(() -> {
+		execAfterDisposalCheck(modelSet, () -> {
 			SetDiff diff = Diffs.computeSetDiff(Collections.EMPTY_SET, modelSet);
 			doUpdate(modelSet, (IObservableSet) getTarget(), diff, modelToTarget, true, true);
 		});
@@ -123,7 +123,7 @@
 	@Override
 	public void updateTargetToModel() {
 		final IObservableSet targetSet = (IObservableSet) getTarget();
-		targetSet.getRealm().exec(() -> {
+		execAfterDisposalCheck(targetSet, () -> {
 			SetDiff diff = Diffs.computeSetDiff(Collections.EMPTY_SET, targetSet);
 			doUpdate(targetSet, (IObservableSet) getModel(), diff, targetToModel, true, true);
 		});
@@ -160,7 +160,7 @@
 			diff.getAdditions();
 			diff.getRemovals();
 		}
-		destination.getRealm().exec(() -> {
+		execAfterDisposalCheck(destination, () -> {
 			if (destination == getTarget()) {
 				updatingTarget = true;
 			} else {
diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ValueBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ValueBinding.java
index 35ce5ad..87eebd3 100644
--- a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ValueBinding.java
+++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ValueBinding.java
@@ -159,7 +159,7 @@
 		if (policy == UpdateValueStrategy.POLICY_ON_REQUEST && !explicit)
 			return;
 
-		source.getRealm().exec(() -> {
+		execAfterDisposalCheck(source, () -> {
 			boolean destinationRealmReached = false;
 			final MultiStatus multiStatus = BindingStatus.ok();
 			try {
@@ -190,7 +190,7 @@
 
 				// Set value
 				destinationRealmReached = true;
-				destination.getRealm().exec(() -> {
+				execAfterDisposalCheck(destination, () -> {
 					if (destination == target) {
 						updatingTarget = true;
 					} else {
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/observable/ThreadRealm.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/observable/ThreadRealm.java
index 351b73e..ec6cb85 100755
--- a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/observable/ThreadRealm.java
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/observable/ThreadRealm.java
@@ -28,6 +28,7 @@
     private final LinkedList<Runnable> queue = new LinkedList<Runnable>();
 
     private volatile boolean block;
+	private volatile boolean doProcess;
 
     /**
      * Initializes the realm.
@@ -90,12 +91,15 @@
 
         try {
             synchronized (queue) {
+				doProcess = true;
+				queue.notifyAll();
                 while (!queue.isEmpty()) {
                     if (!block)
                         throw new IllegalStateException(
                                 "Cannot process queue, ThreadRealm is not blocking on its thread");
                     queue.wait();
                 }
+				doProcess = false;
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -127,13 +131,14 @@
 
             while (block) {
                 Runnable runnable = null;
-                synchronized (queue) {
-                    if (queue.isEmpty()) {
-                        queue.wait();
-                    } else {
-                        runnable = queue.getFirst();
-                    }
+				while (!doProcess) {
+					synchronized (queue) {
+						queue.wait();
+					}
                 }
+				synchronized (queue) {
+					runnable = queue.peek();
+				}
 
                 if (runnable != null) {
                     safeRun(runnable);
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/DifferentRealmsBindingTest.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/DifferentRealmsBindingTest.java
index f2d141a..b35985c 100644
--- a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/DifferentRealmsBindingTest.java
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/DifferentRealmsBindingTest.java
@@ -20,9 +20,9 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.eclipse.core.databinding.DataBindingContext;
+import org.eclipse.core.databinding.observable.IObservable;
 import org.eclipse.core.databinding.observable.list.ComputedList;
 import org.eclipse.core.databinding.observable.list.IObservableList;
-import org.eclipse.core.databinding.observable.list.ObservableList;
 import org.eclipse.core.databinding.observable.list.WritableList;
 import org.eclipse.core.databinding.observable.set.ComputedSet;
 import org.eclipse.core.databinding.observable.set.IObservableSet;
@@ -41,8 +41,10 @@
 
 public class DifferentRealmsBindingTest {
 
-	ThreadRealm targetAndModelRealm = new ThreadRealm();
 	ThreadRealm validationRealm = new ThreadRealm();
+	ThreadRealm targetRealm = new ThreadRealm();
+	ThreadRealm modelRealm = new ThreadRealm();
+	ThreadRealm mainThread = new ThreadRealm();
 
 	List<IStatus> errorStatusses = new ArrayList<>();
 
@@ -59,54 +61,65 @@
 	@Before
 	public void setUp() throws Exception {
 		errorStatusses.clear();
-		new Thread() {
-			@Override
-			public void run() {
-				targetAndModelRealm.init(Thread.currentThread());
-				targetAndModelRealm.block();
-			}
-		}.start();
+		initRealm(targetRealm, true);
+		initRealm(modelRealm, true);
+		initRealm(validationRealm, false);
+		mainThread.init(Thread.currentThread());
 
-		validationRealm.init(Thread.currentThread());
 		dbc = new DataBindingContext(validationRealm);
 		Policy.setLog(logger);
 	}
 
+	private void initRealm(final ThreadRealm realm, final boolean block) {
+		new Thread() {
+			@Override
+			public void run() {
+				realm.init(Thread.currentThread());
+				if (block) {
+					realm.block();
+				}
+			}
+		}.start();
+		if (block) {
+			realm.waitUntilBlocking();
+		}
+	}
+
 	@After
 	public void tearDown() throws Exception {
-		dbc.dispose();
+		validationRealm.exec(() -> dbc.dispose());
 	}
 
 	@Test
 	public void testListBindingValidationRealm() throws Throwable {
-		final ObservableList model = new WritableList(targetAndModelRealm);
-		final ObservableList target = new WritableList(targetAndModelRealm);
+		final IObservableList<?> model = new WritableList<>(targetRealm);
+		final IObservableList<?> target = new WritableList<>(targetRealm);
 
 		dbc.bindList(target, model);
-		targetAndModelRealm.waitUntilBlocking();
-		targetAndModelRealm.processQueue();
-		targetAndModelRealm.unblock();
+		targetRealm.waitUntilBlocking();
+		targetRealm.processQueue();
+		targetRealm.unblock();
 		assertTrue(errorStatusses.toString(), errorStatusses.isEmpty());
 	}
 
 	@Test
 	public void testSetBindingValidationRealm() throws Throwable {
-		final ObservableSet model = new WritableSet(targetAndModelRealm);
-		final ObservableSet target = new WritableSet(targetAndModelRealm);
+		final IObservableSet<?> model = new WritableSet<>(targetRealm);
+		final IObservableSet<?> target = new WritableSet<>(targetRealm);
 
 		dbc.bindSet(target, model);
-		targetAndModelRealm.waitUntilBlocking();
-		targetAndModelRealm.processQueue();
-		targetAndModelRealm.unblock();
+		targetRealm.waitUntilBlocking();
+		targetRealm.processQueue();
+		targetRealm.unblock();
 		assertTrue(errorStatusses.toString(), errorStatusses.isEmpty());
 	}
 
 	@Test
 	public void testBindingCanBeCreatedOutsideOfValidationRealm() throws Exception {
-		final ObservableSet<String> model = new WritableSet<>(targetAndModelRealm);
-		final ObservableSet<String> target = new WritableSet<>(targetAndModelRealm);
+		final ObservableSet<String> model = new WritableSet<>(targetRealm);
+		final ObservableSet<String> target = new WritableSet<>(targetRealm);
 
-		targetAndModelRealm.unblock();
+		targetRealm.unblock();
 
 		AtomicReference<Exception> exceptionOccured = new AtomicReference<>();
 		Thread t = new Thread() {
@@ -127,61 +140,119 @@
 
 	@Test
 	public void testBindComputedListToWritableListInDifferentRealm() {
-		// The validationRealm is the current realm.
-		final IObservableValue<String> modelValue = new WritableValue<>(validationRealm);
-		final IObservableList<String> model = new ComputedList<String>(validationRealm) {
+		final IObservableValue<String> modelValue = new WritableValue<>(mainThread);
+		final IObservableList<String> model = new ComputedList<String>(mainThread) {
 			@Override
 			protected List<String> calculate() {
 				return Collections.singletonList(modelValue.getValue());
 			}
 		};
-		final IObservableList<String> target = new WritableList<>(targetAndModelRealm);
+		final IObservableList<String> target = new WritableList<>(targetRealm);
 
 		dbc.bindList(target, model);
 		modelValue.setValue("Test");
-		targetAndModelRealm.waitUntilBlocking();
-		targetAndModelRealm.processQueue();
-		targetAndModelRealm.unblock();
+		targetRealm.waitUntilBlocking();
+		targetRealm.processQueue();
+		targetRealm.unblock();
 		assertTrue(errorStatusses.toString(), errorStatusses.isEmpty());
 	}
 
 	@Test
 	public void testBindComputedSetToWritableSetInDifferentRealm() {
-		// The validationRealm is the current realm.
-		final IObservableValue<String> modelValue = new WritableValue<>(validationRealm);
-		final IObservableSet<String> model = new ComputedSet<String>(validationRealm) {
+		final IObservableValue<String> modelValue = new WritableValue<>(mainThread);
+		final IObservableSet<String> model = new ComputedSet<String>(mainThread) {
 			@Override
 			protected Set<String> calculate() {
 				return Collections.singleton(modelValue.getValue());
 			}
 		};
-		final IObservableSet<String> target = new WritableSet<>(targetAndModelRealm);
+		final IObservableSet<String> target = new WritableSet<>(targetRealm);
 
 		dbc.bindSet(target, model);
 		modelValue.setValue("Test");
-		targetAndModelRealm.waitUntilBlocking();
-		targetAndModelRealm.processQueue();
-		targetAndModelRealm.unblock();
+		targetRealm.waitUntilBlocking();
+		targetRealm.processQueue();
+		targetRealm.unblock();
 		assertTrue(errorStatusses.toString(), errorStatusses.isEmpty());
 	}
 
 	@Test
 	public void testBindComputedValueToWritableValueInDifferentRealm() {
-		// The validationRealm is the current realm.
-		final IObservableValue<String> modelValue = new WritableValue<>(validationRealm);
-		final IObservableValue<String> model = new ComputedValue<String>(validationRealm) {
+		final IObservableValue<String> modelValue = new WritableValue<>(mainThread);
+		final IObservableValue<String> model = new ComputedValue<String>(mainThread) {
 			@Override
 			protected String calculate() {
 				return modelValue.getValue();
 			}
 		};
-		final IObservableValue<String> target = new WritableValue<>(targetAndModelRealm);
+		final IObservableValue<String> target = new WritableValue<>(targetRealm);
 
 		dbc.bindValue(target, model);
 		modelValue.setValue("Test");
-		targetAndModelRealm.waitUntilBlocking();
-		targetAndModelRealm.processQueue();
-		targetAndModelRealm.unblock();
+		targetRealm.waitUntilBlocking();
+		targetRealm.processQueue();
+		targetRealm.unblock();
 		assertTrue(errorStatusses.toString(), errorStatusses.isEmpty());
 	}
+
+	@Test
+	public void testSetBindingUpdatesDontInterferWithObservableDisposing() throws Exception {
+		final IObservableSet<String> model = new WritableSet<>(modelRealm);
+		final IObservableSet<String> target = new WritableSet<>(targetRealm);
+		dbc.bindSet(target, model);
+
+		waitForBindingInitiated();
+
+		modelRealm.exec(() -> model.add("one"));
+
+		testDisposeRaceCondition(target);
+	}
+
+	@Test
+	public void testListBindingUpdatesDontInterferWithObservableDisposing() throws Exception {
+		final IObservableList<String> model = new WritableList<>(modelRealm);
+		final IObservableList<String> target = new WritableList<>(targetRealm);
+		dbc.bindList(target, model);
+
+		waitForBindingInitiated();
+
+		modelRealm.exec(() -> model.add("one"));
+
+		testDisposeRaceCondition(target);
+	}
+
+	@Test
+	public void testValueBindingUpdatesDontInterferWithObservableDisposing() throws Exception {
+		final IObservableValue<String> model = new WritableValue<>(modelRealm);
+		final IObservableValue<String> target = new WritableValue<>(targetRealm);
+		dbc.bindValue(target, model);
+
+		waitForBindingInitiated();
+
+		modelRealm.exec(() -> model.setValue("one"));
+
+		testDisposeRaceCondition(target);
+	}
+
+	private void testDisposeRaceCondition(final IObservable target) {
+		modelRealm.processQueue();
+		targetRealm.waitUntilBlocking();
+
+		target.dispose();
+
+		modelRealm.unblock();
+		targetRealm.processQueue();
+		targetRealm.unblock();
+		validationRealm.unblock();
+
+		assertTrue(errorStatusses.toString(), errorStatusses.isEmpty());
+	}
+
+	private void waitForBindingInitiated() {
+		modelRealm.waitUntilBlocking();
+		modelRealm.processQueue();
+
+		targetRealm.waitUntilBlocking();
+		targetRealm.processQueue();
+	}
 }