summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Ganssle <paul@ganssle.io>2019-11-02 21:24:29 -0400
committerPaul Ganssle <paul@ganssle.io>2019-11-02 21:33:04 -0400
commit6e92f2111589ab061b66bd332dcb4a33a58acb84 (patch)
tree2d39ee4522ab4ae5d8acd6602869968abb8ed950
parent2e36ff7e48da1ee248881bbba361a5f2538cfa90 (diff)
downloaddateutil-git-6e92f2111589ab061b66bd332dcb4a33a58acb84.tar.gz
Lock around strong cache updates
Since the strong cache uses operations in OrderedDict that are not thread-safe in Python 2, it is necessary to acquire a lock while updating the dictionary. When Python 2 support is dropped, we can likely refactor this to avoid locking.
-rw-r--r--changelog.d/973.bugfix.rst1
-rw-r--r--dateutil/tz/_factories.py29
2 files changed, 19 insertions, 11 deletions
diff --git a/changelog.d/973.bugfix.rst b/changelog.d/973.bugfix.rst
new file mode 100644
index 0000000..6af867c
--- /dev/null
+++ b/changelog.d/973.bugfix.rst
@@ -0,0 +1 @@
+Fixed a race condition in the ``tzoffset`` and ``tzstr`` "strong" caches on Python 2.7. Reported by @kainjow (gh issue #901).
diff --git a/dateutil/tz/_factories.py b/dateutil/tz/_factories.py
index d2560eb..f8a6589 100644
--- a/dateutil/tz/_factories.py
+++ b/dateutil/tz/_factories.py
@@ -2,6 +2,8 @@ from datetime import timedelta
import weakref
from collections import OrderedDict
+from six.moves import _thread
+
class _TzSingleton(type):
def __init__(cls, *args, **kwargs):
@@ -26,6 +28,8 @@ class _TzOffsetFactory(_TzFactory):
cls.__strong_cache = OrderedDict()
cls.__strong_cache_size = 8
+ cls._cache_lock = _thread.allocate_lock()
+
def __call__(cls, name, offset):
if isinstance(offset, timedelta):
key = (name, offset.total_seconds())
@@ -37,12 +41,13 @@ class _TzOffsetFactory(_TzFactory):
instance = cls.__instances.setdefault(key,
cls.instance(name, offset))
- cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+ # This lock may not be necessary in Python 3. See GH issue #901
+ with cls._cache_lock:
+ cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
- # Remove an item if the strong cache is overpopulated
- # TODO: Maybe this should be under a lock?
- if len(cls.__strong_cache) > cls.__strong_cache_size:
- cls.__strong_cache.popitem(last=False)
+ # Remove an item if the strong cache is overpopulated
+ if len(cls.__strong_cache) > cls.__strong_cache_size:
+ cls.__strong_cache.popitem(last=False)
return instance
@@ -53,6 +58,8 @@ class _TzStrFactory(_TzFactory):
cls.__strong_cache = OrderedDict()
cls.__strong_cache_size = 8
+ cls.__cache_lock = _thread.allocate_lock()
+
def __call__(cls, s, posix_offset=False):
key = (s, posix_offset)
instance = cls.__instances.get(key, None)
@@ -61,13 +68,13 @@ class _TzStrFactory(_TzFactory):
instance = cls.__instances.setdefault(key,
cls.instance(s, posix_offset))
- cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
-
+ # This lock may not be necessary in Python 3. See GH issue #901
+ with cls.__cache_lock:
+ cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
- # Remove an item if the strong cache is overpopulated
- # TODO: Maybe this should be under a lock?
- if len(cls.__strong_cache) > cls.__strong_cache_size:
- cls.__strong_cache.popitem(last=False)
+ # Remove an item if the strong cache is overpopulated
+ if len(cls.__strong_cache) > cls.__strong_cache_size:
+ cls.__strong_cache.popitem(last=False)
return instance