Skip to content

Commit 0bd0f17

Browse files
committed
experiment
1 parent 9f8db96 commit 0bd0f17

11 files changed

Lines changed: 910 additions & 30 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Route one task through several brokers with a shared router."""
2+
3+
import asyncio
4+
5+
from taskiq import Flow, InMemoryBroker, TaskiqRouter
6+
7+
router = TaskiqRouter()
8+
9+
default_email_flow = Flow.queue("emails.default")
10+
priority_email_flow = Flow.queue("emails.priority")
11+
bulk_email_flow = Flow.queue("emails.bulk")
12+
13+
default_broker = InMemoryBroker(
14+
router=router,
15+
broker_name="default",
16+
default_flow=default_email_flow,
17+
await_inplace=True,
18+
)
19+
priority_broker = InMemoryBroker(
20+
router=router,
21+
broker_name="priority",
22+
default_flow=priority_email_flow,
23+
await_inplace=True,
24+
)
25+
26+
27+
@default_broker.task(task_name="examples.send_email", domain="notifications")
28+
async def send_email(user_id: int, template: str) -> str:
29+
"""Pretend to render and send an email."""
30+
return f"{template} email sent to user {user_id}"
31+
32+
33+
router.route_task(
34+
send_email.task_name,
35+
broker="priority",
36+
flow=priority_email_flow,
37+
)
38+
39+
40+
async def _main() -> None:
41+
await default_broker.startup()
42+
await priority_broker.startup()
43+
try:
44+
direct_result = await send_email(7, "welcome")
45+
46+
routed_task = await send_email.kiq(7, "welcome")
47+
routed_result = await routed_task.wait_result(timeout=2)
48+
49+
bulk_task = await send_email.kicker().with_route(
50+
"default",
51+
bulk_email_flow,
52+
).kiq(8, "digest")
53+
bulk_result = await bulk_task.wait_result(timeout=2)
54+
55+
print(f"Direct call: {direct_result}")
56+
print(f"Default route: {router.resolve_route(send_email.task_name)}")
57+
print(f"Routed call: {routed_result.return_value}")
58+
print(f"Route override: {bulk_result.return_value}")
59+
finally:
60+
await priority_broker.shutdown()
61+
await default_broker.shutdown()
62+
63+
64+
if __name__ == "__main__":
65+
asyncio.run(_main())
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Declare shared task definitions and bind them in the final application."""
2+
3+
import asyncio
4+
5+
from taskiq import Flow, InMemoryBroker, TaskiqRouter, task_builder
6+
7+
8+
@task_builder("billing.calculate_total", domain="billing")
9+
async def calculate_total(price: int, quantity: int) -> int:
10+
"""Package-level task definition that is not bound to any broker."""
11+
return price * quantity
12+
13+
14+
router = TaskiqRouter()
15+
billing_flow = Flow.queue("billing.tasks")
16+
priority_billing_flow = Flow.queue("billing.priority")
17+
18+
billing_broker = InMemoryBroker(
19+
router=router,
20+
broker_name="billing",
21+
default_flow=billing_flow,
22+
await_inplace=True,
23+
)
24+
25+
registered_calculate_total = billing_broker.register_task(calculate_total)
26+
27+
28+
async def _main() -> None:
29+
await billing_broker.startup()
30+
try:
31+
direct_result = await calculate_total.call(19, 3)
32+
33+
prepared_task = registered_calculate_total.kicker().with_flow(
34+
priority_billing_flow,
35+
).prepare(19, 3)
36+
37+
queued_task = await prepared_task.kiq()
38+
queued_result = await queued_task.wait_result(timeout=2)
39+
40+
print(f"Shared task direct call: {direct_result}")
41+
print(f"Prepared message: {prepared_task.message.task_name}")
42+
print(f"Registered queued call: {queued_result.return_value}")
43+
finally:
44+
await billing_broker.shutdown()
45+
46+
47+
if __name__ == "__main__":
48+
asyncio.run(_main())

‎taskiq/__init__.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,22 @@
2424
TaskiqError,
2525
TaskiqResultTimeoutError,
2626
)
27+
from taskiq.flow import Flow, FlowKind
2728
from taskiq.funcs import gather
29+
from taskiq.kicker import PreparedKiq
2830
from taskiq.message import BrokerMessage, TaskiqMessage
2931
from taskiq.middlewares import (
3032
PrometheusMiddleware,
3133
SimpleRetryMiddleware,
3234
SmartRetryMiddleware,
3335
)
3436
from taskiq.result import TaskiqResult
37+
from taskiq.router import TaskiqRoute, TaskiqRouter
3538
from taskiq.scheduler.scheduled_task import ScheduledTask
3639
from taskiq.scheduler.scheduler import TaskiqScheduler
3740
from taskiq.state import TaskiqState
3841
from taskiq.task import AsyncTaskiqTask
42+
from taskiq.task_builder import TaskDefinition, task_builder
3943

4044
__version__ = version("taskiq")
4145

