From 5731cf99b697bd809b31d54fe86af957e298889a Mon Sep 17 00:00:00 2001 From: Chris Wilcox Date: Mon, 11 Mar 2019 15:26:44 -0700 Subject: [PATCH 1/2] Adds a ready event to BackgroundConsumer to allow waiting for the background thread to start --- api_core/google/api_core/bidi.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api_core/google/api_core/bidi.py b/api_core/google/api_core/bidi.py index 795a8d295f17..cd1e62e21bba 100644 --- a/api_core/google/api_core/bidi.py +++ b/api_core/google/api_core/bidi.py @@ -499,6 +499,7 @@ def __init__(self, bidi_rpc, on_response): self._wake = threading.Condition() self._thread = None self._operational_lock = threading.Lock() + self._ready = threading.Event() def _on_call_done(self, future): # Resume the thread if it's paused, this prevents blocking forever @@ -507,6 +508,7 @@ def _on_call_done(self, future): def _thread_main(self): try: + self._ready.set() self._bidi_rpc.add_done_callback(self._on_call_done) self._bidi_rpc.open() @@ -560,6 +562,11 @@ def start(self): ) thread.daemon = True thread.start() + # Other parts of the code rely on `thread.is_alive` which + # isn't sufficient to know if a thread is active, just that it may + # soon be active. This can cause races. Further protect + # against races by using a ready event and wait on it to be set. + self._ready.wait() self._thread = thread _LOGGER.debug("Started helper thread %s", thread.name) @@ -574,11 +581,12 @@ def stop(self): self._thread.join() self._thread = None + self._ready.clear() @property def is_active(self): """bool: True if the background thread is active.""" - return self._thread is not None and self._thread.is_alive() + return self._thread is not None and self._ready.is_set() and self._thread.is_alive() def pause(self): """Pauses the response stream. From f1508edfe954ddd985956f553cf33b0d29c56711 Mon Sep 17 00:00:00 2001 From: Chris Wilcox Date: Mon, 11 Mar 2019 17:08:20 -0700 Subject: [PATCH 2/2] incorporate feedback from dpcollins to further limit the scope of ready --- api_core/google/api_core/bidi.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/api_core/google/api_core/bidi.py b/api_core/google/api_core/bidi.py index cd1e62e21bba..68d13001a772 100644 --- a/api_core/google/api_core/bidi.py +++ b/api_core/google/api_core/bidi.py @@ -499,16 +499,15 @@ def __init__(self, bidi_rpc, on_response): self._wake = threading.Condition() self._thread = None self._operational_lock = threading.Lock() - self._ready = threading.Event() def _on_call_done(self, future): # Resume the thread if it's paused, this prevents blocking forever # when the RPC has terminated. self.resume() - def _thread_main(self): + def _thread_main(self, ready): try: - self._ready.set() + ready.set() self._bidi_rpc.add_done_callback(self._on_call_done) self._bidi_rpc.open() @@ -557,8 +556,11 @@ def _thread_main(self): def start(self): """Start the background thread and begin consuming the thread.""" with self._operational_lock: + ready = threading.Event() thread = threading.Thread( - name=_BIDIRECTIONAL_CONSUMER_NAME, target=self._thread_main + name=_BIDIRECTIONAL_CONSUMER_NAME, + target=self._thread_main, + args=(ready,) ) thread.daemon = True thread.start() @@ -566,7 +568,7 @@ def start(self): # isn't sufficient to know if a thread is active, just that it may # soon be active. This can cause races. Further protect # against races by using a ready event and wait on it to be set. - self._ready.wait() + ready.wait() self._thread = thread _LOGGER.debug("Started helper thread %s", thread.name) @@ -581,12 +583,11 @@ def stop(self): self._thread.join() self._thread = None - self._ready.clear() @property def is_active(self): """bool: True if the background thread is active.""" - return self._thread is not None and self._ready.is_set() and self._thread.is_alive() + return self._thread is not None and self._thread.is_alive() def pause(self): """Pauses the response stream.