Skip to content

Commit f48fe03

Browse files
authored
Poll for new tasks (#176)
- Setting to configure a polling interval for new task to reduce load when there are many workers & tasks. Instead of subscribing to the activity channel, which results in N_WORKERS messages for each task (and then each of them polling for the task), we just poll periodically. Downside is that with polling enabled and few workers it might take up to the polling interval to pick up a new task. - Setting to disable publishing to the activity channel. This should be only used if all workers are polling.
1 parent 6d96607 commit f48fe03

3 files changed

Lines changed: 47 additions & 16 deletions

File tree

tasktiger/task.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def _move(self, from_state=None, to_state=None, when=None, mode=None):
285285
_key(from_state), queue, _key(from_state, queue), client=pipeline
286286
)
287287

288-
if to_state == QUEUED:
288+
if to_state == QUEUED and self.tiger.config["PUBLISH_QUEUED_TASKS"]:
289289
pipeline.publish(_key('activity'), queue)
290290

291291
try:
@@ -361,7 +361,7 @@ def delay(self, when=None, max_queue_size=None):
361361
mode='nx',
362362
client=pipeline,
363363
)
364-
if state == QUEUED:
364+
if state == QUEUED and tiger.config["PUBLISH_QUEUED_TASKS"]:
365365
pipeline.publish(tiger._key('activity'), self.queue)
366366
pipeline.execute()
367367

tasktiger/tasktiger.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ def init(self, connection=None, config=None, setup_structlog=False):
195195
# increase Redis storage requirements and therefore can be disabled
196196
# if that is a concern.
197197
'STORE_TRACEBACKS': True,
198+
# Set to > 0 to poll periodically for queues with tasks. Otherwise
199+
# subscribe to the activity channel. Use for more efficient task
200+
# processing with a large amount of workers.
201+
'POLL_TASK_QUEUES_INTERVAL': 0,
202+
# Whether to publish new tasks to the activity channel. Only set to
203+
# False if all the workers are polling queues.
204+
'PUBLISH_QUEUED_TASKS': True,
198205
}
199206
if config:
200207
self.config.update(config)

tasktiger/worker.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,27 @@ def _worker_queue_scheduled_tasks(self):
207207
# XXX: ideally this would be in the same pipeline, but we only want
208208
# to announce if there was a result.
209209
if result:
210-
self.connection.publish(self._key('activity'), queue)
210+
if self.config["PUBLISH_QUEUED_TASKS"]:
211+
self.connection.publish(self._key('activity'), queue)
211212
self._did_work = True
212213

213-
def _wait_for_new_tasks(self, timeout=0, batch_timeout=0):
214+
def _poll_for_queues(self):
214215
"""
215-
Check activity channel and wait as necessary.
216+
Refresh list of queues.
217+
218+
Wait if we did not do any work.
219+
220+
This is only used when using polling to get queues with queued tasks.
221+
"""
222+
if not self._did_work:
223+
time.sleep(self.config["POLL_TASK_QUEUES_INTERVAL"])
224+
self._refresh_queue_set()
225+
226+
def _pubsub_for_queues(self, timeout=0, batch_timeout=0):
227+
"""
228+
Check activity channel for new queues and wait as necessary.
229+
230+
This is only used when using pubsub to get queues with queued tasks.
216231
217232
This method is also used to slow down the main processing loop to reduce
218233
the effects of rapidly sending Redis commands. This method will exit
@@ -223,7 +238,6 @@ def _wait_for_new_tasks(self, timeout=0, batch_timeout=0):
223238
3. Timeout seconds have passed, this is the maximum time to stay in
224239
this method
225240
"""
226-
227241
new_queue_found = False
228242
start_time = batch_exit = time.time()
229243
while True:
@@ -1091,6 +1105,11 @@ def _queue_periodic_tasks(self):
10911105
'queued periodic task', func=task.serialized_func, when=when
10921106
)
10931107

1108+
def _refresh_queue_set(self):
1109+
self._queue_set = set(
1110+
self._filter_queues(self.connection.smembers(self._key(QUEUED)))
1111+
)
1112+
10941113
def run(self, once=False, force_once=False):
10951114
"""
10961115
Main loop of the worker.
@@ -1125,21 +1144,25 @@ def run(self, once=False, force_once=False):
11251144
# Then, listen to the activity channel.
11261145
# XXX: This can get inefficient when having lots of queues.
11271146

1128-
self._pubsub = self.connection.pubsub()
1129-
self._pubsub.subscribe(self._key('activity'))
1147+
if self.config["POLL_TASK_QUEUES_INTERVAL"]:
1148+
self._pubsub = None
1149+
else:
1150+
self._pubsub = self.connection.pubsub()
1151+
self._pubsub.subscribe(self._key('activity'))
11301152

1131-
self._queue_set = set(
1132-
self._filter_queues(self.connection.smembers(self._key(QUEUED)))
1133-
)
1153+
self._refresh_queue_set()
11341154

11351155
try:
11361156
while True:
11371157
# Update the queue set on every iteration so we don't get stuck
11381158
# on processing a specific queue.
1139-
self._wait_for_new_tasks(
1140-
timeout=self.config['SELECT_TIMEOUT'],
1141-
batch_timeout=self.config['SELECT_BATCH_TIMEOUT'],
1142-
)
1159+
if self._pubsub:
1160+
self._pubsub_for_queues(
1161+
timeout=self.config['SELECT_TIMEOUT'],
1162+
batch_timeout=self.config['SELECT_BATCH_TIMEOUT'],
1163+
)
1164+
else:
1165+
self._poll_for_queues()
11431166

11441167
self._install_signal_handlers()
11451168
self._did_work = False
@@ -1162,5 +1185,6 @@ def run(self, once=False, force_once=False):
11621185
self.stats_thread = None
11631186

11641187
# Free up Redis connection
1165-
self._pubsub.reset()
1188+
if self._pubsub:
1189+
self._pubsub.reset()
11661190
self.log.info('done')

0 commit comments

Comments
 (0)