-
Notifications
You must be signed in to change notification settings - Fork 286
Add support for asynchronously handling requests #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@evandrocoan @tomv564 @lgeiger as regular contributors, wanted to get your thoughts on this PR. One notable regression is the lack of batch JSON RPC support, are you aware of this impacting any clients? |
|
I didn't want the lack of batch requests to be a blocker, so I went ahead and added support for them |
|
I just merged this into my master and started my LSP client: File: python-language-server/pyls/python_ls.py
87: def handle_request(self, method, params):
88: """Provides callables to handle requests or responses to those reqeuests
89:
90: Args:
91: method (str): name of the message
92: params (dict): body of the message
93:
94: Returns:
95: Callable if method is to be handled
96:
97: Raises:
98: KeyError: Handler for method is not found
99: """
100:
101: method_call = 'm_{}'.format(_method_to_string(method))
102: if hasattr(self, method_call):
103: return getattr(self, method_call)(**params)
104: elif self._dispatchers: |
|
@evandrocoan are you sure you have latest? https://github.com/palantir/python-language-server/pull/261/files#diff-f68667852a14e9f761f6ebf07ba02fc8R138 would indicate that it can handle extra keyword args |
|
Sorry, I though this was on your fork, but it is directly put on the upstream. |
|
@ferozco, I noticed you seem to have fixed #250 now, how can I access the language server configuration: File: python-language-server/pyls/python_ls.py
147: def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializationOptions=None, **_kwargs):
148: log.debug('Language server initialized with %s %s %s %s', processId, rootUri, rootPath, initializationOptions)
149: if rootUri is None:
150: rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ''
151:
152: self.config = config.Config(rootUri, initializationOptions or {})
153: self.workspace = Workspace(rootUri, rpc_manager=self.rpc_manager)from the Workspace class constructor? I this would be required to fix #256 Currently on my Workspace constructor I have this: File: python-language-server/pyls/workspace.py
77: def __init__(self, root_uri, rpc_manager=None):
78: self._root_uri = root_uri
79: self._rpc_manager = rpc_manager
80: self._root_uri_scheme = uris.urlparse(self._root_uri)[0]
81: self._root_path = uris.to_fs_path(self._root_uri)
82: self._docs = {}
83:
84: # Whilst incubating, keep private
85: if self._rpc_manager.config.plugin_settings('rope').get('create_folder', False):
86: self.__rope = Project(self._root_path)
87: else:
88: self.__rope = Project(self._root_path, ropefolder=None)
89: self.__rope.prefs.set('extension_modules', self.PRELOADED_MODULES)This way, you can fix #179 once we load the client configurations before the creating the Workspace() object. |
|
One could argue that the config should just live within the workspace. In this world the language server just becomes a dumb dispatcher. The workspace would then be responsible for loading plugins etc. thoughts @gatesn? I think something like that is outside of the scope of this PR (which already pretty large) |
|
Perhaps we should keep a class diagram somewhere like using https://github.com/plantuml/plantum For now, I used Putting the The old problem still #256 because would make no difference for #179 as we need to already have loaded the user configuration while instantiating the |
|
@ferozco, now I got this from my client: This is the log within this so far:
Even running directly from you branch it does this: |
|
LSP does not use batch JSON-RPC requests and I have no immediate plans of implementing them. I think offering async request handling (at least for all the linters) could make a good improvement in responsiveness for some user's configurations. Great initiative! |
|
@ferozco, even after merging your fix, the problem still happening: |
|
@evandrocoan I wasn't able to find a location where we would be sending responses with a null id. it would be helpful if you could either give me some repro steps or add some debug logic to print out what message is causing this error to occur. |
|
@ferozco, I added this: File: Data/Packages/LSP/plugin/core/rpc.py
339: def response_handler(self, response):
340: print( "response:", response )
341: handler_id = int(response.get("id")) # dotty sends strings back :(And got: I would suggest to change |
|
@ferozco, after merging these changes, the server is crashing some times. This the last stack track logged:
|
|
@evandrocoan I think i've found and resolved the issue around the null Ids. Since none of the handlers have been converted to use the new async functionality, it would be unexpected if my change caused this exception. It appears as if there is an unhandled exception coming from the rope plugin. If people are ok, i'd like to move forward and have this merged in and i'll handle any other issues as they come up |
|
@ferozco Thanks for the work 🎉 |
gatesn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lots of little nits, but overall looks awesome.
pyls/json_rpc_server.py
Outdated
| from jsonrpc.jsonrpc2 import JSONRPC20Response, JSONRPC20BatchRequest, JSONRPC20BatchResponse | ||
| from jsonrpc.jsonrpc import JSONRPCRequest | ||
| from jsonrpc.exceptions import ( | ||
| JSONRPCInvalidRequestException, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for multiline import
pyls/json_rpc_server.py
Outdated
| from jsonrpc.exceptions import ( | ||
| JSONRPCInvalidRequestException, | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One blank line
pyls/json_rpc_server.py
Outdated
| # we do not send out batch requests so no need to support batch responses | ||
| messages = [JSONRPC20Response(**message_blob)] | ||
| except (KeyError, ValueError): | ||
| log.error("Could not parse message %s", request_str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change to log.exception so we don't lose the stack trace.
pyls/json_rpc_server.py
Outdated
| def get_messages(self): | ||
| """Generator that produces well structured JSON RPC message. | ||
| Returns: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be Yields: not Returns:
pyls/python_ls.py
Outdated
| from .json_rpc_server import JSONRPCServer | ||
| from .rpc_manager import JSONRPCManager | ||
| from .workspace import Workspace | ||
| from .rpc_manager import MissingMethodException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you collapse the two imports from .rpc_manager into one?
pyls/rpc_manager.py
Outdated
| output = _make_response(request, error=e.error._data) | ||
| except Exception as e: # pylint: disable=broad-except | ||
| # TODO(forozco): add more descriptive error | ||
| log.error('endpoint exception %s %s %s', e.__class__.__name__, e.args, str(e)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again log.exception here. Perhaps "Asynchronous dispatched method exception`
| try: | ||
| request = self._sent_requests[response._id] | ||
| except KeyError: | ||
| log.error('Received unexpected response %s', response.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually prefer an explicit check for these sorts of things if response._id not in self._sent_requests. Particularly in this case where the try/else block makes up the bulk of the function.
pyls/workspace.py
Outdated
| PRELOADED_MODULES = get_preferred_submodules() | ||
|
|
||
| def __init__(self, root_uri, lang_server=None): | ||
| def __init__(self, root_uri, rpc_manager=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make rpc_manager required
test/test_language_server.py
Outdated
| except JSONRPCDispatchException as e: | ||
| assert e.error.code == JSONRPCMethodNotFound.CODE | ||
| else: | ||
| assert False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a message here? e.g. assert False, "reason for failure"
tox.ini
Outdated
| pycodestyle pyls | ||
| pyflakes pyls | ||
| pylint pyls test | ||
| pycodestyle pyls test --exclude=test/plugins/.ropeproject,test/.ropeproject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add these excludes to the [pycodestyle] block above?
| output = _make_response(request, error=e.error._data) | ||
| except Exception as e: # pylint: disable=broad-except | ||
| log.error('endpoint exception %s %s %s', e.__class__.__name__, e.args, str(e)) | ||
| log.exception('synchronous method handler exception') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we log the original request here like:
log.exception('synchronous method handler exception for request %s', request)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure! in a follow up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| except Exception as e: # pylint: disable=broad-except | ||
| # TODO(forozco): add more descriptive error | ||
| log.error('endpoint exception %s %s %s', e.__class__.__name__, e.args, str(e)) | ||
| log.exception('asynchronous method handler exception') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.exception('asynchronous method handler exception for request %s, handler %s '
'and completed_future %s', request, handler, completed_future)

Previously we had a single thread that would handle and subsequently respond to each message in order. This presented performance problems since long running handlers would cause the server to hang from the perspective of the client. Furthermore, prevented a handler from sending (and receiving) multiple message.
This change allows handlers to be executed either synchronously or asynchronously with the choice being left to the specific hookimpl.