44import inspect
55from typing import Any , Callable , Dict , List , Optional , Tuple
66from .models import Event , Handler
7+ import re
78
89class EventBus :
10+ """
11+ The EventBus is the main entity of this library.
12+ It handles events through associated listeners and
13+ organizes the execution flow.
14+ """
915 def __init__ (self ) -> None :
1016 self ._subscribers : Dict [str , List [Handler ]] = {}
1117 self ._lock = asyncio .Lock ()
1218
13- async def subscribe_async (self , topic : str , handler : Callable [[Event ], Any ], * , priority : int = 0 , once : bool = False ) -> str :
14- h = Handler (neg_priority = - priority , callback = handler , once = once )
19+ async def subscribe_async (self , topic : str , handler : Callable [[Event ], Any ], * , priority : int = 0 , once : bool = False , max_retries : int = 0 ) -> str :
20+ """Same of `subscribe`, but is asynchronous."""
21+ h = Handler (neg_priority = - priority , callback = handler , once = once , max_retries = max_retries )
1522
1623 async with self ._lock :
1724 self ._subscribers .setdefault (topic , []).append (h )
1825
1926 return h .identifier
2027
21- def subscribe (self , topic : str , handler : Callable [[Event ], Any ], * , priority : int = 0 , once : bool = False ) -> str :
22- h = Handler (neg_priority = - priority , callback = handler , once = once )
28+ def subscribe (self , topic : str , handler : Callable [[Event ], Any ], * , priority : int = 0 , once : bool = False , max_retries : int = 0 ) -> str :
29+ """Subscribes a listener to an event/topic. When the event is published, the listener is executed."""
30+ h = Handler (neg_priority = - priority , callback = handler , once = once , max_retries = max_retries )
2331 self ._subscribers .setdefault (topic , []).append (h )
2432
2533 return h .identifier
2634
2735 async def unsubscribe_async (self , topic : str , handler_id : str ) -> bool :
36+ """Same of `unsubscribe`, but is asynchronous."""
2837 async with self ._lock :
2938 handlers = self ._subscribers .get (topic )
3039
@@ -40,6 +49,7 @@ async def unsubscribe_async(self, topic: str, handler_id: str) -> bool:
4049 return len (handlers ) != before
4150
4251 def unsubscribe (self , topic : str , handler_id : str ) -> bool :
52+ """Removes a listener from a topic."""
4353 handlers = self ._subscribers .get (topic )
4454
4555 if not handlers :
@@ -53,69 +63,84 @@ def unsubscribe(self, topic: str, handler_id: str) -> bool:
5363
5464 return len (handlers ) != before
5565
56- def on (self , topic : str , * , priority : int = 0 , once : bool = False ) -> Callable [[Callable [[Event ], Any ]], Callable [[Event ], Any ]]:
57- def decorator (fn : Callable [[Event ], Any ]):
58- self .subscribe (topic , fn , priority = priority , once = once )
66+ def on (self , topic : str , * , priority : int = 0 , once : bool = False , max_retries : int = 0 ) -> Callable [[Callable [[Event ], Any ]], Callable [[Event ], Any ]]:
67+ """Decorator to help creating listeners. It does the same as `subscribe`, but in an easier way."""
68+ def decorator (fn : Callable [[Event ], Any ]) -> Callable [[Event ], Any ]:
69+ self .subscribe (topic , fn , priority = priority , once = once , max_retries = max_retries )
5970 return fn
60-
6171 return decorator
62-
72+
6373 async def publish_async (self , topic : str , payload : Any = None , * , metadata : Optional [Dict [str , Any ]] = None ) -> None :
74+ """Same of `publish`, but is asynchronous."""
6475 event = Event (topic = topic , payload = payload , metadata = metadata or {})
6576
6677 async with self ._lock :
67- handlers = list ( self ._subscribers . get (topic , []) )
78+ handlers = self ._find_handlers (topic )
6879
6980 if not handlers :
7081 return
7182
72- handlers .sort ()
73- loop = asyncio .get_running_loop ()
7483 to_remove : List [str ] = []
7584
7685 for h in handlers :
77- cb = h . callback
86+ await self . _execute_handler ( h , event )
7887
79- try :
80- if inspect .iscoroutinefunction (cb ):
81- await cb (event )
82- else :
83- await loop .run_in_executor (None , cb , event )
84- except Exception :
85- import traceback
86- traceback .print_exc ()
87- finally :
88- if h .once :
89- to_remove .append (h .identifier )
90-
88+ if h .once :
89+ to_remove .append ((topic , h .identifier ))
90+
9191 if to_remove :
9292 async with self ._lock :
93- current = self ._subscribers .get (topic , [])
94- current [:] = [h for h in current if h .identifier not in to_remove ]
95-
96- if not current :
97- self ._subscribers .pop (topic , None )
93+ for t , hid in to_remove :
94+ hs = self ._subscribers .get (t , [])
95+ self ._subscribers [t ] = [x for x in hs if x .identifier != hid ]
9896
9997 def publish (self , topic : str , payload : Any = None , * , metadata : Optional [Dict [str , Any ]] = None ) -> None :
100- try :
101- loop = asyncio .get_running_loop ()
102- except RuntimeError :
103- loop = None
104-
105- if loop is not None and loop .is_running ():
106- raise RuntimeError ("synchronous publish cannot be called from a running asyncio event loop." )
107-
98+ """Publishes an event. All the associated listeners will be executed."""
10899 asyncio .run (self .publish_async (topic , payload , metadata = metadata ))
109100
110- def list_subscribers (self , topic : Optional [str ] = None ) -> Dict [str , List [Tuple [str , int , bool ]]]:
111- out : Dict [str , List [Tuple [str , int , bool ]]] = {}
112-
113- for t , handlers in self ._subscribers .items ():
114- if topic is not None and t != topic :
115- continue
116- out [t ] = [(h .identifier , h .priority , h .once ) for h in handlers ]
117-
118- return out
101+ def list_subscribers (self ) -> Dict [str , List [Tuple [str , int , bool , int ]]]:
102+ return {t : [(h .identifier , h .priority , h .once , h .max_retries ) for h in hs ] for t , hs in self ._subscribers .items ()}
119103
120104 def clear (self ) -> None :
121- self ._subscribers .clear ()
105+ self ._subscribers .clear ()
106+
107+ @staticmethod
108+ def _match_topic (pattern : str , topic : str ) -> bool :
109+ regex_pattern = re .escape (pattern )
110+ regex_pattern = regex_pattern .replace (r"\*" , "[^.]+" )
111+ regex_pattern = regex_pattern .replace (r"\#" , ".*" )
112+ regex_pattern = f"^{ regex_pattern } $"
113+ return re .match (regex_pattern , topic ) is not None
114+
115+ def _find_handlers (self , topic : str ) -> List [Handler ]:
116+ handlers : List [Handler ] = []
117+ for pattern , hs in self ._subscribers .items ():
118+ if self ._match_topic (pattern , topic ):
119+ handlers .extend (hs )
120+
121+ handlers .sort ()
122+ return handlers
123+
124+ async def _execute_handler (self , handler : Handler , event : Event ) -> None :
125+ retries = 0
126+ backoff_base = 0.2
127+
128+ while True :
129+ try :
130+ if inspect .iscoroutinefunction (handler .callback ):
131+ await handler .callback (event )
132+ else :
133+ loop = asyncio .get_running_loop ()
134+ await loop .run_in_executor (None , handler .callback , event )
135+ break
136+ except Exception as e :
137+ retries += 1
138+ if retries > handler .max_retries :
139+ await self .publish_async ('dead.letter' , {
140+ 'original_topic' : event .topic ,
141+ 'event_id' : event .identifier ,
142+ 'payload' : event .payload ,
143+ 'error' : str (e ),
144+ })
145+ break
146+ await asyncio .sleep (backoff_base * (2 ** (retries - 1 )))
0 commit comments