Skip to content

Commit 92c65ef

Browse files
committed
switch wait > Semaphore
1 parent a15ee0e commit 92c65ef

File tree

6 files changed

+31
-24
lines changed

6 files changed

+31
-24
lines changed

HISTORY.rst

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
History
44
-------
55

6-
v0.8.0 (TBC)
7-
............
6+
v0.8.0 (2017-06-05)
7+
...................
88
* add ``async-timeout`` dependency and use async timeout around ``shadow_factory``
99
* change logger name for control process log messages
10+
* use ``Semaphore`` rather than ``asyncio.wait(...return_when=asyncio.FIRST_COMPLETED)`` for improved performance
11+
* improve log display
1012

1113
v0.7.0 (2017-06-01)
1214
...................

arq/drain.py

+12-14
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(self, *,
4747
self.redis_pool = redis_pool
4848
self.loop = redis_pool._loop
4949
self.max_concurrent_tasks = max_concurrent_tasks
50+
self.task_semaphore = asyncio.Semaphore(value=max_concurrent_tasks, loop=self.loop)
5051
self.shutdown_delay = max(shutdown_delay, 0.1)
5152
self.timeout_seconds = timeout_seconds
5253
self.burst_mode = burst_mode
@@ -71,6 +72,10 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
7172
e = self.task_exception
7273
raise TaskError(f'A processed task failed: {e.__class__.__name__}, {e}') from e
7374

75+
@property
76+
def jobs_in_progress(self):
77+
return self.max_concurrent_tasks - self.task_semaphore._value
78+
7479
async def iter(self, *raw_queues: bytes, pop_timeout=1):
7580
"""
7681
blpop jobs from redis queues and yield them. Waits for the number of tasks to drop below max_concurrent_tasks
@@ -88,17 +93,21 @@ async def iter(self, *raw_queues: bytes, pop_timeout=1):
8893
work_logger.debug('populating quit queue to prompt exit: %s', quit_queue.decode())
8994
await self.redis.rpush(quit_queue, b'1')
9095
raw_queues = tuple(raw_queues) + (quit_queue,)
91-
while self.running:
96+
while True:
97+
await self.task_semaphore.acquire()
98+
if not self.running:
99+
break
92100
msg = await self.redis.blpop(*raw_queues, timeout=pop_timeout)
93101
if msg is None:
94102
yield None, None
103+
self.task_semaphore.release()
95104
continue
96105
raw_queue, raw_data = msg
97106
if self.burst_mode and raw_queue == quit_queue:
98107
work_logger.debug('got job from the quit queue, stopping')
99108
break
109+
work_logger.debug('jobs in progress %d', self.jobs_in_progress)
100110
yield raw_queue, raw_data
101-
await self.wait()
102111

103112
def add(self, coro, job, re_enqueue=False):
104113
"""
@@ -115,18 +124,6 @@ def add(self, coro, job, re_enqueue=False):
115124
self.loop.call_later(self.timeout_seconds, self._cancel_job, task, job)
116125
self.pending_tasks.add(task)
117126

118-
async def wait(self):
119-
"""
120-
Wait for a the number of pending tasks to drop bellow ``max_concurrent_tasks``
121-
"""
122-
while True:
123-
pt_cnt = len(self.pending_tasks)
124-
if pt_cnt < self.max_concurrent_tasks:
125-
return
126-
work_logger.info('%d pending tasks, waiting for one to finish', pt_cnt)
127-
_, self.pending_tasks = await asyncio.wait(self.pending_tasks, loop=self.loop,
128-
return_when=asyncio.FIRST_COMPLETED)
129-
130127
async def finish(self, timeout=None):
131128
"""
132129
Cancel all pending tasks and optionally re-enqueue jobs which haven't finished after the timeout.
@@ -150,6 +147,7 @@ async def finish(self, timeout=None):
150147
self.pending_tasks = set()
151148

152149
def _job_callback(self, task):
150+
self.task_semaphore.release()
153151
self.jobs_complete += 1
154152
task_exception = task.exception()
155153
if task_exception:

arq/logs.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99

1010
__all__ = ['ColourHandler', 'default_log_config']
1111

12-
LOG_COLOURS = {
13-
logging.DEBUG: 'white',
14-
logging.INFO: 'green',
15-
logging.WARN: 'yellow',
12+
LOG_FORMATS = {
13+
logging.DEBUG: {'fg': 'white', 'dim': True},
14+
logging.INFO: {'fg': 'green'},
15+
logging.WARN: {'fg': 'yellow'},
1616
}
1717

1818

19+
def get_log_format(record):
20+
return LOG_FORMATS.get(record.levelno, {'fg': 'red'})
21+
22+
1923
class ColourHandler(logging.StreamHandler):
2024
"""
2125
Coloured log handler. Levels: debug: white, info: green, warning: yellow, else: red.
@@ -24,14 +28,13 @@ class ColourHandler(logging.StreamHandler):
2428
"""
2529
def emit(self, record):
2630
log_entry = self.format(record)
27-
colour = LOG_COLOURS.get(record.levelno, 'red')
2831
m = re.match('^(.*?: )', log_entry)
2932
if m:
3033
prefix = click.style(m.groups()[0], fg='magenta')
31-
msg = click.style(log_entry[m.end():], fg=colour)
34+
msg = click.style(log_entry[m.end():], **get_log_format(record))
3235
click.echo(prefix + msg)
3336
else:
34-
click.secho(log_entry, fg=colour)
37+
click.secho(log_entry, **get_log_format(record))
3538

3639

3740
def default_log_config(verbose: bool) -> dict:

arq/utils.py

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ def create_tz(utcoffset=0) -> timezone:
106106

107107
def timestamp() -> float:
108108
"""
109+
This should be exactly the same as time.time(), we use this approach for consistency with
110+
other methods and possibly greater accuracy.
109111
:return: now in unix time, eg. seconds since 1970
110112
"""
111113
return (datetime.utcnow() - EPOCH).total_seconds()

tests/test_main.py

+2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ async def test_dispatch_work(tmpworkdir, loop, caplog, redis_conn):
7575
'recording health: <date time2> j_complete=0 j_failed=0 j_timedout=0 j_ongoing=0 q_high=1 q_dft=1 q_low=0\n'
7676
'starting main blpop loop\n'
7777
'populating quit queue to prompt exit: arq:quit-<random>\n'
78+
'jobs in progress 1\n'
7879
'scheduling job <Job MockRedisDemoActor.high_add_numbers(3, 4, c=5) on high>, re-enqueue: False\n'
80+
'jobs in progress 2\n'
7981
'scheduling job <Job MockRedisDemoActor.add_numbers(1, 2) on dft>, re-enqueue: False\n'
8082
'got job from the quit queue, stopping\n'
8183
'drain waiting 5.0s for 2 tasks to finish\n'

tests/test_worker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ async def test_run_quit(tmpworkdir, redis_conn, actor, caplog):
157157
await worker.run()
158158
# the third job should be remove from the queue and readded
159159
assert tmpworkdir.join('save_slow').read() == '2'
160-
assert '1 pending tasks, waiting for one to finish' in caplog
160+
# assert '1 pending tasks, waiting for one to finish' in caplog
161161
assert 2 == await redis_conn.llen(b'arq:q:dft')
162162

163163

0 commit comments

Comments
 (0)