@@ -47,8 +51,11 @@
4751
"AsyncTaskiqTask",
4852
"BrokerMessage",
4953
"Context",
54+
"Flow",
55+
"FlowKind",
5056
"InMemoryBroker",
5157
"NoResultError",
58+
"PreparedKiq",
5259
"PrometheusMiddleware",
5360
"ResultGetError",
5461
"ResultIsReadyError",
@@ -58,6 +65,7 @@
5865
"SendTaskError",
5966
"SimpleRetryMiddleware",
6067
"SmartRetryMiddleware",
68+
"TaskDefinition",
6169
"TaskiqDepends",
6270
"TaskiqError",
6371
"TaskiqEvents",
@@ -66,10 +74,13 @@
6674
"TaskiqMiddleware",
6775
"TaskiqResult",
6876
"TaskiqResultTimeoutError",
77+
"TaskiqRoute",
78+
"TaskiqRouter",
6979
"TaskiqScheduler",
7080
"TaskiqState",
7181
"ZeroMQBroker",
7282
"__version__",
7383
"async_shared_broker",
7484
"gather",
85+
"task_builder",
7586
]

‎taskiq/abc/broker.py‎

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
from taskiq.decor import AsyncTaskiqDecoratedTask
2525
from taskiq.events import TaskiqEvents
2626
from taskiq.exceptions import TaskBrokerMismatchError
27+
from taskiq.flow import Flow
2728
from taskiq.formatters.proxy_formatter import ProxyFormatter
2829
from taskiq.message import BrokerMessage
2930
from taskiq.result_backends.dummy import DummyResultBackend
31+
from taskiq.router import TaskiqRouter
3032
from taskiq.serializers.json_serializer import JSONSerializer
3133
from taskiq.state import TaskiqState
34+
from taskiq.task_builder import TaskDefinition
3235
from taskiq.utils import maybe_awaitable
3336
from taskiq.warnings import TaskiqDeprecationWarning
3437

