@@ -10,17 +10,47 @@ class BackgroundScheduler:
10
10
11
11
def __init__ (self ):
12
12
self ._next_timer = None
13
+ self ._event_loops = []
14
+ self ._lock = threading .Lock ()
15
+ self ._stopped = False
13
16
14
17
def __del__ (self ):
15
- if self ._next_timer :
16
- self ._next_timer .cancel ()
18
+ self .stop ()
19
+
20
+ def stop (self ):
21
+ """
22
+ Stop all scheduled tasks and clean up resources.
23
+ """
24
+ with self ._lock :
25
+ if self ._stopped :
26
+ return
27
+ self ._stopped = True
28
+
29
+ if self ._next_timer :
30
+ self ._next_timer .cancel ()
31
+ self ._next_timer = None
32
+
33
+ # Stop all event loops
34
+ for loop in self ._event_loops :
35
+ if loop .is_running ():
36
+ loop .call_soon_threadsafe (loop .stop )
37
+
38
+ self ._event_loops .clear ()
17
39
18
40
def run_once (self , delay : float , callback : Callable , * args ):
19
41
"""
20
42
Runs callable task once after certain delay in seconds.
21
43
"""
44
+ with self ._lock :
45
+ if self ._stopped :
46
+ return
47
+
22
48
# Run loop in a separate thread to unblock main thread.
23
49
loop = asyncio .new_event_loop ()
50
+
51
+ with self ._lock :
52
+ self ._event_loops .append (loop )
53
+
24
54
thread = threading .Thread (
25
55
target = _start_event_loop_in_thread ,
26
56
args = (loop , self ._call_later , delay , callback , * args ),
@@ -32,9 +62,16 @@ def run_recurring(self, interval: float, callback: Callable, *args):
32
62
"""
33
63
Runs recurring callable task with given interval in seconds.
34
64
"""
65
+ with self ._lock :
66
+ if self ._stopped :
67
+ return
68
+
35
69
# Run loop in a separate thread to unblock main thread.
36
70
loop = asyncio .new_event_loop ()
37
71
72
+ with self ._lock :
73
+ self ._event_loops .append (loop )
74
+
38
75
thread = threading .Thread (
39
76
target = _start_event_loop_in_thread ,
40
77
args = (loop , self ._call_later_recurring , interval , callback , * args ),
@@ -49,10 +86,17 @@ async def run_recurring_async(
49
86
Runs recurring coroutine with given interval in seconds in the current event loop.
50
87
To be used only from an async context. No additional threads are created.
51
88
"""
89
+ with self ._lock :
90
+ if self ._stopped :
91
+ return
92
+
52
93
loop = asyncio .get_running_loop ()
53
94
wrapped = _async_to_sync_wrapper (loop , coro , * args )
54
95
55
96
def tick ():
97
+ with self ._lock :
98
+ if self ._stopped :
99
+ return
56
100
# Schedule the coroutine
57
101
wrapped ()
58
102
# Schedule next tick
@@ -64,6 +108,9 @@ def tick():
64
108
def _call_later (
65
109
self , loop : asyncio .AbstractEventLoop , delay : float , callback : Callable , * args
66
110
):
111
+ with self ._lock :
112
+ if self ._stopped :
113
+ return
67
114
self ._next_timer = loop .call_later (delay , callback , * args )
68
115
69
116
def _call_later_recurring (
@@ -73,6 +120,9 @@ def _call_later_recurring(
73
120
callback : Callable ,
74
121
* args ,
75
122
):
123
+ with self ._lock :
124
+ if self ._stopped :
125
+ return
76
126
self ._call_later (
77
127
loop , interval , self ._execute_recurring , loop , interval , callback , * args
78
128
)
@@ -87,7 +137,19 @@ def _execute_recurring(
87
137
"""
88
138
Executes recurring callable task with given interval in seconds.
89
139
"""
90
- callback (* args )
140
+ with self ._lock :
141
+ if self ._stopped :
142
+ return
143
+
144
+ try :
145
+ callback (* args )
146
+ except Exception :
147
+ # Silently ignore exceptions during shutdown
148
+ pass
149
+
150
+ with self ._lock :
151
+ if self ._stopped :
152
+ return
91
153
92
154
self ._call_later (
93
155
loop , interval , self ._execute_recurring , loop , interval , callback , * args
@@ -106,7 +168,22 @@ def _start_event_loop_in_thread(
106
168
"""
107
169
asyncio .set_event_loop (event_loop )
108
170
event_loop .call_soon (call_soon_cb , event_loop , * args )
109
- event_loop .run_forever ()
171
+ try :
172
+ event_loop .run_forever ()
173
+ finally :
174
+ try :
175
+ # Clean up pending tasks
176
+ pending = asyncio .all_tasks (event_loop )
177
+ for task in pending :
178
+ task .cancel ()
179
+ # Run loop once more to process cancellations
180
+ event_loop .run_until_complete (
181
+ asyncio .gather (* pending , return_exceptions = True )
182
+ )
183
+ except Exception :
184
+ pass
185
+ finally :
186
+ event_loop .close ()
110
187
111
188
112
189
def _async_to_sync_wrapper (loop , coro_func , * args , ** kwargs ):
0 commit comments