RE: Patch to DorothyLocker

"Garth T Kidd" <garth-OnzZ1s1DREKDegMON/[email protected]> Thu, 23 Sep 2004 07:05:59 +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_0000_01C4A13B.CB1FFC70
Content-Type: text/plain;
	charset="us-ascii"
Content-Transfer-Encoding: 7bit

Okay, this should be MUCH better. It might still have problems when it does
report on a long-held lock, but I can run PyDS without hanging or crashing
now. I'll write a suite of unit tests to raise everyone's confidence levels
(mine included) that it's stable. 

If you don't want to be blinded with detail, change the setting of
``verbose`` in Tool.py to:

			self.lock = DorothyRLock(verbose=None,
toolName=name) 

-----Original Message-----
From: Garth T Kidd [mailto:garth-OnzZ1s1DREKDegMON/[email protected]] 
Sent: Wednesday, 22 September 2004 5:22 PM
To: 'Garth T Kidd'; 'Thomas Klaeger'; 'pyds-dev-iYtK5bfT9M//Ad8WF/[email protected]'
Subject: RE: [Pyds-dev] Patch to DorothyLocker

Aah, bugger. I've replaced one deadlock with another; it's just happening on
Events because every other tool talks to it. I'll have to re-do large
chunks. 

-----Original Message-----
From: Garth T Kidd [mailto:garth-OnzZ1s1DREKDegMON/[email protected]]
Sent: Wednesday, 22 September 2004 2:31 PM
To: 'Garth T Kidd'; 'Thomas Klaeger'; 'pyds-dev-iYtK5bfT9M//Ad8WF/[email protected]'
Subject: RE: [Pyds-dev] Patch to DorothyLocker

Okay; my first and most major blunder was that DorothyRLock wasn't
thread-safe. What's left, after fixing that and a couple more bugs, is that
the code to print details about foreign owners just isn't right. I'll fix
it. What'll be left after *that* is a blockade on the Events tool. 

-----Original Message-----
From: pyds-dev-admin-iYtK5bfT9M//Ad8WF/[email protected]
[mailto:pyds-dev-admin-iYtK5bfT9M//Ad8WF/[email protected]] On Behalf Of Garth T Kidd
Sent: Wednesday, 22 September 2004 11:18 AM
To: 'Thomas Klaeger'; pyds-dev-iYtK5bfT9M//Ad8WF/[email protected]
Subject: RE: [Pyds-dev] Patch to DorothyLocker

I'll have to re-test. I can't incorporate the patches as-is because they're
broken: Rlocks can be locked multiple times by the same thread, and your
added lockContext attributes bypass my stack of lock details. 

-----Original Message-----
From: pyds-dev-admin-iYtK5bfT9M//Ad8WF/[email protected]
[mailto:pyds-dev-admin-iYtK5bfT9M//Ad8WF/[email protected]] On Behalf Of Thomas Klaeger
Sent: Monday, 20 September 2004 8:41 PM
To: pyds-dev-iYtK5bfT9M//Ad8WF/[email protected]
Subject: [Pyds-dev] Patch to DorothyLocker

DorothyLocker raised exceptions if a lock was held by another task. In this
case the lockContext and lockTime was not defined although it should have
been.

Additionally it whined about unable to lock long after the lock had been
acquired due to the LockWhiner not checking if it has been stopped before
whining.


_______________________________________________
Pyds-dev mailing list
Pyds-dev-iYtK5bfT9M//Ad8WF/[email protected]
http://www.westfalen.de/cgi-bin/mailman/listinfo/pyds-dev

------=_NextPart_000_0000_01C4A13B.CB1FFC70
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 print_minitrace(frame):=20
	"Print a mini-trace from a locking context."
	for filename, line, name in extract_minitrace(frame):=20
		print 'File "%s", line %d, in %s' % (
		      filename, line, name)
	=09
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
				print "-"*70
				print "Just in case this is a deadlock, here's some additional "
				print "information 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)
				print "Lock most recently obtained at %s by thread id %d, name %s" % =
(timeRep, id(owner), owner.getName())
				if not owner.isAlive():=20
					print "**THREAD IS DEAD**"
				print "\nLock holding stack:"
				if topContext is None:=20
					print "(unknown)"
				else:
					print_minitrace(topContext[-1])
				if waits=3D=3D1:=20
					print "\nWaiting thread: id %d, name %s" % (id(self.whineThread), =
self.whineThread.getName())
					print "\nWaiting stack:"=20
					print_minitrace(self.whineContext[-1])
				print "-"*70

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_0000_01C4A13B.CB1FFC70--