public inbox for gentoo-portage-dev@lists.gentoo.org
 help / color / mirror / Atom feed
* [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236)
@ 2019-05-07  0:14 Zac Medico
  2019-05-07 14:55 ` Alec Warner
  0 siblings, 1 reply; 6+ messages in thread
From: Zac Medico @ 2019-05-07  0:14 UTC (permalink / raw
  To: gentoo-portage-dev; +Cc: Zac Medico

Add a reentrant lock/unlock method which is useful for things like
emaint binhost and eclean-pkg. The vardbapi class already provides
lock/unlock methods that behave the same way.

Bug: https://bugs.gentoo.org/685236
Signed-off-by: Zac Medico <zmedico@gentoo.org>
---
 lib/portage/dbapi/bintree.py                  | 46 ++++++++++++++-----
 lib/portage/emaint/modules/binhost/binhost.py |  9 ++--
 2 files changed, 38 insertions(+), 17 deletions(-)

diff --git a/lib/portage/dbapi/bintree.py b/lib/portage/dbapi/bintree.py
index 9c2d877e7..707958858 100644
--- a/lib/portage/dbapi/bintree.py
+++ b/lib/portage/dbapi/bintree.py
@@ -1,4 +1,4 @@
-# Copyright 1998-2018 Gentoo Foundation
+# Copyright 1998-2019 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2
 
 from __future__ import unicode_literals
@@ -94,6 +94,8 @@ class bindbapi(fakedbapi):
 			])
 		self._aux_cache_slot_dict = slot_dict_class(self._aux_cache_keys)
 		self._aux_cache = {}
+		self._lock = None
+		self._lock_count = 0
 
 	@property
 	def writable(self):
@@ -106,6 +108,34 @@ class bindbapi(fakedbapi):
 		"""
 		return os.access(first_existing(self.bintree.pkgdir), os.W_OK)
 
+	def lock(self):
+		"""
+		Acquire a reentrant lock, blocking, for cooperation with concurrent
+		processes.
+		"""
+		if self._lock_count <= 0:
+			if self._lock is not None:
+				raise AssertionError("already locked")
+			# At least the parent needs to exist for the lock file.
+			ensure_dirs(self.bintree.pkgdir)
+			self._lock = lockfile(self.bintree._pkgindex_file, wantnewlockfile=True)
+
+		self._lock_count += 1
+
+	def unlock(self):
+		"""
+		Release a lock, decrementing the recursion level. Each unlock() call
+		must be matched with a prior lock() call, or else an AssertionError
+		will be raised if unlock() is called while not locked.
+		"""
+		if self._lock_count <= 1:
+			if self._lock is None:
+				raise AssertionError("not locked")
+			unlockfile(self._lock)
+			self._lock = None
+
+		self._lock_count -= 1
+
 	def match(self, *pargs, **kwargs):
 		if self.bintree and not self.bintree.populated:
 			self.bintree.populate()
@@ -545,16 +575,13 @@ class binarytree(object):
 				# that changes made by a concurrent process cannot be lost. This
 				# case is avoided when possible, in order to minimize lock
 				# contention.
-				pkgindex_lock = None
+				self.dbapi.lock()
 				try:
-					pkgindex_lock = lockfile(self._pkgindex_file,
-						wantnewlockfile=True)
 					update_pkgindex = self._populate_local()
 					if update_pkgindex:
 						self._pkgindex_write(update_pkgindex)
 				finally:
-					if pkgindex_lock:
-						unlockfile(pkgindex_lock)
+					self.dbapi.unlock()
 
 			if getbinpkgs:
 				if not self.settings.get("PORTAGE_BINHOST"):
@@ -1110,10 +1137,8 @@ class binarytree(object):
 
 		# Reread the Packages index (in case it's been changed by another
 		# process) and then updated it, all while holding a lock.
-		pkgindex_lock = None
+		self.dbapi.lock()
 		try:
-			pkgindex_lock = lockfile(self._pkgindex_file,
-				wantnewlockfile=1)
 			if filename is not None:
 				new_filename = self.getname(cpv, allocate_new=True)
 				try:
@@ -1154,8 +1179,7 @@ class binarytree(object):
 			self._pkgindex_write(pkgindex)
 
 		finally:
-			if pkgindex_lock:
-				unlockfile(pkgindex_lock)
+			self.dbapi.unlock()
 
 		# This is used to record BINPKGMD5 in the installed package
 		# database, for a package that has just been built.
diff --git a/lib/portage/emaint/modules/binhost/binhost.py b/lib/portage/emaint/modules/binhost/binhost.py
index d3df0cbce..ceb9e87f3 100644
--- a/lib/portage/emaint/modules/binhost/binhost.py
+++ b/lib/portage/emaint/modules/binhost/binhost.py
@@ -1,4 +1,4 @@
-# Copyright 2005-2014 Gentoo Foundation
+# Copyright 2005-2019 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2
 
 import errno
@@ -27,7 +27,6 @@ class BinhostHandler(object):
 		eroot = portage.settings['EROOT']
 		self._bintree = portage.db[eroot]["bintree"]
 		self._bintree.populate()
-		self._pkgindex_file = self._bintree._pkgindex_file
 		self._pkgindex = self._bintree._load_pkgindex()
 
 	def _need_update(self, cpv, data):
@@ -118,9 +117,7 @@ class BinhostHandler(object):
 				missing.append(cpv)
 
 		if missing or stale:
-			from portage import locks
-			pkgindex_lock = locks.lockfile(
-				self._pkgindex_file, wantnewlockfile=1)
+			bintree.dbapi.lock()
 			try:
 				# Repopulate with lock held. If _populate_local returns
 				# data then use that, since _load_pkgindex would return
@@ -174,7 +171,7 @@ class BinhostHandler(object):
 				bintree._pkgindex_write(self._pkgindex)
 
 			finally:
-				locks.unlockfile(pkgindex_lock)
+				bintree.dbapi.unlock()
 
 		if onProgress:
 			if maxval == 0:
-- 
2.21.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236)
  2019-05-07  0:14 [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236) Zac Medico
@ 2019-05-07 14:55 ` Alec Warner
  2019-05-07 20:01   ` Zac Medico
  0 siblings, 1 reply; 6+ messages in thread
From: Alec Warner @ 2019-05-07 14:55 UTC (permalink / raw
  To: gentoo-portage-dev; +Cc: Zac Medico

[-- Attachment #1: Type: text/plain, Size: 7815 bytes --]

On Mon, May 6, 2019 at 8:15 PM Zac Medico <zmedico@gentoo.org> wrote:

> Add a reentrant lock/unlock method which is useful for things like
> emaint binhost and eclean-pkg. The vardbapi class already provides
> lock/unlock methods that behave the same way.
>

Curious why we are not doing more encapsulation here.

I'd suggest a new class type in locks.py:

class RefCountedLock(object): ...
# This implements the Lock() / Unlock() interface, as well as perhaps
__enter__ and __exit__.
# It uses a lockfile at path and a reference count.

Then in bintree's init, set self.lock to
locks.refCountedLock(self.bintree._pkgindex_file)

def lock(self): # on bintree
  self._lock.Lock()

def unlock(self):
  self._lock.Unlock()

Also curious why we are not implementing enter and exit so we can avoid
unbalanced pairs by using context managers.

e.g. in match(), we could likely write:

with self.dbapi.lock():
  # try to match
  update_pkgindex = self._populate_local()
  if update_pkgindex:
    self._pkgindex_write(update_pkgindex)

If the block raises an exception, __exit__ is still called, so we can
eliminate the finally: blocks and replace them with a careful __exit__
implementation?

-A


>
> Bug: https://bugs.gentoo.org/685236
> Signed-off-by: Zac Medico <zmedico@gentoo.org>
> ---
>  lib/portage/dbapi/bintree.py                  | 46 ++++++++++++++-----
>  lib/portage/emaint/modules/binhost/binhost.py |  9 ++--
>  2 files changed, 38 insertions(+), 17 deletions(-)
>
> diff --git a/lib/portage/dbapi/bintree.py b/lib/portage/dbapi/bintree.py
> index 9c2d877e7..707958858 100644
> --- a/lib/portage/dbapi/bintree.py
> +++ b/lib/portage/dbapi/bintree.py
> @@ -1,4 +1,4 @@
> -# Copyright 1998-2018 Gentoo Foundation
> +# Copyright 1998-2019 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>
>  from __future__ import unicode_literals
> @@ -94,6 +94,8 @@ class bindbapi(fakedbapi):
>                         ])
>                 self._aux_cache_slot_dict =
> slot_dict_class(self._aux_cache_keys)
>                 self._aux_cache = {}
> +               self._lock = None
> +               self._lock_count = 0
>
>         @property
>         def writable(self):
> @@ -106,6 +108,34 @@ class bindbapi(fakedbapi):
>                 """
>                 return os.access(first_existing(self.bintree.pkgdir),
> os.W_OK)
>
> +       def lock(self):
> +               """
> +               Acquire a reentrant lock, blocking, for cooperation with
> concurrent
> +               processes.
> +               """
> +               if self._lock_count <= 0:
> +                       if self._lock is not None:
> +                               raise AssertionError("already locked")
> +                       # At least the parent needs to exist for the lock
> file.
> +                       ensure_dirs(self.bintree.pkgdir)
> +                       self._lock = lockfile(self.bintree._pkgindex_file,
> wantnewlockfile=True)
> +
> +               self._lock_count += 1
> +
> +       def unlock(self):
> +               """
> +               Release a lock, decrementing the recursion level. Each
> unlock() call
> +               must be matched with a prior lock() call, or else an
> AssertionError
> +               will be raised if unlock() is called while not locked.
> +               """
> +               if self._lock_count <= 1:
> +                       if self._lock is None:
> +                               raise AssertionError("not locked")
> +                       unlockfile(self._lock)
> +                       self._lock = None
> +
> +               self._lock_count -= 1
> +
>         def match(self, *pargs, **kwargs):
>                 if self.bintree and not self.bintree.populated:
>                         self.bintree.populate()
> @@ -545,16 +575,13 @@ class binarytree(object):
>                                 # that changes made by a concurrent
> process cannot be lost. This
>                                 # case is avoided when possible, in order
> to minimize lock
>                                 # contention.
> -                               pkgindex_lock = None
> +                               self.dbapi.lock()
>                                 try:
> -                                       pkgindex_lock =
> lockfile(self._pkgindex_file,
> -                                               wantnewlockfile=True)
>                                         update_pkgindex =
> self._populate_local()
>                                         if update_pkgindex:
>
> self._pkgindex_write(update_pkgindex)
>                                 finally:
> -                                       if pkgindex_lock:
> -                                               unlockfile(pkgindex_lock)
> +                                       self.dbapi.unlock()
>
>                         if getbinpkgs:
>                                 if not
> self.settings.get("PORTAGE_BINHOST"):
> @@ -1110,10 +1137,8 @@ class binarytree(object):
>
>                 # Reread the Packages index (in case it's been changed by
> another
>                 # process) and then updated it, all while holding a lock.
> -               pkgindex_lock = None
> +               self.dbapi.lock()
>                 try:
> -                       pkgindex_lock = lockfile(self._pkgindex_file,
> -                               wantnewlockfile=1)
>                         if filename is not None:
>                                 new_filename = self.getname(cpv,
> allocate_new=True)
>                                 try:
> @@ -1154,8 +1179,7 @@ class binarytree(object):
>                         self._pkgindex_write(pkgindex)
>
>                 finally:
> -                       if pkgindex_lock:
> -                               unlockfile(pkgindex_lock)
> +                       self.dbapi.unlock()
>
>                 # This is used to record BINPKGMD5 in the installed package
>                 # database, for a package that has just been built.
> diff --git a/lib/portage/emaint/modules/binhost/binhost.py
> b/lib/portage/emaint/modules/binhost/binhost.py
> index d3df0cbce..ceb9e87f3 100644
> --- a/lib/portage/emaint/modules/binhost/binhost.py
> +++ b/lib/portage/emaint/modules/binhost/binhost.py
> @@ -1,4 +1,4 @@
> -# Copyright 2005-2014 Gentoo Foundation
> +# Copyright 2005-2019 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>
>  import errno
> @@ -27,7 +27,6 @@ class BinhostHandler(object):
>                 eroot = portage.settings['EROOT']
>                 self._bintree = portage.db[eroot]["bintree"]
>                 self._bintree.populate()
> -               self._pkgindex_file = self._bintree._pkgindex_file
>                 self._pkgindex = self._bintree._load_pkgindex()
>
>         def _need_update(self, cpv, data):
> @@ -118,9 +117,7 @@ class BinhostHandler(object):
>                                 missing.append(cpv)
>
>                 if missing or stale:
> -                       from portage import locks
> -                       pkgindex_lock = locks.lockfile(
> -                               self._pkgindex_file, wantnewlockfile=1)
> +                       bintree.dbapi.lock()
>                         try:
>                                 # Repopulate with lock held. If
> _populate_local returns
>                                 # data then use that, since _load_pkgindex
> would return
> @@ -174,7 +171,7 @@ class BinhostHandler(object):
>                                 bintree._pkgindex_write(self._pkgindex)
>
>                         finally:
> -                               locks.unlockfile(pkgindex_lock)
> +                               bintree.dbapi.unlock()
>
>                 if onProgress:
>                         if maxval == 0:
> --
> 2.21.0
>
>
>

[-- Attachment #2: Type: text/html, Size: 10544 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236)
  2019-05-07 14:55 ` Alec Warner
@ 2019-05-07 20:01   ` Zac Medico
  2019-05-08 18:05     ` Zac Medico
  0 siblings, 1 reply; 6+ messages in thread
From: Zac Medico @ 2019-05-07 20:01 UTC (permalink / raw
  To: gentoo-portage-dev, Alec Warner; +Cc: Zac Medico


[-- Attachment #1.1: Type: text/plain, Size: 1894 bytes --]

On 5/7/19 7:55 AM, Alec Warner wrote:
> 
> 
> On Mon, May 6, 2019 at 8:15 PM Zac Medico <zmedico@gentoo.org
> <mailto:zmedico@gentoo.org>> wrote:
> 
>     Add a reentrant lock/unlock method which is useful for things like
>     emaint binhost and eclean-pkg. The vardbapi class already provides
>     lock/unlock methods that behave the same way.
> 
> 
> Curious why we are not doing more encapsulation here.
> 
> I'd suggest a new class type in locks.py:
> 
> class RefCountedLock(object): ...
> # This implements the Lock() / Unlock() interface, as well as perhaps
> __enter__ and __exit__.
> # It uses a lockfile at path and a reference count.
> 
> Then in bintree's init, set self.lock to
> locks.refCountedLock(self.bintree._pkgindex_file)
> 
> def lock(self): # on bintree
>   self._lock.Lock()
> 
> def unlock(self):
>   self._lock.Unlock()

Yes, splitting out a reference counted lock class like this is a good
idea. I'll do that.

> Also curious why we are not implementing enter and exit so we can avoid
> unbalanced pairs by using context managers.
> 
> e.g. in match(), we could likely write:
> 
> with self.dbapi.lock():
>   # try to match
>   update_pkgindex = self._populate_local()
>   if update_pkgindex:
>     self._pkgindex_write(update_pkgindex)
> 
> If the block raises an exception, __exit__ is still called, so we can
> eliminate the finally: blocks and replace them with a careful __exit__
> implementation?
> 
> -A

Yeah, the reference counted lock class could have a method that returns
a context manager which increments/decrements the lock count. The
context manager can be implemented with the contextlib.contextmanager
decorator like this:

@contextlib.contextmanager
def contextmanager(self):
    self.lock()
    try:
        yield self
    finally:
        self.unlock()

-- 
Thanks,
Zac


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 981 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236)
  2019-05-07 20:01   ` Zac Medico
@ 2019-05-08 18:05     ` Zac Medico
  2019-05-08 18:09       ` Alec Warner
  2019-05-10  9:05       ` Zac Medico
  0 siblings, 2 replies; 6+ messages in thread
From: Zac Medico @ 2019-05-08 18:05 UTC (permalink / raw
  To: Zac Medico, gentoo-portage-dev, Alec Warner


[-- Attachment #1.1: Type: text/plain, Size: 1431 bytes --]

On 5/7/19 1:01 PM, Zac Medico wrote:
> On 5/7/19 7:55 AM, Alec Warner wrote:
> 
>> Also curious why we are not implementing enter and exit so we can avoid
>> unbalanced pairs by using context managers.
>>
>> e.g. in match(), we could likely write:
>>
>> with self.dbapi.lock():
>>   # try to match
>>   update_pkgindex = self._populate_local()
>>   if update_pkgindex:
>>     self._pkgindex_write(update_pkgindex)
>>
>> If the block raises an exception, __exit__ is still called, so we can
>> eliminate the finally: blocks and replace them with a careful __exit__
>> implementation?
>>
>> -A
> 
> Yeah, the reference counted lock class could have a method that returns
> a context manager which increments/decrements the lock count. The
> context manager can be implemented with the contextlib.contextmanager
> decorator like this:
> 
> @contextlib.contextmanager
> def contextmanager(self):
>     self.lock()
>     try:
>         yield self
>     finally:
>         self.unlock()
> 

Since we really don't want asynchronous code to block waiting for a
lock, we really should use a (python3.5+ only) asynchronous context manager:

https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with

Given that python2.7 is scheduled for retirement in 2020
(https://pythonclock.org/), maybe it's time to drop support for
python3.4 and earlier.
-- 
Thanks,
Zac


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 981 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236)
  2019-05-08 18:05     ` Zac Medico
@ 2019-05-08 18:09       ` Alec Warner
  2019-05-10  9:05       ` Zac Medico
  1 sibling, 0 replies; 6+ messages in thread
From: Alec Warner @ 2019-05-08 18:09 UTC (permalink / raw
  To: Zac Medico; +Cc: gentoo-portage-dev

[-- Attachment #1: Type: text/plain, Size: 1641 bytes --]

On Wed, May 8, 2019 at 2:05 PM Zac Medico <zmedico@gentoo.org> wrote:

> On 5/7/19 1:01 PM, Zac Medico wrote:
> > On 5/7/19 7:55 AM, Alec Warner wrote:
> >
> >> Also curious why we are not implementing enter and exit so we can avoid
> >> unbalanced pairs by using context managers.
> >>
> >> e.g. in match(), we could likely write:
> >>
> >> with self.dbapi.lock():
> >>   # try to match
> >>   update_pkgindex = self._populate_local()
> >>   if update_pkgindex:
> >>     self._pkgindex_write(update_pkgindex)
> >>
> >> If the block raises an exception, __exit__ is still called, so we can
> >> eliminate the finally: blocks and replace them with a careful __exit__
> >> implementation?
> >>
> >> -A
> >
> > Yeah, the reference counted lock class could have a method that returns
> > a context manager which increments/decrements the lock count. The
> > context manager can be implemented with the contextlib.contextmanager
> > decorator like this:
> >
> > @contextlib.contextmanager
> > def contextmanager(self):
> >     self.lock()
> >     try:
> >         yield self
> >     finally:
> >         self.unlock()
> >
>
> Since we really don't want asynchronous code to block waiting for a
> lock, we really should use a (python3.5+ only) asynchronous context
> manager:
>
>
> https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with
>
> Given that python2.7 is scheduled for retirement in 2020
> (https://pythonclock.org/), maybe it's time to drop support for
> python3.4 and earlier.
>

I haven't used that stuff yet, so I will defer to you on its use; same with
python support.

-A


> --
> Thanks,
> Zac
>
>

[-- Attachment #2: Type: text/html, Size: 2616 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236)
  2019-05-08 18:05     ` Zac Medico
  2019-05-08 18:09       ` Alec Warner
@ 2019-05-10  9:05       ` Zac Medico
  1 sibling, 0 replies; 6+ messages in thread
From: Zac Medico @ 2019-05-10  9:05 UTC (permalink / raw
  To: Zac Medico, gentoo-portage-dev, Alec Warner


[-- Attachment #1.1: Type: text/plain, Size: 2103 bytes --]

On 5/8/19 11:05 AM, Zac Medico wrote:
> On 5/7/19 1:01 PM, Zac Medico wrote:
>> On 5/7/19 7:55 AM, Alec Warner wrote:
>>
>>> Also curious why we are not implementing enter and exit so we can avoid
>>> unbalanced pairs by using context managers.
>>>
>>> e.g. in match(), we could likely write:
>>>
>>> with self.dbapi.lock():
>>>   # try to match
>>>   update_pkgindex = self._populate_local()
>>>   if update_pkgindex:
>>>     self._pkgindex_write(update_pkgindex)
>>>
>>> If the block raises an exception, __exit__ is still called, so we can
>>> eliminate the finally: blocks and replace them with a careful __exit__
>>> implementation?
>>>
>>> -A
>>
>> Yeah, the reference counted lock class could have a method that returns
>> a context manager which increments/decrements the lock count. The
>> context manager can be implemented with the contextlib.contextmanager
>> decorator like this:
>>
>> @contextlib.contextmanager
>> def contextmanager(self):
>>     self.lock()
>>     try:
>>         yield self
>>     finally:
>>         self.unlock()
>>
> 
> Since we really don't want asynchronous code to block waiting for a
> lock, we really should use a (python3.5+ only) asynchronous context manager:
> 
> https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with
> 
> Given that python2.7 is scheduled for retirement in 2020
> (https://pythonclock.org/), maybe it's time to drop support for
> python3.4 and earlier.

Adapting the sample code provided in PEP 492, we can do something like
this for compatibility with older versions of python:

from portage.util.futures.compat_coroutine

@compat_coroutine.coroutine
def async_with(mgr, async_func):
  aexit = type(mgr).__aexit__
  aenter = type(mgr).__aenter__(mgr)

  aenter_result = (yield aenter)
  try:
    return_value = (yield async_func(aenter_result))
  except:
    if not (yield aexit(mgr, *sys.exc_info()):
      raise
  else:
    yield aexit(mgr, None, None, None)

  compat_coroutine.coroutine_return(return_value)


-- 
Thanks,
Zac


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 981 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2019-05-10  9:05 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2019-05-07  0:14 [gentoo-portage-dev] [PATCH] bindbapi: add reentrant lock method (bug 685236) Zac Medico
2019-05-07 14:55 ` Alec Warner
2019-05-07 20:01   ` Zac Medico
2019-05-08 18:05     ` Zac Medico
2019-05-08 18:09       ` Alec Warner
2019-05-10  9:05       ` Zac Medico

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox