Skip to content

Commit 82a56b7

Browse files
authored
Use FastStream's native acknowledgements (#106)
1 parent 0b66575 commit 82a56b7

File tree

10 files changed

+115
-58
lines changed

10 files changed

+115
-58
lines changed

conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ def anyio_backend(request: pytest.FixtureRequest) -> object:
1414
return request.param
1515

1616

17+
@pytest.fixture
18+
def first_server_connection_parameters() -> stompman.ConnectionParameters:
19+
return stompman.ConnectionParameters(host="127.0.0.1", port=9000, login="admin", passcode=":=123")
20+
21+
1722
@pytest.fixture(
1823
params=[
1924
stompman.ConnectionParameters(host="127.0.0.1", port=9000, login="admin", passcode=":=123"),

packages/faststream-stomp/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ if __name__ == "__main__":
4848
```
4949

5050
Also there are `StompRouter` and `TestStompBroker` for testing. It works similarly to built-in brokers from FastStream, I recommend to read the original [FastStream documentation](https://faststream.airt.ai/latest/getting-started).
51+
52+
### Caveats
53+
54+
- When exception is raised in consumer handler, the message will be nacked (FastStream doesn't do this by default)

packages/faststream-stomp/faststream_stomp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from faststream_stomp.broker import StompBroker
2+
from faststream_stomp.message import StompStreamMessage
23
from faststream_stomp.publisher import StompPublisher
34
from faststream_stomp.router import StompRoute, StompRoutePublisher, StompRouter
45
from faststream_stomp.subscriber import StompSubscriber
@@ -10,6 +11,7 @@
1011
"StompRoute",
1112
"StompRoutePublisher",
1213
"StompRouter",
14+
"StompStreamMessage",
1315
"StompSubscriber",
1416
"TestStompBroker",
1517
]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import typing
2+
from typing import cast
3+
4+
import stompman
5+
from faststream.broker.message import StreamMessage, gen_cor_id
6+
7+
8+
class StompStreamMessage(StreamMessage[stompman.AckableMessageFrame]):
9+
async def ack(self) -> None:
10+
if not self.committed:
11+
await self.raw_message.ack()
12+
return await super().ack()
13+
14+
async def nack(self) -> None:
15+
if not self.committed:
16+
await self.raw_message.nack()
17+
return await super().nack()
18+
19+
async def reject(self) -> None:
20+
if not self.committed:
21+
await self.raw_message.nack()
22+
return await super().reject()
23+
24+
@classmethod
25+
async def from_frame(cls, message: stompman.AckableMessageFrame) -> typing.Self:
26+
return cls(
27+
raw_message=message,
28+
body=message.body,
29+
headers=cast("dict[str, str]", message.headers),
30+
content_type=message.headers.get("content-type"),
31+
message_id=message.headers["message-id"],
32+
correlation_id=cast("str", message.headers.get("correlation-id", gen_cor_id())),
33+
)

packages/faststream-stomp/faststream_stomp/parser.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

packages/faststream-stomp/faststream_stomp/registrator.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Callable, Iterable, Mapping, Sequence
1+
from collections.abc import Iterable, Mapping, Sequence
22
from typing import Any, cast
33

44
import stompman
@@ -11,9 +11,6 @@
1111
from faststream_stomp.subscriber import StompSubscriber
1212

1313

14-
def noop_handle_suppressed_exception(exception: Exception, message: stompman.MessageFrame) -> None: ...
15-
16-
1714
class StompRegistrator(ABCBroker[stompman.MessageFrame]):
1815
_subscribers: Mapping[int, StompSubscriber]
1916
_publishers: Mapping[int, StompPublisher]
@@ -22,12 +19,11 @@ def subscriber( # type: ignore[override]
2219
self,
2320
destination: str,
2421
*,
25-
ack: stompman.AckMode = "client-individual",
22+
ack_mode: stompman.AckMode = "client-individual",
2623
headers: dict[str, str] | None = None,
27-
on_suppressed_exception: Callable[[Exception, stompman.MessageFrame], Any] = noop_handle_suppressed_exception,
28-
suppressed_exception_classes: tuple[type[Exception], ...] = (Exception,),
2924
# other args
3025
dependencies: Iterable[Depends] = (),
26+
no_ack: bool = False,
3127
parser: CustomCallable | None = None,
3228
decoder: CustomCallable | None = None,
3329
middlewares: Sequence[SubscriberMiddleware[stompman.MessageFrame]] = (),
@@ -41,11 +37,10 @@ def subscriber( # type: ignore[override]
4137
super().subscriber(
4238
StompSubscriber(
4339
destination=destination,
44-
ack=ack,
40+
ack_mode=ack_mode,
4541
headers=headers,
46-
on_suppressed_exception=on_suppressed_exception,
47-
suppressed_exception_classes=suppressed_exception_classes,
4842
retry=retry,
43+
no_ack=no_ack,
4944
broker_middlewares=self._middlewares,
5045
broker_dependencies=self._dependencies,
5146
title_=title,

packages/faststream-stomp/faststream_stomp/router.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from faststream.broker.types import BrokerMiddleware, CustomCallable, PublisherMiddleware, SubscriberMiddleware
88
from faststream.types import SendableMessage
99

10-
from faststream_stomp.registrator import StompRegistrator, noop_handle_suppressed_exception
10+
from faststream_stomp.registrator import StompRegistrator
1111

1212

1313
class StompRoutePublisher(ArgsContainer):
@@ -47,13 +47,12 @@ def __init__(
4747
call: Callable[..., SendableMessage] | Callable[..., Awaitable[SendableMessage]],
4848
destination: str,
4949
*,
50-
ack: stompman.AckMode = "client-individual",
50+
ack_mode: stompman.AckMode = "client-individual",
5151
headers: dict[str, str] | None = None,
52-
on_suppressed_exception: Callable[[Exception, stompman.MessageFrame], Any] = noop_handle_suppressed_exception,
53-
suppressed_exception_classes: tuple[type[Exception], ...] = (Exception,),
5452
# other args
5553
publishers: Iterable[StompRoutePublisher] = (),
5654
dependencies: Iterable[Depends] = (),
55+
no_ack: bool = False,
5756
parser: CustomCallable | None = None,
5857
decoder: CustomCallable | None = None,
5958
middlewares: Sequence[SubscriberMiddleware[stompman.MessageFrame]] = (),
@@ -65,12 +64,11 @@ def __init__(
6564
super().__init__(
6665
call=call,
6766
destination=destination,
68-
ack=ack,
67+
ack_mode=ack_mode,
6968
headers=headers,
70-
on_suppressed_exception=on_suppressed_exception,
71-
suppressed_exception_classes=suppressed_exception_classes,
7269
publishers=publishers,
7370
dependencies=dependencies,
71+
no_ack=no_ack,
7472
parser=parser,
7573
decoder=decoder,
7674
middlewares=middlewares,

packages/faststream-stomp/faststream_stomp/subscriber.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,42 @@
55
from fast_depends.dependencies import Depends
66
from faststream.asyncapi.schema import Channel, CorrelationId, Message, Operation
77
from faststream.asyncapi.utils import resolve_payloads
8-
from faststream.broker.message import StreamMessage
8+
from faststream.broker.message import StreamMessage, decode_message
99
from faststream.broker.publisher.fake import FakePublisher
1010
from faststream.broker.publisher.proto import ProducerProto
1111
from faststream.broker.subscriber.usecase import SubscriberUsecase
1212
from faststream.broker.types import AsyncCallable, BrokerMiddleware, CustomCallable
1313
from faststream.types import AnyDict, Decorator, LoggerProto
14+
from faststream.utils.functions import to_async
1415

15-
from faststream_stomp import parser
16+
from faststream_stomp.message import StompStreamMessage
1617

1718

1819
class StompSubscriber(SubscriberUsecase[stompman.MessageFrame]):
1920
def __init__(
2021
self,
2122
*,
2223
destination: str,
23-
ack: stompman.AckMode = "client-individual",
24-
headers: dict[str, str] | None = None,
25-
on_suppressed_exception: Callable[[Exception, stompman.MessageFrame], Any],
26-
suppressed_exception_classes: tuple[type[Exception], ...] = (Exception,),
24+
ack_mode: stompman.AckMode,
25+
headers: dict[str, str] | None,
2726
retry: bool | int,
27+
no_ack: bool,
2828
broker_dependencies: Iterable[Depends],
2929
broker_middlewares: Sequence[BrokerMiddleware[stompman.MessageFrame]],
30-
default_parser: AsyncCallable = parser.parse_message,
31-
default_decoder: AsyncCallable = parser.decode_message,
30+
default_parser: AsyncCallable = StompStreamMessage.from_frame,
31+
default_decoder: AsyncCallable = to_async(decode_message), # noqa: B008
3232
# AsyncAPI information
3333
title_: str | None,
3434
description_: str | None,
3535
include_in_schema: bool,
3636
) -> None:
3737
self.destination = destination
38-
self.ack = ack
38+
self.ack_mode = ack_mode
3939
self.headers = headers
40-
self.on_suppressed_exception = on_suppressed_exception
41-
self.suppressed_exception_classes = suppressed_exception_classes
42-
self._subscription: stompman.AutoAckSubscription | None = None
40+
self._subscription: stompman.ManualAckSubscription | None = None
4341

4442
super().__init__(
45-
no_ack=self.ack == "auto",
43+
no_ack=no_ack or self.ack_mode == "auto",
4644
no_reply=True,
4745
retry=retry,
4846
broker_dependencies=broker_dependencies,
@@ -85,13 +83,11 @@ def setup( # type: ignore[override]
8583

8684
async def start(self) -> None:
8785
await super().start()
88-
self._subscription = await self.client.subscribe(
86+
self._subscription = await self.client.subscribe_with_manual_ack(
8987
destination=self.destination,
9088
handler=self.consume,
91-
ack=self.ack,
89+
ack=self.ack_mode,
9290
headers=self.headers,
93-
on_suppressed_exception=self.on_suppressed_exception,
94-
suppressed_exception_classes=self.suppressed_exception_classes,
9591
)
9692

9793
async def close(self) -> None:

packages/faststream-stomp/faststream_stomp/testing.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import uuid
22
from typing import TYPE_CHECKING, Any
3+
from unittest import mock
34
from unittest.mock import AsyncMock
45

6+
import stompman
57
from faststream.broker.message import encode_message
68
from faststream.testing.broker import TestBroker
79
from faststream.types import SendableMessage
8-
from stompman import MessageFrame
910

1011
from faststream_stomp.broker import StompBroker
1112
from faststream_stomp.publisher import StompProducer, StompPublisher
@@ -44,6 +45,12 @@ async def _fake_connect(
4445
broker._producer = FakeStompProducer(broker) # noqa: SLF001
4546

4647

48+
class FakeAckableMessageFrame(stompman.AckableMessageFrame):
49+
async def ack(self) -> None: ...
50+
51+
async def nack(self) -> None: ...
52+
53+
4754
class FakeStompProducer(StompProducer):
4855
def __init__(self, broker: StompBroker) -> None:
4956
self.broker = broker
@@ -66,7 +73,7 @@ async def publish( # type: ignore[override]
6673
all_headers["correlation-id"] = correlation_id # type: ignore[typeddict-unknown-key]
6774
if content_type:
6875
all_headers["content-type"] = content_type
69-
frame = MessageFrame(headers=all_headers, body=body)
76+
frame = FakeAckableMessageFrame(headers=all_headers, body=body, _subscription=mock.AsyncMock())
7077

7178
for handler in self.broker._subscribers.values(): # noqa: SLF001
7279
if handler.destination == destination:

packages/faststream-stomp/test_faststream_stomp/test_integration.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import asyncio
2+
from typing import Annotated
23

34
import faker
45
import faststream_stomp
56
import pytest
67
import stompman
78
from faststream import BaseMiddleware, Context, FastStream
89
from faststream.broker.message import gen_cor_id
10+
from faststream.exceptions import AckMessage, NackMessage, RejectMessage
11+
from faststream_stomp.message import StompStreamMessage
912

1013
pytestmark = pytest.mark.anyio
1114

1215

1316
@pytest.fixture
14-
def broker(connection_parameters: stompman.ConnectionParameters) -> faststream_stomp.StompBroker:
15-
return faststream_stomp.StompBroker(stompman.Client([connection_parameters]))
17+
def broker(first_server_connection_parameters: stompman.ConnectionParameters) -> faststream_stomp.StompBroker:
18+
return faststream_stomp.StompBroker(stompman.Client([first_server_connection_parameters]))
1619

1720

1821
async def test_simple(faker: faker.Faker, broker: faststream_stomp.StompBroker) -> None:
@@ -114,3 +117,37 @@ async def test_no_connection(self, broker: faststream_stomp.StompBroker) -> None
114117
async def test_timeout(self, broker: faststream_stomp.StompBroker) -> None:
115118
async with broker:
116119
assert not await broker.ping(0)
120+
121+
122+
@pytest.mark.parametrize("exception", [Exception, NackMessage, AckMessage, RejectMessage])
123+
async def test_ack_nack_reject_exception(
124+
faker: faker.Faker, broker: faststream_stomp.StompBroker, exception: type[Exception]
125+
) -> None:
126+
event = asyncio.Event()
127+
128+
@broker.subscriber(destination := faker.pystr())
129+
def _() -> None:
130+
event.set()
131+
raise exception
132+
133+
async with broker:
134+
await broker.start()
135+
await broker.publish(faker.pystr(), destination)
136+
await event.wait()
137+
138+
139+
@pytest.mark.parametrize("method_name", ["ack", "nack", "reject"])
140+
async def test_ack_nack_reject_method_call(
141+
faker: faker.Faker, broker: faststream_stomp.StompBroker, method_name: str
142+
) -> None:
143+
event = asyncio.Event()
144+
145+
@broker.subscriber(destination := faker.pystr())
146+
async def _(message: Annotated[StompStreamMessage, Context()]) -> None:
147+
await getattr(message, method_name)()
148+
event.set()
149+
150+
async with broker:
151+
await broker.start()
152+
await broker.publish(faker.pystr(), destination)
153+
await event.wait()

0 commit comments

Comments
 (0)