@@ -78,6 +81,10 @@ def __init__(
7881
self,
7982
result_backend: "AsyncResultBackend[_T] | None" = None,
8083
task_id_generator: Callable[[], str] | None = None,
84+
*,
85+
router: TaskiqRouter | None = None,
86+
broker_name: str | None = None,
87+
default_flow: Flow | None = None,
8188
) -> None:
8289
if result_backend is None:
8390
result_backend = DummyResultBackend()
@@ -103,6 +110,13 @@ def __init__(
103110
self.serializer: TaskiqSerializer = JSONSerializer()
104111
self.formatter: TaskiqFormatter = ProxyFormatter(self)
105112
self.id_generator = task_id_generator
113+
self.router = router or TaskiqRouter()
114+
self.default_flow = default_flow
115+
self.broker_name = self.router.set_broker(
116+
self,
117+
name=broker_name,
118+
default_flow=default_flow,
119+
)
106120
self.local_task_registry: dict[str, AsyncTaskiqDecoratedTask[Any, Any]] = {}
107121
# Every event has a list of handlers.
108122
# Every handler is a function which takes state as a first argument.
@@ -133,10 +147,14 @@ def find_task(self, task_name: str) -> AsyncTaskiqDecoratedTask[Any, Any] | None
133147
:param task_name: name of a task.
134148
:returns: found task or None.
135149
"""
136-
return self.local_task_registry.get(
137-
task_name,
138-
) or self.global_task_registry.get(
139-
task_name,
150+
return (
151+
self.local_task_registry.get(
152+
task_name,
153+
)
154+
or self.router.find_task(task_name)
155+
or self.global_task_registry.get(
156+
task_name,
157+
)
140158
)
141159

142160
def get_all_tasks(self) -> dict[str, AsyncTaskiqDecoratedTask[Any, Any]]:
@@ -152,7 +170,11 @@ def get_all_tasks(self) -> dict[str, AsyncTaskiqDecoratedTask[Any, Any]]:
152170
153171
:return: dict of all tasks. Keys are task names, values are tasks.
154172
"""
155-
return {**self.global_task_registry, **self.local_task_registry}
173+
return {
174+
**self.global_task_registry,
175+
**self.router.get_all_tasks(),
176+
**self.local_task_registry,
177+
}
156178

157179
def add_dependency_context(self, new_ctx: dict[Any, Any]) -> None:
158180
"""
@@ -237,6 +259,23 @@ async def kick(
237259
:param message: name of a task.
238260
"""
239261

262+
async def kick_to_flow(
263+
self,
264+
message: BrokerMessage,
265+
flow: Flow | None = None,
266+
) -> None:
267+
"""
268+
Send message to a flow-aware broker.
269+
270+
Existing brokers can keep implementing only `kick`. New brokers may
271+
override this method and use `flow` to route to a concrete queue, topic,
272+
stream or any other transport address.
273+
274+
:param message: message to send.
275+
:param flow: optional transport-neutral flow.
276+
"""
277+
await self.kick(message)
278+
240279
@abstractmethod
241280
def listen(self) -> AsyncGenerator[bytes | AckableMessage, None]:
242281
"""
@@ -362,7 +401,8 @@ def inner(
362401

363402
def register_task(
364403
self,
365-
func: Callable[_FuncParams, _ReturnType],
404+
func: Callable[_FuncParams, _ReturnType]
405+
| TaskDefinition[_FuncParams, _ReturnType],
366406
task_name: str | None = None,
367407
**labels: Any,
368408
) -> AsyncTaskiqDecoratedTask[_FuncParams, _ReturnType]:
@@ -380,6 +420,12 @@ def register_task(
380420
381421
:returns: registered task.
382422
"""
423+
if isinstance(func, TaskDefinition):
424+
return self.router.register_task(
425+
func,
426+
broker=self,
427+
flow=self.default_flow,
428+
)
383429
return self.task(task_name=task_name, **labels)(func)
384430

385431
def on_event(self, *events: TaskiqEvents) -> Callable[[EventHandler], EventHandler]:
@@ -533,6 +579,11 @@ def _register_task(
533579
if task.broker != self:
534580
raise TaskBrokerMismatchError(broker=task.broker)
535581
self.local_task_registry[task_name] = task
582+
self.router.register_task(
583+
task,
584+
broker=self,
585+
flow=self.default_flow,
586+
)
536587

537588
async def __aenter__(self) -> None:
538589
"""Starts the broker as ctx manager."""

‎taskiq/brokers/inmemory_broker.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from taskiq.depends.progress_tracker import TaskProgress
1010
from taskiq.events import TaskiqEvents
1111
from taskiq.exceptions import UnknownTaskError
12+
from taskiq.flow import Flow
1213
from taskiq.message import BrokerMessage
1314
from taskiq.receiver import Receiver
15+
from taskiq.router import TaskiqRouter
1416
from taskiq.utils import maybe_awaitable
1517

1618
_ReturnType = TypeVar("_ReturnType")
@@ -130,8 +132,16 @@ def __init__(
130132
max_async_tasks_jitter: int = 0,
131133
propagate_exceptions: bool = True,
132134
await_inplace: bool = False,
135+
*,
136+
router: TaskiqRouter | None = None,
137+
broker_name: str | None = None,
138+
default_flow: Flow | None = None,
133139
) -> None:
134-
super().__init__()
140+
super().__init__(
141+
router=router,
142+
broker_name=broker_name,
143+
default_flow=default_flow,
144+
)
135145
self.result_backend: InmemoryResultBackend[Any] = InmemoryResultBackend(
136146
max_stored_results=max_stored_results,
137147
)

‎taskiq/context.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async def requeue(self) -> None:
3030
requeue_count = int(self.message.labels.get("X-Taskiq-requeue", 0))
3131
requeue_count += 1
3232
self.message.labels["X-Taskiq-requeue"] = str(requeue_count)
33-
await self.broker.kick(self.broker.formatter.dumps(self.message))
33+
await self.broker.router.requeue(self.message, broker=self.broker)
3434
raise NoResultError
3535

3636
def reject(self) -> None:

‎taskiq/flow.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import enum
2+
from dataclasses import dataclass, field, replace
3+
from typing import Any
4+
5+
__all__ = ("Flow", "FlowKind")
6+
7+
8+
@enum.unique
9+
class FlowKind(str, enum.Enum):
10+
"""Transport-neutral flow shape."""
11+
12+
QUEUE = "queue"
13+
TOPIC = "topic"
14+
STREAM = "stream"
15+
16+
17+
@dataclass(frozen=True, slots=True)
18+
class Flow:
19+
"""Transport-neutral publish or subscribe address.
20+
21+
Plain flows are intentionally generic. Every broker may interpret a flow
22+
using its own defaults: queue name, topic, stream, channel, list key, or any
23+
other transport address.
24+
25+
Broker packages can subclass this value object to expose transport-specific
26+
details while still accepting plain Flow instances.
27+
"""
28+
29+
name: str
30+
kind: FlowKind = FlowKind.QUEUE
31+
options: dict[str, Any] = field(
32+
default_factory=dict,
33+
compare=False,
34+
hash=False,
35+
)
36+
37+
@classmethod
38+
def queue(cls, name: str, **options: Any) -> "Flow":
39+
"""Create a queue-like flow."""
40+
return cls(name=name, kind=FlowKind.QUEUE, options=options)
41+
42+
@classmethod
43+
def topic(cls, name: str, **options: Any) -> "Flow":
44+
"""Create a topic-like flow."""
45+
return cls(name=name, kind=FlowKind.TOPIC, options=options)
46+
47+
@classmethod
48+
def stream(cls, name: str, **options: Any) -> "Flow":
49+
"""Create a stream-like flow."""
50+
return cls(name=name, kind=FlowKind.STREAM, options=options)
51+
52+
def with_options(self, **options: Any) -> "Flow":
53+
"""Return the same flow with additional generic options."""
54+
return replace(self, options={**self.options, **options})
55+
56+
def broker_options(self, broker_name: str) -> dict[str, Any]:
57+
"""Return transport options for broker-specific implementations."""
58+
return dict(self.options)

0 commit comments

Comments
 (0)