Skip to content

Commit 19da693

Browse files
fix: ignore the implementation details of message acknowledgements from the cloud Pub/Sub Message (#312)
* fix: ignore the implementation details of message acknowledgements from the cloud Pub/Sub Message fixes: #311 * fix: ignore the implementation details of message acknowledgements from the cloud Pub/Sub Message fixes: #311 * fix: ignore the implementation details of message acknowledgements from the cloud Pub/Sub Message fixes: #311 * fix: ignore the implementation details of message acknowledgements from the cloud Pub/Sub Message fixes: #311 * fix: ignore the implementation details of message acknowledgements from the cloud Pub/Sub Message fixes: #311
1 parent 1d223f4 commit 19da693

File tree

3 files changed

+100
-93
lines changed

3 files changed

+100
-93
lines changed

google/cloud/pubsublite/cloudpubsub/internal/single_partition_subscriber.py

Lines changed: 34 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@
1313
# limitations under the License.
1414

1515
import asyncio
16-
from typing import Callable, Union, List, Dict, NamedTuple
17-
import queue
16+
from typing import Callable, List, Dict, NamedTuple
1817

19-
from google.api_core.exceptions import FailedPrecondition, GoogleAPICallError
18+
from google.api_core.exceptions import GoogleAPICallError
2019
from google.cloud.pubsub_v1.subscriber.message import Message
2120
from google.pubsub_v1 import PubsubMessage
2221

23-
from google.cloud.pubsublite.internal.wait_ignore_cancelled import wait_ignore_cancelled
2422
from google.cloud.pubsublite.internal.wire.permanent_failable import adapt_error
25-
from google.cloud.pubsublite.internal import fast_serialize
2623
from google.cloud.pubsublite.types import FlowControlSettings
2724
from google.cloud.pubsublite.cloudpubsub.internal.ack_set_tracker import AckSetTracker
25+
from google.cloud.pubsublite.cloudpubsub.internal.wrapped_message import (
26+
AckId,
27+
WrappedMessage,
28+
)
2829
from google.cloud.pubsublite.cloudpubsub.message_transformer import MessageTransformer
2930
from google.cloud.pubsublite.cloudpubsub.nack_handler import NackHandler
3031
from google.cloud.pubsublite.cloudpubsub.internal.single_subscriber import (
@@ -36,27 +37,13 @@
3637
SubscriberResetHandler,
3738
)
3839
from google.cloud.pubsublite_v1 import FlowControlRequest, SequencedMessage
39-
from google.cloud.pubsub_v1.subscriber._protocol import requests
4040

4141

4242
class _SizedMessage(NamedTuple):
4343
message: PubsubMessage
4444
size_bytes: int
4545

4646

47-
class _AckId(NamedTuple):
48-
generation: int
49-
offset: int
50-
51-
def encode(self) -> str:
52-
return fast_serialize.dump([self.generation, self.offset])
53-
54-
@staticmethod
55-
def parse(payload: str) -> "_AckId": # pytype: disable=invalid-annotation
56-
loaded = fast_serialize.load(payload)
57-
return _AckId(generation=loaded[0], offset=loaded[1])
58-
59-
6047
ResettableSubscriberFactory = Callable[[SubscriberResetHandler], Subscriber]
6148

6249

@@ -69,10 +56,10 @@ class SinglePartitionSingleSubscriber(
6956
_nack_handler: NackHandler
7057
_transformer: MessageTransformer
7158

72-
_queue: queue.Queue
7359
_ack_generation_id: int
74-
_messages_by_ack_id: Dict[str, _SizedMessage]
75-
_looper_future: asyncio.Future
60+
_messages_by_ack_id: Dict[AckId, _SizedMessage]
61+
62+
_loop: asyncio.AbstractEventLoop
7663

7764
def __init__(
7865
self,
@@ -89,7 +76,6 @@ def __init__(
8976
self._nack_handler = nack_handler
9077
self._transformer = transformer
9178

92-
self._queue = queue.Queue()
9379
self._ack_generation_id = 0
9480
self._messages_by_ack_id = {}
9581

@@ -104,19 +90,33 @@ def _wrap_message(self, message: SequencedMessage.meta.pb) -> Message:
10490
rewrapped._pb = message
10591
cps_message = self._transformer.transform(rewrapped)
10692
offset = message.cursor.offset
107-
ack_id_str = _AckId(self._ack_generation_id, offset).encode()
93+
ack_id = AckId(self._ack_generation_id, offset)
10894
self._ack_set_tracker.track(offset)
109-
self._messages_by_ack_id[ack_id_str] = _SizedMessage(
95+
self._messages_by_ack_id[ack_id] = _SizedMessage(
11096
cps_message, message.size_bytes
11197
)
112-
wrapped_message = Message(
113-
cps_message._pb,
114-
ack_id=ack_id_str,
115-
delivery_attempt=0,
116-
request_queue=self._queue,
98+
wrapped_message = WrappedMessage(
99+
pb=cps_message._pb,
100+
ack_id=ack_id,
101+
ack_handler=lambda id, ack: self._on_ack_threadsafe(id, ack),
117102
)
118103
return wrapped_message
119104

105+
def _on_ack_threadsafe(self, ack_id: AckId, should_ack: bool) -> None:
106+
"""A function called when a message is acked, may happen from any thread."""
107+
if should_ack:
108+
self._loop.call_soon_threadsafe(lambda: self._handle_ack(ack_id))
109+
return
110+
try:
111+
sized_message = self._messages_by_ack_id[ack_id]
112+
# Call the threadsafe version on ack since the callback may be called from another thread.
113+
self._nack_handler.on_nack(
114+
sized_message.message, lambda: self._on_ack_threadsafe(ack_id, True)
115+
)
116+
except Exception as e:
117+
e2 = adapt_error(e)
118+
self._loop.call_soon_threadsafe(lambda: self.fail(e2))
119+
120120
async def read(self) -> List[Message]:
121121
try:
122122
latest_batch = await self.await_unless_failed(self._underlying.read())
@@ -126,78 +126,23 @@ async def read(self) -> List[Message]:
126126
self.fail(e)
127127
raise e
128128

129-
def _handle_ack(self, message: requests.AckRequest):
129+
def _handle_ack(self, ack_id: AckId):
130130
flow_control = FlowControlRequest()
131131
flow_control._pb.allowed_messages = 1
132-
flow_control._pb.allowed_bytes = self._messages_by_ack_id[
133-
message.ack_id
134-
].size_bytes
132+
flow_control._pb.allowed_bytes = self._messages_by_ack_id[ack_id].size_bytes
135133
self._underlying.allow_flow(flow_control)
136-
del self._messages_by_ack_id[message.ack_id]
134+
del self._messages_by_ack_id[ack_id]
137135
# Always refill flow control tokens, but do not commit offsets from outdated generations.
138-
ack_id = _AckId.parse(message.ack_id)
139136
if ack_id.generation == self._ack_generation_id:
140137
try:
141138
self._ack_set_tracker.ack(ack_id.offset)
142139
except GoogleAPICallError as e:
143140
self.fail(e)
144141

145-
def _handle_nack(self, message: requests.NackRequest):
146-
sized_message = self._messages_by_ack_id[message.ack_id]
147-
try:
148-
# Put the ack request back into the queue since the callback may be called from another thread.
149-
self._nack_handler.on_nack(
150-
sized_message.message,
151-
lambda: self._queue.put(
152-
requests.AckRequest(
153-
ack_id=message.ack_id,
154-
byte_size=0, # Ignored
155-
time_to_ack=0, # Ignored
156-
ordering_key="", # Ignored
157-
)
158-
),
159-
)
160-
except GoogleAPICallError as e:
161-
self.fail(e)
162-
163-
async def _handle_queue_message(
164-
self,
165-
message: Union[
166-
requests.AckRequest,
167-
requests.DropRequest,
168-
requests.ModAckRequest,
169-
requests.NackRequest,
170-
],
171-
):
172-
if isinstance(message, requests.DropRequest) or isinstance(
173-
message, requests.ModAckRequest
174-
):
175-
self.fail(
176-
FailedPrecondition(
177-
"Called internal method of google.cloud.pubsub_v1.subscriber.message.Message "
178-
f"Pub/Sub Lite does not support: {message}"
179-
)
180-
)
181-
elif isinstance(message, requests.AckRequest):
182-
self._handle_ack(message)
183-
else:
184-
self._handle_nack(message)
185-
186-
async def _looper(self):
187-
while True:
188-
try:
189-
# This is not an asyncio.Queue, and therefore we cannot do `await self._queue.get()`.
190-
# A blocking wait would block the event loop, this needs to be a queue.Queue for
191-
# compatibility with the Cloud Pub/Sub Message's requirements.
192-
queue_message = self._queue.get_nowait()
193-
await self._handle_queue_message(queue_message)
194-
except queue.Empty:
195-
await asyncio.sleep(0.1)
196-
197142
async def __aenter__(self):
143+
self._loop = asyncio.get_event_loop()
198144
await self._ack_set_tracker.__aenter__()
199145
await self._underlying.__aenter__()
200-
self._looper_future = asyncio.ensure_future(self._looper())
201146
self._underlying.allow_flow(
202147
FlowControlRequest(
203148
allowed_messages=self._flow_control_settings.messages_outstanding,
@@ -207,7 +152,5 @@ async def __aenter__(self):
207152
return self
208153

209154
async def __aexit__(self, exc_type, exc_value, traceback):
210-
self._looper_future.cancel()
211-
await wait_ignore_cancelled(self._looper_future)
212155
await self._underlying.__aexit__(exc_type, exc_value, traceback)
213156
await self._ack_set_tracker.__aexit__(exc_type, exc_value, traceback)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from concurrent import futures
2+
import logging
3+
from typing import NamedTuple, Callable
4+
5+
from google.cloud.pubsub_v1.subscriber.message import Message
6+
from google.pubsub_v1 import PubsubMessage
7+
from google.cloud.pubsub_v1.subscriber.exceptions import AcknowledgeStatus
8+
9+
10+
class AckId(NamedTuple):
11+
generation: int
12+
offset: int
13+
14+
def encode(self) -> str:
15+
return str(self.generation) + "," + str(self.offset)
16+
17+
18+
_SUCCESS_FUTURE = futures.Future()
19+
_SUCCESS_FUTURE.set_result(AcknowledgeStatus.SUCCESS)
20+
21+
22+
class WrappedMessage(Message):
23+
_id: AckId
24+
_ack_handler: Callable[[AckId, bool], None]
25+
26+
def __init__(
27+
self,
28+
pb: PubsubMessage.meta.pb,
29+
ack_id: AckId,
30+
ack_handler: Callable[[AckId, bool], None],
31+
):
32+
super().__init__(pb, ack_id.encode(), 1, None)
33+
self._id = ack_id
34+
self._ack_handler = ack_handler
35+
36+
def ack(self):
37+
self._ack_handler(self._id, True)
38+
39+
def ack_with_response(self) -> "futures.Future":
40+
self._ack_handler(self._id, True)
41+
return _SUCCESS_FUTURE
42+
43+
def nack(self):
44+
self._ack_handler(self._id, False)
45+
46+
def nack_with_response(self) -> "futures.Future":
47+
self._ack_handler(self._id, False)
48+
return _SUCCESS_FUTURE
49+
50+
def drop(self):
51+
logging.warning(
52+
"Likely incorrect call to drop() on Pub/Sub Lite message. Pub/Sub Lite does not support redelivery in this way."
53+
)
54+
55+
def modify_ack_deadline(self, seconds: int):
56+
logging.warning(
57+
"Likely incorrect call to modify_ack_deadline() on Pub/Sub Lite message. Pub/Sub Lite does not support redelivery in this way."
58+
)
59+
60+
def modify_ack_deadline_with_response(self, seconds: int) -> "futures.Future":
61+
logging.warning(
62+
"Likely incorrect call to modify_ack_deadline_with_response() on Pub/Sub Lite message. Pub/Sub Lite does not support redelivery in this way."
63+
)
64+
return _SUCCESS_FUTURE

tests/unit/pubsublite/cloudpubsub/internal/single_partition_subscriber_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from google.cloud.pubsublite.cloudpubsub.internal.ack_set_tracker import AckSetTracker
2626
from google.cloud.pubsublite.cloudpubsub.internal.single_partition_subscriber import (
2727
SinglePartitionSingleSubscriber,
28-
_AckId,
28+
AckId,
2929
)
3030
from google.cloud.pubsublite.cloudpubsub.message_transformer import MessageTransformer
3131
from google.cloud.pubsublite.cloudpubsub.nack_handler import NackHandler
@@ -48,7 +48,7 @@ def mock_async_context_manager(cm):
4848

4949

5050
def ack_id(generation, offset) -> str:
51-
return _AckId(generation, offset).encode()
51+
return AckId(generation, offset).encode()
5252

5353

5454
@pytest.fixture()

0 commit comments

Comments
 (0)