RE: Patch to DorothyLocker
"Garth T Kidd" <garth-OnzZ1s1DREKDegMON/[email protected]> Thu, 23 Sep 2004 07:25:13 +1000
| Newsgroups | gmane.comp.pythin.pyds.devel |
|---|---|
| Organization | Deadly Bloody Serious |
| Message-ID | <[email protected]> |
This is a multi-part message in MIME format. ------=_NextPart_000_0004_01C4A13E.79676500 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Excellent idea, that. Whining and whining loudly is fine; all I had to do was consolidate down to only one ``print`` so output didn't interleave. New version attached. -----Original Message----- From: Garth T Kidd [mailto:garth-OnzZ1s1DREKDegMON/[email protected]] Sent: Thursday, 23 September 2004 7:18 AM To: 'Thomas Klaeger' Cc: 'pyds-dev-iYtK5bfT9M//Ad8WF/[email protected]' Subject: RE: [Pyds-dev] Patch to DorothyLocker No need, but thanks for the offer! I stripped back and simplified the code, kept in mind that if the calling thread was the lock owner I knew I wasn't re-entrant, and it all behaved pretty quickly. My main cause for concern now is not knowing how well the LockWhiner reporting will go. I guess I should set the whine period to a tenth of a second to try it out. :) -----Original Message----- From: pyds-dev-admin-iYtK5bfT9M//Ad8WF/[email protected] [mailto:pyds-dev-admin-iYtK5bfT9M//Ad8WF/[email protected]] On Behalf Of Thomas Klaeger Sent: Wednesday, 22 September 2004 7:55 PM To: Garth T Kidd Cc: pyds-dev-iYtK5bfT9M//Ad8WF/[email protected] Subject: Re: [Pyds-dev] Patch to DorothyLocker Is there any possibility I can help out? ------=_NextPart_000_0004_01C4A13E.79676500 Content-Type: text/plain; name="DorothyLocker.py" Content-Transfer-Encoding: quoted-printable Content-Disposition: attachment; filename="DorothyLocker.py" """ "There's no place like home." -- Dorothy The DorothyLocker module provides a subclass of threading.RLock that=20 insists upon release() being called from the same execution frame that=20 acquire()d it in the first place. RLock itself is vulnerable to missing=20 a release() or tossing in an acquire() too many times within a single=20 thread, and it won't show until some *other* thread tries to acquire a=20 lock you think is free, and blocks.=20 As a PyDS-specific feature, DorothyRLock skips functions named in = IGNORE.=20 This is because each tool routes calls to it's lock's acquire method=20 via PyDS.Tool._acquire, and similarly treats release. By ignoring frames = from _acquire and _release, DorothyRLock can concentrate on the frames=20 actually causing the locks and releases.=20 """ import threading from threading import _Verbose, currentThread, Thread from thread import allocate_lock import time import inspect import sys import traceback import os.path IGNORE =3D ['_acquire', '_release', '__acquire', '__release', = '__DorothyRLock_acquire', '__DorothyRLock_release'] def ltrim_common(oblist1, oblist2):=20 "Return the arguments with all common first elements removed." for pos in range(min(len(oblist1), len(oblist2))):=20 if oblist1[pos] is not oblist2[pos]:=20 break else:=20 pos =3D pos + 1 return oblist1[pos:], oblist2[pos:] def ping(*args):=20 myContext =3D context(ignorecodes=3D[ping.func_code]) frame =3D myContext[-1] filename, line, name =3D frame.f_code.co_filename, frame.f_lineno, = frame.f_code.co_name filename =3D os.path.basename(filename) me =3D currentThread() args =3D ", ".join([repr(a) for a in args]) print "ping! thread %d/%s %s line %d %s" % ( id(me), me.getName(), filename, line, args ) =09 def extract_minitrace(frame): "Extract a mini-trace from a locking context." mycontext =3D context() items =3D [] while frame:=20 if frame in mycontext:=20 break items.append(( frame.f_code.co_filename,=20 frame.f_lineno,=20 frame.f_code.co_name)) frame =3D frame.f_back return items def format_minitrace(frame):=20 "Format a mini-trace from a locking context." msg =3D [] for filename, line, name in extract_minitrace(frame):=20 msg.append('File "%s", line %d, in %s' % ( filename, line, name)) return msg def print_minitrace(frame):=20 "Print a mini-trace from a locking context." print '\n'.join(format_minitrace(frame)) def context(ignorecodes=3D[], ignorenames=3D[]):=20 """Distil a calling context, ignoring certain code objects and=20 function names.""" try:=20 stack =3D inspect.stack() codestack =3D [] for frame, filename, lineno, co_name, lines, index in stack:=20 if frame.f_code is context.func_code \ or frame.f_code in ignorecodes \ or co_name in ignorenames:=20 continue codestack.append(frame) codestack.reverse() return tuple(codestack) finally:=20 del frame class ReleaseFailure(Exception):=20 "Raised if DorothyRLock detects a failure to release an acquired lock." pass class DorothyRLock(_Verbose):=20 """DorothyRLock insists upon release() being called from the same=20 execution frame that acquire()d it in the first place. Frames named=20 in DorothyLocker.IGNORE will be ignored. =09 If a subframe re-acquires, that's okay, so long as it releases before=20 acquire or release is called via further up the frame stack.""" def squawk(cls):=20 "Let users know if we're in use." att =3D 'squawked' if not hasattr(cls, att):=20 msg =3D "Enabled verbose lock debugging via DorothyLocker." dash =3D '-' * len(msg) print "%s\n%s\n%s\n" % (dash, msg, dash) setattr(cls, att, True) squawk =3D classmethod(squawk) =09 def __init__(self, verbose=3DNone, toolName=3D'(unknown)'):=20 "Initialise the DorothyRLock." _Verbose.__init__(self, verbose) self.toolName =3D toolName self.__ignores =3D [ self.acquire.func_code,=20 self.release.func_code, self.callContext.func_code] self.__owner =3D None self.__lockStack =3D [] self.__block =3D allocate_lock() self.__class__.squawk() =09 def callContext(self):=20 "Distil our calling context, with instance-specific ignores." return context(self.__ignores, IGNORE) =09 def acquire(self, blocking=3D1):=20 """Acquire the lock, first checking that any prior locks by this=20 thread have been released if they're not still on our call chain. """ =09 # First, get our context.=20 me =3D currentThread() mycontext, myt =3D self.callContext(), time.time() # Are we the owner?=20 # This check is safe because owner is me only if we've called=20 # acquire() from the same thread.=20 if self.__owner is me:=20 # Try to figure out whether the previous lock should have been=20 # released.=20 prevcontext, prevt =3D self.__lockStack[-1] prevframe =3D prevcontext[-1] # eliminate the common parts of the call stack pc, mc =3D ltrim_common(prevcontext, mycontext) if pc: if mc:=20 # different parts of the call stack # =3D> failure to release raise ReleaseFailure, ("failure to release", prevframe, prevt) else:=20 # mc matched, but shorter # =3D> re-acquire from calling frame raise ReleaseFailure, ("failure to release", prevframe, prevt) else:=20 if mc:=20 # re-acquired from further down the call stack pass else:=20 # not pc AND not mc # =3D> identical contexts raise ReleaseFailure, ("frame re-called acquire", prevframe, prevt) # If we got this far, we're OK.=20 # Add our details to the lock stack and return success.=20 self.__lockStack.append((mycontext, myt)) if __debug__:=20 self._note("%s.acquire(%s): recursive success", self, blocking) return 1 =09 # The lock isn't owned by this thread. The only way to know for # sure it isn't owned by anyone else is to acquire it. Let's try=20 # doing it without blocking, first.=20 result =3D self.__block.acquire(0) =09 if not result: # Our attempt to lock failed.=20 if not blocking:=20 # If we didn't want a blocking call, we can fail outright here.=20 if __debug__:=20 self._note("%s.acquire(%s): failure", self, blocking) return 0 # We know for sure someone else had self.__block at the time=20 # we tried to get it, but by now the ownership might have=20 # changed or it might have been released. So, we'll start a=20 # LockWhiner and then perform a blocking wait.=20 whiner =3D LockWhiner(self, mycontext, myt) whiner.start() result =3D self.__block.acquire() =09 # We can ONLY get here if we succeeded... assert result # ... but you can never be too careful.=20 =09 # Stop the whiner.=20 whiner.stop() =09 # Success! Let's grab the goodies and run.=20 self.__owner =3D me self.__lockStack =3D [(mycontext, myt)] if __debug__:=20 self._note("%s.acquire(%s): initial success", self, blocking) return 1 def release(self):=20 """Release the lock, first checking that we're releasing from the=20 same frame that acquired us.""" =09 # First, get our context.=20 me =3D currentThread() mycontext, myt =3D self.callContext(), time.time() if self.__owner is me:=20 prevcontext, prevt =3D self.__lockStack[-1] if not prevcontext =3D=3D mycontext:=20 # TODO: unwind to some adequate level raise ReleaseFailure, ("failure to release", prevcontext[-1], prevt) else:=20 raise ReleaseFailure, ("releasing thread doesn't own the lock", = mycontext[-1], myt) =09 # If we got here, all is well.=20 topContext, topTime =3D self.__lockStack.pop() delay =3D myt - topTime if self.__lockStack:=20 if __debug__:=20 self._note("%s.release(): non-final release after %.2fs", self, = delay) else:=20 if __debug__:=20 self._note("%s.release(): final release after %.2fs", self, delay) self.__owner =3D None self.__block.release() def __repr__(self):=20 "Return a representation of this object." return "<%s.lock owned by %s with count %d>" % ( self.toolName,=20 self.__owner and self.__owner.getName(),=20 len(self.__lockStack)) def lockDetails(self):=20 """Return top lock details: owner, lock count, top context, time. Because of race conditions, details might not be consistent.""" try:=20 top =3D self.__lockStack[-1] mycontext, myt =3D top except IndexError:=20 mycontext =3D myt =3D None return self.__owner, len(self.__lockStack), mycontext, myt class LockWhiner(Thread):=20 def __init__(self, dorothy, whineContext, whineTime, every=3D10, = loudEvery=3D60):=20 Thread.__init__(self) self.dorothy =3D dorothy self.whineTime =3D whineTime self.whineContext =3D whineContext self.whineEvery =3D every self.whineLoudlyEvery =3D loudEvery self.whineThread =3D currentThread() self.active =3D 1 self.setDaemon(1) # let PyDS shut down even if we're active =09 def stop(self):=20 self.stopTime =3D time.time() self.active =3D 0 print "%s: LockWhiner %d stopped for %s.lock; acquired by thread %d = (%s) after %.2f seconds" % ( time.ctime(self.stopTime),=20 id(self),=20 self.dorothy.toolName,=20 id(self.whineThread),=20 self.whineThread.getName(), self.stopTime - self.whineTime) def run(self):=20 try:=20 #import pprint #pprint.pprint(self.__dict__) self._run() except:=20 print "_LockWhiner.run: Bugger." (e, d, tb) =3D sys.exc_info() print 'Exception %s: %s' % (e, d) for row in traceback.extract_tb(tb): print repr(row) print 'Dict:'=20 for key in self.__dict__.keys():=20 print ' %s =3D %s' % (key, repr(self.__dict__[key])) def _run(self):=20 print "%s: LockWhiner %d started for %s.lock; thread %d (%s) waiting" = % (\ time.ctime(self.whineTime),=20 id(self),=20 self.dorothy.toolName,=20 id(self.whineThread), self.whineThread.getName()) waits =3D 0 louds =3D int(self.whineLoudlyEvery/self.whineEvery) while self.active:=20 time.sleep(self.whineEvery) if not self.active:=20 break waits =3D waits + 1 now =3D time.time() print "%s: LockWhiner %d has been waiting for %.2fs" % ( time.ctime(), id(self),=20 now - self.whineTime) if not (waits-1) % louds:=20 msg =3D [] add =3D msg.append add("-"*70) add("Just in case this is a deadlock, here's some additional = information") add("for debugging purposes:\n") owner, lockCount, topContext, topTime =3D self.dorothy.lockDetails() if topTime is None:=20 timeRep =3D "(unknown)" else:=20 timeRep =3D time.ctime(topTime) add("Lock most recently obtained at %s by thread id %d, name %s" % = (timeRep, id(owner), owner.getName())) if not owner.isAlive():=20 add("**THREAD IS DEAD**") add("\nLock holding stack:") if topContext is None:=20 add("(unknown)") else: msg.extend(format_minitrace(topContext[-1])) if waits=3D=3D1:=20 add("\nWaiting thread: id %d, name %s" % (id(self.whineThread), = self.whineThread.getName())) add("\nWaiting stack:") msg.extend(format_minitrace(self.whineContext[-1])) add("-"*70) print '\n'.join(msg) if __name__ =3D=3D '__main__':=20 import pprint nrl =3D DorothyRLock() def spit():=20 pprint.pprint(nrl.callContext()) def a():=20 spit() def b():=20 a() a() print b() def _release(): assert nrl.callContext()[-1].f_code.co_name !=3D '_release' _release() =09 def fail1_top():=20 fail1_mid1() fail1_mid2() def fail1_mid1():=20 nrl.acquire() def fail1_mid2():=20 nrl.release() def fail2_top():=20 fail2_mid1() fail2_mid2() def fail2_mid1():=20 nrl.acquire() def fail2_mid2():=20 nrl.acquire() nrl.release() for test in [fail1_top, fail2_top]:=20 nrl =3D DorothyRLock() nrl.acquire() nrl.release() try:=20 print "\nTest: %s" % test.func_name test() except ReleaseFailure, e:=20 msg, frame, t =3D e print "%s caught; lock was acquired in frame id %d at time %s" % ( msg, id(frame), time.ctime(t)) print_minitrace(frame) ------=_NextPart_000_0004_01C4A13E.79676500--