Skip to content

Commit 7884309

Browse files
authored
Merge pull request #335 from nateprewitt/cache_race_condition
Fix concurrency issues with cache
2 parents 2ad18b0 + 4a84922 commit 7884309

File tree

3 files changed

+28
-14
lines changed

3 files changed

+28
-14
lines changed

.github/workflows/run-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
matrix:
1111
os: [ubuntu-latest]
12-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
12+
python-version: ["3.9", "3.10", "3.11"]
1313

1414
steps:
1515
- uses: actions/checkout@v2
@@ -25,4 +25,4 @@ jobs:
2525
pip install dist/*.whl
2626
- name: Test with pytest
2727
run: |
28-
cd tests/ && py.test --cov jmespath --cov-report term-missing
28+
cd tests/ && python -m pytest --cov jmespath --cov-report term-missing

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
Next Release (TBD)
2+
==================
3+
4+
* Fix concurrency issue with cache
5+
(`pr #335 <https://github.com/jmespath/jmespath.py/pull/335>`__)
6+
7+
18
1.0.1
29
=====
310

jmespath/parser.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
consuming from the token iterator one token at a time.
2626
2727
"""
28-
import random
29-
3028
from jmespath import lexer
3129
from jmespath.compat import with_repr_method
3230
from jmespath import ast
@@ -73,7 +71,7 @@ class Parser(object):
7371
# The _MAX_SIZE most recent expressions are cached in
7472
# _CACHE dict.
7573
_CACHE = {}
76-
_MAX_SIZE = 128
74+
_MAX_SIZE = 512
7775

7876
def __init__(self, lookahead=2):
7977
self.tokenizer = None
@@ -82,13 +80,26 @@ def __init__(self, lookahead=2):
8280
self._index = 0
8381

8482
def parse(self, expression):
85-
cached = self._CACHE.get(expression)
86-
if cached is not None:
87-
return cached
83+
try:
84+
return self._CACHE[expression]
85+
except KeyError:
86+
pass
8887
parsed_result = self._do_parse(expression)
88+
if len(self._CACHE) >= self._MAX_SIZE:
89+
try:
90+
del self._CACHE[next(iter(self._CACHE))]
91+
except (KeyError, StopIteration, RuntimeError):
92+
# KeyError - Another thread else already deleted the key.
93+
# RuntimeError - Another modified the cache.
94+
# StopIteration - (Unlikely) Cache is empty.
95+
#
96+
# If we encounter an error we should NOT be adding to the
97+
# cache. To ensure we do not exceed self._MAX_SIZE, we
98+
# can only add to the cache if we successfully removed
99+
# an element from the cache, otherwise this can grow
100+
# unbounded.
101+
return parsed_result
89102
self._CACHE[expression] = parsed_result
90-
if len(self._CACHE) > self._MAX_SIZE:
91-
self._free_cache_entries()
92103
return parsed_result
93104

94105
def _do_parse(self, expression):
@@ -488,10 +499,6 @@ def _raise_parse_error_maybe_eof(self, expected_type, token):
488499
raise exceptions.ParseError(
489500
lex_position, actual_value, actual_type, message)
490501

491-
def _free_cache_entries(self):
492-
for key in random.sample(list(self._CACHE.keys()), int(self._MAX_SIZE / 2)):
493-
self._CACHE.pop(key, None)
494-
495502
@classmethod
496503
def purge(cls):
497504
"""Clear the expression compilation cache."""

0 commit comments

Comments
 (0)