Skip to content

Commit 3596353

Browse files
committed
Handle subscriptions w/ deleted topics cleanly.
Handle subscriptions w/ deleted topics cleanly. Problem observed when trying to clean up subscriptions after failing system tests: the API returns '_deleted-topic_' for the topic path, which prevented us from un-marshalling the subscription. We now allow 'topic' passed to 'Subscription' to be None: In that case (representing subscriptions whose topic has been deleted), caller must pass in an explicit 'client' (which is otherwised not allowed).
1 parent 5471959 commit 3596353

3 files changed

Lines changed: 317 additions & 332 deletions

File tree

gcloud/pubsub/subscription.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ class Subscription(object):
2929
:type name: string
3030
:param name: the name of the subscription
3131
32-
:type topic: :class:`gcloud.pubsub.topic.Topic`
33-
:param topic: the topic to which the subscription belongs..
32+
:type topic: :class:`gcloud.pubsub.topic.Topic` or ``NoneType``
33+
:param topic: the topic to which the subscription belongs; if ``None``,
34+
the subscription's topic has been deleted.
3435
3536
:type ack_deadline: int
3637
:param ack_deadline: the deadline (in seconds) by which messages pulled
@@ -39,10 +40,32 @@ class Subscription(object):
3940
:type push_endpoint: string
4041
:param push_endpoint: URL to which messages will be pushed by the back-end.
4142
If not set, the application must pull messages.
43+
44+
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
45+
:param client: the client to use. If not passed, falls back to the
46+
``client`` stored on the topic.
4247
"""
43-
def __init__(self, name, topic, ack_deadline=None, push_endpoint=None):
48+
49+
_DELETED_TOPIC_PATH = '_deleted-topic_'
50+
"""Value of ``projects.subscriptions.topic`` when topic has been deleted.
51+
52+
See:
53+
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions#Subscription.FIELDS.topic
54+
"""
55+
56+
def __init__(self, name, topic=None, ack_deadline=None, push_endpoint=None,
57+
client=None):
58+
59+
if client is None and topic is None:
60+
raise TypeError("Pass only one of 'topic' or 'client'.")
61+
62+
if client is not None and topic is not None:
63+
raise TypeError("Pass only one of 'topic' or 'client'.")
64+
4465
self.name = name
4566
self.topic = topic
67+
self._client = client or topic._client
68+
self._project = self._client.project
4669
self.ack_deadline = ack_deadline
4770
self.push_endpoint = push_endpoint
4871

@@ -67,23 +90,33 @@ def from_api_repr(cls, resource, client, topics=None):
6790
if topics is None:
6891
topics = {}
6992
topic_path = resource['topic']
70-
topic = topics.get(topic_path)
71-
if topic is None:
72-
# NOTE: This duplicates behavior from Topic.from_api_repr to avoid
73-
# an import cycle.
74-
topic_name = topic_name_from_path(topic_path, client.project)
75-
topic = topics[topic_path] = client.topic(topic_name)
93+
if topic_path == cls._DELETED_TOPIC_PATH:
94+
topic = None
95+
else:
96+
topic = topics.get(topic_path)
97+
if topic is None:
98+
# NOTE: This duplicates behavior from Topic.from_api_repr to
99+
# avoid an import cycle.
100+
topic_name = topic_name_from_path(topic_path, client.project)
101+
topic = topics[topic_path] = client.topic(topic_name)
76102
_, _, _, name = resource['name'].split('/')
77103
ack_deadline = resource.get('ackDeadlineSeconds')
78104
push_config = resource.get('pushConfig', {})
79105
push_endpoint = push_config.get('pushEndpoint')
106+
if topic is None:
107+
return cls(name, ack_deadline=ack_deadline,
108+
push_endpoint=push_endpoint, client=client)
80109
return cls(name, topic, ack_deadline, push_endpoint)
81110

111+
@property
112+
def project(self):
113+
"""Project bound to the subscription."""
114+
return self._client.project
115+
82116
@property
83117
def path(self):
84118
"""URL path for the subscription's APIs"""
85-
project = self.topic.project
86-
return '/projects/%s/subscriptions/%s' % (project, self.name)
119+
return '/projects/%s/subscriptions/%s' % (self.project, self.name)
87120

88121
def _require_client(self, client):
89122
"""Check client or verify over-ride.
@@ -97,7 +130,7 @@ def _require_client(self, client):
97130
:returns: The client passed in or the currently bound client.
98131
"""
99132
if client is None:
100-
client = self.topic._client
133+
client = self._client
101134
return client
102135

103136
def create(self, client=None):

0 commit comments

Comments
 (0)