Merge lp:~mbp/launchpad/flags-webapp into lp:launchpad
- flags-webapp
- Merge into devel
Proposed by
Martin Pool
Status: | Merged |
---|---|
Approved by: | Martin Pool |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11616 |
Proposed branch: | lp:~mbp/launchpad/flags-webapp |
Merge into: | lp:launchpad |
Diff against target: |
320 lines (+138/-136) 3 files modified
lib/lp/services/features/__init__.py (+130/-4) lib/lp/services/features/doc/features.txt (+0/-127) lib/lp/services/features/flags.py (+8/-5) |
To merge this branch: | bzr merge lp:~mbp/launchpad/flags-webapp |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Review via email: mp+32967@code.launchpad.net |
Commit message
move feature flag documentation into docstrings
Description of the change
This makes the feature flag developer documentation more accessible in the Pydoctor web view.
To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote : | # |
Revision history for this message
Abel Deuring (adeuring) : | # |
review:
Approve
(code)
Revision history for this message
Martin Pool (mbp) wrote : | # |
@Abel would you please land this for me?
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/services/features/__init__.py' | |||
2 | --- lib/lp/services/features/__init__.py 2010-09-12 05:19:38 +0000 | |||
3 | +++ lib/lp/services/features/__init__.py 2010-09-21 06:46:51 +0000 | |||
4 | @@ -1,10 +1,136 @@ | |||
5 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
6 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
7 | 3 | 3 | ||
12 | 4 | """lp.services.features provide dynamically configurable feature flags. | 4 | """Dynamic feature configuration. |
13 | 5 | 5 | ||
14 | 6 | These can be turned on and off by admins, and can affect particular | 6 | Introduction |
15 | 7 | defined scopes such as "beta users" or "production servers." | 7 | ============ |
16 | 8 | |||
17 | 9 | The point of feature flags is to let us turn some features of Launchpad on | ||
18 | 10 | and off without changing the code or restarting the application, and to | ||
19 | 11 | expose different features to different subsets of users. | ||
20 | 12 | |||
21 | 13 | See U{https://dev.launchpad.net/LEP/FeatureFlags} for more discussion and | ||
22 | 14 | rationale. | ||
23 | 15 | |||
24 | 16 | The typical use for feature flags is within web page requests but they can | ||
25 | 17 | also be used in asynchronous jobs or apis or other parts of Launchpad. | ||
26 | 18 | |||
27 | 19 | Internal model for feature flags | ||
28 | 20 | ================================ | ||
29 | 21 | |||
30 | 22 | A feature flag maps from a I{name} to a I{value}. The specific value used | ||
31 | 23 | for a particular request is determined by a set of zero or more I{scopes} | ||
32 | 24 | that apply to that request, by finding the I{rule} with the highest | ||
33 | 25 | I{priority}. | ||
34 | 26 | |||
35 | 27 | Flags are defined by a I{name} that typically looks like a Python | ||
36 | 28 | identifier, for example C{notification.global.text}. A definition is | ||
37 | 29 | given for a particular I{scope}, which also looks like a dotted identifier, | ||
38 | 30 | for example C{user.beta} or C{server.edge}. This is just a naming | ||
39 | 31 | convention, and they do not need to correspond to Python modules. | ||
40 | 32 | |||
41 | 33 | The value is stored in the database as just a Unicode string, and it might | ||
42 | 34 | be interpreted as a boolean, number, human-readable string or whatever. | ||
43 | 35 | |||
44 | 36 | The default for flags is to be None if they're not set in the database, so | ||
45 | 37 | that should be a sensible baseline default state. | ||
46 | 38 | |||
47 | 39 | Performance model | ||
48 | 40 | ================= | ||
49 | 41 | |||
50 | 42 | Flags are supposed to be cheap enough that you can introduce them without | ||
51 | 43 | causing a performance concern. | ||
52 | 44 | |||
53 | 45 | If the page does not check any flags, no extra work will be done. The | ||
54 | 46 | first time a page checks a flag, all the rules will be read from the | ||
55 | 47 | database and held in memory for the duration of the request. | ||
56 | 48 | |||
57 | 49 | Scopes may be expensive in some cases, such as checking group membership. | ||
58 | 50 | Whether a scope is active or not is looked up the first time it's needed | ||
59 | 51 | within a particular request. | ||
60 | 52 | |||
61 | 53 | The standard page footer identifies the flags and scopes that were | ||
62 | 54 | actually used by the page. | ||
63 | 55 | |||
64 | 56 | Naming conventions | ||
65 | 57 | ================== | ||
66 | 58 | |||
67 | 59 | We have naming conventions for feature flags and scopes, so that people can | ||
68 | 60 | understand the likely impact of a particular flag and so they can find all | ||
69 | 61 | the flags likely to affect a feature. | ||
70 | 62 | |||
71 | 63 | So for any flag we want to say: | ||
72 | 64 | |||
73 | 65 | - What application area does this affect? (malone, survey, questions, | ||
74 | 66 | code, etc) | ||
75 | 67 | |||
76 | 68 | - What specific feature does it change? | ||
77 | 69 | |||
78 | 70 | - What affect does it have on this feature? The most common is "enabled" | ||
79 | 71 | but for some other we want to specify a specific value as well such as | ||
80 | 72 | "date" or "size". | ||
81 | 73 | |||
82 | 74 | These are concatenated with dots so the overall feature name looks a bit | ||
83 | 75 | like a Python module name. | ||
84 | 76 | |||
85 | 77 | A similar approach is used for scopes. | ||
86 | 78 | |||
87 | 79 | Checking flags in page templates | ||
88 | 80 | ================================ | ||
89 | 81 | |||
90 | 82 | You can conditionally show some text like this:: | ||
91 | 83 | |||
92 | 84 | <tal:survey condition="features/user_survey.enabled"> | ||
93 | 85 | • | ||
94 | 86 | <a href="http://survey.example.com/">Take our survey!</a> | ||
95 | 87 | </tal:survey> | ||
96 | 88 | |||
97 | 89 | You can use the built-in TAL feature of prepending C{not:} to the | ||
98 | 90 | condition, and for flags that have a value you could use them in | ||
99 | 91 | C{tal:replace} or C{tal:attributes}. | ||
100 | 92 | |||
101 | 93 | If you just want to simply insert some text taken from a feature, say | ||
102 | 94 | something like:: | ||
103 | 95 | |||
104 | 96 | Message of the day: ${motd.text} | ||
105 | 97 | |||
106 | 98 | Templates can also check whether the request is in a particular scope, but | ||
107 | 99 | before using this consider whether the code will always be bound to that | ||
108 | 100 | scope or whether it would be more correct to define a new feature:: | ||
109 | 101 | |||
110 | 102 | <p tal:condition="feature_scopes/server.staging"> | ||
111 | 103 | Staging server: all data will be discarded daily!</p> | ||
112 | 104 | |||
113 | 105 | Checking flags in code | ||
114 | 106 | ====================== | ||
115 | 107 | |||
116 | 108 | The Zope traversal code establishes a `FeatureController` for the duration | ||
117 | 109 | of a request. The object can be obtained through either | ||
118 | 110 | `request.features` or `lp.services.features.per_thread.features`. This | ||
119 | 111 | provides various useful methods including `getFlag` to look up one feature | ||
120 | 112 | (memoized), and `isInScope` to check one scope (also memoized). | ||
121 | 113 | |||
122 | 114 | As a convenience, `lp.services.features.getFeatureFlag` looks up a single | ||
123 | 115 | flag in the thread default controller. | ||
124 | 116 | |||
125 | 117 | To simply check a boolean:: | ||
126 | 118 | |||
127 | 119 | if features.getFeatureFlag('soyuz.derived-series-ui.enabled'): | ||
128 | 120 | ... | ||
129 | 121 | |||
130 | 122 | and if you want to use the value :: | ||
131 | 123 | |||
132 | 124 | value = features.getFeatureFlag('soyuz.derived-series-ui.enabled') | ||
133 | 125 | if value: | ||
134 | 126 | print value | ||
135 | 127 | |||
136 | 128 | Debugging feature usage | ||
137 | 129 | ======================= | ||
138 | 130 | |||
139 | 131 | The flags active during a page request, and the scopes that were looked | ||
140 | 132 | up are visible in the comment at the bottom of every standard Launchpad | ||
141 | 133 | page. | ||
142 | 8 | """ | 134 | """ |
143 | 9 | 135 | ||
144 | 10 | import threading | 136 | import threading |
145 | 11 | 137 | ||
146 | === modified file 'lib/lp/services/features/doc/features.txt' | |||
147 | --- lib/lp/services/features/doc/features.txt 2010-07-27 09:30:45 +0000 | |||
148 | +++ lib/lp/services/features/doc/features.txt 2010-09-21 06:46:51 +0000 | |||
149 | @@ -1,127 +0,0 @@ | |||
150 | 1 | **************************** | ||
151 | 2 | Feature Flag Developer Guide | ||
152 | 3 | **************************** | ||
153 | 4 | |||
154 | 5 | Introduction | ||
155 | 6 | ************ | ||
156 | 7 | |||
157 | 8 | The point of feature flags is to let us turn some features of Launchpad on | ||
158 | 9 | and off without changing the code or restarting the application, and to | ||
159 | 10 | expose different features to different subsets of users. | ||
160 | 11 | |||
161 | 12 | See <https://dev.launchpad.net/LEP/FeatureFlags> for more discussion and | ||
162 | 13 | rationale. | ||
163 | 14 | |||
164 | 15 | The typical use for feature flags is within web page requests but they can | ||
165 | 16 | also be used in asynchronous jobs or apis or other parts of Launchpad. | ||
166 | 17 | |||
167 | 18 | Internal model for feature flags | ||
168 | 19 | ******************************** | ||
169 | 20 | |||
170 | 21 | A feature flag maps from a *name* to a *value*. The specific value used | ||
171 | 22 | for a particular request is determined by a set of zero or more *scopes* | ||
172 | 23 | that apply to that request, by finding the *rule* with the highest | ||
173 | 24 | *priority*. | ||
174 | 25 | |||
175 | 26 | Flags are defined by a *name* that typically looks like a Python | ||
176 | 27 | identifier, for example ``notification.global.text``. A definition is | ||
177 | 28 | given for a particular *scope*, which also looks like a dotted identifier, | ||
178 | 29 | for example ``user.beta`` or ``server.edge``. This is just a naming | ||
179 | 30 | convention, and they do not need to correspond to Python modules. | ||
180 | 31 | |||
181 | 32 | The value is stored in the database as just a Unicode string, and it might | ||
182 | 33 | be interpreted as a boolean, number, human-readable string or whatever. | ||
183 | 34 | |||
184 | 35 | The default for flags is to be None if they're not set in the database, so | ||
185 | 36 | that should be a sensible baseline default state. | ||
186 | 37 | |||
187 | 38 | Performance model | ||
188 | 39 | ***************** | ||
189 | 40 | |||
190 | 41 | Flags are supposed to be cheap enough that you can introduce them without | ||
191 | 42 | causing a performance concern. | ||
192 | 43 | |||
193 | 44 | If the page does not check any flags, no extra work will be done. The | ||
194 | 45 | first time a page checks a flag, all the rules will be read from the | ||
195 | 46 | database and held in memory for the duration of the request. | ||
196 | 47 | |||
197 | 48 | Scopes may be expensive in some cases, such as checking group membership. | ||
198 | 49 | Whether a scope is active or not is looked up the first time it's needed | ||
199 | 50 | within a particular request. | ||
200 | 51 | |||
201 | 52 | The standard page footer identifies the flags and scopes that were | ||
202 | 53 | actually used by the page. | ||
203 | 54 | |||
204 | 55 | Naming conventions | ||
205 | 56 | ****************** | ||
206 | 57 | |||
207 | 58 | We have naming conventions for feature flags and scopes, so that people can | ||
208 | 59 | understand the likely impact of a particular flag and so they can find all | ||
209 | 60 | the flags likely to affect a feature. | ||
210 | 61 | |||
211 | 62 | So for any flag we want to say: | ||
212 | 63 | |||
213 | 64 | * What application area does this affect? (malone, survey, questions, | ||
214 | 65 | code, etc) | ||
215 | 66 | |||
216 | 67 | * What specific feature does it change? | ||
217 | 68 | |||
218 | 69 | * What affect does it have on this feature? The most common is "enabled" | ||
219 | 70 | but for some other we want to specify a specific value as well such as | ||
220 | 71 | "date" or "size". | ||
221 | 72 | |||
222 | 73 | These are concatenated with dots so the overall feature name looks a bit | ||
223 | 74 | like a Python module name. | ||
224 | 75 | |||
225 | 76 | A similar approach is used for scopes. | ||
226 | 77 | |||
227 | 78 | Checking flags in page templates | ||
228 | 79 | ******************************** | ||
229 | 80 | |||
230 | 81 | You can conditionally show some text like this:: | ||
231 | 82 | |||
232 | 83 | <tal:survey condition="features/user_survey.enabled"> | ||
233 | 84 | • | ||
234 | 85 | <a href="http://survey.example.com/">Take our survey!</a> | ||
235 | 86 | </tal:survey> | ||
236 | 87 | |||
237 | 88 | You can use the built-in TAL feature of prepending ``not:`` to the | ||
238 | 89 | condition, and for flags that have a value you could use them in | ||
239 | 90 | ``tal:replace`` or ``tal:attributes``. | ||
240 | 91 | |||
241 | 92 | If you just want to simply insert some text taken from a feature, say | ||
242 | 93 | something like:: | ||
243 | 94 | |||
244 | 95 | Message of the day: ${motd.text} | ||
245 | 96 | |||
246 | 97 | Templates can also check whether the request is in a particular scope, but | ||
247 | 98 | before using this consider whether the code will always be bound to that | ||
248 | 99 | scope or whether it would be more correct to define a new feature:: | ||
249 | 100 | |||
250 | 101 | <p tal:condition="feature_scopes/server.staging"> | ||
251 | 102 | Staging server: all data will be discarded daily!</p> | ||
252 | 103 | |||
253 | 104 | Checking flags in code | ||
254 | 105 | ********************** | ||
255 | 106 | |||
256 | 107 | The Zope traversal code establishes a `FeatureController` for the duration | ||
257 | 108 | of a request. The object can be obtained through either | ||
258 | 109 | `request.features` or `lp.services.features.per_thread.features`. This | ||
259 | 110 | provides various useful methods including `getFlag` to look up one feature | ||
260 | 111 | (memoized), and `isInScope` to check one scope (also memoized). | ||
261 | 112 | |||
262 | 113 | As a convenience, `lp.services.features.getFeatureFlag` looks up a single | ||
263 | 114 | flag in the thread default controller. | ||
264 | 115 | |||
265 | 116 | Debugging feature usage | ||
266 | 117 | *********************** | ||
267 | 118 | |||
268 | 119 | The flags active during a page request, and the scopes that were looked | ||
269 | 120 | up are visible in the comment at the bottom of every standard Launchpad | ||
270 | 121 | page. | ||
271 | 122 | |||
272 | 123 | Defining scopes | ||
273 | 124 | *************** | ||
274 | 125 | |||
275 | 126 | |||
276 | 127 | .. vim: ft=rst | ||
277 | 128 | 0 | ||
278 | === modified file 'lib/lp/services/features/flags.py' | |||
279 | --- lib/lp/services/features/flags.py 2010-08-10 04:35:41 +0000 | |||
280 | +++ lib/lp/services/features/flags.py 2010-09-21 06:46:51 +0000 | |||
281 | @@ -46,8 +46,10 @@ | |||
282 | 46 | """A FeatureController tells application code what features are active. | 46 | """A FeatureController tells application code what features are active. |
283 | 47 | 47 | ||
284 | 48 | It does this by meshing together two sources of data: | 48 | It does this by meshing together two sources of data: |
287 | 49 | - feature flags, typically set by an administrator into the database | 49 | |
288 | 50 | - feature scopes, which would typically be looked up based on attributes | 50 | - feature flags, typically set by an administrator into the database |
289 | 51 | |||
290 | 52 | - feature scopes, which would typically be looked up based on attributes | ||
291 | 51 | of the current web request, or the user for whom a job is being run, or | 53 | of the current web request, or the user for whom a job is being run, or |
292 | 52 | something similar. | 54 | something similar. |
293 | 53 | 55 | ||
294 | @@ -72,14 +74,14 @@ | |||
295 | 72 | The controller is then supposed to be held in a thread-local and reused | 74 | The controller is then supposed to be held in a thread-local and reused |
296 | 73 | for the duration of the request. | 75 | for the duration of the request. |
297 | 74 | 76 | ||
299 | 75 | See <https://dev.launchpad.net/LEP/FeatureFlags> | 77 | @see: U{https://dev.launchpad.net/LEP/FeatureFlags} |
300 | 76 | """ | 78 | """ |
301 | 77 | 79 | ||
302 | 78 | def __init__(self, scope_check_callback): | 80 | def __init__(self, scope_check_callback): |
303 | 79 | """Construct a new view of the features for a set of scopes. | 81 | """Construct a new view of the features for a set of scopes. |
304 | 80 | 82 | ||
305 | 81 | :param scope_check_callback: Given a scope name, says whether | 83 | :param scope_check_callback: Given a scope name, says whether |
307 | 82 | it's active or not. | 84 | it's active or not. |
308 | 83 | """ | 85 | """ |
309 | 84 | self._known_scopes = Memoize(scope_check_callback) | 86 | self._known_scopes = Memoize(scope_check_callback) |
310 | 85 | self._known_flags = Memoize(self._checkFlag) | 87 | self._known_flags = Memoize(self._checkFlag) |
311 | @@ -91,8 +93,9 @@ | |||
312 | 91 | """Get the value of a specific flag. | 93 | """Get the value of a specific flag. |
313 | 92 | 94 | ||
314 | 93 | :param flag: A name to lookup. e.g. 'recipes.enabled' | 95 | :param flag: A name to lookup. e.g. 'recipes.enabled' |
315 | 96 | |||
316 | 94 | :return: The value of the flag determined by the highest priority rule | 97 | :return: The value of the flag determined by the highest priority rule |
318 | 95 | that matched. | 98 | that matched. |
319 | 96 | """ | 99 | """ |
320 | 97 | return self._known_flags.lookup(flag) | 100 | return self._known_flags.lookup(flag) |
321 | 98 | 101 |
(This is not super valuable to merge on its own, but I wanted to get the bulk change separated out from substantial changes.)