From 243f5332061c88fde1f0efc67714ee9d78ec9a58 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Thu, 1 Jan 2026 21:55:51 +0100 Subject: [PATCH 1/5] Unblock hanging main process --- python/ycm/client/messages_request.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/python/ycm/client/messages_request.py b/python/ycm/client/messages_request.py index d382d14456..b7d9d8f4f9 100644 --- a/python/ycm/client/messages_request.py +++ b/python/ycm/client/messages_request.py @@ -18,6 +18,7 @@ from ycm.client.base_request import BaseRequest, BuildRequestData from ycm.vimsupport import PostVimMessage +import json import logging _logger = logging.getLogger( __name__ ) @@ -56,8 +57,21 @@ def Poll( self, diagnostics_handler ): # Nothing yet... return True - response = self.HandleFuture( self._response_future, - display_message = False ) + # Non-blocking extraction since done() is True. We avoid HandleFuture() + # because it calls response.read() which blocks on large responses. + try: + response = self._response_future.result( timeout = 0 ) + response_text = response.read() + response.close() + if response_text: + response = json.loads( response_text ) + else: + response = None + except Exception as e: + _logger.exception( 'Error while handling server response in Poll' ) + # Server returned an exception. + return False + if response is None: # Server returned an exception. return False From 969781389f1d1d7aac01d74413205a81d58a8888 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Thu, 1 Jan 2026 22:20:00 +0100 Subject: [PATCH 2/5] Add tests for non-blocking MessagesPoll --- .../ycm/tests/client/messages_request_test.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/python/ycm/tests/client/messages_request_test.py b/python/ycm/tests/client/messages_request_test.py index 1e1a10e900..f313c1cef3 100644 --- a/python/ycm/tests/client/messages_request_test.py +++ b/python/ycm/tests/client/messages_request_test.py @@ -138,3 +138,125 @@ def test_HandlePollResponse_MultipleMessagesAndDiagnostics( warning=False, truncate=True ), ] ) + + + def test_Poll_FirstCall_StartsRequest( self ): + from ycm.client.messages_request import MessagesPoll + from unittest.mock import MagicMock + + # Create MessagesPoll with mock buffer + mock_buffer = MagicMock() + mock_buffer.number = 1 + poller = MessagesPoll( mock_buffer ) + + # Mock the async request method + poller.PostDataToHandlerAsync = MagicMock( return_value = MagicMock() ) + + # First poll should start request + result = poller.Poll( None ) + + assert_that( result, equal_to( True ) ) + poller.PostDataToHandlerAsync.assert_called_once() + + + def test_Poll_FutureNotDone_ReturnsTrue( self ): + from ycm.client.messages_request import MessagesPoll + from unittest.mock import MagicMock + + mock_buffer = MagicMock() + mock_buffer.number = 1 + poller = MessagesPoll( mock_buffer ) + + # Mock future that is not done + mock_future = MagicMock() + mock_future.done.return_value = False + poller._response_future = mock_future + + # Should return True without extracting result + result = poller.Poll( None ) + + assert_that( result, equal_to( True ) ) + mock_future.result.assert_not_called() + + + def test_Poll_FutureReady_ExtractsResponseNonBlocking( self ): + from ycm.client.messages_request import MessagesPoll + from unittest.mock import MagicMock + import json + + mock_buffer = MagicMock() + mock_buffer.number = 1 + poller = MessagesPoll( mock_buffer ) + + # Mock completed future with response + mock_response = MagicMock() + mock_response.read.return_value = json.dumps( [ { 'message': 'test' } ] ).encode() + mock_response.close = MagicMock() + + mock_future = MagicMock() + mock_future.done.return_value = True + mock_future.result.return_value = mock_response + poller._response_future = mock_future + + # Mock diagnostics handler + mock_handler = MagicMock() + + # Should extract result with timeout=0 (non-blocking) + with patch( 'ycm.client.messages_request.PostVimMessage' ): + result = poller.Poll( mock_handler ) + + # Verify non-blocking extraction + mock_future.result.assert_called_once_with( timeout = 0 ) + mock_response.read.assert_called_once() + mock_response.close.assert_called_once() + assert_that( result, equal_to( True ) ) + + + def test_Poll_FutureException_ReturnsFalse( self ): + from ycm.client.messages_request import MessagesPoll + from unittest.mock import MagicMock + + mock_buffer = MagicMock() + mock_buffer.number = 1 + poller = MessagesPoll( mock_buffer ) + + # Mock future that raises exception + mock_future = MagicMock() + mock_future.done.return_value = True + mock_future.result.side_effect = Exception( 'Connection error' ) + poller._response_future = mock_future + + # Should catch exception and return False + result = poller.Poll( None ) + + assert_that( result, equal_to( False ) ) + + + def test_Poll_DoesNotCallHandleFuture( self ): + """Verify that Poll() does NOT call HandleFuture() to avoid blocking.""" + from ycm.client.messages_request import MessagesPoll + from unittest.mock import MagicMock, patch + import json + + mock_buffer = MagicMock() + mock_buffer.number = 1 + poller = MessagesPoll( mock_buffer ) + + # Mock completed future + mock_response = MagicMock() + mock_response.read.return_value = json.dumps( True ).encode() + mock_response.close = MagicMock() + + mock_future = MagicMock() + mock_future.done.return_value = True + mock_future.result.return_value = mock_response + poller._response_future = mock_future + + # Spy on HandleFuture to ensure it's NOT called + original_handle_future = poller.HandleFuture + poller.HandleFuture = MagicMock( side_effect = original_handle_future ) + + # Poll should not call HandleFuture + poller.Poll( None ) + + poller.HandleFuture.assert_not_called() From ceb61d47afbbe7feecdfecf1e57b35d784564428 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Thu, 1 Jan 2026 23:42:41 +0100 Subject: [PATCH 3/5] Fix tests --- python/ycm/client/messages_request.py | 2 +- .../ycm/tests/client/messages_request_test.py | 206 +++++++++--------- python/ycm/tests/mock_utils.py | 2 +- 3 files changed, 99 insertions(+), 111 deletions(-) diff --git a/python/ycm/client/messages_request.py b/python/ycm/client/messages_request.py index b7d9d8f4f9..8b9bf8314b 100644 --- a/python/ycm/client/messages_request.py +++ b/python/ycm/client/messages_request.py @@ -67,7 +67,7 @@ def Poll( self, diagnostics_handler ): response = json.loads( response_text ) else: response = None - except Exception as e: + except Exception: _logger.exception( 'Error while handling server response in Poll' ) # Server returned an exception. return False diff --git a/python/ycm/tests/client/messages_request_test.py b/python/ycm/tests/client/messages_request_test.py index f313c1cef3..dd614318ee 100644 --- a/python/ycm/tests/client/messages_request_test.py +++ b/python/ycm/tests/client/messages_request_test.py @@ -15,15 +15,16 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +import json from ycm.tests.test_utils import MockVimModule MockVimModule() from hamcrest import assert_that, equal_to from unittest import TestCase -from unittest.mock import patch, call +from unittest.mock import patch, call, MagicMock -from ycm.client.messages_request import _HandlePollResponse -from ycm.tests.test_utils import ExtendedMock +from ycm.client.messages_request import _HandlePollResponse, MessagesPoll +from ycm.tests.test_utils import ExtendedMock, MockVimBuffers, VimBuffer class MessagesRequestTest( TestCase ): @@ -141,122 +142,109 @@ def test_HandlePollResponse_MultipleMessagesAndDiagnostics( def test_Poll_FirstCall_StartsRequest( self ): - from ycm.client.messages_request import MessagesPoll - from unittest.mock import MagicMock - - # Create MessagesPoll with mock buffer - mock_buffer = MagicMock() - mock_buffer.number = 1 - poller = MessagesPoll( mock_buffer ) - - # Mock the async request method - poller.PostDataToHandlerAsync = MagicMock( return_value = MagicMock() ) - - # First poll should start request - result = poller.Poll( None ) - - assert_that( result, equal_to( True ) ) - poller.PostDataToHandlerAsync.assert_called_once() + test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] ) + + with MockVimBuffers( [ test_buffer ], [ test_buffer ] ): + poller = MessagesPoll( test_buffer ) + + # Mock the async request method to avoid actual HTTP call + mock_future = MagicMock() + with patch.object( poller, 'PostDataToHandlerAsync', + return_value = mock_future ) as mock_post: + # First poll should start request + result = poller.Poll( None ) + + assert_that( result, equal_to( True ) ) + mock_post.assert_called_once() def test_Poll_FutureNotDone_ReturnsTrue( self ): - from ycm.client.messages_request import MessagesPoll - from unittest.mock import MagicMock - - mock_buffer = MagicMock() - mock_buffer.number = 1 - poller = MessagesPoll( mock_buffer ) - - # Mock future that is not done - mock_future = MagicMock() - mock_future.done.return_value = False - poller._response_future = mock_future - - # Should return True without extracting result - result = poller.Poll( None ) - - assert_that( result, equal_to( True ) ) - mock_future.result.assert_not_called() + test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] ) + + with MockVimBuffers( [ test_buffer ], [ test_buffer ] ): + poller = MessagesPoll( test_buffer ) + + # Mock future that is not done + mock_future = MagicMock() + mock_future.done.return_value = False + poller._response_future = mock_future + + # Should return True without extracting result + result = poller.Poll( None ) + + assert_that( result, equal_to( True ) ) + mock_future.result.assert_not_called() def test_Poll_FutureReady_ExtractsResponseNonBlocking( self ): - from ycm.client.messages_request import MessagesPoll - from unittest.mock import MagicMock - import json - - mock_buffer = MagicMock() - mock_buffer.number = 1 - poller = MessagesPoll( mock_buffer ) - - # Mock completed future with response - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( [ { 'message': 'test' } ] ).encode() - mock_response.close = MagicMock() - - mock_future = MagicMock() - mock_future.done.return_value = True - mock_future.result.return_value = mock_response - poller._response_future = mock_future - - # Mock diagnostics handler - mock_handler = MagicMock() - - # Should extract result with timeout=0 (non-blocking) - with patch( 'ycm.client.messages_request.PostVimMessage' ): - result = poller.Poll( mock_handler ) - - # Verify non-blocking extraction - mock_future.result.assert_called_once_with( timeout = 0 ) - mock_response.read.assert_called_once() - mock_response.close.assert_called_once() - assert_that( result, equal_to( True ) ) + test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] ) + + with MockVimBuffers( [ test_buffer ], [ test_buffer ] ): + poller = MessagesPoll( test_buffer ) + + # Mock completed future with response + mock_response = MagicMock() + mock_response.read.return_value = json.dumps( + [ { 'message': 'test' } ] ).encode() + mock_response.close = MagicMock() + + mock_future = MagicMock() + mock_future.done.return_value = True + mock_future.result.return_value = mock_response + poller._response_future = mock_future + + # Mock diagnostics handler + mock_handler = MagicMock() + + # Should extract result with timeout=0 (non-blocking) + with patch( 'ycm.client.messages_request.PostVimMessage' ): + result = poller.Poll( mock_handler ) + + # Verify non-blocking extraction + mock_future.result.assert_called_once_with( timeout = 0 ) + mock_response.read.assert_called_once() + mock_response.close.assert_called_once() + assert_that( result, equal_to( True ) ) def test_Poll_FutureException_ReturnsFalse( self ): - from ycm.client.messages_request import MessagesPoll - from unittest.mock import MagicMock - - mock_buffer = MagicMock() - mock_buffer.number = 1 - poller = MessagesPoll( mock_buffer ) - - # Mock future that raises exception - mock_future = MagicMock() - mock_future.done.return_value = True - mock_future.result.side_effect = Exception( 'Connection error' ) - poller._response_future = mock_future - - # Should catch exception and return False - result = poller.Poll( None ) - - assert_that( result, equal_to( False ) ) + test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] ) + + with MockVimBuffers( [ test_buffer ], [ test_buffer ] ): + poller = MessagesPoll( test_buffer ) + + # Mock future that raises exception + mock_future = MagicMock() + mock_future.done.return_value = True + mock_future.result.side_effect = Exception( 'Connection error' ) + poller._response_future = mock_future + + # Should catch exception and return False + result = poller.Poll( None ) + + assert_that( result, equal_to( False ) ) def test_Poll_DoesNotCallHandleFuture( self ): """Verify that Poll() does NOT call HandleFuture() to avoid blocking.""" - from ycm.client.messages_request import MessagesPoll - from unittest.mock import MagicMock, patch - import json - - mock_buffer = MagicMock() - mock_buffer.number = 1 - poller = MessagesPoll( mock_buffer ) - - # Mock completed future - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( True ).encode() - mock_response.close = MagicMock() - - mock_future = MagicMock() - mock_future.done.return_value = True - mock_future.result.return_value = mock_response - poller._response_future = mock_future - - # Spy on HandleFuture to ensure it's NOT called - original_handle_future = poller.HandleFuture - poller.HandleFuture = MagicMock( side_effect = original_handle_future ) - - # Poll should not call HandleFuture - poller.Poll( None ) - - poller.HandleFuture.assert_not_called() + test_buffer = VimBuffer( 'test_buffer', number = 1, contents = [ '' ] ) + + with MockVimBuffers( [ test_buffer ], [ test_buffer ] ): + poller = MessagesPoll( test_buffer ) + + # Mock completed future + mock_response = MagicMock() + mock_response.read.return_value = json.dumps( True ).encode() + mock_response.close = MagicMock() + + mock_future = MagicMock() + mock_future.done.return_value = True + mock_future.result.return_value = mock_response + poller._response_future = mock_future + + # Spy on HandleFuture to ensure it's NOT called + with patch.object( poller, 'HandleFuture' ) as mock_handle_future: + # Poll should not call HandleFuture + poller.Poll( None ) + + mock_handle_future.assert_not_called() diff --git a/python/ycm/tests/mock_utils.py b/python/ycm/tests/mock_utils.py index a200f5c0f6..ab353b584b 100644 --- a/python/ycm/tests/mock_utils.py +++ b/python/ycm/tests/mock_utils.py @@ -56,7 +56,7 @@ def done( self ): return self._done - def result( self ): + def result( self, timeout = None ): return self._result From c09e0f323a66adef5e6e7bd04e29f3c8fe2b4c06 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Fri, 2 Jan 2026 00:07:09 +0100 Subject: [PATCH 4/5] Add comment, why plain response.read is used in MessagesPoll.Poll --- python/ycm/client/messages_request.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/python/ycm/client/messages_request.py b/python/ycm/client/messages_request.py index 8b9bf8314b..129fc95721 100644 --- a/python/ycm/client/messages_request.py +++ b/python/ycm/client/messages_request.py @@ -57,8 +57,19 @@ def Poll( self, diagnostics_handler ): # Nothing yet... return True - # Non-blocking extraction since done() is True. We avoid HandleFuture() - # because it calls response.read() which blocks on large responses. + # Avoid HandleFuture() to prevent blocking in timer callbacks. + # HandleFuture() does: + # 1. Complex exception handling (UnknownExtraConf, DisplayServerException) + # 2. User dialogs that can block waiting for input + # 3. Vim UI updates during callback execution + # By extracting the response directly with minimal error handling, we avoid + # blocking vim's main thread. Note that response.read() is still technically + # blocking, but: + # - The future is already done() (data received from localhost ycmd server) + # - Network I/O is complete, read() just copies from buffer to memory + # - No user interaction or complex processing + # The real performance issue was HandleFuture's heavy exception handling, + # not the I/O itself. try: response = self._response_future.result( timeout = 0 ) response_text = response.read() From 8043981af84129a68b626af0e0accae88c9406d5 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Fri, 2 Jan 2026 00:18:06 +0100 Subject: [PATCH 5/5] Fix flake8 --- python/ycm/client/messages_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ycm/client/messages_request.py b/python/ycm/client/messages_request.py index 129fc95721..7da8bd6a89 100644 --- a/python/ycm/client/messages_request.py +++ b/python/ycm/client/messages_request.py @@ -65,7 +65,7 @@ def Poll( self, diagnostics_handler ): # By extracting the response directly with minimal error handling, we avoid # blocking vim's main thread. Note that response.read() is still technically # blocking, but: - # - The future is already done() (data received from localhost ycmd server) + # - The future is already done(), data received from localhost ycmd server # - Network I/O is complete, read() just copies from buffer to memory # - No user interaction or complex processing # The real performance issue was HandleFuture's heavy exception handling,