Merge lp:~stub/launchpad/bug-675562-readonly-violation into lp:launchpad

Proposed by Stuart Bishop
Status: Merged
Approved by: Stuart Bishop
Approved revision: no longer in the source branch.
Merged at revision: 11932
Proposed branch: lp:~stub/launchpad/bug-675562-readonly-violation
Merge into: lp:launchpad
Diff against target: 229 lines (+82/-14)
4 files modified
lib/canonical/launchpad/doc/db-policy.txt (+26/-0)
lib/canonical/launchpad/tests/readonly.py (+18/-1)
lib/canonical/launchpad/webapp/adapter.py (+33/-13)
lib/canonical/launchpad/webapp/dbpolicy.py (+5/-0)
To merge this branch: bzr merge lp:~stub/launchpad/bug-675562-readonly-violation
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+41034@code.launchpad.net

Commit message

[r=jtv][ui=none][bug=675562] Fail requests for master stores if we are in read-only mode, no matter what database policy is currently installed.

Description of the change

Fail requests for master stores if we are in read-only mode, no matter what database policy is currently installed.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Nice branch. Thanks for the drive-by cleanups, too.

Only found some very small things to nag about:
 * May be better to test against the absence of IMasterStore (as well as, or in place of) the presence of ISlaveStore. In case the design changes.
 * In lib/canonical/launchpad/webapp/adapter.py, our coding standard says the "if/elif" should have an "else" as well.

