Defer IWorkspace service registration onto a background job#5
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates ResourcesPlugin to perform IWorkspace service registration asynchronously, optimizing the startup path. The review feedback suggests enhancing the lifecycle management of the registration job by checking for cancellation within the task, explicitly canceling the job during service removal, and clearing service registration references to prevent potential leaks or stale states.
| Job job = Job.create("Register IWorkspace service", (IProgressMonitor monitor) -> { //$NON-NLS-1$ | ||
| Workspace ws = workspace; | ||
| if (ws != null) { | ||
| try { | ||
| workspaceRegistration = context.registerService(IWorkspace.class, ws, null); | ||
| } catch (IllegalStateException e) { | ||
| // bundle context became invalid during shutdown | ||
| } | ||
| } | ||
| return Status.OK_STATUS; | ||
| }); |
There was a problem hiding this comment.
To prevent potential service leaks during rapid startup/shutdown or service removal, the background job should check if it has been canceled before proceeding with the service registration. This works in conjunction with calling job.cancel() in removedService.
| Job job = Job.create("Register IWorkspace service", (IProgressMonitor monitor) -> { //$NON-NLS-1$ | |
| Workspace ws = workspace; | |
| if (ws != null) { | |
| try { | |
| workspaceRegistration = context.registerService(IWorkspace.class, ws, null); | |
| } catch (IllegalStateException e) { | |
| // bundle context became invalid during shutdown | |
| } | |
| } | |
| return Status.OK_STATUS; | |
| }); | |
| Job job = Job.create("Register IWorkspace service", (IProgressMonitor monitor) -> { //$NON-NLS-1$ | |
| Workspace ws = workspace; | |
| if (ws != null && !monitor.isCanceled()) { | |
| try { | |
| workspaceRegistration = context.registerService(IWorkspace.class, ws, null); | |
| } catch (IllegalStateException e) { | |
| // bundle context became invalid during shutdown | |
| } | |
| } | |
| return Status.OK_STATUS; | |
| }); |
| Job job = workspaceRegistrationJob; | ||
| if (job != null) { | ||
| try { | ||
| job.join(); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| } | ||
| } | ||
| ServiceRegistration<IWorkspace> registration = workspaceRegistration; | ||
| if (registration != null) { | ||
| try { | ||
| registration.unregister(); | ||
| } catch (RuntimeException e) { | ||
| getLog().log(Status.warning("Unregistering workspaces throws an exception", e)); //$NON-NLS-1$ | ||
| } | ||
| } |
There was a problem hiding this comment.
It is recommended to cancel the registration job before joining it to ensure that if the job hasn't started yet, it won't run at all. Additionally, nulling out the registration fields after unregistering is a good practice to avoid stale references and potential double-unregistration issues.
Job job = workspaceRegistrationJob;
if (job != null) {
job.cancel();
try {
job.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
workspaceRegistrationJob = null;
}
ServiceRegistration<IWorkspace> registration = workspaceRegistration;
if (registration != null) {
workspaceRegistration = null;
try {
registration.unregister();
} catch (RuntimeException e) {
getLog().log(Status.warning("Unregistering workspaces throws an exception", e)); //$NON-NLS-1$
}
}registerService(IWorkspace, ...) dispatches ServiceEvent.REGISTERED synchronously to every ServiceTracker<IWorkspace> listener. Several listeners (e.g. save-participant registrations in DebugPlugin and LaunchingPlugin) do non-trivial work that blocks the startup critical path by ~250 ms on a medium workspace. Schedule the registration on a system Job so the fan-out runs off the main thread. removedService joins the job before unregistering to preserve shutdown ordering. Why this is safe ---------------- - ResourcesPlugin.getWorkspace() reads the workspace field directly rather than going through the OSGi service registry. The field is populated synchronously in addingService before the Job is scheduled, so every static caller sees the workspace the moment ResourcesPlugin bundle activation returns. The Job only defers OSGi service publication, not workspace creation. - ServiceTracker<IWorkspace> consumers are inherently asynchronous: they register a tracker and react to the addingService callback whenever it arrives. Receiving that callback a few milliseconds later, on a worker thread, does not change the contract. - Shutdown ordering is preserved: removedService joins the registration job before calling unregister(), so the OSGi ServiceRegistration handle always refers to a completed registration (or stays null and the null-check skips unregister). - The Job runnable does not check monitor.isCanceled(), so a cancel issued while the job is running has no effect. There is no user- or framework-initiated cancel path for this job under normal operation. Areas to watch for potential regressions ---------------------------------------- - ServiceTracker<IWorkspace> addingService callbacks now run on a Jobs worker thread instead of the main thread. Listeners that implicitly assume UI-thread or main-thread affinity in addingService could break. Known in-tree consumers (DebugPlugin, jdt LaunchingPlugin) only register save participants and do not depend on thread identity. - Code that looks up IWorkspace via context.getServiceReference(IWorkspace.class) immediately after bundle activation will now briefly return null until the Job has run. Static ResourcesPlugin.getWorkspace() is unaffected. - If startup.complete headline time does not drop by a comparable amount after this change, something downstream is serialised on the IWorkspace OSGi service in a way the trace did not show, and the fix needs to be reconsidered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c82101f to
4045954
Compare
Summary
context.registerService(IWorkspace.class, workspace, null)dispatchesServiceEvent.REGISTEREDsynchronously to everyServiceTracker<IWorkspace>listener. Several listeners (e.g. save-participant registrations inDebugPlugin,LaunchingPlugin) do non-trivial work that blocks the startup critical path by ~250 ms on a medium workspace.Jobso the fan-out runs off the main thread.removedServicenow joins the job before unregistering to preserve shutdown ordering.ResourcesPlugin.getWorkspace()is unaffected — it reads the field directly rather than going through the OSGi service.Test plan
openInstanceLocationTrackerleaf (~257 ms) to drop near zero, with the work reappearing off the critical path.startup.completeheadline drops by a comparable amount; if not, something downstream is serialized on theIWorkspaceservice.removedServicejoins the job correctly with no spurious warnings.org.eclipse.core.tests.resourcesbundle test suite.🤖 Generated with Claude Code