Auto merge of #2514 - micbou:connect-timeout, r=Valloric

[READY] Rely on connect timeout instead of checking that the server is alive

Currently, we always check that the ycmd process is up (with the `IsServerAlive` method) before sending a request. Without this check, each request could block Vim until a `NewConnectionError` exception is raised if the server crashed. This is the case on Windows where it takes ~1s before the exception is raised which makes Vim unusable. However, even with this check, Vim may still be blocked in the following cases:
 - the server crashes just after the check but before sending the request;
 - the server is up but unresponsive (e.g. its port is closed).

To avoid both cases, we instead use [the connect timeout parameter from Requests](http://docs.python-requests.org/en/master/user/advanced/?highlight=connect%20timeout#timeouts) and set it to a duration sufficiently short (10 ms) that the blocking can't be noticed by the user. Since the server is supposed to run locally (this is what YCM is designed for), 10ms is largely enough to establish a connection.

The `IsServerAlive` check is removed almost everywhere except in `OnFileReadyToParse` because we still want to notify the user if the server crashed.

This change makes it possible to not have to [wait for the server to be healthy before sending asynchronous requests](https://github.com/Valloric/YouCompleteMe/blob/master/python/ycm/client/base_request.py#L137-L138). This will dramatically improve startup time (see issue #2085) and fixes #2071. Next PR once this one is merged.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/valloric/youcompleteme/2514)
<!-- Reviewable:end -->
This commit is contained in:
Homu 2017-01-26 19:01:59 +09:00
commit dc44597674
4 changed files with 22 additions and 41 deletions

View File

@ -735,9 +735,6 @@ function! youcompleteme#Complete( findstart, base )
return -2 return -2
endif endif
if !s:Pyeval( 'ycm_state.IsServerAlive()' )
return -2
endif
exec s:python_command "ycm_state.CreateCompletionRequest()" exec s:python_command "ycm_state.CreateCompletionRequest()"
return s:Pyeval( 'base.CompletionStartColumn()' ) return s:Pyeval( 'base.CompletionStartColumn()' )
else else
@ -748,9 +745,6 @@ endfunction
function! youcompleteme#OmniComplete( findstart, base ) function! youcompleteme#OmniComplete( findstart, base )
if a:findstart if a:findstart
if !s:Pyeval( 'ycm_state.IsServerAlive()' )
return -2
endif
let s:omnifunc_mode = 1 let s:omnifunc_mode = 1
exec s:python_command "ycm_state.CreateCompletionRequest(" . exec s:python_command "ycm_state.CreateCompletionRequest(" .
\ "force_semantic = True )" \ "force_semantic = True )"

View File

@ -40,8 +40,9 @@ from ycmd.responses import ServerError, UnknownExtraConf
_HEADERS = {'content-type': 'application/json'} _HEADERS = {'content-type': 'application/json'}
_EXECUTOR = UnsafeThreadPoolExecutor( max_workers = 30 ) _EXECUTOR = UnsafeThreadPoolExecutor( max_workers = 30 )
_CONNECT_TIMEOUT_SEC = 0.01
# Setting this to None seems to screw up the Requests/urllib3 libs. # Setting this to None seems to screw up the Requests/urllib3 libs.
_DEFAULT_TIMEOUT_SEC = 30 _READ_TIMEOUT_SEC = 30
_HMAC_HEADER = 'x-ycm-hmac' _HMAC_HEADER = 'x-ycm-hmac'
_logger = logging.getLogger( __name__ ) _logger = logging.getLogger( __name__ )
@ -67,7 +68,7 @@ class BaseRequest( object ):
# |timeout| is num seconds to tolerate no response from server before giving # |timeout| is num seconds to tolerate no response from server before giving
# up; see Requests docs for details (we just pass the param along). # up; see Requests docs for details (we just pass the param along).
@staticmethod @staticmethod
def GetDataFromHandler( handler, timeout = _DEFAULT_TIMEOUT_SEC ): def GetDataFromHandler( handler, timeout = _READ_TIMEOUT_SEC ):
return JsonFromFuture( BaseRequest._TalkToHandlerAsync( '', return JsonFromFuture( BaseRequest._TalkToHandlerAsync( '',
handler, handler,
'GET', 'GET',
@ -78,7 +79,7 @@ class BaseRequest( object ):
# |timeout| is num seconds to tolerate no response from server before giving # |timeout| is num seconds to tolerate no response from server before giving
# up; see Requests docs for details (we just pass the param along). # up; see Requests docs for details (we just pass the param along).
@staticmethod @staticmethod
def PostDataToHandler( data, handler, timeout = _DEFAULT_TIMEOUT_SEC ): def PostDataToHandler( data, handler, timeout = _READ_TIMEOUT_SEC ):
return JsonFromFuture( BaseRequest.PostDataToHandlerAsync( data, return JsonFromFuture( BaseRequest.PostDataToHandlerAsync( data,
handler, handler,
timeout ) ) timeout ) )
@ -88,7 +89,7 @@ class BaseRequest( object ):
# |timeout| is num seconds to tolerate no response from server before giving # |timeout| is num seconds to tolerate no response from server before giving
# up; see Requests docs for details (we just pass the param along). # up; see Requests docs for details (we just pass the param along).
@staticmethod @staticmethod
def PostDataToHandlerAsync( data, handler, timeout = _DEFAULT_TIMEOUT_SEC ): def PostDataToHandlerAsync( data, handler, timeout = _READ_TIMEOUT_SEC ):
return BaseRequest._TalkToHandlerAsync( data, handler, 'POST', timeout ) return BaseRequest._TalkToHandlerAsync( data, handler, 'POST', timeout )
@ -100,7 +101,7 @@ class BaseRequest( object ):
def _TalkToHandlerAsync( data, def _TalkToHandlerAsync( data,
handler, handler,
method, method,
timeout = _DEFAULT_TIMEOUT_SEC ): timeout = _READ_TIMEOUT_SEC ):
def SendRequest( data, handler, method, timeout ): def SendRequest( data, handler, method, timeout ):
request_uri = _BuildUri( handler ) request_uri = _BuildUri( handler )
if method == 'POST': if method == 'POST':
@ -111,12 +112,12 @@ class BaseRequest( object ):
headers = BaseRequest._ExtraHeaders( method, headers = BaseRequest._ExtraHeaders( method,
request_uri, request_uri,
sent_data ), sent_data ),
timeout = timeout ) timeout = ( _CONNECT_TIMEOUT_SEC, timeout ) )
if method == 'GET': if method == 'GET':
return BaseRequest.session.get( return BaseRequest.session.get(
request_uri, request_uri,
headers = BaseRequest._ExtraHeaders( method, request_uri ), headers = BaseRequest._ExtraHeaders( method, request_uri ),
timeout = timeout ) timeout = ( _CONNECT_TIMEOUT_SEC, timeout ) )
@retries( 5, delay = 0.5, backoff = 1.5 ) @retries( 5, delay = 0.5, backoff = 1.5 )
def DelayedSendRequest( data, handler, method ): def DelayedSendRequest( data, handler, method ):
@ -222,6 +223,11 @@ def HandleServerException( display = True, truncate = False ):
_LoadExtraConfFile( e.extra_conf_file ) _LoadExtraConfFile( e.extra_conf_file )
else: else:
_IgnoreExtraConfFile( e.extra_conf_file ) _IgnoreExtraConfFile( e.extra_conf_file )
except requests.exceptions.ConnectTimeout:
# We don't display this exception to the user since it is likely to happen
# for each subsequent request (typically if the server crashed) and we
# don't want to spam the user with it.
_logger.exception( 'Unable to connect to server' )
except Exception as e: except Exception as e:
_logger.exception( 'Error while handling server response' ) _logger.exception( 'Error while handling server response' )
if display: if display:

View File

@ -171,7 +171,7 @@ def YouCompleteMe_DebugInfo_ServerNotRunning_test( ycm ):
ycm.DebugInfo(), ycm.DebugInfo(),
matches_regexp( matches_regexp(
'Client logfile: .+\n' 'Client logfile: .+\n'
'Server crashed, no debug info from server\n' 'Server errored, no debug info from server\n'
'Server running at: .+\n' 'Server running at: .+\n'
'Server process ID: \d+\n' 'Server process ID: \d+\n'
'Server logfiles:\n' 'Server logfiles:\n'

View File

@ -254,8 +254,7 @@ class YouCompleteMe( object ):
def _ShutdownServer( self ): def _ShutdownServer( self ):
if self.IsServerAlive(): SendShutdownRequest()
SendShutdownRequest()
def RestartServer( self ): def RestartServer( self ):
@ -298,15 +297,13 @@ class YouCompleteMe( object ):
def SendCommandRequest( self, arguments, completer ): def SendCommandRequest( self, arguments, completer ):
if self.IsServerAlive(): return SendCommandRequest( arguments, completer )
return SendCommandRequest( arguments, completer )
def GetDefinedSubcommands( self ): def GetDefinedSubcommands( self ):
if self.IsServerAlive(): with HandleServerException():
with HandleServerException(): return BaseRequest.PostDataToHandler( BuildRequestData(),
return BaseRequest.PostDataToHandler( BuildRequestData(), 'defined_subcommands' )
'defined_subcommands' )
return [] return []
@ -324,9 +321,6 @@ class YouCompleteMe( object ):
except KeyError: except KeyError:
pass pass
if not self.IsServerAlive():
return False
exists_completer = SendCompleterAvailableRequest( filetype ) exists_completer = SendCompleterAvailableRequest( filetype )
if exists_completer is None: if exists_completer is None:
return False return False
@ -363,22 +357,16 @@ class YouCompleteMe( object ):
def OnBufferUnload( self, deleted_buffer_file ): def OnBufferUnload( self, deleted_buffer_file ):
if not self.IsServerAlive():
return
SendEventNotificationAsync( 'BufferUnload', filepath = deleted_buffer_file ) SendEventNotificationAsync( 'BufferUnload', filepath = deleted_buffer_file )
def OnBufferVisit( self ): def OnBufferVisit( self ):
if not self.IsServerAlive():
return
extra_data = {} extra_data = {}
self._AddUltiSnipsDataIfNeeded( extra_data ) self._AddUltiSnipsDataIfNeeded( extra_data )
SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data ) SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data )
def OnInsertLeave( self ): def OnInsertLeave( self ):
if not self.IsServerAlive():
return
SendEventNotificationAsync( 'InsertLeave' ) SendEventNotificationAsync( 'InsertLeave' )
@ -399,8 +387,6 @@ class YouCompleteMe( object ):
def OnCurrentIdentifierFinished( self ): def OnCurrentIdentifierFinished( self ):
if not self.IsServerAlive():
return
SendEventNotificationAsync( 'CurrentIdentifierFinished' ) SendEventNotificationAsync( 'CurrentIdentifierFinished' )
@ -633,8 +619,6 @@ class YouCompleteMe( object ):
def ShowDetailedDiagnostic( self ): def ShowDetailedDiagnostic( self ):
if not self.IsServerAlive():
return
with HandleServerException(): with HandleServerException():
detailed_diagnostic = BaseRequest.PostDataToHandler( detailed_diagnostic = BaseRequest.PostDataToHandler(
BuildRequestData(), 'detailed_diagnostic' ) BuildRequestData(), 'detailed_diagnostic' )
@ -648,10 +632,7 @@ class YouCompleteMe( object ):
debug_info = '' debug_info = ''
if self._client_logfile: if self._client_logfile:
debug_info += 'Client logfile: {0}\n'.format( self._client_logfile ) debug_info += 'Client logfile: {0}\n'.format( self._client_logfile )
if self.IsServerAlive(): debug_info += FormatDebugInfoResponse( SendDebugInfoRequest() )
debug_info += FormatDebugInfoResponse( SendDebugInfoRequest() )
else:
debug_info += 'Server crashed, no debug info from server\n'
debug_info += ( debug_info += (
'Server running at: {0}\n' 'Server running at: {0}\n'
'Server process ID: {1}\n'.format( BaseRequest.server_location, 'Server process ID: {1}\n'.format( BaseRequest.server_location,
@ -669,8 +650,8 @@ class YouCompleteMe( object ):
self._server_stdout, self._server_stdout,
self._server_stderr ] self._server_stderr ]
if self.IsServerAlive(): debug_info = SendDebugInfoRequest()
debug_info = SendDebugInfoRequest() if debug_info:
completer = debug_info[ 'completer' ] completer = debug_info[ 'completer' ]
if completer: if completer:
for server in completer[ 'servers' ]: for server in completer[ 'servers' ]: