feat: lazy module-scoped jdtls initialization with incremental loading#46
Merged
aviadshiber merged 3 commits intomainfrom Apr 10, 2026
Merged
feat: lazy module-scoped jdtls initialization with incremental loading#46aviadshiber merged 3 commits intomainfrom
aviadshiber merged 3 commits intomainfrom
Conversation
The default 30s REQUEST_TIMEOUT was too short for jdtls to initialize on large Maven monorepos (e.g., products with hundreds of modules). jdtls needs to scan pom.xml files, resolve classpaths, and build its index before responding to the initialize handshake. Added INITIALIZE_TIMEOUT = 120s used only for the initialize request. Normal request timeout stays at 30s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jdtls no longer starts at on_initialized. Instead: 1. on_initialized: lightweight PATH check only (check_available) 2. First didOpen: start jdtls scoped to the nearest Maven/Gradle module via find_module_root() — fast init (2-3s vs 30-120s for full monorepo) 3. Subsequent didOpen: add new modules incrementally via workspace/didChangeWorkspaceFolders (add_module_if_new) 4. Background: expand to full workspace root (expand_full_workspace) so cross-module references work for all files Key design decisions: - Non-blocking: lazy start runs as background task so on_did_open returns immediately with custom diagnostics (never delayed by jdtls cold-start) - asyncio.Lock prevents double-start from rapid didOpen calls - _start_failed flag prevents retry loops after failure - Data-dir hash based on original monorepo root (stable across restarts) - Notification queue: didOpen/didChange/didSave/didClose buffered during jdtls startup, replayed after initialization completes - INITIALIZE_TIMEOUT removed (30s REQUEST_TIMEOUT sufficient for single module) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, helpers Correctness: - Fix didOpen during startup silently dropped: now queued like didChange/save - Use _lazy_start_fired flag to prevent TOCTOU race on task creation - Deep copy init_params before mutation (ws['workspaceFolders'] = True) - expand_full_workspace removes initial module folder to avoid double-indexing - Return early in add_module_if_new when from_fs_path returns None Performance: - Cap notification queue at 200 entries (drop oldest on overflow) - Cache find_module_root results with lru_cache (avoid repeated stat walks) Quality: - Extract _resolve_module_uri helper (DRY: was duplicated 3 times) - Extract _forward_or_queue helper (DRY: was duplicated in 3 handlers) - Extract _WORKSPACE_DID_CHANGE_FOLDERS constant Tests: - Assert flush/expand called in test_lazy_start_jdtls_success - Add test_lazy_start_jdtls_silent_failure (ensure_started returns False) - Convert test_queue_and_flush to async (fix deprecated get_event_loop) - Add test_queue_caps_at_max - Add test_expand_full_workspace_noop_when_not_available - Assert queue cleared in test_ensure_started_no_retry_after_failure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aviadshiber
pushed a commit
that referenced
this pull request
Apr 11, 2026
The _lazy_start_jdtls function was calling _expand_workspace_background() immediately after jdtls initialized, which added the full monorepo root and removed the initial module — undoing the module-scoping optimization from #46 and causing 120s initialize timeouts on large workspaces. Changes: - Remove eager expansion; modules load on-demand via add_module_if_new() - Gate READY signal on publishDiagnostics (reliable indexing-complete signal) instead of first non-None response - Add _start_failed retry with 5-minute cooldown so transient failures don't permanently disable jdtls for the session Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 tasks
aviadshiber
added a commit
that referenced
this pull request
Apr 11, 2026
The _lazy_start_jdtls function was calling _expand_workspace_background() immediately after jdtls initialized, which added the full monorepo root and removed the initial module — undoing the module-scoping optimization from #46 and causing 120s initialize timeouts on large workspaces. Changes: - Remove eager expansion; modules load on-demand via add_module_if_new() - Gate READY signal on publishDiagnostics (reliable indexing-complete signal) instead of first non-None response - Add _start_failed retry with 5-minute cooldown so transient failures don't permanently disable jdtls for the session
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
jdtls no longer blocks on startup. Instead of initializing with the full monorepo root (30-120s), it starts lazily scoped to the nearest Maven/Gradle module (~2-3s).
How it works
on_initialized: lightweight PATH check only — no subprocessdidOpen: finds nearestpom.xml/build.gradle, starts jdtls scoped to that module. Runs in background so custom diagnostics publish immediately.didOpen: if the file is in a different module, adds it viaworkspace/didChangeWorkspaceFolders— incrementally loading what the user is actively editingKey design decisions
on_did_openreturns immediately with custom diagnosticsasyncio.Lock: prevents double-start from rapiddidOpencalls_start_failedflag: prevents retry loops after failuredidOpen/didChange/didSave/didClosebuffered during startup, replayed after initINITIALIZE_TIMEOUTremoved: 30sREQUEST_TIMEOUTsufficient for single moduleNumbers
Test plan
🤖 Generated with Claude Code