Jeroen

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/db-policy.txt'
2--- lib/canonical/launchpad/doc/db-policy.txt 2010-02-22 12:16:02 +0000
3+++ lib/canonical/launchpad/doc/db-policy.txt 2010-11-17 11:08:00 +0000
4@@ -124,3 +124,29 @@
5 >>> IMasterObject(ro_janitor) is writable_janitor
6 True
7
8+Read-Only Mode
9+--------------
10+
11+During database outages, we run in read-only mode. In this mode, no
12+matter what database policy is currently installed, explicit requests
13+for a master store fail and the default store is always the slave.
14+
15+ >>> from canonical.launchpad.tests.readonly import read_only_mode
16+ >>> from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
17+ >>> from contextlib import nested
18+
19+ >>> with nested(read_only_mode(), MasterDatabasePolicy()):
20+ ... default_store = IStore(Person)
21+ ... IMasterStore.providedBy(default_store)
22+ False
23+
24+ >>> with nested(read_only_mode(), MasterDatabasePolicy()):
25+ ... slave_store = ISlaveStore(Person)
26+ ... IMasterStore.providedBy(slave_store)
27+ False
28+
29+ >>> with nested(read_only_mode(), MasterDatabasePolicy()):
30+ ... master_store = IMasterStore(Person)
31+ Traceback (most recent call last):
32+ ...
33+ ReadOnlyModeDisallowedStore: ('main', 'master')
34
35=== modified file 'lib/canonical/launchpad/tests/readonly.py'
36--- lib/canonical/launchpad/tests/readonly.py 2010-08-20 20:31:18 +0000
37+++ lib/canonical/launchpad/tests/readonly.py 2010-11-17 11:08:00 +0000
38@@ -7,15 +7,20 @@
39 __metaclass__ = type
40 __all__ = [
41 'touch_read_only_file',
42+ 'read_only_mode',
43 'remove_read_only_file',
44 ]
45
46+from contextlib import contextmanager
47 import os
48
49+from lazr.restful.utils import get_current_browser_request
50+
51 from canonical.launchpad.readonly import (
52 is_read_only,
53 read_only_file_exists,
54 read_only_file_path,
55+ READ_ONLY_MODE_ANNOTATIONS_KEY,
56 )
57
58
59@@ -37,7 +42,7 @@
60 def remove_read_only_file(assert_mode_switch=True):
61 """Remove the file named read-only.txt from the root of the tree.
62
63- May also assert that the mode switch actually happened (i.e. not
64+ May also assert that the mode switch actually happened (i.e. not
65 is_read_only()). This assertion has to be conditional because some tests
66 will use this during the processing of a request, when a mode change can't
67 happen (i.e. is_read_only() will still return True during that request's
68@@ -48,3 +53,15 @@
69 # Assert that the switch succeeded and make sure the mode change is
70 # logged.
71 assert not is_read_only(), "Switching to read-write failed."
72+
73+
74+@contextmanager
75+def read_only_mode(flag=True):
76+ request = get_current_browser_request()
77+ current = request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY]
78+ request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = flag
79+ try:
80+ assert is_read_only() == flag, 'Failed to set read-only mode'
81+ yield
82+ finally:
83+ request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = current
84
85=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
86--- lib/canonical/launchpad/webapp/adapter.py 2010-11-08 12:52:43 +0000
87+++ lib/canonical/launchpad/webapp/adapter.py 2010-11-17 11:08:00 +0000
88@@ -60,6 +60,7 @@
89 IStoreSelector,
90 MAIN_STORE,
91 MASTER_FLAVOR,
92+ ReadOnlyModeDisallowedStore,
93 ReadOnlyModeViolation,
94 SLAVE_FLAVOR,
95 )
96@@ -129,6 +130,7 @@
97
98
99 class CommitLogger:
100+
101 def __init__(self, txn):
102 self.txn = txn
103
104@@ -261,15 +263,16 @@
105 def set_permit_timeout_from_features(enabled):
106 """Control request timeouts being obtained from the 'hard_timeout' flag.
107
108- Until we've fully setup a page to render - routed the request to the right
109- object, setup a participation etc, feature flags cannot be completely used;
110- and because doing feature flag lookups will trigger DB access, attempting
111- to do a DB lookup will cause a nested DB lookup (the one being done, and
112- the flags lookup). To resolve all of this, timeouts start as a config file
113- only setting, and are then overridden once the request is ready to execute.
114+ Until we've fully setup a page to render - routed the request to the
115+ right object, setup a participation etc, feature flags cannot be
116+ completely used; and because doing feature flag lookups will trigger
117+ DB access, attempting to do a DB lookup will cause a nested DB
118+ lookup (the one being done, and the flags lookup). To resolve all of
119+ this, timeouts start as a config file only setting, and are then
120+ overridden once the request is ready to execute.
121
122- :param enabled: If True permit looking up request timeouts in feature
123- flags.
124+ :param enabled: If True permit looking up request timeouts in
125+ feature flags.
126 """
127 _local._permit_feature_timeout = enabled
128
129@@ -350,6 +353,7 @@
130
131 _main_thread_id = None
132
133+
134 def break_main_thread_db_access(*ignored):
135 """Ensure that Storm connections are not made in the main thread.
136
137@@ -390,6 +394,7 @@
138
139 class ReadOnlyModeConnection(PostgresConnection):
140 """storm.database.Connection for read-only mode Launchpad."""
141+
142 def execute(self, statement, params=None, noresult=False):
143 """See storm.database.Connection."""
144 try:
145@@ -550,13 +555,14 @@
146 # XXX: This code does not belong here - see bug=636804.
147 # Robert Collins 20100913.
148 OpStats.stats['timeouts'] += 1
149- # XXX bug=636801 Robert Colins 20100914 This is duplicated from the
150- # statement tracer, because the tracers are not arranged in a stack
151- # rather a queue: the done-code in the statement tracer never runs.
152+ # XXX bug=636801 Robert Colins 20100914 This is duplicated
153+ # from the statement tracer, because the tracers are not
154+ # arranged in a stack rather a queue: the done-code in the
155+ # statement tracer never runs.
156 action = getattr(connection, '_lp_statement_action', None)
157 if action is not None:
158- # action may be None if the tracer was installed after the
159- # statement was submitted.
160+ # action may be None if the tracer was installed after
161+ # the statement was submitted.
162 action.finish()
163 info = sys.exc_info()
164 transaction.doom()
165@@ -666,6 +672,20 @@
166 @staticmethod
167 def get(name, flavor):
168 """See `IStoreSelector`."""
169+ if is_read_only():
170+ # If we are in read-only mode, override the default to the
171+ # slave no matter what the existing policy says (it might
172+ # work), and raise an exception if the master was explicitly
173+ # requested. Most of the time, this doesn't matter as when
174+ # we are in read-only mode we have a suitable database
175+ # policy installed. However, code can override the policy so
176+ # we still need to catch disallowed requests here.
177+ if flavor == DEFAULT_FLAVOR:
178+ flavor = SLAVE_FLAVOR
179+ elif flavor == MASTER_FLAVOR:
180+ raise ReadOnlyModeDisallowedStore(name, flavor)
181+ else:
182+ pass
183 db_policy = StoreSelector.get_current()
184 if db_policy is None:
185 db_policy = MasterDatabasePolicy(None)
186
187=== modified file 'lib/canonical/launchpad/webapp/dbpolicy.py'
188--- lib/canonical/launchpad/webapp/dbpolicy.py 2010-11-08 12:52:43 +0000
189+++ lib/canonical/launchpad/webapp/dbpolicy.py 2010-11-17 11:08:00 +0000
190@@ -149,6 +149,7 @@
191
192 class DatabaseBlockedPolicy(BaseDatabasePolicy):
193 """`IDatabasePolicy` that blocks all access to the database."""
194+
195 def getStore(self, name, flavor):
196 """Raises `DisallowedStore`. No Database access is allowed."""
197 raise DisallowedStore(name, flavor)
198@@ -180,6 +181,7 @@
199 This policy is used for Feeds requests and other always-read only request.
200 """
201 default_flavor = SLAVE_FLAVOR
202+
203 def getStore(self, name, flavor):
204 """See `IDatabasePolicy`."""
205 if flavor == MASTER_FLAVOR:
206@@ -210,6 +212,7 @@
207
208 Selects the DEFAULT_FLAVOR based on the request.
209 """
210+
211 def __init__(self, request):
212 # The super constructor is a no-op.
213 # pylint: disable-msg=W0231
214@@ -364,6 +367,7 @@
215
216 Access to all master Stores is blocked.
217 """
218+
219 def getStore(self, name, flavor):
220 """See `IDatabasePolicy`.
221
222@@ -383,6 +387,7 @@
223
224 class WhichDbView(LaunchpadView):
225 "A page that reports which database is being used by default."
226+
227 def render(self):
228 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
229 dbname = store.execute("SELECT current_database()").get_one()[0]