ADFA-4314 Make IndexingServiceManager.close() block until services close#1406
Conversation
close() launched fire-and-forget coroutines on the manager scope to close
each service, then returned immediately after clearing the services map.
So when close() returned, service.close() may not have run yet -- the
registered services were not reliably closed.
This violated the Closeable contract that ProjectManagerImpl.destroy()
relies on (it drops its reference right after close()), and made the
'close calls close on each registered service' test race the not-yet-run
coroutine.
Wrap the per-service and registry close coroutines in runBlocking { joinAll
(...) } so close() blocks until shutdown completes. Per-service
withTimeoutOrNull(SERVICE_CLOSE_TIMEOUT) protection and error isolation are
preserved; in-flight indexing work is still cancelled afterward.
Surfaced by the nightly Jacoco/SonarQube analyze.yml run after ADFA-4306.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughRelease Notes
|
| Layer / File(s) | Summary |
|---|---|
Synchronous close() implementation lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt |
Adds the runBlocking import and replaces the previous async close() with a blocking flow: concurrent service/registry close jobs on Dispatchers.Default, joinAll, scope.coroutineContext.cancelChildren(), state reset, and updated log messages. Registry exception logging now passes the exception object to log.error. |
Service initialization ordering documentation lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt |
Updates register() documentation to clarify that service initialization order is unspecified, reflecting ConcurrentHashMap iteration. |
Estimated code review effort
🎯 2 (Simple) | ⏱️ ~10 minutes
Possibly related PRs
- appdevforall/CodeOnTheGo#1308: Directly modifies the same
IndexingServiceManager.close()function with overlapping concerns — concurrent service shutdown in reverse order, timeout/join handling, and registry-close behavior.
Poem
🐇 Hoppity-hop, no more async delay,
runBlockingnow waits till the services say,
Reversed in their order, they close with a grace,
The registry joins them at its own steady pace.
✨ "Shut down!" logs the manager, tidy and bright —
The coroutine children tucked in for the night.
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | The title accurately and concisely describes the main change: making IndexingServiceManager.close() block synchronously until services close, which is the primary objective of the PR. |
| Description check | ✅ Passed | The description is comprehensive and directly related to the changeset, clearly explaining the bug, the fix, and verification results. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
📝 Generate docstrings
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
ADFA-4314-indexing-close-not-closing-all-services
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt`:
- Around line 127-137: The issue is that services is a ConcurrentHashMap which
has undefined iteration order per JDK specification, so reversing
services.values does not guarantee reverse registration order as the comment
claims. To fix this, introduce a separate ordered collection such as
CopyOnWriteArrayList to explicitly track the registration order of services as
they are added to the IndexingServiceManager. Then in the runBlocking block
where services are closed, use this ordered collection instead of
services.values.reversed() to iterate through services in the correct reverse
registration order. Update both the service registration logic (where services
are added to the map) and the close logic to maintain this separate ordered
collection alongside the existing ConcurrentHashMap.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4ec079a8-2472-4b4c-9564-968f6af32ec5
📒 Files selected for processing (1)
lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt
services is a ConcurrentHashMap (undefined iteration order), so
services.values.reversed() never yielded reverse registration order as the
comment claimed. Order is moot anyway: services close concurrently (one
launch{} each, awaited by joinAll), so launch order has no effect on close
behavior. Remove the no-op .reversed() and correct the comment.
Addresses CodeRabbit review feedback on #1406.
services is a ConcurrentHashMap, so initializeServices() runs in an unspecified order -- the previous 'initialized in registration order' claim was false. Document the actual contract (no cross-service init-order dependency) rather than add ordering machinery nothing currently needs (YAGNI).
What
IndexingServiceManager.close()was fire-and-forget: itlaunched coroutines on the manager'sscopeto close each service, then immediately ranservices.clear()and returned without awaiting them. So whenclose()returned,service.close()may not have executed yet — registered services were not reliably closed.This is a real product bug, not a test-environment artifact:
ProjectManagerImpl.destroy()callsclose()then drops its reference (_indexingServiceManager = null). It expects shutdown complete on return; instead services may close late, or be cancelled mid-close on a fast teardown → leaked index/file handles.close calls close on each registered servicecallsclose()then synchronously assertssvc.closed == true, racing the not-yet-run coroutine.Violates the
Closeable.close()contract (synchronous resource release).Fix
Wrap the per-service and registry close coroutines in
runBlocking { ... joinAll(...) }soclose()blocks until shutdown completes before returning. Preserved:withTimeoutOrNull(SERVICE_CLOSE_TIMEOUT)bound + error isolationscope.coroutineContext.cancelChildren())Real service
close()impls (JvmLibraryIndexingService,JvmGeneratedIndexingService) andIndexRegistry.close()are fast synchronous releases, so blocking is cheap; the timeout is a safety net.Verification
Full
sonarqubegradle task run locally (the same one analyze.yml drives):IndexingServiceManagerTest: 0 failures across all 6 variants (V7/V8 x Debug/Release/Instrumentation),tests=10each — was 1 failure.:sonarqubetask fails only onSONAR_TOKENupload (expected locally; official analysis runs in CI).Remaining 6 failures are tracked separately:
CompilerTestx2 (ADFA-4313, fixed on its own branch) andCloneRepositoryViewModelTestx4 (separate ticket).Closes ADFA-4314.