Merge ~andreserl/maas:2.2_lp1507712 into maas:2.2

Proposed by Andres Rodriguez
Status: Merged
Approved by: Andres Rodriguez
Approved revision: 9f1a33aa86a568880747574c37d1bd01dafe31a4
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~andreserl/maas:2.2_lp1507712
Merge into: maas:2.2
Diff against target: 105 lines (+38/-2)
2 files modified
src/maascli/config.py (+26/-1)
src/maascli/tests/test_config.py (+12/-1)
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+331000@code.launchpad.net

Commit message

Backport dd962958f73efb95216044f93de9aea3aace01e8 from master.

LP: #1507712 - cli: maas logout causes KeyError for other profiles

There is a race where when profiles are being deleted, if this happens
between the __iter__ and __getitem__ (of a different invocation of the
cli) which are two different SQL queries, there is an inconsistent view.

So instead use a cache with a consistent view of the data. This
guarantees correct -- if stale behavior in the cli.

Note that this is acceptable because when running two commands at the
same time ordering is unknown and the cache only lives for the lifetime
of a single command.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

selfie as it is a backport!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maascli/config.py b/src/maascli/config.py
2index e3145dd..129e944 100644
3--- a/src/maascli/config.py
4+++ b/src/maascli/config.py
5@@ -24,23 +24,41 @@ class ProfileConfig:
6
7 def __init__(self, database):
8 self.database = database
9+ self.cache = {}
10 with self.cursor() as cursor:
11 cursor.execute(
12 "CREATE TABLE IF NOT EXISTS profiles "
13 "(id INTEGER PRIMARY KEY,"
14 " name TEXT NOT NULL UNIQUE,"
15 " data BLOB)")
16+ self.__fill_cache()
17
18 def cursor(self):
19 return closing(self.database.cursor())
20
21+ def __fill_cache(self):
22+ """Touch each entry in the database to fill the cache. This cache is
23+ needed to enforce a consistent view. Without it, the list of items can
24+ be out of sync with the items actually in the database leading to
25+ KeyErrors when traversing the profiles.
26+ """
27+ for name in self:
28+ try:
29+ self[name]
30+ except KeyError:
31+ pass
32+
33 def __iter__(self):
34+ if self.cache:
35+ return (name for name in self.cache)
36 with self.cursor() as cursor:
37 results = cursor.execute(
38 "SELECT name FROM profiles").fetchall()
39 return (name for (name,) in results)
40
41 def __getitem__(self, name):
42+ if name in self.cache:
43+ return self.cache[name]
44 with self.cursor() as cursor:
45 data = cursor.execute(
46 "SELECT data FROM profiles"
47@@ -48,19 +66,26 @@ class ProfileConfig:
48 if data is None:
49 raise KeyError(name)
50 else:
51- return json.loads(data[0])
52+ info = json.loads(data[0])
53+ self.cache[name] = info
54+ return info
55
56 def __setitem__(self, name, data):
57 with self.cursor() as cursor:
58 cursor.execute(
59 "INSERT OR REPLACE INTO profiles (name, data) "
60 "VALUES (?, ?)", (name, json.dumps(data)))
61+ self.cache[name] = data
62
63 def __delitem__(self, name):
64 with self.cursor() as cursor:
65 cursor.execute(
66 "DELETE FROM profiles"
67 " WHERE name = ?", (name,))
68+ try:
69+ del self.cache[name]
70+ except KeyError:
71+ pass
72
73 @classmethod
74 def create_database(cls, dbpath):
75diff --git a/src/maascli/tests/test_config.py b/src/maascli/tests/test_config.py
76index 5500a91..d83211c 100644
77--- a/src/maascli/tests/test_config.py
78+++ b/src/maascli/tests/test_config.py
79@@ -9,7 +9,10 @@ import contextlib
80 from contextlib import contextmanager
81 import os.path
82 import sqlite3
83-from unittest.mock import call
84+from unittest.mock import (
85+ call,
86+ patch,
87+)
88
89 from maascli import (
90 api,
91@@ -59,6 +62,14 @@ class TestProfileConfig(MAASTestCase):
92 self.assertEqual({"alice"}, set(config))
93 self.assertEqual({"def": 456}, config["alice"])
94
95+ def test_cache(self):
96+ database = sqlite3.connect(":memory:")
97+ config = api.ProfileConfig(database)
98+ config["alice"] = {"abc": 123}
99+ with patch.object(config, 'cursor') as cursor:
100+ self.assertEqual({"abc": 123}, config["alice"])
101+ cursor.assert_not_called()
102+
103 def test_getting_profile(self):
104 database = sqlite3.connect(":memory:")
105 config = api.ProfileConfig(database)

Subscribers

People subscribed via source and target branches