Skip to content

Commit 6f7765a

Browse files
committed
Added events, state and event handlers.
Signed-off-by: Pavel Kirilin <win10@list.ru>
1 parent bcc9d9e commit 6f7765a

File tree

16 files changed

+263
-18
lines changed

16 files changed

+263
-18
lines changed

docs/examples/introduction/aio_pika_broker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ async def main() -> None:
2222
print(f"Returned value: {result.return_value}")
2323
else:
2424
print("Error found while executing task.")
25+
await broker.shutdown()
2526

2627

2728
if __name__ == "__main__":

docs/examples/introduction/full_example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ async def main() -> None:
2626
print(f"Returned value: {result.return_value}")
2727
else:
2828
print("Error found while executing task.")
29+
await broker.shutdown()
2930

3031

3132
if __name__ == "__main__":

docs/examples/introduction/inmemory_run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ async def add_one(value: int) -> int:
1212

1313

1414
async def main() -> None:
15+
await broker.startup()
1516
# Send the task to the broker.
1617
task = await add_one.kiq(1)
1718
# Wait for the result.
@@ -21,6 +22,7 @@ async def main() -> None:
2122
print(f"Returned value: {result.return_value}")
2223
else:
2324
print("Error found while executing task.")
25+
await broker.shutdown()
2426

2527

2628
if __name__ == "__main__":
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import asyncio
2+
from typing import Optional
3+
4+
from redis.asyncio import ConnectionPool, Redis # type: ignore
5+
from taskiq_aio_pika import AioPikaBroker
6+
from taskiq_redis import RedisAsyncResultBackend
7+
8+
from taskiq import Context, TaskiqEvents, TaskiqState
9+
from taskiq.context import default_context
10+
11+
# To run this example, please install:
12+
# * taskiq
13+
# * taskiq-redis
14+
# * taskiq-aio-pika
15+
16+
broker = AioPikaBroker(
17+
"amqp://localhost",
18+
result_backend=RedisAsyncResultBackend(
19+
"redis://localhost/0",
20+
),
21+
)
22+
23+
24+
@broker.on_event(TaskiqEvents.WORKER_STARTUP)
25+
async def startup(state: TaskiqState) -> None:
26+
# Here we store connection pool on startup for later use.
27+
state.redis = ConnectionPool.from_url("redis://localhost/1")
28+
29+
30+
@broker.on_event(TaskiqEvents.WORKER_SHUTDOWN)
31+
async def shutdown(state: TaskiqState) -> None:
32+
# Here we close our pool on shutdown event.
33+
await state.redis.disconnect()
34+
35+
36+
@broker.task
37+
async def get_val(key: str, context: Context = default_context) -> Optional[str]:
38+
# Now we can use our pool.
39+
redis = Redis(connection_pool=context.state.redis, decode_responses=True)
40+
return await redis.get(key)
41+
42+
43+
@broker.task
44+
async def set_val(key: str, value: str, context: Context = default_context) -> None:
45+
# Now we can use our pool to set value.
46+
await Redis(connection_pool=context.state.redis).set(key, value)
47+
48+
49+
async def main() -> None:
50+
await broker.startup()
51+
52+
set_task = await set_val.kiq("key", "value")
53+
set_result = await set_task.wait_result(with_logs=True)
54+
if set_result.is_err:
55+
print(set_result.log)
56+
raise ValueError("Cannot set value in redis. See logs.")
57+
58+
get_task = await get_val.kiq("key")
59+
get_res = await get_task.wait_result()
60+
print(f"Got redis value: {get_res.return_value}")
61+
62+
await broker.shutdown()
63+
64+
65+
if __name__ == "__main__":
66+
asyncio.run(main())

docs/guide/getting-started.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,16 @@ from taskiq import InMemoryBroker
5454
broker = InMemoryBroker()
5555
```
5656

57-
And that's it. Now let's add some tasks and the main function. You can add tasks in separate modules. You can find more information about that further.
57+
And that's it. Now let's add some tasks and the main function. You can add tasks in separate modules. You can find more information about that further. Also, we call the `startup` method at the beginning of the `main` function.
5858

5959
@[code python](../examples/introduction/inmemory_run.py)
6060

61+
::: tip Cool tip!
62+
63+
Calling the `startup` method is not required, but we strongly recommend you do so.
64+
65+
:::
66+
6167
If you run this code, you will get this in your terminal:
6268

6369
```bash:no-line-numbers

docs/guide/scheduling-tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
order: 7
2+
order: 8
33
---
44

