Skip to content

Opening files in SyncLanguageServer may cause deadlocks #124

Description

@irreg

A quick investigation revealed that while start_server launches processing in a separate thread, open_file itself is not launched in the thread. The issue was resolved by executing open_file's processing in the thread, suggesting that internally, there may be operations that are not thread-safe.
I haven't tested all languages, but it occurs at least for Java and C++.
SyncLanguageServer should be modified to completely hide the existence of separate threads.

Environment

OS: Windows 11
Python: 3.13

Reproduction Code

Freezes within 10 times

from pathlib import Path

from multilspy import SyncLanguageServer
from multilspy.multilspy_config import MultilspyConfig
from multilspy.multilspy_logger import MultilspyLogger


def main():
    path = Path("temp")
    path.mkdir(exist_ok=True)
    with open(path / "test.java", "w"):
        pass
    params = {
        "code_language": "java",
    }
    config = MultilspyConfig.from_dict(params)
    logger = MultilspyLogger()
    for i in range(100):
        print(f"start: {i}")
        lsp = SyncLanguageServer.create(config, logger, str(path.absolute()))

        with lsp.start_server():
            with lsp.open_file("test.java"):
                pass
            # Changing the processing as described in the comment-out will avoid the problem.
            # async def sub():
            #     with lsp.open_file("test.java"):
            #         pass

            # asyncio.run_coroutine_threadsafe(sub(), lsp.loop).result(
            #     timeout=lsp.timeout
            # )
        print(f"end: {i}")


if __name__ == "__main__":
    main()

I don't fully understand the internal processing, but the following fixes are likely candidates:

    @contextmanager
    def open_file(self, relative_file_path: str) -> Iterator[None]:
        """
        Open a file in the Language Server. This is required before making any requests to the Language Server.

        :param relative_file_path: The relative path of the file to open.
        """
        f = self.language_server.open_file(relative_file_path)
        async def enter():
            f.__enter__()
        async def exit():
            f.__exit__(None, None, None)
        asyncio.run_coroutine_threadsafe(enter(), lsp.loop).result(
             timeout=lsp.timeout
        )
        try:
            yield
        finally:
           asyncio.run_coroutine_threadsafe(exit(), lsp.loop).result(
             timeout=lsp.timeout
           )

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions