Skip to content

Commit

Permalink
Issue #25. Detect deadlocks and raise if detected during select.
Browse files Browse the repository at this point in the history
  • Loading branch information
rgalanakis committed Jul 11, 2014
1 parent a3d36ad commit a0c3a84
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 2 deletions.
21 changes: 21 additions & 0 deletions goless/backends.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import contextlib as _contextlib
import gc as _gc
import os as _os
import platform as _platform
import sys as _sys
Expand Down Expand Up @@ -51,6 +52,11 @@ def propagate_exc(self, errtype, *args):
so the program hears it and it doesn't die lonely in a tasklet."""
raise NotImplementedError()

def would_deadlock(self):
"""Return True if a send or receive would deadlock
(current tasklet/greenlet is the last one running)."""
raise NotImplementedError()


# We can't easily use stackless on our CI,
# so don't worry about covering it.
Expand Down Expand Up @@ -90,13 +96,17 @@ def resume(self, tasklet):
def propagate_exc(self, errtype, *args):
stackless.getmain().throw(errtype, *args)

def would_deadlock(self):
return stackless.runcount == 1

return StacklessBackend()


def _make_gevent():
import gevent
import gevent.hub
import gevent.queue
import greenlet
deadlock_errtype = SystemError if _os.name == 'nt' else gevent.hub.LoopExit

class Channel(gevent.queue.Channel):
Expand Down Expand Up @@ -133,6 +143,17 @@ def resume(self, tasklet):
def propagate_exc(self, errtype, *args):
raise errtype

def would_deadlock(self):
# The Hub and main greenlet are always running,
# if there are more than those alive, we aren't going to deadlock.
count = 0
for obj in _gc.get_objects():
if isinstance(obj, greenlet.greenlet) and not obj.dead:
count += 1
if count > 2:
return False
return True

return GeventBackend()


Expand Down
10 changes: 9 additions & 1 deletion goless/selecting.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .backends import current as _be
from .backends import current as _be, Deadlock as _Deadlock


# noinspection PyPep8Naming,PyShadowingNames
Expand Down Expand Up @@ -77,6 +77,14 @@ def select(*cases):
# noinspection PyCallingNonCallable
return default, None

# We need to check for deadlocks before selecting.
# We can't rely on the underlying backend to do it,
# as we do for channels, since we don't do an actual send or recv here.
# It's possible to still have a deadlock unless we move the check into
# the loop, but since the check is slow
# (gevent doesn't provide a fast way), let's leave it out here.
if _be.would_deadlock():
raise _Deadlock('No other tasklets running, cannot select.')
while True:
for c in cases:
if c.ready():
Expand Down
6 changes: 5 additions & 1 deletion tests/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,8 @@ def test_select_raises_for_list_and_args(self):

def test_select_with_no_args_should_do_nothing(self):
goless.select()
goless.select([])
goless.select([])

def test_raises_deadlock_if_no_goroutines(self):
with self.assertRaises(goless.Deadlock):
goless.select(goless.rcase(goless.chan()))

0 comments on commit a0c3a84

Please # to comment.