Repository: googleapis/google-cloud-python

Issue:
PubSub: AttributeError: 'NoneType' object has no attribute 'received_messages'
#### Environment details

OS: OS X and Linux
Python 3.8.1rc1
google-cloud-pubsub==1.1.0
PubSub Emulator running out of the `google/cloud-sdk:latest` docker image

#### Steps to reproduce

I'm working on getting an isolated reproduction of this, but at current it's "run my integration tests against the PubSub emulator" (closed source, unfortunately) and see that on 1.1.0 the stack trace listed below gets written to stdout/stderr as it's seemingly occurring in another thread (not in my logs), and the test suite continues on normally and eventually passes. When pinned on 1.0.2, the stack trace below does not occur.

For whatever it's worth, our code using PubSub via a streaming pull future has not changed recently.

I get that this isn't very helpful by itself but I wanted to get report this sooner than later, instead of just pinning to 1.0.2 and forgetting about it.

#### Stack trace
```python
ERROR:google.api_core.bidi:Thread-ConsumeBidirectionalStream caught unexpected exception 'NoneType' object has no attribute 'received_messages' and will exit.
Traceback (most recent call last):
  File "/Users/briancurtin/elastic/cloud/python-services-v3/.tox/integration/lib/python3.8/site-packages/google/api_core/bidi.py", line 657, in _thread_main
    self._on_response(response)
  File "/Users/briancurtin/elastic/cloud/python-services-v3/.tox/integration/lib/python3.8/site-packages/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py", line 547, in _on_response
    len(response.received_messages),
AttributeError: 'NoneType' object has no attribute 'received_messages'
```


Discussion:
While this won't replicate every time for me I can get it to trigger this exception every 5th or so run. It's odd to manually close the underlying channel, but might be useful to track the issue down. Of note when this occurs for me in normal code i'm not manually closing the channel.

```
from google.cloud import pubsub_v1


def callback(message):
    print(message)


if __name__ == "__main__":
    sc = pubsub_v1.SubscriberClient()
    future = sc.subscribe(sc.subscription_path("development", "pstest"), callback)
    sc.api.transport.channel.close()
    future.result()
```

```
$ python main.py
Thread-ConsumeBidirectionalStream caught unexpected exception 'NoneType' object has no attribute 'received_messages' and will exit.
Traceback (most recent call last):
  File "/tmp/pstest/.direnv/python-3.7.5/lib/python3.7/site-packages/google/api_core/bidi.py", line 657, in _thread_main
    self._on_response(response)
  File "/tmp/pstest/.direnv/python-3.7.5/lib/python3.7/site-packages/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py", line 547, in _on_response
    len(response.received_messages),
AttributeError: 'NoneType' object has no attribute 'received_messages'
Traceback (most recent call last):
  File "main.py", line 13, in <module>
    future.result()
  File "/tmp/pstest/.direnv/python-3.7.5/lib/python3.7/site-packages/google/cloud/pubsub_v1/futures.py", line 105, in result
    raise err
google.api_core.exceptions.Cancelled: 499 Channel closed!
```

Running with the same configuration as the initial report.

Resolving patch:
diff --git a/pubsub/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py b/pubsub/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py
--- a/pubsub/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py
+++ b/pubsub/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py
@@ -542,6 +542,13 @@ def _on_response(self, response):
         After the messages have all had their ack deadline updated, execute
         the callback for each message using the executor.
         """
+        if response is None:
+            _LOGGER.debug(
+                "Response callback invoked with None, likely due to a "
+                "transport shutdown."
+            )
+            return
+
         _LOGGER.debug(
             "Processing %s received message(s), currenty on hold %s (bytes %s).",
             len(response.received_messages),


Test patch:
diff --git a/pubsub/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py b/pubsub/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py
--- a/pubsub/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py
+++ b/pubsub/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py
@@ -721,6 +721,21 @@ def test__on_response_with_leaser_overload():
             assert msg.message_id in ("2", "3")
 
 
+def test__on_response_none_data(caplog):
+    caplog.set_level(logging.DEBUG)
+
+    manager, _, dispatcher, leaser, _, scheduler = make_running_manager()
+    manager._callback = mock.sentinel.callback
+
+    # adjust message bookkeeping in leaser
+    fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=10)
+
+    manager._on_response(response=None)
+
+    scheduler.schedule.assert_not_called()
+    assert "callback invoked with None" in caplog.text
+
+
 def test_retryable_stream_errors():
     # Make sure the config matches our hard-coded tuple of exceptions.
     interfaces = subscriber_client_config.config["interfaces"]