removed Job & friends from coros; use proc instead
This commit is contained in:
@@ -266,226 +266,12 @@ class event(object):
|
|||||||
return self.send(None, args)
|
return self.send(None, args)
|
||||||
|
|
||||||
|
|
||||||
class Job(object):
|
|
||||||
"""Spawn a greenlet, control its execution and collect the result.
|
|
||||||
|
|
||||||
use spawn_new() classmethod to spawn a new coroutine and get a new Job instance;
|
|
||||||
use kill() method to kill the greenlet running the function;
|
|
||||||
use wait() method to collect the result of the function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ev=None):
|
|
||||||
if ev is None:
|
|
||||||
ev = event()
|
|
||||||
self.event = event()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def spawn_new(cls, function, *args, **kwargs):
|
|
||||||
job = cls()
|
|
||||||
job.spawn(function, *args, **kwargs)
|
|
||||||
return job
|
|
||||||
|
|
||||||
def spawn(self, function, *args, **kwargs):
|
|
||||||
assert not hasattr(self, 'greenlet_ref'), 'spawn can only be used once per instance'
|
|
||||||
g = api.spawn(_collect_result, weakref.ref(self), function, *args, **kwargs)
|
|
||||||
self.greenlet_ref = weakref.ref(g)
|
|
||||||
|
|
||||||
# spawn_later can be also implemented here
|
|
||||||
|
|
||||||
@property
|
|
||||||
def greenlet(self):
|
|
||||||
return self.greenlet_ref()
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
greenlet = self.greenlet_ref()
|
|
||||||
if greenlet is not None and not greenlet.dead:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
klass = type(self).__name__
|
|
||||||
if self.greenlet is not None and self.greenlet.dead:
|
|
||||||
dead = '(dead)'
|
|
||||||
else:
|
|
||||||
dead = ''
|
|
||||||
return '<%s greenlet=%r%s event=%s>' % (klass, self.greenlet, dead, self.event)
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
"""Wait for the spawned greenlet to exit.
|
|
||||||
Return the result of the function if it completed without errors;
|
|
||||||
re-raise the exception otherwise.
|
|
||||||
|
|
||||||
Return GreenletExit() object if the greenlet was killed.
|
|
||||||
"""
|
|
||||||
return self.event.wait()
|
|
||||||
|
|
||||||
def poll(self, notready=None):
|
|
||||||
return self.event.poll(notready)
|
|
||||||
|
|
||||||
def poll_result(self, notready=None):
|
|
||||||
return self.event.poll_result(notready)
|
|
||||||
|
|
||||||
def poll_exception(self, notready=None):
|
|
||||||
return self.event.poll_exception(notready)
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
return self.event.ready()
|
|
||||||
|
|
||||||
def has_result(self):
|
|
||||||
return self.event.has_result()
|
|
||||||
|
|
||||||
def has_exception(self):
|
|
||||||
return self.event.has_exception()
|
|
||||||
|
|
||||||
def _send(self, result):
|
|
||||||
self.event.send(result)
|
|
||||||
|
|
||||||
def _send_exception(self, *throw_args):
|
|
||||||
self.event.send_exception(*throw_args)
|
|
||||||
|
|
||||||
def kill(self, *throw_args):
|
|
||||||
greenlet = self.greenlet_ref()
|
|
||||||
if greenlet is not None:
|
|
||||||
return api.kill(greenlet, *throw_args)
|
|
||||||
|
|
||||||
def kill_after(self, seconds):
|
|
||||||
return api.call_after_global(seconds, _kill_by_ref, weakref.ref(self))
|
|
||||||
|
|
||||||
def _kill_by_ref(async_job_ref):
|
|
||||||
async_job = async_job_ref()
|
|
||||||
if async_job is not None:
|
|
||||||
async_job.kill()
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_result(job_ref, function, *args, **kwargs):
|
|
||||||
"""Execute *function* and send its result to job_ref().
|
|
||||||
|
|
||||||
If function raises GreenletExit() it's trapped and sent as a regular value.
|
|
||||||
If job_ref points to a dead object or if DEBUG is true the exception
|
|
||||||
will be re-raised.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = function(*args, **kwargs)
|
|
||||||
except api.GreenletExit, ex:
|
|
||||||
job = job_ref()
|
|
||||||
if job is not None:
|
|
||||||
job._send(ex)
|
|
||||||
except:
|
|
||||||
job = job_ref()
|
|
||||||
if job is not None:
|
|
||||||
job._send_exception(*sys.exc_info())
|
|
||||||
if not DEBUG:
|
|
||||||
return
|
|
||||||
raise # let hub log the exception
|
|
||||||
else:
|
|
||||||
job = job_ref()
|
|
||||||
if job is not None:
|
|
||||||
job._send(result)
|
|
||||||
|
|
||||||
class GroupMemberJob(Job):
|
|
||||||
|
|
||||||
def __init__(self, group_queue, event=None):
|
|
||||||
self._group_queue = group_queue
|
|
||||||
Job.__init__(self, event)
|
|
||||||
|
|
||||||
def _send(self, result):
|
|
||||||
self._group_queue.send((self, result, None))
|
|
||||||
|
|
||||||
def _send_exception(self, *throw_args):
|
|
||||||
self._group_queue.send((self, None, throw_args))
|
|
||||||
|
|
||||||
|
|
||||||
class JobGroupExit(api.GreenletExit):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class JobGroup(object):
|
|
||||||
"""Spawn jobs in the context of the group: when one job raises an exception,
|
|
||||||
all other jobs are killed immediatelly.
|
|
||||||
|
|
||||||
To spawn a job use spawn_job method which returns a Job instance.
|
|
||||||
>>> group = JobGroup()
|
|
||||||
>>> job = group.spawn_new(api.get_hub().switch) # give up control to hub forever
|
|
||||||
>>> _ = group.spawn_new(int, 'bad') # raise ValueError
|
|
||||||
>>> job.wait()
|
|
||||||
JobGroupExit('Killed because of ValueError in the group',)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._queue = queue()
|
|
||||||
self._jobs = []
|
|
||||||
self._waiter_job = Job.spawn_new(self._waiter)
|
|
||||||
self._killerror = None
|
|
||||||
|
|
||||||
def spawn_new(self, function, *args, **kwargs):
|
|
||||||
assert self._waiter_job.poll('run') == 'run'
|
|
||||||
job = GroupMemberJob(self._queue)
|
|
||||||
self._jobs.append(job)
|
|
||||||
if self._killerror is None:
|
|
||||||
job.spawn(function, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
job.event.send(self._killerror)
|
|
||||||
return job
|
|
||||||
|
|
||||||
def kill_all(self, *throw_args):
|
|
||||||
assert self._waiter_job.poll('run') == 'run', '_waiter_job must live'
|
|
||||||
for job in self._jobs:
|
|
||||||
g = job.greenlet
|
|
||||||
if g is not None:
|
|
||||||
api.get_hub().schedule_call(0, g.throw, *throw_args)
|
|
||||||
api.sleep(0)
|
|
||||||
|
|
||||||
# QQQ: add kill_all_later(seconds, throw_args)
|
|
||||||
# add kill_delay attribute
|
|
||||||
|
|
||||||
def complete(self, *jobs):
|
|
||||||
assert self._waiter_job.poll('run') == 'run'
|
|
||||||
left = set(jobs)
|
|
||||||
for job in jobs:
|
|
||||||
if job.ready():
|
|
||||||
left.remove(job)
|
|
||||||
for job in left:
|
|
||||||
job.wait()
|
|
||||||
|
|
||||||
# XXX make jobs a list, because wait methods will have timeout parameter soon
|
|
||||||
def wait(self, *jobs):
|
|
||||||
self.complete(*jobs)
|
|
||||||
return [x.wait() for x in jobs]
|
|
||||||
|
|
||||||
def complete_all(self):
|
|
||||||
while True:
|
|
||||||
count = len(self._jobs)
|
|
||||||
self.complete(*self._jobs)
|
|
||||||
# it's possible that more jobs were added while we were waiting
|
|
||||||
if count == len(self._jobs):
|
|
||||||
break
|
|
||||||
|
|
||||||
def wait_all(self):
|
|
||||||
self.complete_all()
|
|
||||||
return [x.wait() for x in self._jobs]
|
|
||||||
|
|
||||||
def _waiter(self):
|
|
||||||
# XXX: this lives forever, fix it to exit after all jobs died
|
|
||||||
# XXX: add __nonzero__ method that returns whether JobGroup is alive
|
|
||||||
# XXX: 3 states: True (alive), finishing, False (all dead)
|
|
||||||
while True:
|
|
||||||
job, result, throw_args = self._queue.wait()
|
|
||||||
if throw_args is None:
|
|
||||||
if not job.event.ready():
|
|
||||||
job.event.send(result)
|
|
||||||
else:
|
|
||||||
if not job.event.ready():
|
|
||||||
job.event.send_exception(*throw_args)
|
|
||||||
if self._killerror is None:
|
|
||||||
type = throw_args[0]
|
|
||||||
self._killerror = JobGroupExit('Killed because of %s in the group' % type.__name__)
|
|
||||||
self.kill_all(self._killerror)
|
|
||||||
# cannot exit here, as I need to deliver GreenExits
|
|
||||||
|
|
||||||
class multievent(object):
|
class multievent(object):
|
||||||
"""is an event that can hold more than one value (it cannot be cancelled though)
|
"""is an event that can hold more than one value (it cannot be cancelled though)
|
||||||
is like a queue, but if there're waiters blocked, send/send_exception will wake up
|
is like a queue, but if there're waiters blocked, send/send_exception will wake up
|
||||||
all of them, just like an event will do (queue will wake up only one)
|
all of them, just like an event will do (queue will wake up only one)
|
||||||
"""
|
"""
|
||||||
|
# XXX to be removed
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.items = collections.deque()
|
self.items = collections.deque()
|
||||||
@@ -589,7 +375,6 @@ class BoundedSemaphore(object):
|
|||||||
the calling coroutine until count becomes nonzero again. Attempting to
|
the calling coroutine until count becomes nonzero again. Attempting to
|
||||||
release() after count has reached limit suspends the calling coroutine until
|
release() after count has reached limit suspends the calling coroutine until
|
||||||
count becomes less than limit again.
|
count becomes less than limit again.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, count, limit):
|
def __init__(self, count, limit):
|
||||||
if count > limit:
|
if count > limit:
|
||||||
|
@@ -57,134 +57,6 @@ class TestEvent(unittest.TestCase):
|
|||||||
result = event2.wait()
|
result = event2.wait()
|
||||||
raise AssertionError('Nobody sent anything to event2 yet it received %r' % (result, ))
|
raise AssertionError('Nobody sent anything to event2 yet it received %r' % (result, ))
|
||||||
|
|
||||||
class CommonJobTests:
|
|
||||||
|
|
||||||
def test_simple_return(self):
|
|
||||||
res = self.Job.spawn_new(lambda: 25).wait()
|
|
||||||
assert res==25, res
|
|
||||||
|
|
||||||
def test_exception(self):
|
|
||||||
try:
|
|
||||||
self.Job.spawn_new(sys.exit, 'bye').wait()
|
|
||||||
except SystemExit, ex:
|
|
||||||
assert ex.args == ('bye', )
|
|
||||||
else:
|
|
||||||
assert False, "Shouldn't get there"
|
|
||||||
|
|
||||||
def _test_kill(self, sync):
|
|
||||||
def func():
|
|
||||||
sleep(DELAY)
|
|
||||||
return 101
|
|
||||||
res = self.Job.spawn_new(func)
|
|
||||||
assert res
|
|
||||||
if sync:
|
|
||||||
res.kill()
|
|
||||||
else:
|
|
||||||
spawn(res.kill)
|
|
||||||
wait_result = res.wait()
|
|
||||||
assert not res, repr(res)
|
|
||||||
assert isinstance(wait_result, GreenletExit), repr(wait_result)
|
|
||||||
|
|
||||||
def test_kill_sync(self):
|
|
||||||
return self._test_kill(True)
|
|
||||||
|
|
||||||
def test_kill_async(self):
|
|
||||||
return self._test_kill(False)
|
|
||||||
|
|
||||||
def test_poll(self):
|
|
||||||
def func():
|
|
||||||
sleep(DELAY)
|
|
||||||
return 25
|
|
||||||
job = self.Job.spawn_new(func)
|
|
||||||
self.assertEqual(job.poll(), None)
|
|
||||||
assert job, repr(job)
|
|
||||||
self.assertEqual(job.wait(), 25)
|
|
||||||
self.assertEqual(job.poll(), 25)
|
|
||||||
assert not job, repr(job)
|
|
||||||
|
|
||||||
job = self.Job.spawn_new(func)
|
|
||||||
self.assertEqual(job.poll(5), 5)
|
|
||||||
assert job, repr(job)
|
|
||||||
self.assertEqual(job.wait(), 25)
|
|
||||||
self.assertEqual(job.poll(5), 25)
|
|
||||||
assert not job, repr(job)
|
|
||||||
|
|
||||||
def test_kill_after(self):
|
|
||||||
def func():
|
|
||||||
sleep(DELAY)
|
|
||||||
return 25
|
|
||||||
job = self.Job.spawn_new(func)
|
|
||||||
job.kill_after(DELAY/2)
|
|
||||||
result = job.wait()
|
|
||||||
assert isinstance(result, GreenletExit), repr(result)
|
|
||||||
|
|
||||||
job = self.Job.spawn_new(func)
|
|
||||||
job.kill_after(DELAY*2)
|
|
||||||
self.assertEqual(job.wait(), 25)
|
|
||||||
sleep(DELAY*2)
|
|
||||||
self.assertEqual(job.wait(), 25)
|
|
||||||
|
|
||||||
class TestJob(CommonJobTests, unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.Job = Job
|
|
||||||
|
|
||||||
class TestJobGroup(CommonJobTests, unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.Job = JobGroup()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
del self.Job
|
|
||||||
|
|
||||||
def check_raises_badint(self, wait):
|
|
||||||
try:
|
|
||||||
wait()
|
|
||||||
except ValueError, ex:
|
|
||||||
assert 'badint' in str(ex), str(ex)
|
|
||||||
else:
|
|
||||||
raise AssertionError('must raise ValueError')
|
|
||||||
|
|
||||||
def check_killed(self, wait, text=''):
|
|
||||||
result = wait()
|
|
||||||
assert isinstance(result, GreenletExit), repr(result)
|
|
||||||
assert str(result) == text, str(result)
|
|
||||||
|
|
||||||
def test_group_error(self):
|
|
||||||
x = self.Job.spawn_new(int, 'badint')
|
|
||||||
y = self.Job.spawn_new(sleep, DELAY)
|
|
||||||
self.check_killed(y.wait, 'Killed because of ValueError in the group')
|
|
||||||
self.check_raises_badint(x.wait)
|
|
||||||
z = self.Job.spawn_new(sleep, DELAY)
|
|
||||||
self.check_killed(z.wait, 'Killed because of ValueError in the group')
|
|
||||||
|
|
||||||
def test_wait_all(self):
|
|
||||||
x = self.Job.spawn_new(lambda : 1)
|
|
||||||
y = self.Job.spawn_new(lambda : 2)
|
|
||||||
z = self.Job.spawn_new(lambda : 3)
|
|
||||||
assert self.Job.wait_all() == [1, 2, 3], repr(self.Job.wait_all())
|
|
||||||
assert [x.wait(), y.wait(), z.wait()] == [1, 2, 3], [x.wait(), y.wait(), z.wait()]
|
|
||||||
|
|
||||||
def test_error_wait_all(self):
|
|
||||||
def x():
|
|
||||||
sleep(DELAY)
|
|
||||||
return 1
|
|
||||||
# x will be killed
|
|
||||||
x = self.Job.spawn_new(x)
|
|
||||||
# y will raise ValueError
|
|
||||||
y = self.Job.spawn_new(int, 'badint')
|
|
||||||
# z cannot be killed because it does not yield. it will finish successfully
|
|
||||||
z = self.Job.spawn_new(lambda : 3)
|
|
||||||
self.check_raises_badint(self.Job.wait_all)
|
|
||||||
self.check_killed(x.poll, 'Killed because of ValueError in the group')
|
|
||||||
self.check_killed(x.wait, 'Killed because of ValueError in the group')
|
|
||||||
assert z.wait() == 3, repr(z.wait())
|
|
||||||
self.check_raises_badint(y.wait)
|
|
||||||
|
|
||||||
# zz won't be even started, because there's already an error in the group
|
|
||||||
zz = self.Job.spawn_new(lambda : 4)
|
|
||||||
self.check_killed(x.poll, 'Killed because of ValueError in the group')
|
|
||||||
self.check_killed(x.wait, 'Killed because of ValueError in the group')
|
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__=='__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Reference in New Issue
Block a user