Bug 578386 - Deadlock between ApiBaseline.doDispose() and
ApiBaselineManager.initializeStateCache()

An attempt to remove locks between ApiBaseline and ApiBaselineManager
for the baseline loading and disposing.

Change-Id: Ia6e5e5242a99cd84e654db7efb9a7c6c89c9f0fa
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
Reviewed-on: https://git.eclipse.org/r/c/pde/eclipse.pde.ui/+/190028
Tested-by: PDE Bot <pde-bot@eclipse.org>
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiBaselineManager.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiBaselineManager.java
index 73f7205..4a8b6bc 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiBaselineManager.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiBaselineManager.java
@@ -25,10 +25,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -107,10 +105,10 @@
 	 * The main cache for the manager. The form of the cache is:
 	 *
 	 * <pre>
-	 * HashMap<String(baselineid), {@link IApiBaseline}>
+	 * Map<String(baselineid), {@link IApiBaseline}>
 	 * </pre>
 	 */
-	private HashMap<String, IApiBaseline> baselinecache = null;
+	private volatile ConcurrentHashMap<String, IApiBaseline> baselinecache;
 
 	/**
 	 * Cache of baseline names to the location with their infos in it
@@ -127,7 +125,7 @@
 	/**
 	 * The current workspace baseline
 	 */
-	private IApiBaseline workspacebaseline = null;
+	private volatile IApiBaseline workspacebaseline;
 
 	/**
 	 * The default save location for persisting the cache from this manager.
@@ -137,7 +135,7 @@
 	/**
 	 * If the cache of baselines needs to be saved or not.
 	 */
-	private boolean fNeedsSaving = false;
+	private volatile boolean fNeedsSaving;
 
 	/**
 	 * The singleton instance
@@ -168,19 +166,22 @@
 	}
 
 	@Override
-	public synchronized IApiBaseline getApiBaseline(String name) {
+	public IApiBaseline getApiBaseline(String name) {
+		if (name == null) {
+			return null;
+		}
 		initializeStateCache();
 		return baselinecache.get(name);
 	}
 
 	@Override
-	public synchronized IApiBaseline[] getApiBaselines() {
+	public IApiBaseline[] getApiBaselines() {
 		initializeStateCache();
-		return baselinecache.values().toArray(new IApiBaseline[baselinecache.size()]);
+		return baselinecache.values().toArray(new IApiBaseline[0]);
 	}
 
 	@Override
-	public synchronized void addApiBaseline(IApiBaseline newbaseline) {
+	public void addApiBaseline(IApiBaseline newbaseline) {
 		if (newbaseline != null) {
 			initializeStateCache();
 			baselinecache.put(newbaseline.getName(), newbaseline);
@@ -192,33 +193,36 @@
 	}
 
 	@Override
-	public synchronized boolean removeApiBaseline(String name) {
-		if (name != null) {
-			initializeStateCache();
-			IApiBaseline baseline = baselinecache.remove(name);
-			if (baseline != null) {
-				baseline.dispose();
-				boolean success = true;
-				if (savelocation == null) {
-					return success;
-				}
-				// remove from filesystem
-				File file = savelocation.append(name + BASELINE_FILE_EXTENSION).toFile();
-				if (file.exists()) {
-					try {
-						success &= Files.deleteIfExists(file.toPath());
-					} catch (IOException e) {
-						ApiPlugin.log(e);
-					}
-				}
-				fNeedsSaving = true;
-
-				// flush the model cache
-				ApiModelCache.getCache().removeElementInfo(baseline);
+	public boolean removeApiBaseline(String name) {
+		if (name == null) {
+			return false;
+		}
+		initializeStateCache();
+		IApiBaseline baseline = baselinecache.remove(name);
+		if (baseline == null) {
+			return false;
+		}
+		synchronized (this) {
+			baseline.dispose();
+			boolean success = true;
+			if (savelocation == null) {
 				return success;
 			}
+			// remove from filesystem
+			File file = savelocation.append(name + BASELINE_FILE_EXTENSION).toFile();
+			if (file.exists()) {
+				try {
+					success &= Files.deleteIfExists(file.toPath());
+				} catch (IOException e) {
+					ApiPlugin.log(e);
+				}
+			}
+			fNeedsSaving = true;
+
+			// flush the model cache
+			ApiModelCache.getCache().removeElementInfo(baseline);
+			return success;
 		}
-		return false;
 	}
 
 	/**
@@ -228,7 +232,7 @@
 	 * @param baseline the given baseline
 	 * @throws CoreException if an exception occurs while loading baseline infos
 	 */