55
# Scheduling tasks

docs/guide/state-and-events.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
order: 7
3+
---
4+
5+
# State and events
6+
7+
The `TaskiqState` is a global variable where you can keep the variables you want to use later.
8+
For example, you want to open a database connection pool at a broker's startup.
9+
10+
This can be acieved by adding event handlers.
11+
12+
You can use one of these events:
13+
* `WORKER_STARTUP`
14+
* `CLIENT_STARTUP`
15+
* `WORKER_SHUTDOWN`
16+
* `CLIENT_SHUTDOWN`
17+
18+
Worker events are called when you start listening to the broker messages using taskiq.
19+
Client events are called when you call the `startup` method of your broker from your code.
20+
21+
This is an example of code using event handlers:
22+
23+
@[code python](../examples/state/events_example.py)
24+
25+
::: tip Cool tip!
26+
27+
If you want to add handlers programmatically, you can use the `broker.add_event_handler` function.
28+
29+
:::
30+
31+
As you can see in this example, this worker will initialize the Redis pool at the startup.
32+
You can access the state from the context.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ authors = ["Pavel Kirilin <win10@list.ru>"]
66
maintainers = ["Pavel Kirilin <win10@list.ru>"]
77
readme = "README.md"
88
repository = "https://github.com/taskiq-python/taskiq"
9+
homepage = "https://taskiq-python.github.io/"
10+
documentation = "https://taskiq-python.github.io/"
911
license = "LICENSE"
1012
classifiers = [
1113
"Typing :: Typed",
@@ -21,7 +23,6 @@ classifiers = [
2123
"Topic :: System :: Networking",
2224
"Development Status :: 3 - Alpha",
2325
]
24-
homepage = "https://github.com/taskiq-python/taskiq"
2526
keywords = ["taskiq", "tasks", "distributed", "async"]
2627

2728
[tool.poetry.dependencies]

taskiq/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@
88
from taskiq.brokers.shared_broker import async_shared_broker
99
from taskiq.brokers.zmq_broker import ZeroMQBroker
1010
from taskiq.context import Context
11+
from taskiq.events import TaskiqEvents
1112
from taskiq.exceptions import TaskiqError
1213
from taskiq.funcs import gather
1314
from taskiq.message import BrokerMessage, TaskiqMessage
1415
from taskiq.result import TaskiqResult
1516
from taskiq.scheduler import ScheduledTask, TaskiqScheduler
17+
from taskiq.state import TaskiqState
1618
from taskiq.task import AsyncTaskiqTask
1719

1820
__all__ = [
1921
"gather",
2022
"Context",
2123
"AsyncBroker",
2224
"TaskiqError",
25+
"TaskiqState",
2326
"TaskiqResult",
2427
"ZeroMQBroker",
28+
"TaskiqEvents",
2529
"TaskiqMessage",
2630
"BrokerMessage",
2731
"InMemoryBroker",

taskiq/abc/broker.py

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,47 @@
1-
import inspect
21
import os
32
import sys
43
from abc import ABC, abstractmethod
4+
from collections import defaultdict
55
from functools import wraps
66
from logging import getLogger
77
from typing import ( # noqa: WPS235
88
TYPE_CHECKING,
99
Any,
10+
Awaitable,
1011
Callable,
1112
Coroutine,
13+
DefaultDict,
1214
Dict,
1315
List,
16+
Literal,
1417
Optional,
1518
TypeVar,
1619
Union,
1720
overload,
1821
)
1922
from uuid import uuid4
2023

21-
from typing_extensions import ParamSpec
24+
from typing_extensions import ParamSpec, TypeAlias
2225

26+
from taskiq.abc.middleware import TaskiqMiddleware
2327
from taskiq.decor import AsyncTaskiqDecoratedTask
28+
from taskiq.events import TaskiqEvents
2429
from taskiq.formatters.json_formatter import JSONFormatter
2530
from taskiq.message import BrokerMessage
2631
from taskiq.result_backends.dummy import DummyResultBackend
32+
from taskiq.state import TaskiqState
33+
from taskiq.utils import maybe_awaitable
2734

2835
if TYPE_CHECKING:
2936
from taskiq.abc.formatter import TaskiqFormatter
30-
from taskiq.abc.middleware import TaskiqMiddleware
3137
from taskiq.abc.result_backend import AsyncResultBackend
3238

3339
_T = TypeVar("_T") # noqa: WPS111
3440
_FuncParams = ParamSpec("_FuncParams")
3541
_ReturnType = TypeVar("_ReturnType")
3642

43+
EventHandler: TypeAlias = Callable[[TaskiqState], Optional[Awaitable[None]]]
44+
3745
logger = getLogger("taskiq")
3846

3947

@@ -49,7 +57,7 @@ def default_id_generator() -> str:
4957
return uuid4().hex
5058

5159

52-
class AsyncBroker(ABC):
60+
class AsyncBroker(ABC): # noqa: WPS230
5361
"""
5462
Async broker.
5563
@@ -75,8 +83,16 @@ def __init__(
7583
self.decorator_class = AsyncTaskiqDecoratedTask
7684
self.formatter: "TaskiqFormatter" = JSONFormatter()
7785
self.id_generator = task_id_generator
78-
79-
def add_middlewares(self, middlewares: "List[TaskiqMiddleware]") -> None:
86+
# Every event has a list of handlers.
87+
# Every handler is a function which takes state as a first argument.
88+
# And handler can be either sync or async.
89+
self.event_handlers: DefaultDict[ # noqa: WPS234
90+
TaskiqEvents,
91+
List[Callable[[TaskiqState], Optional[Awaitable[None]]]],
92+
] = defaultdict(list)
93+
self.state = TaskiqState()
94+
95+
def add_middlewares(self, *middlewares: "TaskiqMiddleware") -> None:
8096
"""
8197
Add a list of middlewares.
8298
@@ -86,11 +102,23 @@ def add_middlewares(self, middlewares: "List[TaskiqMiddleware]") -> None:
86102
:param middlewares: list of middlewares.
87103
"""
88104
for middleware in middlewares:
105+
if not isinstance(middleware, TaskiqMiddleware):
106+
logger.warning(
107+
f"Middleware {middleware} is not an instance of TaskiqMiddleware. "
108+
"Skipping...",
109+
)
110+
continue
89111
middleware.set_broker(self)
90112
self.middlewares.append(middleware)
91113

92114
async def startup(self) -> None:
93115
"""Do something when starting broker."""
116+
event = TaskiqEvents.CLIENT_STARTUP
117+
if self.is_worker_process:
118+
event = TaskiqEvents.WORKER_STARTUP
119+
120+
for handler in self.event_handlers[event]:
121+
await maybe_awaitable(handler(self.state))
94122

95123
async def shutdown(self) -> None:
96124
"""
@@ -99,11 +127,13 @@ async def shutdown(self) -> None:
99127
This method is called,
100128
when broker is closig.
101129
"""
102-
for middleware in self.middlewares:
103-
middleware_shutdown = middleware.shutdown()
104-
if inspect.isawaitable(middleware_shutdown):
105-
await middleware_shutdown
106-
await self.result_backend.shutdown()
130+
event = TaskiqEvents.CLIENT_SHUTDOWN
131+
if self.is_worker_process:
132+
event = TaskiqEvents.WORKER_SHUTDOWN
133+
134+
# Call all shutdown events.
135+
for handler in self.event_handlers[event]:
136+
await maybe_awaitable(handler(self.state))
107137

108138
@abstractmethod
109139
async def kick(
@@ -232,3 +262,43 @@ def inner(
232262
inner_task_name=task_name,
233263
inner_labels=labels or {},
234264
)
265+
266+
def on_event(self, *events: TaskiqEvents) -> Callable[[EventHandler], EventHandler]:
267+
"""
268+
Adds event handler.
269+
270+
This function adds function to call when event occurs.
271+
272+
:param events: events to react to.
273+
:return: a decorator function.
274+
"""
275+
276+
def handler(function: EventHandler) -> EventHandler:
277+
for event in events:
278+
self.event_handlers[event].append(function)
279+
return function
280+
281+
return handler
282+
283+
def add_event_handler(
284+
self,
285+
event: TaskiqEvents,
286+
handler: EventHandler,
287+
) -> None:
288+
"""
289+
Adds event handler.
290+
291+
this function is the same as on_event.
292+
293+
>>> broker.add_event_handler(TaskiqEvents.WORKER_STARTUP, my_startup)
294+
295+
if similar to:
296+
297+
>>> @broker.on_event(TaskiqEvents.WORKER_STARTUP)
298+
>>> async def my_startup(context: Context) -> None:
299+
>>> ...
300+
301+
:param event: Event to react to.
302+
:param handler: handler to call when event is started.
303+
"""
304+
self.event_handlers[event].append(handler)

0 commit comments

Comments
 (0)