Skip to content

Commit 476c930

Browse files
authored
MRG: Merge pull request #523 from octue/enhancement/remove-service-argument-from-topic
Remove `service` argument from `Topic` instantiation
2 parents 1404cdb + 5d3dadf commit 476c930

File tree

17 files changed

+244
-239
lines changed

17 files changed

+244
-239
lines changed

octue/cloud/deployment/google/cloud_run/deployer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from octue.cloud.deployment.google.base_deployer import BaseDeployer, ProgressMessage
2-
from octue.cloud.pub_sub.service import OCTUE_NAMESPACE, Service
2+
from octue.cloud.pub_sub.service import OCTUE_NAMESPACE
33
from octue.cloud.pub_sub.subscription import Subscription
44
from octue.cloud.pub_sub.topic import Topic
55
from octue.exceptions import DeploymentError
6-
from octue.resources.service_backends import GCPPubSubBackend
76

87

98
DEFAULT_CLOUD_RUN_DOCKERFILE_URL = (
@@ -188,11 +187,12 @@ def _create_eventarc_run_trigger(self, update=False):
188187
5,
189188
self.TOTAL_NUMBER_OF_STAGES,
190189
) as progress_message:
191-
service = Service(
192-
backend=GCPPubSubBackend(project_name=self.service_configuration.project_name),
193-
service_id=self.service_id,
190+
topic = Topic(
191+
name=self.service_id,
192+
project_name=self.service_configuration.project_name,
193+
namespace=OCTUE_NAMESPACE,
194194
)
195-
topic = Topic(name=self.service_id, namespace=OCTUE_NAMESPACE, service=service)
195+
196196
topic.create(allow_existing=True)
197197

198198
command = [

octue/cloud/deployment/google/dataflow/pipeline.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
from octue import REPOSITORY_ROOT
1010
from octue.cloud.deployment.google.answer_pub_sub_question import answer_question
1111
from octue.cloud.pub_sub import Topic
12-
from octue.cloud.pub_sub.service import OCTUE_NAMESPACE, Service
12+
from octue.cloud.pub_sub.service import OCTUE_NAMESPACE
1313
from octue.exceptions import DeploymentError
14-
from octue.resources.service_backends import GCPPubSubBackend
1514

1615

1716
logger = logging.getLogger(__name__)
@@ -81,8 +80,8 @@ def create_streaming_job(
8180

8281
service_topic = Topic(
8382
name=service_id,
83+
project_name=project_name,
8484
namespace=OCTUE_NAMESPACE,
85-
service=Service(backend=GCPPubSubBackend(project_name=project_name)),
8685
)
8786

8887
service_topic.create(allow_existing=True)

octue/cloud/emulators/_pub_sub.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def cancel(self):
9292
class MockPublisher:
9393
"""A mock publisher that puts messages in a global dictionary instead of Google Pub/Sub."""
9494

95+
def __init__(self, *args, **kwargs):
96+
pass
97+
9598
def publish(self, topic, data, retry=None, **attributes):
9699
"""Put the data and attributes into a MockMessage and add it to the global messages dictionary before returning
97100
a MockFuture.

octue/cloud/emulators/child.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def __init__(self):
312312
patches=[
313313
patch("octue.cloud.pub_sub.service.Topic", new=MockTopic),
314314
patch("octue.cloud.pub_sub.service.Subscription", new=MockSubscription),
315+
patch("octue.cloud.pub_sub.message_handler.SubscriberClient", new=MockSubscriber),
315316
patch("google.cloud.pubsub_v1.SubscriberClient", new=MockSubscriber),
316317
]
317318
)

octue/cloud/pub_sub/message_handler.py

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pkg_resources
99
from google.api_core import retry
10+
from google.cloud.pubsub_v1 import SubscriberClient
1011

1112
from octue.cloud import EXCEPTIONS_MAPPING
1213
from octue.compatibility import warn_if_incompatible
@@ -30,8 +31,8 @@
3031
class OrderedMessageHandler:
3132
"""A handler for Google Pub/Sub messages that ensures messages are handled in the order they were sent.
3233
33-
:param google.pubsub_v1.services.subscriber.client.SubscriberClient subscriber: a Google Pub/Sub subscriber
3434
:param octue.cloud.pub_sub.subscription.Subscription subscription: the subscription messages are pulled from
35+
:param octue.cloud.pub_sub.service.Service receiving_service: the service that's receiving the messages
3536
:param callable|None handle_monitor_message: a function to handle monitor messages (e.g. send them to an endpoint for plotting or displaying) - this function should take a single JSON-compatible python primitive
3637
:param str|None record_messages_to: if given a path to a JSON file, received messages are saved to it
3738
:param str service_name: an arbitrary name to refer to the service subscribed to by (used for labelling its remote log messages)
@@ -41,20 +42,21 @@ class OrderedMessageHandler:
4142

4243
def __init__(
4344
self,
44-
subscriber,
4545
subscription,
46+
receiving_service,
4647
handle_monitor_message=None,
4748
record_messages_to=None,
4849
service_name="REMOTE",
4950
message_handlers=None,
5051
):
51-
self.subscriber = subscriber
5252
self.subscription = subscription
53+
self.receiving_service = receiving_service
5354
self.handle_monitor_message = handle_monitor_message
5455
self.record_messages_to = record_messages_to
5556
self.service_name = service_name
5657

5758
self.received_delivery_acknowledgement = None
59+
self._subscriber = SubscriberClient()
5860
self._child_sdk_version = None
5961
self._heartbeat_checker = None
6062
self._last_heartbeat = None
@@ -108,55 +110,56 @@ def handle_messages(self, timeout=60, delivery_acknowledgement_timeout=120, maxi
108110
kwargs={"maximum_heartbeat_interval": maximum_heartbeat_interval},
109111
)
110112

111-
self._heartbeat_checker.daemon = True
112-
self._heartbeat_checker.start()
113+
try:
114+
self._heartbeat_checker.daemon = True
115+
self._heartbeat_checker.start()
113116

114-
while self._alive:
117+
while self._alive:
115118

116-
if timeout is not None:
117-
run_time = time.perf_counter() - self._start_time
119+
if timeout is not None:
120+
run_time = time.perf_counter() - self._start_time
118121

119-
if run_time > timeout:
120-
raise TimeoutError(
121-
f"No final answer received from topic {self.subscription.topic.path!r} after {timeout} seconds.",
122-
)
122+
if run_time > timeout:
123+
raise TimeoutError(
124+
f"No final answer received from topic {self.subscription.topic.path!r} after {timeout} seconds.",
125+
)
123126

124-
pull_timeout = timeout - run_time
127+
pull_timeout = timeout - run_time
125128

126-
message = self._pull_message(
127-
timeout=pull_timeout,
128-
delivery_acknowledgement_timeout=delivery_acknowledgement_timeout,
129-
)
129+
message = self._pull_message(
130+
timeout=pull_timeout,
131+
delivery_acknowledgement_timeout=delivery_acknowledgement_timeout,
132+
)
130133

131-
self._waiting_messages[int(message["message_number"])] = message
134+
self._waiting_messages[int(message["message_number"])] = message
132135

133-
try:
134-
while self._waiting_messages:
135-
message = self._waiting_messages.pop(self._previous_message_number + 1)
136+
try:
137+
while self._waiting_messages:
138+
message = self._waiting_messages.pop(self._previous_message_number + 1)
136139

137-
if self.record_messages_to:
138-
recorded_messages.append(message)
140+
if self.record_messages_to:
141+
recorded_messages.append(message)
139142

140-
result = self._handle_message(message)
143+
result = self._handle_message(message)
141144

142-
if result is not None:
143-
self._heartbeat_checker.cancel()
144-
return result
145+
if result is not None:
146+
return result
145147

146-
except KeyError:
147-
pass
148+
except KeyError:
149+
pass
148150

149-
finally:
150-
self._heartbeat_checker.cancel()
151+
finally:
152+
self._heartbeat_checker.cancel()
153+
self._subscriber.close()
151154

152-
if self.record_messages_to:
153-
directory_name = os.path.dirname(self.record_messages_to)
155+
if self.record_messages_to:
156+
directory_name = os.path.dirname(self.record_messages_to)
154157

155-
if not os.path.exists(directory_name):
156-
os.makedirs(directory_name)
158+
if not os.path.exists(directory_name):
159+
os.makedirs(directory_name)
157160

158-
with open(self.record_messages_to, "w") as f:
159-
json.dump(recorded_messages, f)
161+
with open(self.record_messages_to, "w") as f:
162+
json.dump(recorded_messages, f)
160163

161164
raise TimeoutError(
162165
f"No heartbeat has been received within the maximum allowed interval of {maximum_heartbeat_interval}s."
@@ -193,7 +196,7 @@ def _pull_message(self, timeout, delivery_acknowledgement_timeout):
193196
while True:
194197
logger.debug("Pulling messages from Google Pub/Sub: attempt %d.", attempt)
195198

196-
pull_response = self.subscriber.pull(
199+
pull_response = self._subscriber.pull(
197200
request={"subscription": self.subscription.path, "max_messages": 1},
198201
retry=retry.Retry(),
199202
)
@@ -220,11 +223,11 @@ def _pull_message(self, timeout, delivery_acknowledgement_timeout):
220223
f"after {delivery_acknowledgement_timeout} seconds."
221224
)
222225

223-
self.subscriber.acknowledge(request={"subscription": self.subscription.path, "ack_ids": [answer.ack_id]})
226+
self._subscriber.acknowledge(request={"subscription": self.subscription.path, "ack_ids": [answer.ack_id]})
224227

225228
logger.debug(
226229
"%r received a message related to question %r.",
227-
self.subscription.topic.service,
230+
self.receiving_service,
228231
self.subscription.topic.path.split(".")[-1],
229232
)
230233

@@ -260,7 +263,7 @@ def _handle_message(self, message):
260263
if isinstance(error, KeyError):
261264
logger.warning(
262265
"%r received a message of unknown type %r.",
263-
self.subscription.topic.service,
266+
self.receiving_service,
264267
message.get("type", "unknown"),
265268
)
266269
return
@@ -275,7 +278,7 @@ def _handle_delivery_acknowledgement(self, message):
275278
:return None:
276279
"""
277280
self.received_delivery_acknowledgement = True
278-
logger.info("%r's question was delivered at %s.", self.subscription.topic.service, message["delivery_time"])
281+
logger.info("%r's question was delivered at %s.", self.receiving_service, message["delivery_time"])
279282

280283
def _handle_heartbeat(self, message):
281284
"""Record the time the heartbeat was received.
@@ -292,7 +295,7 @@ def _handle_monitor_message(self, message):
292295
:param dict message:
293296
:return None:
294297
"""
295-
logger.debug("%r received a monitor message.", self.subscription.topic.service)
298+
logger.debug("%r received a monitor message.", self.receiving_service)
296299

297300
if self.handle_monitor_message is not None:
298301
self.handle_monitor_message(json.loads(message["data"]))
@@ -359,7 +362,7 @@ def _handle_result(self, message):
359362
"""
360363
logger.info(
361364
"%r received an answer to question %r.",
362-
self.subscription.topic.service,
365+
self.receiving_service,
363366
self.subscription.topic.path.split(".")[-1],
364367
)
365368

octue/cloud/pub_sub/service.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import google.api_core.exceptions
1010
import pkg_resources
11-
from google import auth
1211
from google.api_core import retry
1312
from google.cloud import pubsub_v1
1413

@@ -70,7 +69,6 @@ def __init__(self, backend, service_id=None, run_function=None, *args, **kwargs)
7069
self._record_sent_messages = False
7170
self._sent_messages = []
7271
self._publisher = None
73-
self._credentials = None
7472
super().__init__(*args, **kwargs)
7573

7674
def __repr__(self):
@@ -85,23 +83,10 @@ def publisher(self):
8583
:return google.cloud.pubsub_v1.PublisherClient:
8684
"""
8785
if not self._publisher:
88-
self._publisher = pubsub_v1.PublisherClient(credentials=self.credentials, batch_settings=BATCH_SETTINGS)
86+
self._publisher = pubsub_v1.PublisherClient(batch_settings=BATCH_SETTINGS)
8987

9088
return self._publisher
9189

92-
@property
93-
def credentials(self):
94-
"""Get or instantiate the Google Cloud credentials for the service. No credentials are instantiated until this
95-
property is called for the first time. This allows checking for the `GOOGLE_APPLICATION_CREDENTIALS` environment
96-
variable to be put off until it's needed.
97-
98-
:return google.auth.credentials.Credentials:
99-
"""
100-
if not self._credentials:
101-
self._credentials = auth.default()[0]
102-
103-
return self._credentials
104-
10590
def serve(self, timeout=None, delete_topic_and_subscription_on_exit=False, allow_existing=False):
10691
"""Start the service as a child, waiting to accept questions from any other Octue service using Google Pub/Sub
10792
on the same Google Cloud project. Questions are accepted, processed, and answered asynchronously.
@@ -113,18 +98,18 @@ def serve(self, timeout=None, delete_topic_and_subscription_on_exit=False, allow
11398
"""
11499
logger.info("Starting %r.", self)
115100

116-
topic = Topic(name=self.id, namespace=OCTUE_NAMESPACE, service=self)
117-
subscriber = pubsub_v1.SubscriberClient(credentials=self.credentials)
101+
topic = Topic(name=self.id, project_name=self.backend.project_name, namespace=OCTUE_NAMESPACE)
118102

119103
subscription = Subscription(
120104
name=self.id,
121105
topic=topic,
122106
namespace=OCTUE_NAMESPACE,
123107
project_name=self.backend.project_name,
124-
subscriber=subscriber,
125108
expiration_time=None,
126109
)
127110

111+
subscriber = pubsub_v1.SubscriberClient()
112+
128113
try:
129114
topic.create(allow_existing=allow_existing)
130115
subscription.create(allow_existing=allow_existing)
@@ -276,7 +261,7 @@ def ask(
276261

277262
unlinted_service_id = service_id
278263
service_id = self._clean_service_id(service_id)
279-
question_topic = Topic(name=service_id, namespace=OCTUE_NAMESPACE, service=self)
264+
question_topic = Topic(name=service_id, project_name=self.backend.project_name, namespace=OCTUE_NAMESPACE)
280265

281266
if not question_topic.exists(timeout=timeout):
282267
raise octue.exceptions.ServiceNotFound(f"Service with ID {unlinted_service_id!r} cannot be found.")
@@ -291,7 +276,6 @@ def ask(
291276
topic=answer_topic,
292277
namespace=OCTUE_NAMESPACE,
293278
project_name=self.backend.project_name,
294-
subscriber=pubsub_v1.SubscriberClient(credentials=self.credentials),
295279
push_endpoint=push_endpoint,
296280
)
297281
answer_subscription.create(allow_existing=False)
@@ -342,11 +326,9 @@ def wait_for_answer(
342326
f"Cannot pull from {subscription.path!r} subscription as it is a push subscription."
343327
)
344328

345-
subscriber = pubsub_v1.SubscriberClient(credentials=self.credentials)
346-
347329
message_handler = OrderedMessageHandler(
348-
subscriber=subscriber,
349330
subscription=subscription,
331+
receiving_service=self,
350332
handle_monitor_message=handle_monitor_message,
351333
service_name=service_name,
352334
record_messages_to=record_messages_to,
@@ -360,7 +342,6 @@ def wait_for_answer(
360342
)
361343
finally:
362344
subscription.delete()
363-
subscriber.close()
364345

365346
def instantiate_answer_topic(self, question_uuid, service_id=None):
366347
"""Instantiate the answer topic for the given question UUID and child service ID.
@@ -371,8 +352,8 @@ def instantiate_answer_topic(self, question_uuid, service_id=None):
371352
"""
372353
return Topic(
373354
name=".".join((service_id or self.id, ANSWERS_NAMESPACE, question_uuid)),
355+
project_name=self.backend.project_name,
374356
namespace=OCTUE_NAMESPACE,
375-
service=self,
376357
)
377358

378359
def send_exception(self, topic, timeout=30):

0 commit comments

Comments
 (0)