Skip to content

feat: lazy module-scoped jdtls initialization with incremental loading#46

Merged
aviadshiber merged 3 commits intomainfrom
fix/jdtls-init-timeout
Apr 10, 2026
Merged

feat: lazy module-scoped jdtls initialization with incremental loading#46
aviadshiber merged 3 commits intomainfrom
fix/jdtls-init-timeout

Conversation

@aviadshiber
Copy link
Copy Markdown
Owner

@aviadshiber aviadshiber commented Apr 10, 2026

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

  1. on_initialized: lightweight PATH check only — no subprocess
  2. First didOpen: finds nearest pom.xml/build.gradle, starts jdtls scoped to that module. Runs in background so custom diagnostics publish immediately.
  3. Subsequent didOpen: if the file is in a different module, adds it via workspace/didChangeWorkspaceFolders — incrementally loading what the user is actively editing
  4. Background: expands to full workspace root so cross-module references work for all files

Key design decisions

  • Non-blocking: jdtls startup is a background task — on_did_open returns immediately with custom diagnostics
  • 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 startup, replayed after init
  • INITIALIZE_TIMEOUT removed: 30s REQUEST_TIMEOUT sufficient for single module

Numbers

  • 354 tests, 83% coverage
  • Custom diagnostics: immediate (< 1s)
  • jdtls features: 2-3s after first file open (was 30-120s)

Test plan

  • All tests pass (354 unit + 19 e2e + 7 jdtls e2e)
  • Manual: restart LSP in IntelliJ on products monorepo

🤖 Generated with Claude Code

aviadsTaboola and others added 2 commits April 10, 2026 21:38
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>
@aviadshiber aviadshiber changed the title fix: increase jdtls initialize timeout to 120s for large monorepos feat: lazy module-scoped jdtls initialization with incremental loading Apr 10, 2026
…, 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 aviadshiber merged commit a239874 into main Apr 10, 2026
14 checks passed
@aviadshiber aviadshiber deleted the fix/jdtls-init-timeout branch April 10, 2026 19:45
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>
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants