Skip to content

Commit d91f219

Browse files
committed
feat: forward broker-specific ack options
1 parent ae2b788 commit d91f219

5 files changed

Lines changed: 74 additions & 12 deletions

File tree

‎docs/extending-taskiq/broker.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,19 @@ async def listen(self) -> AsyncGenerator[AckableMessage, None]:
3636
for message in self.my_channel:
3737
yield AckableMessage(
3838
data=message.bytes,
39-
# Ack is a function that takes no parameters.
40-
# So you either set here method of a message,
39+
# Ack can accept broker-specific keyword options.
40+
# So you either set here a method of a message,
4141
# or you can make a closure.
4242
ack=message.ack,
4343
)
4444
```
4545

46+
For manual acknowledgement, options passed to `Context.ack(**kwargs)` are
47+
forwarded unchanged to this callback. Taskiq does not define or interpret these
48+
options; they are specific to the broker. Automatic acknowledgement calls the
49+
callback without options. Brokers that do not support options can continue to
50+
provide a no-argument callback.
51+
4652
## Conventions
4753

4854
For brokers, we have several conventions. It's good if your broker implements them.

‎taskiq/acks.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import enum
22
from collections.abc import Awaitable, Callable
3-
from typing import Any
3+
from typing import Any, TypeAlias
44

55
from pydantic import BaseModel
66

77
from taskiq.utils import maybe_awaitable
88

9+
AckCallback: TypeAlias = Callable[..., None | Awaitable[None]]
10+
911

1012
@enum.unique
1113
class AcknowledgeType(str, enum.Enum):
@@ -54,13 +56,13 @@ class AckableMessage(BaseModel):
5456
"""
5557

5658
data: bytes
57-
ack: Callable[[], None | Awaitable[None]]
59+
ack: AckCallback
5860

5961

6062
class AckController:
6163
"""Controls acknowledgement state for a received message."""
6264

63-
def __init__(self, ack: Callable[[], None | Awaitable[None]] | None) -> None:
65+
def __init__(self, ack: AckCallback | None) -> None:
6466
self._ack = ack
6567
self.is_acked = False
6668

@@ -69,11 +71,11 @@ def is_ackable(self) -> bool:
6971
"""Whether the current message supports acknowledgement."""
7072
return self._ack is not None
7173

72-
async def ack(self) -> None:
73-
"""Acknowledge the current message once."""
74+
async def ack(self, **kwargs: Any) -> None:
75+
"""Acknowledge the current message once with broker-specific options."""
7476
if self._ack is None:
7577
raise RuntimeError("Current message is not ackable.")
7678
if self.is_acked:
7779
return
78-
await maybe_awaitable(self._ack())
80+
await maybe_awaitable(self._ack(**kwargs))
7981
self.is_acked = True

‎taskiq/context.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING
1+
from typing import TYPE_CHECKING, Any
22

33
from taskiq.abc.broker import AsyncBroker
44
from taskiq.acks import AckController
@@ -34,15 +34,17 @@ def is_acked(self) -> bool:
3434
"""Whether the current message has already been acknowledged."""
3535
return self._ack_controller is not None and self._ack_controller.is_acked
3636

37-
async def ack(self) -> None:
37+
async def ack(self, **kwargs: Any) -> None:
3838
"""
3939
Acknowledge current message.
4040
41+
:param kwargs: Broker-specific options forwarded to the acknowledgement
42+
callback.
4143
:raises RuntimeError: if current broker message is not ackable.
4244
"""
4345
if self._ack_controller is None:
4446
raise RuntimeError("Current message is not ackable.")
45-
await self._ack_controller.ack()
47+
await self._ack_controller.ack(**kwargs)
4648

4749
async def requeue(self) -> None:
4850
"""

‎tests/receiver/test_receiver.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,42 @@ def ack_callback() -> None:
560560
assert events == ["task", "ack", "post_execute", "save", "post_save"]
561561

562562

563+
async def test_manual_task_ack_forwards_broker_specific_options() -> None:
564+
"""Context.ack forwards options to the broker's acknowledgement callback."""
565+
events: list[str] = []
566+
received_options: dict[str, Any] = {}
567+
broker = (
568+
InMemoryBroker()
569+
.with_result_backend(
570+
_EventResultBackend(events),
571+
)
572+
.with_middlewares(_EventMiddleware(events))
573+
)
574+
575+
@broker.task(ack_type="manual")
576+
async def my_task(context: Context = Depends()) -> int:
577+
events.append("task")
578+
await context.ack(delete_after_ack=True)
579+
return 1
580+
581+
def ack_callback(**kwargs: Any) -> None:
582+
received_options.update(kwargs)
583+
events.append("ack")
584+
585+
receiver = get_receiver(broker, ack_type=AcknowledgeType.WHEN_SAVED)
586+
broker_message = broker.formatter.dumps(my_task.kicker()._prepare_message())
587+
588+
await receiver.callback(
589+
AckableMessage(
590+
data=broker_message.message,
591+
ack=ack_callback,
592+
),
593+
)
594+
595+
assert received_options == {"delete_after_ack": True}
596+
assert events == ["task", "ack", "post_execute", "save", "post_save"]
597+
598+
563599
async def test_manual_task_ack_is_idempotent() -> None:
564600
"""Calling Context.ack twice acknowledges the message once."""
565601
events: list[str] = []

‎tests/test_acks.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1+
from typing import Any
2+
13
import pytest
24

3-
from taskiq.acks import AcknowledgeType, parse_acknowledge_type
5+
from taskiq.acks import AckController, AcknowledgeType, parse_acknowledge_type
6+
7+
8+
async def test_ack_forwards_broker_specific_options() -> None:
9+
"""AckController forwards explicit options to a supporting callback."""
10+
received_options: dict[str, Any] = {}
11+
12+
async def ack_callback(**kwargs: Any) -> None:
13+
received_options.update(kwargs)
14+
15+
controller = AckController(ack_callback)
16+
await controller.ack(delete_after_ack=True)
17+
18+
assert controller.is_acked
19+
assert received_options == {"delete_after_ack": True}
420

521

622
def test_parse_acknowledge_type_from_enum() -> None:

0 commit comments

Comments
 (0)