Skip to content

Commit ddf3a1f

Browse files
authored
jobqueue: Thread safety fixes (python-telegram-bot#977)
- Fix JobQueue.jobs to obtain a lock on the internal queue object prior to iterating over it. - Rename JobQueue.queue to JobQueue._queue. This shouldn't be accessible by the user directly, but rather only with sanitized thread safe methods. - JobQueue.interval_seconds - access self.interval only once to avoid race conditions. Fixes python-telegram-bot#968
1 parent 820f4e1 commit ddf3a1f

File tree

2 files changed

+13
-22
lines changed

2 files changed

+13
-22
lines changed

telegram/ext/jobqueue.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class JobQueue(object):
3838
"""This class allows you to periodically perform tasks with the bot.
3939
4040
Attributes:
41-
queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
41+
_queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
4242
bot (:class:`telegram.Bot`): Bot that's send to the handlers.
4343
4444
Args:
@@ -54,7 +54,7 @@ def __init__(self, bot, prevent_autostart=None):
5454
if prevent_autostart is not None:
5555
warnings.warn("prevent_autostart is being deprecated, use `start` method instead.")
5656

57-
self.queue = PriorityQueue()
57+
self._queue = PriorityQueue()
5858
self.bot = bot
5959
self.logger = logging.getLogger(self.__class__.__name__)
6060
self.__start_lock = Lock()
@@ -88,7 +88,6 @@ def put(self, job, next_t=None):
8888
tomorrow.
8989
9090
"""
91-
9291
warnings.warn("'JobQueue.put' is being deprecated, use 'JobQueue.run_once', "
9392
"'JobQueue.run_daily' or 'JobQueue.run_repeating' instead")
9493
if job.job_queue is None:
@@ -119,7 +118,7 @@ def _put(self, job, next_t=None, last_t=None):
119118

120119
self.logger.debug('Putting job %s with t=%f', job.name, next_t)
121120

122-
self.queue.put((next_t, job))
121+
self._queue.put((next_t, job))
123122

124123
# Wake up the loop if this job should be executed next
125124
self._set_next_peek(next_t)
@@ -196,7 +195,6 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None)
196195
queue.
197196
198197
"""
199-
200198
job = Job(callback,
201199
interval=interval,
202200
repeat=True,
@@ -227,7 +225,6 @@ def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None
227225
queue.
228226
229227
"""
230-
231228
job = Job(callback,
232229
interval=datetime.timedelta(days=1),
233230
repeat=True,
@@ -250,14 +247,13 @@ def _set_next_peek(self, t):
250247

251248
def tick(self):
252249
"""Run all jobs that are due and re-enqueue them with their interval."""
253-
254250
now = time.time()
255251

256252
self.logger.debug('Ticking jobs with t=%f', now)
257253

258254
while True:
259255
try:
260-
t, job = self.queue.get(False)
256+
t, job = self._queue.get(False)
261257
except Empty:
262258
break
263259

@@ -270,7 +266,7 @@ def tick(self):
270266
# 2. At the first iteration of the loop only if `self.put()` had triggered
271267
# `self.__tick` because `self._next_peek` wasn't set
272268
self.logger.debug("Next task isn't due yet. Finished!")
273-
self.queue.put((t, job))
269+
self._queue.put((t, job))
274270
self._set_next_peek(t)
275271
break
276272

@@ -298,7 +294,6 @@ def tick(self):
298294

299295
def start(self):
300296
"""Starts the job_queue thread."""
301-
302297
self.__start_lock.acquire()
303298

304299
if not self._running:
@@ -335,7 +330,6 @@ def _main_loop(self):
335330

336331
def stop(self):
337332
"""Stops the thread."""
338-
339333
with self.__start_lock:
340334
self._running = False
341335

@@ -345,8 +339,8 @@ def stop(self):
345339

346340
def jobs(self):
347341
"""Returns a tuple of all jobs that are currently in the ``JobQueue``."""
348-
349-
return tuple(job[1] for job in self.queue.queue if job)
342+
with self._queue.mutex:
343+
return tuple(job[1] for job in self._queue.queue if job)
350344

351345

352346
class Job(object):
@@ -407,7 +401,6 @@ def __init__(self,
407401

408402
def run(self, bot):
409403
"""Executes the callback function."""
410-
411404
self.callback(bot, self)
412405

413406
def schedule_removal(self):
@@ -416,7 +409,6 @@ def schedule_removal(self):
416409
its callback function again.
417410
418411
"""
419-
420412
self._remove.set()
421413

422414
@property
@@ -459,10 +451,11 @@ def interval(self, interval):
459451
@property
460452
def interval_seconds(self):
461453
""":obj:`int`: The interval for this job in seconds."""
462-
if isinstance(self.interval, datetime.timedelta):
463-
return self.interval.total_seconds()
454+
interval = self.interval
455+
if isinstance(interval, datetime.timedelta):
456+
return interval.total_seconds()
464457
else:
465-
return self.interval
458+
return interval
466459

467460
@property
468461
def repeat(self):
@@ -478,7 +471,6 @@ def repeat(self, repeat):
478471
@property
479472
def days(self):
480473
"""Tuple[:obj:`int`]: Optional. Defines on which days of the week the job should run."""
481-
482474
return self._days
483475

484476
@days.setter
@@ -498,7 +490,6 @@ def days(self, days):
498490
@property
499491
def job_queue(self):
500492
""":class:`telegram.ext.JobQueue`: Optional. The ``JobQueue`` this job belongs to."""
501-
502493
return self._job_queue
503494

504495
@job_queue.setter

tests/test_jobqueue.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def test_time_unit_dt_time_tomorrow(self, job_queue):
202202
expected_time = time.time() + delta + 60 * 60 * 24
203203

204204
job_queue.run_once(self.job_datetime_tests, when)
205-
assert pytest.approx(job_queue.queue.get(False)[0]) == expected_time
205+
assert pytest.approx(job_queue._queue.get(False)[0]) == expected_time
206206

207207
def test_run_daily(self, job_queue):
208208
delta = 0.5
@@ -212,7 +212,7 @@ def test_run_daily(self, job_queue):
212212
job_queue.run_daily(self.job_run_once, time_of_day)
213213
sleep(0.6)
214214
assert self.result == 1
215-
assert pytest.approx(job_queue.queue.get(False)[0]) == expected_time
215+
assert pytest.approx(job_queue._queue.get(False)[0]) == expected_time
216216

217217
def test_warnings(self, job_queue):
218218
j = Job(self.job_run_once, repeat=False)

0 commit comments

Comments
 (0)