Merge "Make memoize behave better in concurrent environments"

This commit is contained in:
Zuul 2020-12-10 17:44:34 +00:00 committed by Gerrit Code Review
commit 3a05eed4d2
4 changed files with 117 additions and 80 deletions

View File

@ -55,6 +55,7 @@ Sphinx==2.0.0
sphinxcontrib-websupport==1.0.1 sphinxcontrib-websupport==1.0.1
stestr==1.0.0 stestr==1.0.0
stevedore==1.20.0 stevedore==1.20.0
tenacity==6.2.0
testrepository==0.0.20 testrepository==0.0.20
testscenarios==0.4 testscenarios==0.4
testtools==2.2.0 testtools==2.2.0

View File

@ -5,3 +5,4 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0
Flask>=1.0.2 # BSD Flask>=1.0.2 # BSD
requests>=2.14.2 # Apache-2.0 requests>=2.14.2 # Apache-2.0
tenacity>=6.2.0 # Apache-2.0

View File

@ -26,6 +26,8 @@ import pickle
import sqlite3 import sqlite3
import tempfile import tempfile
import tenacity
# Python 3.8 # Python 3.8
MutableMapping = getattr(collections, 'abc', collections).MutableMapping MutableMapping = getattr(collections, 'abc', collections).MutableMapping
@ -71,6 +73,13 @@ def memoize(permanent_cache=None):
return decorator return decorator
_retry = tenacity.retry(
retry=tenacity.retry_if_exception_type(sqlite3.OperationalError),
wait=tenacity.wait_exponential(min=0.1, max=2, multiplier=1),
stop=tenacity.stop_after_delay(5),
reraise=True)
class PersistentDict(MutableMapping): class PersistentDict(MutableMapping):
DBPATH = os.path.join(tempfile.gettempdir(), 'sushy-emulator') DBPATH = os.path.join(tempfile.gettempdir(), 'sushy-emulator')
@ -88,8 +97,6 @@ class PersistentDict(MutableMapping):
'(key blob primary key not null, value blob not null)' '(key blob primary key not null, value blob not null)'
) )
self.update(dict(self))
@staticmethod @staticmethod
def encode(obj): def encode(obj):
return pickle.dumps(obj) return pickle.dumps(obj)
@ -103,16 +110,10 @@ class PersistentDict(MutableMapping):
if not self._dbpath: if not self._dbpath:
raise TypeError('Dict is not yet persistent') raise TypeError('Dict is not yet persistent')
connection = sqlite3.connect(self._dbpath) with sqlite3.connect(self._dbpath) as connection:
try:
yield connection.cursor() yield connection.cursor()
connection.commit() @_retry
finally:
connection.close()
def __getitem__(self, key): def __getitem__(self, key):
key = self.encode(key) key = self.encode(key)
@ -128,6 +129,7 @@ class PersistentDict(MutableMapping):
return self.decode(value[0]) return self.decode(value[0])
@_retry
def __setitem__(self, key, value): def __setitem__(self, key, value):
key = self.encode(key) key = self.encode(key)
value = self.encode(value) value = self.encode(value)
@ -138,23 +140,19 @@ class PersistentDict(MutableMapping):
(key, value) (key, value)
) )
@_retry
def __delitem__(self, key): def __delitem__(self, key):
key = self.encode(key) key = self.encode(key)
with self.connection() as cursor: with self.connection() as cursor:
cursor.execute(
'select count(*) from cache where key=?',
(key,)
)
if cursor.fetchone()[0] == 0:
raise KeyError(key)
cursor.execute( cursor.execute(
'delete from cache where key=?', 'delete from cache where key=?',
(key,) (key,)
) )
if not cursor.rowcount:
raise KeyError(key)
@_retry
def __iter__(self): def __iter__(self):
with self.connection() as cursor: with self.connection() as cursor:
cursor.execute( cursor.execute(
@ -165,6 +163,7 @@ class PersistentDict(MutableMapping):
for r in records: for r in records:
yield self.decode(r[0]) yield self.decode(r[0])
@_retry
def __len__(self): def __len__(self):
with self.connection() as cursor: with self.connection() as cursor:
cursor.execute( cursor.execute(

View File

@ -12,6 +12,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import pickle
import sqlite3
from unittest import mock from unittest import mock
from oslotest import base from oslotest import base
@ -82,112 +85,145 @@ class MemoizeTestCase(base.BaseTestCase):
self.assertEqual(0, driver.call_count) self.assertEqual(0, driver.call_count)
@mock.patch.object(sqlite3, 'connect', autospec=True)
class PersistentDictTestCase(base.BaseTestCase): class PersistentDictTestCase(base.BaseTestCase):
@mock.patch.object(memoize, 'sqlite3', autospec=True)
def test_make_permanent(self, mock_sqlite3): def test_make_permanent(self, mock_sqlite3):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
pd.make_permanent('/', 'file') pd.make_permanent('/', 'file')
mock_sqlite3.assert_called_once_with('/file.sqlite')
mock_sqlite3.connect.assert_called_with('/file.sqlite') def test_encode(self, mock_sqlite3):
@mock.patch.object(memoize, 'pickle', autospec=True)
def test_encode(self, mock_pickle):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
value = pd.encode({1: '2'})
self.assertEqual(pickle.dumps({1: '2'}), value)
pd.encode({1: '2'}) def test_decode(self, mock_sqlite3):
mock_pickle.dumps.assert_called_once_with({1: '2'})
@mock.patch.object(memoize, 'pickle', autospec=True)
def test_decode(self, mock_pickle):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
value = pd.decode(pickle.dumps({1: '2'}))
self.assertEqual({1: '2'}, value)
pd.decode('blob') def test___getitem__(self, mock_sqlite3):
mock_pickle.loads.assert_called_once_with('blob')
@mock.patch.object(memoize, 'pickle', autospec=True)
@mock.patch.object(memoize, 'sqlite3', autospec=True)
def test___getitem__(self, mock_sqlite3, mock_pickle):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
pd.make_permanent('/', 'file') pd.make_permanent('/', 'file')
mock_pickle.dumps.return_value = 'pickled-key' mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_connection = mock_sqlite3.connect.return_value mock_cursor = mock_conn.cursor.return_value
mock_cursor = mock_connection.cursor.return_value mock_cursor.execute.reset_mock()
mock_cursor.fetchone.return_value = ['pickled-value'] mock_cursor.fetchone.return_value = [pickle.dumps('pickled-value')]
pd[1] result = pd[1]
self.assertEqual('pickled-value', result)
mock_cursor.execute.assert_called_with( mock_cursor.execute.assert_called_with(
'select value from cache where key=?', ('pickled-key',)) 'select value from cache where key=?', (pickle.dumps(1),))
mock_pickle.loads.assert_called_once_with('pickled-value')
@mock.patch.object(memoize, 'pickle', autospec=True) def test___getitem__retries(self, mock_sqlite3):
@mock.patch.object(memoize, 'sqlite3', autospec=True)
def test___setitem__(self, mock_sqlite3, mock_pickle):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
pd.make_permanent('/', 'file') pd.make_permanent('/', 'file')
mock_pickle.dumps.side_effect = [ mock_conn = mock_sqlite3.return_value.__enter__.return_value
'pickled-key', 'pickled-value'] mock_cursor = mock_conn.cursor.return_value
mock_cursor.execute.reset_mock()
mock_cursor.execute.side_effect = [
sqlite3.OperationalError,
None
]
mock_cursor.fetchone.return_value = [pickle.dumps('pickled-value')]
mock_connection = mock_sqlite3.connect.return_value result = pd[1]
mock_cursor = mock_connection.cursor.return_value self.assertEqual('pickled-value', result)
mock_cursor.execute.assert_called_with(
'select value from cache where key=?', (pickle.dumps(1),))
self.assertEqual(2, mock_cursor.execute.call_count)
def test___setitem__(self, mock_sqlite3):
pd = memoize.PersistentDict()
pd.make_permanent('/', 'file')
mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_cursor = mock_conn.cursor.return_value
mock_cursor.execute.reset_mock()
pd[1] = 2
mock_cursor.execute.assert_called_once_with(
'insert or replace into cache values (?, ?)',
(pickle.dumps(1), pickle.dumps(2)))
def test___setitem__retries(self, mock_sqlite3):
pd = memoize.PersistentDict()
pd.make_permanent('/', 'file')
mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_cursor = mock_conn.cursor.return_value
mock_cursor.execute.reset_mock()
mock_cursor.execute.side_effect = [
sqlite3.OperationalError,
None
]
pd[1] = 2 pd[1] = 2
mock_cursor.execute.assert_called_with( mock_cursor.execute.assert_called_with(
'insert or replace into cache values (?, ?)', 'insert or replace into cache values (?, ?)',
('pickled-key', 'pickled-value')) (pickle.dumps(1), pickle.dumps(2)))
self.assertEqual(2, mock_cursor.execute.call_count)
@mock.patch.object(memoize, 'pickle', autospec=True) def test___delitem__(self, mock_sqlite3):
@mock.patch.object(memoize, 'sqlite3', autospec=True)
def test___delitem__(self, mock_sqlite3, mock_pickle):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
pd.make_permanent('/', 'file') pd.make_permanent('/', 'file')
mock_pickle.dumps.return_value = 'pickled-key' mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_connection = mock_sqlite3.connect.return_value mock_cursor = mock_conn.cursor.return_value
mock_cursor = mock_connection.cursor.return_value mock_cursor.execute.reset_mock()
del pd[1] del pd[1]
mock_cursor.execute.assert_called_with( mock_cursor.execute.assert_called_once_with(
'delete from cache where key=?', ('pickled-key',)) 'delete from cache where key=?', (pickle.dumps(1),))
@mock.patch.object(memoize, 'pickle', autospec=True) def test___delitem__fails(self, mock_sqlite3):
@mock.patch.object(memoize, 'sqlite3', autospec=True)
def test___iter__(self, mock_sqlite3, mock_pickle):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
pd.make_permanent('/', 'file') pd.make_permanent('/', 'file')
mock_pickle.dumps.return_value = 'pickled-key' mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_connection = mock_sqlite3.connect.return_value mock_cursor = mock_conn.cursor.return_value
mock_cursor = mock_connection.cursor.return_value mock_cursor.execute.reset_mock()
mock_cursor.fetchall.return_value = [['pickled-key']] mock_cursor.rowcount = 0
for x in pd: def _del():
x += x del pd[1]
mock_cursor.execute.assert_called_with('select key from cache') self.assertRaises(KeyError, _del)
mock_pickle.loads.assert_called_once_with('pickled-key')
@mock.patch.object(memoize, 'pickle', autospec=True) mock_cursor.execute.assert_called_once_with(
@mock.patch.object(memoize, 'sqlite3', autospec=True) 'delete from cache where key=?', (pickle.dumps(1),))
def test___len__(self, mock_sqlite3, mock_pickle):
def test___iter__(self, mock_sqlite3):
pd = memoize.PersistentDict() pd = memoize.PersistentDict()
pd.make_permanent('/', 'file') pd.make_permanent('/', 'file')
mock_connection = mock_sqlite3.connect.return_value mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_cursor = mock_connection.cursor.return_value mock_cursor = mock_conn.cursor.return_value
mock_cursor.execute.reset_mock()
mock_cursor.fetchall.return_value = [[pickle.dumps('pickled-key')]]
expected = 1 self.assertEqual(['pickled-key'], list(iter(pd)))
mock_cursor.fetchone.return_value = [expected] mock_cursor.execute.assert_called_once_with('select key from cache')
self.assertEqual(expected, len(pd)) def test___len__(self, mock_sqlite3):
pd = memoize.PersistentDict()
pd.make_permanent('/', 'file')
mock_cursor.execute.assert_called_with('select count(*) from cache') mock_conn = mock_sqlite3.return_value.__enter__.return_value
mock_cursor = mock_conn.cursor.return_value
mock_cursor.execute.reset_mock()
mock_cursor.fetchone.return_value = [42]
self.assertEqual(42, len(pd))
mock_cursor.execute.assert_called_once_with(
'select count(*) from cache')