-
Notifications
You must be signed in to change notification settings - Fork 54
Fix deadlock #937 #939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Fix deadlock #937 #939
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| import java.net.URI; | ||
| import java.util.*; | ||
| import java.util.function.Consumer; | ||
| import org.eclipse.equinox.p2.core.IAgentLocation; | ||
| import org.eclipse.equinox.p2.core.IProvisioningAgent; | ||
| import org.eclipse.equinox.p2.core.spi.IAgentService; | ||
|
|
@@ -28,14 +29,14 @@ | |
| /** | ||
| * Represents a p2 agent instance. | ||
| */ | ||
| public class ProvisioningAgent implements IProvisioningAgent, ServiceTrackerCustomizer<IAgentServiceFactory, Object> { | ||
| public class ProvisioningAgent implements IProvisioningAgent { | ||
|
|
||
| private final Map<String, Object> agentServices = Collections.synchronizedMap(new HashMap<>()); | ||
| private final Map<String, Object> agentServices = Collections.synchronizedMap(new LinkedHashMap<>()); | ||
| private BundleContext context; | ||
| private volatile boolean stopped = false; | ||
| private ServiceRegistration<IProvisioningAgent> reg; | ||
| private final Map<ServiceReference<IAgentServiceFactory>, ServiceTracker<IAgentServiceFactory, Object>> trackers = Collections | ||
| .synchronizedMap(new HashMap<>()); | ||
| private final Map<String, ServiceTracker<IAgentServiceFactory, Object>> trackers = Collections | ||
| .synchronizedMap(new LinkedHashMap<>()); | ||
|
|
||
| /** | ||
| * Instantiates a provisioning agent. | ||
|
|
@@ -50,42 +51,32 @@ public ProvisioningAgent() { | |
| public Object getService(String serviceName) { | ||
| //synchronize so concurrent gets always obtain the same service | ||
| synchronized (agentServices) { | ||
| checkRunning(); | ||
| Object service = agentServices.get(serviceName); | ||
| if (service != null) { | ||
| return service; | ||
| } | ||
| //attempt to get factory service from service registry | ||
| Collection<ServiceReference<IAgentServiceFactory>> refs; | ||
| try { | ||
| refs = context.getServiceReferences(IAgentServiceFactory.class, | ||
| String.format("(|(%s=%s)(p2.agent.servicename=%s))", //$NON-NLS-1$ use old property as fallback | ||
| IAgentServiceFactory.PROP_AGENT_SERVICE_NAME, serviceName, serviceName)); | ||
| } catch (InvalidSyntaxException e) { | ||
| e.printStackTrace(); | ||
| return null; | ||
| } | ||
| if (refs == null || refs.isEmpty()) { | ||
| return null; | ||
| } | ||
| ServiceReference<IAgentServiceFactory> firstRef = Collections.max(refs); | ||
| //track the factory so that we can automatically remove the service when the factory goes away | ||
| ServiceTracker<IAgentServiceFactory, Object> tracker = new ServiceTracker<>(context, firstRef, this); | ||
| tracker.open(); | ||
| IAgentServiceFactory factory = (IAgentServiceFactory) tracker.getService(); | ||
| if (factory == null) { | ||
| tracker.close(); | ||
| return null; | ||
| } | ||
| service = factory.createService(this); | ||
| if (service == null) { | ||
| tracker.close(); | ||
| return null; | ||
| } | ||
| registerService(serviceName, service); | ||
| trackers.put(firstRef, tracker); | ||
| return service; | ||
| } | ||
| ServiceTracker<IAgentServiceFactory, Object> tracker = trackers.computeIfAbsent(serviceName, | ||
| ref -> { | ||
| try { | ||
| if (stopped) { | ||
| return null; | ||
| } | ||
| Filter filter = context.createFilter( | ||
| String.format("(&(%s=%s)(|(%s=%s)(p2.agent.servicename=%s)))", //$NON-NLS-1$ | ||
| Constants.OBJECTCLASS, IAgentServiceFactory.class.getName(), // | ||
| IAgentServiceFactory.PROP_AGENT_SERVICE_NAME, serviceName, // | ||
| serviceName)); // use old property as fallback | ||
| return new ServiceTracker<>(context, filter, trackerCustomizer); | ||
| } catch (InvalidSyntaxException e) { | ||
| throw new AssertionError(e); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As |
||
| } | ||
| }); | ||
| if (tracker == null) { | ||
| return null; | ||
| } | ||
| tracker.open(); | ||
| return tracker.getService(); | ||
| } | ||
|
|
||
| private void checkRunning() { | ||
|
|
@@ -133,94 +124,104 @@ public void setLocation(URI location) { | |
|
|
||
| @Override | ||
| public void unregisterService(String serviceName, Object service) { | ||
| synchronized (agentServices) { | ||
| if (stopped) { | ||
| return; | ||
| } | ||
| if (agentServices.get(serviceName) == service) { | ||
| agentServices.remove(serviceName); | ||
| } | ||
| } | ||
| agentServices.remove(serviceName, service); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also for the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ConcurrentHashMap does not preserve registration order, which is used to dispose services. See stop() |
||
| if (service instanceof IAgentService) { | ||
| ((IAgentService) service).stop(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void stop() { | ||
| List<Object> toStop; | ||
| synchronized (agentServices) { | ||
| toStop = new ArrayList<>(agentServices.values()); | ||
| } | ||
| //give services a chance to do their own shutdown | ||
| for (Object service : toStop) { | ||
| if (service instanceof IAgentService) { | ||
| if (service != this) { | ||
| ((IAgentService) service).stop(); | ||
| } | ||
| } | ||
| } | ||
| stopped = true; | ||
| //close all service trackers | ||
| synchronized (trackers) { | ||
| for (ServiceTracker<IAgentServiceFactory, Object> t : trackers.values()) { | ||
| t.close(); | ||
| try { | ||
| disposeInTurns(agentServices.entrySet(), entry -> { | ||
| unregisterService(entry.getKey(), entry.getValue()); | ||
| }); | ||
| } finally { | ||
| try { | ||
| disposeInTurns(trackers.values(), ServiceTracker::close); | ||
| } finally { | ||
| if (reg != null) { | ||
| reg.unregister(); | ||
| reg = null; | ||
| } | ||
| } | ||
| trackers.clear(); | ||
| } | ||
| if (reg != null) { | ||
| reg.unregister(); | ||
| reg = null; | ||
| } | ||
| } | ||
|
|
||
| public void setServiceRegistration(ServiceRegistration<IProvisioningAgent> reg) { | ||
| this.reg = reg; | ||
| } | ||
|
|
||
| @Override | ||
| public Object addingService(ServiceReference<IAgentServiceFactory> reference) { | ||
| if (stopped) { | ||
| return null; | ||
| private final ServiceTrackerCustomizer<IAgentServiceFactory, Object> trackerCustomizer = new ServiceTrackerCustomizer<>() { | ||
| @Override | ||
| public Object addingService(ServiceReference<IAgentServiceFactory> reference) { | ||
| if (stopped) { | ||
| return null; | ||
| } | ||
| Object result = context.getService(reference).createService(ProvisioningAgent.this); | ||
| if (result instanceof IAgentService agentService) { | ||
| agentService.start(); | ||
| } | ||
| return result; | ||
| } | ||
| return context.getService(reference); | ||
| } | ||
|
|
||
| @Override | ||
| public void modifiedService(ServiceReference<IAgentServiceFactory> reference, Object service) { | ||
| //nothing to do | ||
| } | ||
|
|
||
| @Override | ||
| public void removedService(ServiceReference<IAgentServiceFactory> reference, Object factoryService) { | ||
| if (stopped) { | ||
| return; | ||
| @Override | ||
| public void modifiedService(ServiceReference<IAgentServiceFactory> reference, Object service) { | ||
| // nothing to do | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explain why service reset is not required here. |
||
| } | ||
| String serviceName = getAgentServiceName(reference); | ||
| if (serviceName == null) { | ||
| return; | ||
| } | ||
| Object registered = agentServices.get(serviceName); | ||
| if (registered == null) { | ||
| return; | ||
| } | ||
| if (FrameworkUtil.getBundle(registered.getClass()) == FrameworkUtil.getBundle(factoryService.getClass())) { | ||
| //the service we are holding is going away | ||
| unregisterService(serviceName, registered); | ||
| ServiceTracker<IAgentServiceFactory, Object> toRemove = trackers.remove(reference); | ||
| if (toRemove != null) { | ||
| toRemove.close(); | ||
|
|
||
| @Override | ||
| public void removedService(ServiceReference<IAgentServiceFactory> reference, Object service) { | ||
| if (service instanceof IAgentService agentService) { | ||
| agentService.stop(); | ||
| } | ||
| if (service != null) { | ||
| context.ungetService(reference); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private String getAgentServiceName(ServiceReference<IAgentServiceFactory> reference) { | ||
| Object property = reference.getProperty(IAgentServiceFactory.PROP_AGENT_SERVICE_NAME); | ||
| if (property instanceof String s) { | ||
| return s; | ||
| }; | ||
|
|
||
| /** | ||
| * Given a thread-safe collection, drain all elements one-by-one, disposing each | ||
| * without holding locks and allowing concurrent modification form. Other | ||
| * threads should not attempt to add elements to collection.<br> | ||
| * Usage example: | ||
| * | ||
| * <pre>{@code | ||
| * volatile boolean stopped = false; | ||
| * ... | ||
| * stopped = true; // prevent other threads from adding to the collection | ||
| * disposeInTurns(agentServices.entrySet(), entry -> { | ||
| * unregisterService(entry.getKey(), entry.getValue()); | ||
| * }); | ||
| * }</pre> | ||
| * | ||
| */ | ||
| @SuppressWarnings("unchecked") | ||
| private <T> void disposeInTurns(Collection<T> input, Consumer<T> dispose) { | ||
| RuntimeException error = null; | ||
| while (!input.isEmpty()) { | ||
| Object[] toStop = input.toArray(Object[]::new); | ||
| Collections.reverse(Arrays.asList(toStop)); // Remove in an inverse order of adding | ||
| for (Object item : toStop) { | ||
| if (input.remove(item)) { | ||
| try { | ||
| dispose.accept((T) item); | ||
| } catch (RuntimeException e) { | ||
| if (error == null) { | ||
| error = e; | ||
| } else { | ||
| error.addSuppressed(e); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (error != null) { | ||
| throw error; | ||
| } | ||
| // backward compatibility | ||
| return (String) reference.getProperty("p2.agent.servicename"); //$NON-NLS-1$ | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would here today use a
ConcurrentHashMap, also instead of storing aServiceTrackerobject it would be better to use a dedicated class (that internal holds / manages aServiceTracker), then one can use a quite nice pattern in a way that one first computes that class and then sync on the methods of that particular class. That way the map can work completely lock-free.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While lock-free map is nice, it is not critical in this case, because long-running operations are already done and managed by
ServiceTrackeroutside of locks. The map only holds an instance ofServiceTracker, creation of which does not require any synchronization. ServiceTracker also provides necessary method synchronization, so no additional wrapping is needed. Indeed,ServiceTrackerwas designed for this exact purpose.Registration order is important for correct disposal of interdependent services and
LinkedHashMappreserves it unlikeConcurrentHashMap.Also, performance is not a concern here, but
computeIfAbsent()forConcurrentHashMapcarries same deadlock risks asCollections.synchronizedMap(), just hides some of conflicts.