DorothyLocker and Tool diff
"Garth T Kidd" <garth-OnzZ1s1DREKDegMON/[email protected]> Thu, 15 Jul 2004 17:45:04 +1000
| Newsgroups | gmane.comp.pythin.pyds.devel |
|---|---|
| Organization | Deadly Bloody Serious |
| Message-ID | <[email protected]> |
I've renamed NoisyLocker to DorothyLocker because of its insistence that there's no place like home (i.e. the calling frame :). I've also incorporated LockWhiner into DorothyLocker so Tool doesn't have to do any tracking -- it can just call acquire() and release() and catch the exceptions. With that bit of infrastructure, it should now be pretty easy to figure out if someone's code is somehow screwing up on the locking. Once I suck all my posts from Radio to PyDS, I'll publish DorothyLocker in its own right; I quite like being told when things are breaking, and will use the code anywhere else I tackle threads. It also strikes me that it'd be trivial to hack some kind of timeout into acquire -- something I'll leave for much later, if ever. :) Regards, Garth.
20040715-1737-tool-locking.diff
(application/octet-stream, 2.6 KB)
Index: PyDS/Tool.py
===================================================================
RCS file: /pyds/PyDS/PyDS/Tool.py,v
retrieving revision 1.191
diff -r1.191 Tool.py
51a52
> from PyDS.DorothyLocker import DorothyRLock, ReleaseFailure, print_minitrace
117a119,125
> def traceargs():
> import inspect
> frame = inspect.currentframe().f_back
> name = frame.f_code.co_name
> args = inspect.formatargvalues(*inspect.getargvalues(frame))
> PyDS.Tool.debugOutput("%s%s" % (name, args))
>
131c139
< def getFromWhereCalled():
---
> def getFromWhereCalled(frames=None):
133c141
< frames = traceback.extract_stack()
---
> if frames is None: frames = traceback.extract_stack()
1137c1145
< self.lock = threading.RLock()
---
> self.lock = DorothyRLock()
1365,1381c1373,1389
< if blocking:
< if not self.lock.acquire(blocking=0):
< f = traceback.extract_stack()
< try: fun = f[-2][2]
< except: fun = '?'
< now = time.time()
< if self.lockedFrom:
< print "%s(%s): waiting for lock <%s> at %s" % (self.name, fun, self.lockedFrom or 'unknown', time.ctime(now))
< else:
< print "%s(%s): waiting for lock at %s" % (self.name, fun, time.ctime(now))
< self.lock.acquire(blocking=1)
< then = time.time()
< print "%s(%s): acquired lock at %s after %d seconds" % (self.name, fun, time.ctime(then), int(then-now))
< if _PyDS.verbose: self.lockedFrom = getFromWhereCalled()
<
< else:
< self.lock.acquire(blocking)
---
> try:
> return self.lock.acquire(blocking)
> except ReleaseFailure, e:
> msg, frame, t = e
> print "%s caught; lock was acquired in frame id %d at time %s" % (
> msg, id(frame), time.ctime(t))
> print "Trace of the initial lock acquisition:"
> print_minitrace(frame)
> print
> raise # let the caller know we had a problem
> except:
> print "_acquire: "
> (e, d, tb) = sys.exc_info()
> print 'Exception %s: %s' % (e, d)
> for row in traceback.extract_tb(tb):
> print repr(row)
> raise # let the caller know we had a problem
1384,1385c1392,1407
< self.lockedFrom = None
< self.lock.release()
---
> try:
> return self.lock.release()
> except ReleaseFailure, e:
> msg, frame, t = e
> print "%s caught; lock was acquired in frame id %d at time %s" % (
> msg, id(frame), time.ctime(t))
> print "Trace of the initial lock acquisition:"
> print_minitrace(frame)
> raise # let the caller know we had a problem
> except:
> print "_release: "
> (e, d, tb) = sys.exc_info()
> print 'Exception %s: %s' % (e, d)
> for row in traceback.extract_tb(tb):
> print repr(row)
> raise # let the caller know we had a problem
DorothyLocker.py
(text/plain, 7.6 KB)
"""
"There's no place like home." -- Dorothy
The DorothyLocker module provides a subclass of threading.RLock that
insists upon release() being called from the same execution frame that
acquire()d it in the first place. RLock itself is vulnerable to missing
a release() or tossing in an acquire() too many times within a single
thread, and it won't show until some *other* thread tries to acquire a
lock you think is free, and blocks.
As a PyDS-specific feature, DorothyRLock skips functions named in IGNORE.
This is because each tool routes calls to it's lock's acquire method
via PyDS.Tool._acquire, and similarly treats release. By ignoring frames
from _acquire and _release, DorothyRLock can concentrate on the frames
actually causing the locks and releases.
"""
from threading import _RLock, currentThread, Thread
import time
import inspect
IGNORE = ['_acquire', '_release']
def ltrimCommon(oblist1, oblist2):
"Return the arguments with all common first elements removed."
for pos in range(min(len(oblist1), len(oblist2))):
if oblist1[pos] is not oblist2[pos]:
break
else:
pos = pos + 1
return oblist1[pos:], oblist2[pos:]
def extract_minitrace(f):
"Extract a mini-trace from a locking context."
mycontext = context()
items = []
while f:
if f in mycontext:
break
items.append((
f.f_code.co_filename,
f.f_lineno,
f.f_code.co_name))
f = f.f_back
return items
def print_minitrace(f):
"Print a mini-trace from a locking context."
for filename, line, name in extract_minitrace(f):
print 'File "%s", line %d, in %s' % (
filename, line, name)
def context(ignorecodes=[], ignorenames=[]):
"""Distil a calling context, ignoring certain code objects and
function names."""
try:
stack = inspect.stack()
codestack = []
for frame, filename, lineno, co_name, lines, index in stack:
if frame.f_code is context.func_code \
or frame.f_code in ignorecodes \
or co_name in ignorenames:
continue
codestack.append(frame)
codestack.reverse()
return tuple(codestack)
finally:
del frame
class ReleaseFailure(Exception):
"Raised if DorothyRLock detects a failure to release an acquired lock."
pass
class DorothyRLock(_RLock):
"""DorothyRLock insists upon release() being called from the same
execution frame that acquire()d it in the first place. Frames named
in DorothyLocker.IGNORE will be ignored.
If a subframe re-acquires, that's okay, so long as it releases before
acquire or release is called via further up the frame stack."""
def __init__(self, verbose=None):
"Initialise the RLock."
_RLock.__init__(self, verbose)
self.__threads = {}
self.__ignores = [
self.acquire.func_code,
self.release.func_code,
self.callContext.func_code]
def callContext(self):
"Distil our calling context, with instance-specific ignores."
return context(self.__ignores, IGNORE)
def acquire(self, blocking=1):
"""Acquire the lock, first checking that any previous locks have
been released."""
# First, get our context:
me = currentThread()
myLocks = self.__threads.setdefault(me, [])
mycontext, myt = self.callContext(), time.time()
# If there were any locks...
if myLocks:
# Try to figure out whether the previous lock should have been
# released.
# TODO: shouldn't we check all the locks?
prevcontext, prevt = myLocks[-1]
prevframe = prevcontext[-1]
# eliminate the common parts of the call stack
pc, mc = ltrimCommon(prevcontext, mycontext)
if pc:
if mc:
# different parts of the call stack
# => failure to release
raise ReleaseFailure, ("failure to release", prevframe, prevt)
else:
# mc matched, but shorter
# => re-acquire from calling frame
raise ReleaseFailure, ("failure to release", prevframe, prevt)
else:
if mc:
# re-acquired from further down the call stack
pass
else:
# not pc AND not mc
# => identical contexts
raise ReleaseFailure, ("frame re-called acquire", prevframe, prevt)
# If we got this far, it's safe to lock.
if blocking:
result = _RLock.acquire(self, blocking=0)
if not result:
# Whine whilst we wait for the lock to clear.
whiner = LockWhiner(self, mycontext, myt, prevcontext, prevt)
whiner.start()
result = _RLock.acquire(self, blocking)
whiner.stop()
else:
result = _RLock.acquire(self, blocking=0)
if result:
myLocks.append((mycontext, myt))
return result
def release(self):
"""Release the lock, first checking that we're releasing from the
same frame that acquired us."""
result = _RLock.release(self) # raises AssertionError if not acquired in this thread
context = self.callContext()
me = currentThread()
myLocks = self.__threads[me]
prevcontext, prevt = myLocks[-1]
if prevcontext == context:
myLocks.pop() # Permament! myLocks is bound to the
# original, NOT a copy.
return
raise ReleaseFailure, ("failure to release", prevcontext[-1], prevt)
class LockWhiner(Thread):
def __init__(self, dorothy, waitcontext, waittime, prevcontext, prevtime, every=10):
Thread.__init__(self)
self.dorothy = dorothy
self.whineTime = waittime
self.whineContext = waitcontext
self.lockTime = prevtime
self.lockContext = prevcontext
self.whineEvery = every
self.thread = currentThread()
self.active = 1
self.setDaemon(1) # let PyDS shut down even if we're active
def stop(self):
self.stopTime = time.time()
self.active = 0
print "%s: LockWhiner stopped; lock acquired after %.2f seconds" % (
time.ctime(self.stopTime),
self.stopTime - self.whineTime)
def run(self):
try:
#import pprint
#pprint.pprint(self.__dict__)
self._run()
except:
print "_LockWhiner.run: Bugger."
(e, d, tb) = sys.exc_info()
print 'Exception %s: %s' % (e, d)
for row in traceback.extract_tb(tb):
print repr(row)
def _run(self):
fun = self.waitStack[-1][2]
print "%s: LockWhiner started by %s thread %d" % (\
time.ctime(self.whineTime),
self.tool.name,
id(self.thread))
waits = 0
while self.active:
time.sleep(self.whineEvery)
waits = waits + 1
now = time.time()
print "%s: LockWhiner has been waiting for %.2fs" % (
time.ctime(),
now - self.whineTime)
if waits==1:
print "-"*70
print "Just in case this is a deadlock, here's some additional "
print "information for debugging purposes:\n"
print "Lock obtained: %s" % (time.ctime(self.lockTime))
print "\nLock holding stack:"
print_minitrace(self.lockContext[-1])
print "\nWaiting stack:"
print_minitrace(self.whineContext[-1])
print "\nLock held by:",
lockOwner = self.dorothy._RLock__owner
if lockOwner is None:
print "(None.)"
else:
print id(lockOwner)
print "-"*70
if __name__ == '__main__':
import pprint
import traceback
nrl = DorothyRLock()
def spit():
pprint.pprint(nrl.callContext())
def a():
spit()
def b():
a()
a()
print
b()
def _release():
assert nrl.callContext()[-1].f_code.co_name != '_release'
_release()
nrl.acquire()
nrl.release()
def fail1_top():
fail1_mid1()
fail1_mid2()
def fail1_mid1():
nrl.acquire()
def fail1_mid2():
nrl.release()
def fail2_top():
fail2_mid1()
fail2_mid2()
def fail2_mid1():
nrl.acquire()
def fail2_mid2():
nrl.acquire()
nrl.release()
for test in [fail1_top, fail2_top]:
try:
print "\nTest: %s" % test.func_name
test()
except ReleaseFailure, e:
msg, frame, t = e
print "%s caught; lock was acquired in frame id %d at time %s" % (
msg, id(frame), time.ctime(t))
print_minitrace(frame)