Skip to content

Opening files in SyncLanguageServer may cause deadlocks #124

@irreg

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions