Fixing a race condition in Completer

Since we use one thread per request, one thread could change the
completions_cache while the other one was depending on the data in it.
This commit is contained in:
Strahinja Val Markovic 2014-01-28 16:31:32 -08:00
parent 6faf0e9c20
commit 9f3a3e3019

View File

@ -18,6 +18,7 @@
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import abc import abc
import threading
from collections import defaultdict from collections import defaultdict
from ycm.utils import ToUtf8IfNeeded, ForceSemanticCompletion, RunningInsideVim from ycm.utils import ToUtf8IfNeeded, ForceSemanticCompletion, RunningInsideVim
@ -105,22 +106,29 @@ class Completer( object ):
self.triggers_for_filetype = ( self.triggers_for_filetype = (
TriggersForFiletype( user_options[ 'semantic_triggers' ] ) TriggersForFiletype( user_options[ 'semantic_triggers' ] )
if user_options[ 'auto_trigger' ] else defaultdict( set ) ) if user_options[ 'auto_trigger' ] else defaultdict( set ) )
self._completions_cache = None self._completions_cache = CompletionsCache()
# It's highly likely you DON'T want to override this function but the *Inner # It's highly likely you DON'T want to override this function but the *Inner
# version of it. # version of it.
def ShouldUseNow( self, request_data ): def ShouldUseNow( self, request_data ):
inner_says_yes = self.ShouldUseNowInner( request_data ) if not self.ShouldUseNowInner( request_data ):
if not inner_says_yes: self._completions_cache.Invalidate()
self._completions_cache = None return False
previous_results_were_empty = ( self._completions_cache and # We have to do the cache valid check and get the completions as part of one
self._completions_cache.CacheValid( # call because we have to ensure a different thread doesn't change the cache
request_data[ 'line_num' ], # data.
request_data[ 'start_column' ] ) and cache_completions = self._completions_cache.GetCompletionsIfCacheValid(
not self._completions_cache.raw_completions ) request_data[ 'line_num' ],
return inner_says_yes and not previous_results_were_empty request_data[ 'start_column' ] )
# If None, then the cache isn't valid and we know we should return true
if cache_completions is None:
return True
else:
previous_results_were_valid = bool( cache_completions )
return previous_results_were_valid
def ShouldUseNowInner( self, request_data ): def ShouldUseNowInner( self, request_data ):
@ -167,17 +175,19 @@ class Completer( object ):
def _GetCandidatesFromSubclass( self, request_data ): def _GetCandidatesFromSubclass( self, request_data ):
if ( self._completions_cache and cache_completions = self._completions_cache.GetCompletionsIfCacheValid(
self._completions_cache.CacheValid( request_data[ 'line_num' ], request_data[ 'line_num' ],
request_data[ 'start_column' ] ) ): request_data[ 'start_column' ] )
return self._completions_cache.raw_completions
if cache_completions:
return cache_completions
else: else:
self._completions_cache = CompletionsCache() raw_completions = self.ComputeCandidatesInner( request_data )
self._completions_cache.raw_completions = self.ComputeCandidatesInner( self._completions_cache.Update(
request_data ) request_data[ 'line_num' ],
self._completions_cache.line = request_data[ 'line_num' ] request_data[ 'start_column' ],
self._completions_cache.column = request_data[ 'start_column' ] raw_completions )
return self._completions_cache.raw_completions return raw_completions
def ComputeCandidatesInner( self, request_data ): def ComputeCandidatesInner( self, request_data ):
@ -277,13 +287,41 @@ class Completer( object ):
class CompletionsCache( object ): class CompletionsCache( object ):
def __init__( self ): def __init__( self ):
self.line = -1 self._access_lock = threading.Lock()
self.column = -1 self.Invalidate()
self.raw_completions = []
self.filtered_completions = []
def Invalidate( self ):
with self._access_lock:
self._line = -1
self._column = -1
self._completions = []
def Update( self, line, column, completions ):
with self._access_lock:
self._line = line
self._column = column
self._completions = completions
def GetCompletions( self ):
with self._access_lock:
return self._completions
def GetCompletionsIfCacheValid( self, current_line, start_column ):
with self._access_lock:
if not self._CacheValidNoLock( current_line, start_column ):
return None
return self._completions
def CacheValid( self, current_line, start_column ): def CacheValid( self, current_line, start_column ):
return current_line == self.line and start_column == self.column with self._access_lock:
return self._CacheValidNoLock( current_line, start_column )
def _CacheValidNoLock( self, current_line, start_column ):
return current_line == self._line and start_column == self._column