-	public void loadBaselineInfos(IApiBaseline baseline) throws CoreException {
+	public void loadBaselineInfos(ApiBaseline baseline) throws CoreException {
 		initializeStateCache();
 		if (isBaselineLoaded(baseline)) {
 			return;
@@ -238,7 +242,7 @@
 			File file = new File(filename);
 			if (file.exists()) {
 				try (FileInputStream inputStream = new FileInputStream(file)) {
-					restoreBaseline(baseline, inputStream);
+					baseline.restoreFrom(inputStream);
 				} catch (IOException e) {
 					ApiPlugin.log(e);
 				}
@@ -258,31 +262,48 @@
 	 * @throws FactoryConfigurationError
 	 * @throws ParserConfigurationException
 	 */
-	private synchronized void initializeStateCache() {
-		long time = System.currentTimeMillis();
-		if (baselinecache == null) {
-			handlecache = new HashMap<>(8);
-			hasinfos = ConcurrentHashMap.newKeySet(8);
-			baselinecache = new LinkedHashMap<>(8);
-			if (!ApiPlugin.isRunningInFramework()) {
-				return;
-			}
-			File[] baselines = savelocation.toFile().listFiles((FileFilter) pathname -> pathname.getName().endsWith(BASELINE_FILE_EXTENSION));
-			if (baselines != null) {
-				IApiBaseline newbaseline = null;
-				for (File baseline : baselines) {
-					if (baseline.exists()) {
-						newbaseline = new ApiBaseline(new Path(baseline.getName()).removeFileExtension().toString());
-						handlecache.put(newbaseline.getName(), baseline.getAbsolutePath());
-						baselinecache.put(newbaseline.getName(), newbaseline);
-					}
+	private void initializeStateCache() {
+		if (baselinecache != null) {
+			return;
+		}
+		if (!ApiPlugin.isRunningInFramework()) {
+			synchronized (this) {
+				if (baselinecache == null) {
+					handlecache = new ConcurrentHashMap<>(8);
+					hasinfos = ConcurrentHashMap.newKeySet(8);
+					baselinecache = new ConcurrentHashMap<>(8);
 				}
 			}
-			String def = getDefaultProfilePref();
-			IApiBaseline baseline = baselinecache.get(def);
-			defaultbaseline = (baseline != null ? def : null);
-			if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
-				System.out.println("Time to initialize state cache: " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
+			return;
+		}
+
+		long time = System.currentTimeMillis();
+		synchronized (this) {
+			if (baselinecache == null) {
+				handlecache = new ConcurrentHashMap<>(8);
+				hasinfos = ConcurrentHashMap.newKeySet(8);
+				ConcurrentHashMap<String, IApiBaseline> bcache = new ConcurrentHashMap<>(8);
+				File[] baselines = savelocation.toFile().listFiles((FileFilter) pathname -> pathname.getName().endsWith(BASELINE_FILE_EXTENSION));
+				if (baselines != null) {
+					IApiBaseline newbaseline = null;
+					for (File baseline : baselines) {
+						if (baseline.exists()) {
+							newbaseline = new ApiBaseline(new Path(baseline.getName()).removeFileExtension().toString());
+							handlecache.put(newbaseline.getName(), baseline.getAbsolutePath());
+							bcache.put(newbaseline.getName(), newbaseline);
+						}
+					}
+				}
+				String def = getDefaultProfilePref();
+				if (def != null && bcache.get(def) != null) {
+					defaultbaseline = def;
+				} else {
+					defaultbaseline = null;
+				}
+				baselinecache = bcache;
+				if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
+					System.out.println("Time to initialize state cache: " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
+				}
 			}
 		}
 	}
@@ -420,12 +441,14 @@
 	 * Restore a baseline from the given input stream (persisted baseline).
 	 *
 	 * @param baseline the given baseline to restore
-	 * @param stream the given input stream
+	 * @param stream   the given input stream
 	 * @throws CoreException if unable to restore the baseline
+	 * @return restored baseline components or null if restore didn't work
 	 */
-	private void restoreBaseline(IApiBaseline baseline, InputStream stream) throws CoreException {
+	public IApiComponent[] readBaselineComponents(ApiBaseline baseline, InputStream stream) throws CoreException {
 		long start = System.currentTimeMillis();
 		DocumentBuilder parser = null;
+		IApiComponent[] restored = null;
 		try {
 			parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
 			parser.setErrorHandler(new DefaultHandler());
@@ -482,23 +505,15 @@
 						}
 					}
 				}
-				baseline.addApiComponents(components.toArray(new IApiComponent[components.size()]));
+				restored = components.toArray(new IApiComponent[components.size()]);
 			}
 		} catch (IOException | SAXException e) {
 			abort("Error restoring API baseline", e); //$NON-NLS-1$
-		} finally {
-			try {
-				stream.close();
-			} catch (IOException io) {
-				ApiPlugin.log(io);
-			}
-		}
-		if (baseline == null) {
-			abort("Invalid baseline description", null); //$NON-NLS-1$
 		}
 		if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
 			System.out.println("Time to restore a persisted baseline : " + (System.currentTimeMillis() - start) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
 		}
+		return restored;
 	}
 
 	@Override
@@ -541,7 +556,7 @@
 	 *         otherwise
 	 */
 	public boolean isExistingProfileName(String name) {
-		if (baselinecache == null) {
+		if (baselinecache == null || name == null) {
 			return false;
 		}
 		return baselinecache.containsKey(name);
@@ -595,9 +610,13 @@
 	}
 
 	@Override
-	public synchronized IApiBaseline getDefaultApiBaseline() {
+	public IApiBaseline getDefaultApiBaseline() {
 		initializeStateCache();
-		return baselinecache.get(defaultbaseline);
+		String defbaseline = defaultbaseline;
+		if (defbaseline == null) {
+			return null;
+		}
+		return baselinecache.get(defbaseline);
 	}
 
 	@Override
@@ -607,33 +626,46 @@
 	}
 
 	@Override
-	public synchronized IApiBaseline getWorkspaceBaseline() {
-		if (ApiPlugin.isRunningInFramework()) {
-			if (this.workspacebaseline == null) {
-				try {
-					this.workspacebaseline = createWorkspaceBaseline();
-				} catch (CoreException e) {
-					ApiPlugin.log(e);
-				}
-			}
-			return this.workspacebaseline;
+	public IApiBaseline getWorkspaceBaseline() {
+		if (!ApiPlugin.isRunningInFramework()) {
+			return null;
 		}
-		return null;
+		if (this.workspacebaseline == null) {
+			try {
+				synchronized (this) {
+					if (this.workspacebaseline == null) {
+						this.workspacebaseline = createWorkspaceBaseline();
+					}
+				}
+			} catch (CoreException e) {
+				ApiPlugin.log(e);
+			}
+		}
+		return this.workspacebaseline;
 	}
 
 	/**
 	 * Disposes the workspace baseline such that a new one will be created on
 	 * the next request.
 	 */
-	synchronized void disposeWorkspaceBaseline() {
-		if (workspacebaseline != null) {
-			if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
-				System.out.println("disposing workspace baseline"); //$NON-NLS-1$
+	void disposeWorkspaceBaseline() {
+		if (workspacebaseline == null) {
+			return;
+		}
+		Job.getJobManager().cancel(ApiAnalysisJob.class);
+		IApiBaseline oldBaseline = null;
+		synchronized (this) {
+			if (workspacebaseline != null) {
+				if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
+					System.out.println("disposing workspace baseline"); //$NON-NLS-1$
+				}
+				oldBaseline = workspacebaseline;
+				StubApiComponent.disposeAllCaches();
+				workspacebaseline = null;
 			}
-			Job.getJobManager().cancel(ApiAnalysisJob.class);
-			workspacebaseline.dispose();
-			StubApiComponent.disposeAllCaches();
-			workspacebaseline = null;
+		}
+		if (oldBaseline != null) {
+			oldBaseline.dispose();
 		}
 	}
 
@@ -692,7 +724,7 @@
 	}
 
 	@Override
-	public synchronized IApiComponent getWorkspaceComponent(String symbolicName) {
+	public IApiComponent getWorkspaceComponent(String symbolicName) {
 		IApiBaseline baseline = getWorkspaceBaseline();
 		if (baseline != null) {
 			return baseline.getApiComponent(symbolicName);
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/ApiBaseline.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/ApiBaseline.java
index 2caca5a..656cb6f 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/ApiBaseline.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/ApiBaseline.java
@@ -168,6 +168,8 @@
 
 	private volatile boolean disposed;
 
+	private volatile boolean restored;
+
 	/**
 	 * Constructs a new API baseline with the given name.
 	 *
@@ -183,10 +185,10 @@
 	/**
 	 * Constructs a new API baseline with the given attributes.
 	 *
-	 * @param name baseline name
+	 * @param name          baseline name
 	 * @param eeDescription execution environment description file
 	 * @throws CoreException if unable to create a baseline with the given
-	 *             attributes
+	 *                       attributes
 	 */
 	public ApiBaseline(String name, File eeDescription) throws CoreException {
 		this(name, eeDescription, null);
@@ -195,11 +197,11 @@
 	/**
 	 * Constructs a new API baseline with the given attributes.
 	 *
-	 * @param name baseline name
+	 * @param name          baseline name
 	 * @param eeDescription execution environment description file
-	 * @param location the given baseline location
+	 * @param location      the given baseline location
 	 * @throws CoreException if unable to create a baseline with the given
-	 *             attributes
+	 *                       attributes
 	 */
 	public ApiBaseline(String name, File eeDescription, String location) throws CoreException {
 		this(name);
@@ -302,7 +304,7 @@
 	/**
 	 * Initializes this baseline from the given properties.
 	 *
-	 * @param profile OGSi profile properties
+	 * @param profile     OGSi profile properties
 	 * @param description execution environment description
 	 * @throws CoreException if unable to initialize
 	 */
@@ -379,7 +381,7 @@
 	 * @param component
 	 */
 	protected void addComponent(IApiComponent component) {
-		if (component == null) {
+		if (isDisposed() || component == null) {
 			return;
 		}
 		if (fComponentsById == null) {
@@ -401,15 +403,15 @@
 			} else {
 				TreeSet<IApiComponent> allComponents = new TreeSet<>(
 						(comp1, comp2) -> {
-							if (comp2.getVersion().equals(comp1.getVersion())) {
-								if (comp2.getVersion().contains("JavaSE")) { //$NON-NLS-1$
-									ApiPlugin.logInfoMessage("Multiple locations for the same Java = " //$NON-NLS-1$
-											+ comp1.getLocation() + comp2.getLocation());
-								}
-								return 0;
-							}
-							return new Version(comp2.getVersion()).compareTo(new Version(comp1.getVersion()));
-						});
+					if (comp2.getVersion().equals(comp1.getVersion())) {
+						if (comp2.getVersion().contains("JavaSE")) { //$NON-NLS-1$
+							ApiPlugin.logInfoMessage("Multiple locations for the same Java = " //$NON-NLS-1$
+									+ comp1.getLocation() + comp2.getLocation());
+						}
+						return 0;
+					}
+					return new Version(comp2.getVersion()).compareTo(new Version(comp1.getVersion()));
+				});
 				allComponents.add(comp);
 				allComponents.add(component);
 				fAllComponentsById.put(component.getSymbolicName(), allComponents);
@@ -428,6 +430,9 @@
 
 	@Override
 	public void addApiComponents(IApiComponent[] components) throws CoreException {
+		if (isDisposed()) {
+			return;
+		}
 		HashSet<String> ees = new HashSet<>();
 		for (IApiComponent apiComponent : components) {
 			BundleComponent component = (BundleComponent) apiComponent;
@@ -548,6 +553,10 @@
 	@Override
 	public IApiComponent[] getApiComponents() {
 		loadBaselineInfos();
+		return getAlreadyLoadedApiComponents();
+	}
+
+	protected IApiComponent[] getAlreadyLoadedApiComponents() {
 		Map<String, IApiComponent> componentsById = fComponentsById;
 		if (disposed || componentsById == null) {
 			return EMPTY_COMPONENTS;
@@ -749,23 +758,52 @@
 	 * is accessed
 	 */
 	private void loadBaselineInfos() {
-		if (disposed) {
+		if (disposed || restored) {
 			return;
 		}
 		ApiBaselineManager manager = ApiBaselineManager.getManager();
 		if (fComponentsById != null && manager.isBaselineLoaded(this)) {
 			return;
 		}
-		synchronized (this) {
-			try {
-				manager.loadBaselineInfos(this);
-			} catch (CoreException ce) {
-				ApiPlugin.log(ce);
-			}
+		if (disposed || restored) {
+			return;
+		}
+		try {
+			manager.loadBaselineInfos(this);
+		} catch (CoreException ce) {
+			ApiPlugin.log(ce);
 		}
 	}
 
 	/**
+	 * Restore a baseline from the given input stream (persisted baseline).
+	 *
+	 * @param stream the given input stream, will be closed by caller
+	 * @throws CoreException if unable to restore the baseline
+	 */
+	public void restoreFrom(InputStream stream) throws CoreException {
+		if (disposed || restored) {
+			return;
+		}
+		IApiComponent[] components = ApiBaselineManager.getManager().readBaselineComponents(this, stream);
+		if (components == null) {
+			restored = true;
+			return;
+		}
+		synchronized (this) {
+			if (disposed || restored) {
+				for (IApiComponent component : components) {
+					component.dispose();
+				}
+				return;
+			}
+			this.addApiComponents(components);
+			restored = true;
+		}
+	}
+
+
+	/**
 	 * Returns all errors in the state.
 	 *
 	 * @return state errors
@@ -832,11 +870,15 @@
 		if (disposed) {
 			return;
 		}
+		IApiComponent[] components;
+		synchronized (this) {
+			components = getAlreadyLoadedApiComponents();
+			disposed = true;
+		}
+		clearCachedElements();
 		if (ApiPlugin.isRunningInFramework()) {
 			JavaRuntime.removeVMInstallChangedListener(this);
 		}
-		clearCachedElements();
-		IApiComponent[] components = getApiComponents();
 		for (IApiComponent component2 : components) {
 			component2.dispose();
 		}
@@ -862,7 +904,6 @@
 			}
 			fSystemLibraryComponentList = new ArrayList<>();
 		}
-		disposed = true;
 	}
 
 	/**
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/WorkspaceBaseline.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/WorkspaceBaseline.java
index 046d62e..d7c1642 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/WorkspaceBaseline.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/model/WorkspaceBaseline.java
@@ -83,6 +83,9 @@
 
 	@Override
 	public void addApiComponents(IApiComponent[] components) throws CoreException {
+		if (isDisposed()) {
+			return;
+		}
 		HashSet<String> ees = new HashSet<>();
 		for (IApiComponent apiComponent : components) {
 			BundleComponent component = (BundleComponent) apiComponent;