~launchpad-pqm/launchpad/devel

12998.2.1 by mbp at canonical
Add a feature-flag scram switch for DKIM mail authentication.
1
# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
7675.754.25 by Martin Pool
Fix lint export warning
4
__all__ = [
5
    'FeatureController',
12156.15.1 by Benji York
add feature flag documentation
6
    'flag_info',
7675.754.40 by Martin Pool
Add a NullFeatureController and use that in LaunchpadTestRequest
7
    'NullFeatureController',
12156.15.1 by Benji York
add feature flag documentation
8
    'undocumented_flags',
12378.1.3 by William Grant
Document value domains.
9
    'value_domain_info',
7675.754.25 by Martin Pool
Fix lint export warning
10
    ]
11
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
12
7675.754.57 by Martin Pool
Split out storm access into FeatureRuleSource
13
from lp.services.features.rulesource import (
11539.1.6 by Robert Collins
Fix race condition with using features to control timeouts.
14
    NullFeatureRuleSource,
7675.754.57 by Martin Pool
Split out storm access into FeatureRuleSource
15
    StormFeatureRuleSource,
16
    )
17
18
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
19
__metaclass__ = type
20
12378.1.3 by William Grant
Document value domains.
21
22
value_domain_info = sorted([
23
    ('boolean',
12378.1.4 by William Grant
Make existing empty/non-empty flags boolean.
24
     'Any non-empty value is true; an empty value is false.'),
12378.1.3 by William Grant
Document value domains.
25
    ('float',
26
     'The flag value is set to the given floating point number.'),
13026.1.1 by Jeroen Vermeulen
Document a feature flag I added in too much of a hurry. Document flag defaults.
27
    ('int',
28
     "An integer."),
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
29
    ('space delimited',
30
     'Space-delimited strings.')
12378.1.3 by William Grant
Document value domains.
31
    ])
32
13026.1.1 by Jeroen Vermeulen
Document a feature flag I added in too much of a hurry. Document flag defaults.
33
# Data for generating web-visible feature flag documentation.
34
#
35
# Entries for each flag are:
36
# flag name, value domain, prose documentation, default behaviour.
37
#
38
# Value domain as in value_domain_info above.
39
#
40
# NOTE: "default behaviour" does not specify a default value.  It
41
# merely documents the code's behaviour if no value is specified.
12156.15.1 by Benji York
add feature flag documentation
42
flag_info = sorted([
13155.1.22 by Curtis Hovey
Register the feature flag.
43
    ('bugs.bugtracker_components.enabled',
44
     'boolean',
45
     ('Enables the display of bugtracker components.'),
46
     ''),
13302.8.20 by Andrew Bennetts
Fix feature flag definition in flags.py to agree with the .pt files.
47
    ('code.ajax_revision_diffs.enabled',
13302.8.12 by Jeroen Vermeulen
Wrap inline diff expanders in feature flag. Leave js-action class for the expander icon to the expander widiget.
48
     'boolean',
49
     ("Offer expandable inline diffs for branch revisions."),
50
     ''),
12378.1.1 by William Grant
Document code.branchmergequeue and code.incremental_diffs.enabled feature flags, and clean up the docs for the others.
51
    ('code.branchmergequeue',
12378.1.4 by William Grant
Make existing empty/non-empty flags boolean.
52
     'boolean',
12378.1.8 by William Grant
Sentence-case and expand flag docs.
53
     'Enables merge queue pages and lists them on branch pages.',
12378.1.1 by William Grant
Document code.branchmergequeue and code.incremental_diffs.enabled feature flags, and clean up the docs for the others.
54
     ''),
55
    ('code.incremental_diffs.enabled',
12378.1.4 by William Grant
Make existing empty/non-empty flags boolean.
56
     'boolean',
12378.1.8 by William Grant
Sentence-case and expand flag docs.
57
     'Shows incremental diffs on merge proposals.',
12378.1.1 by William Grant
Document code.branchmergequeue and code.incremental_diffs.enabled feature flags, and clean up the docs for the others.
58
     ''),
12156.15.1 by Benji York
add feature flag documentation
59
    ('hard_timeout',
60
     'float',
12378.1.11 by William Grant
hard_timeout is milliseconds, not seconds.
61
     'Sets the hard request timeout in milliseconds.',
12156.15.1 by Benji York
add feature flag documentation
62
     ''),
12998.2.1 by mbp at canonical
Add a feature-flag scram switch for DKIM mail authentication.
63
    ('mail.dkim_authentication.disabled',
64
     'boolean',
65
     'Disable DKIM authentication checks on incoming mail.',
66
     ''),
12371.1.1 by Robert Collins
Permit turning off targetnamecache like searches.
67
    ('malone.disable_targetnamesearch',
12378.1.4 by William Grant
Make existing empty/non-empty flags boolean.
68
     'boolean',
12378.1.8 by William Grant
Sentence-case and expand flag docs.
69
     'If true, disables consultation of target names during bug text search.',
12371.1.1 by Robert Collins
Permit turning off targetnamecache like searches.
70
     ''),
12156.15.1 by Benji York
add feature flag documentation
71
    ('memcache',
12378.1.7 by William Grant
memcache is now a boolean, defaulting to enabled.
72
     'boolean',
12378.1.8 by William Grant
Sentence-case and expand flag docs.
73
     'Enables use of memcached where it is supported.',
12156.15.1 by Benji York
add feature flag documentation
74
     'enabled'),
12443.2.1 by Robert Collins
Permit enabling profiling via a feature flag.
75
    ('profiling.enabled',
76
     'boolean',
12443.2.5 by Robert Collins
Unbreak +opstats and +haproxy.
77
     'Overrides config.profiling.profiling_allowed to permit profiling.',
12443.2.1 by Robert Collins
Permit enabling profiling via a feature flag.
78
     ''),
13026.1.1 by Jeroen Vermeulen
Document a feature flag I added in too much of a hurry. Document flag defaults.
79
    ('soyuz.derived_series.max_synchronous_syncs',
80
     'int',
81
     "How many package syncs may be done directly in a web request.",
82
     '100'),
7675.1155.1 by Gavin Panella
Rename soyuz.derived-series-(sync|ui).enabled flags to soyuz.derived_series_(sync|ui).enabled.
83
    ('soyuz.derived_series_ui.enabled',
12378.1.9 by William Grant
Document soyuz.derived-series-ui.enabled.
84
     'boolean',
85
     'Enables derivative distributions pages.',
86
     ''),
7675.1155.1 by Gavin Panella
Rename soyuz.derived-series-(sync|ui).enabled flags to soyuz.derived_series_(sync|ui).enabled.
87
    ('soyuz.derived_series_sync.enabled',
13001.1.1 by Julian Edwards
Add soyuz.derived-series-sync.enabled flag to protect the sync button
88
     'boolean',
89
     'Enables syncing of packages on derivative distributions pages.',
90
     ''),
13543.7.1 by Jeroen Vermeulen
Disable Upgrade Packages button.
91
    ('soyuz.derived_series_upgrade.enabled',
92
     'boolean',
93
     'Enables mass-upgrade of packages on derivative distributions pages.',
94
     ''),
12558.2.8 by Jeroen Vermeulen
Standardized boolean feature flag, thanks wgrant.
95
    ('soyuz.derived_series_jobs.enabled',
96
     'boolean',
97
     "Compute package differences for derived distributions.",
98
     ''),
12428.4.16 by Henning Eggers
Added feature flag for sharing information display.
99
    ('translations.sharing_information.enabled',
100
     'boolean',
101
     'Enables display of sharing information on translation pages.',
102
     ''),
12335.2.2 by Robert Collins
Show server render time when visible_render_time is set, with help from huwshimi.
103
    ('visible_render_time',
12378.1.4 by William Grant
Make existing empty/non-empty flags boolean.
104
     'boolean',
12378.1.8 by William Grant
Sentence-case and expand flag docs.
105
     'Shows the server-side page render time in the login widget.',
12335.2.2 by Robert Collins
Show server render time when visible_render_time is set, with help from huwshimi.
106
     ''),
12183.10.4 by Huw Wilkins
Renamed private notification flag to match guidlines and added documentation for it.
107
    ('bugs.private_notification.enabled',
108
     'boolean',
109
     'Changes the appearance of notifications on private bugs.',
110
     ''),
13115.3.2 by Ian Booth
Add feature flag
111
    ('disclosure.picker_enhancements.enabled',
112
     'boolean',
13115.3.3 by Ian Booth
Modify description
113
     ('Enables the display of extra details in the person picker.'),
13115.3.2 by Ian Booth
Add feature flag
114
     ''),
13171.2.6 by William Grant
Document the new flag.
115
    ('disclosure.person_affiliation_rank.enabled',
116
     'boolean',
117
     ('Enables ranking by pillar affiliation in the person picker.'),
118
     ''),
13445.1.10 by Gary Poster
lint and related cleanups
119
    ('bugs.autoconfirm.enabled_distribution_names',
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
120
     'space delimited',
121
     ('Enables auto-confirming bugtasks for distributions (and their '
13445.1.7 by Gary Poster
some small cleanups
122
      'series and packages).  Use the default domain.  Specify a single '
123
      'asterisk ("*") to enable for all distributions.'),
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
124
     'None are enabled'),
13445.1.10 by Gary Poster
lint and related cleanups
125
    ('bugs.autoconfirm.enabled_product_names',
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
126
     'space delimited',
13445.1.7 by Gary Poster
some small cleanups
127
     ('Enables auto-confirming bugtasks for products (and their '
128
      'series).  Use the default domain.  Specify a single '
129
      'asterisk ("*") to enable for all products.'),
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
130
     'None are enabled'),
12156.15.1 by Benji York
add feature flag documentation
131
    ])
132
133
# The set of all flag names that are documented.
134
documented_flags = set(info[0] for info in flag_info)
135
# The set of all the flags names that have been used during the process
136
# lifetime, but were not documented in flag_info.
137
undocumented_flags = set()
138
139
140
class Memoize():
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
141
142
    def __init__(self, calc):
143
        self._known = {}
144
        self._calc = calc
145
146
    def lookup(self, key):
147
        if key in self._known:
148
            return self._known[key]
149
        v = self._calc(key)
150
        self._known[key] = v
151
        return v
152
153
12156.15.1 by Benji York
add feature flag documentation
154
class ScopeDict():
7675.754.35 by Martin Pool
Support looking up feature_scopes in page templates
155
    """Allow scopes to be looked up by getitem"""
156
157
    def __init__(self, features):
158
        self.features = features
159
160
    def __getitem__(self, scope_name):
161
        return self.features.isInScope(scope_name)
162
163
12156.15.1 by Benji York
add feature flag documentation
164
class FeatureController():
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
165
    """A FeatureController tells application code what features are active.
166
7675.754.21 by Martin Pool
lint fixes
167
    It does this by meshing together two sources of data:
11382.7.1 by Martin Pool
Pydoctor/epytext formatting cleanups
168
169
      - feature flags, typically set by an administrator into the database
170
171
      - feature scopes, which would typically be looked up based on attributes
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
172
      of the current web request, or the user for whom a job is being run, or
173
      something similar.
174
7675.754.17 by Martin Pool
doc cleanups
175
    FeatureController presents a high level interface for application code to
7675.754.21 by Martin Pool
lint fixes
176
    query flag values, without it needing to know that they are stored in the
177
    database.
7675.754.10 by Martin Pool
For convenience of Python code, give feature name and scope as strings
178
179
    At this level flag names and scope names are presented as strings for
180
    easier use in Python code, though the values remain unicode.  They
181
    should always be ascii like Python identifiers.
7675.754.12 by Martin Pool
doc
182
7675.754.17 by Martin Pool
doc cleanups
183
    One instance of FeatureController should be constructed for the lifetime
184
    of code that has consistent configuration values.  For instance there will
185
    be one per web app request.
7675.754.14 by Martin Pool
Use plain Storm stores, not lp collections
186
7675.754.57 by Martin Pool
Split out storm access into FeatureRuleSource
187
    Intended performance: when this object is first asked about a flag, it
188
    will read the whole feature flag table from the database.  It is expected
189
    to be reasonably small.  The scopes may be expensive to compute (eg
190
    checking team membership) so they are checked at most once when
191
    they are first needed.
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
192
193
    The controller is then supposed to be held in a thread-local and reused
194
    for the duration of the request.
7675.754.19 by Martin Pool
FeatureController loads everything at construction time
195
11382.7.1 by Martin Pool
Pydoctor/epytext formatting cleanups
196
    @see: U{https://dev.launchpad.net/LEP/FeatureFlags}
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
197
    """
198
7675.754.57 by Martin Pool
Split out storm access into FeatureRuleSource
199
    def __init__(self, scope_check_callback, rule_source=None):
7675.754.9 by Martin Pool
FeatureController knows its scope; emphasizes getting single values
200
        """Construct a new view of the features for a set of scopes.
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
201
202
        :param scope_check_callback: Given a scope name, says whether
203
            it's active or not.
7675.754.62 by Martin Pool
resolve conflicts against devel
204
7675.754.57 by Martin Pool
Split out storm access into FeatureRuleSource
205
        :param rule_source: Instance of StormFeatureRuleSource or similar.
7675.754.9 by Martin Pool
FeatureController knows its scope; emphasizes getting single values
206
        """
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
207
        self._known_scopes = Memoize(scope_check_callback)
208
        self._known_flags = Memoize(self._checkFlag)
7675.754.33 by Martin Pool
Load feature rules from database only when needed
209
        # rules are read from the database the first time they're needed
210
        self._rules = None
7675.754.35 by Martin Pool
Support looking up feature_scopes in page templates
211
        self.scopes = ScopeDict(self)
7675.754.57 by Martin Pool
Split out storm access into FeatureRuleSource
212
        if rule_source is None:
213
            rule_source = StormFeatureRuleSource()
214
        self.rule_source = rule_source
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
215
216
    def getFlag(self, flag):
7675.754.39 by Robert Collins
Tweaks from the review.
217
        """Get the value of a specific flag.
12156.15.1 by Benji York
add feature flag documentation
218
7675.754.39 by Robert Collins
Tweaks from the review.
219
        :param flag: A name to lookup. e.g. 'recipes.enabled'
11382.7.1 by Martin Pool
Pydoctor/epytext formatting cleanups
220
7675.754.39 by Robert Collins
Tweaks from the review.
221
        :return: The value of the flag determined by the highest priority rule
11382.7.1 by Martin Pool
Pydoctor/epytext formatting cleanups
222
        that matched.
7675.754.39 by Robert Collins
Tweaks from the review.
223
        """
12156.15.1 by Benji York
add feature flag documentation
224
        # If this is an undocumented flag, record it.
225
        if flag not in documented_flags:
226
            undocumented_flags.add(flag)
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
227
        return self._known_flags.lookup(flag)
228
229
    def _checkFlag(self, flag):
7675.754.33 by Martin Pool
Load feature rules from database only when needed
230
        self._needRules()
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
231
        if flag in self._rules:
7675.754.58 by Martin Pool
Move code to make text form of rules into FeatureRuleSource
232
            for scope, priority, value in self._rules[flag]:
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
233
                if self._known_scopes.lookup(scope):
234
                    return value
235
236
    def isInScope(self, scope):
237
        return self._known_scopes.lookup(scope)
238
7675.754.27 by Martin Pool
Provide FeatureController[name] and request.features for use in TAL
239
    def __getitem__(self, flag_name):
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
240
        """FeatureController can be indexed.
241
242
        This is to support easy zope traversal through eg
243
        "request/features/a.b.c".  We don't support other collection
244
        protocols.
245
246
        Note that calling this the first time for any key may cause
247
        arbitrarily large amounts of work to be done to determine if the
248
        controller is in any scopes relevant to this flag.
249
        """
7675.754.27 by Martin Pool
Provide FeatureController[name] and request.features for use in TAL
250
        return self.getFlag(flag_name)
251
7675.754.6 by Martin Pool
Do more flag lookups through FeatureController facade
252
    def getAllFlags(self):
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
253
        """Return a dict of all active flags.
7675.754.21 by Martin Pool
lint fixes
254
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
255
        This may be expensive because of evaluating many scopes, so it
256
        shouldn't normally be used by code that only wants to know about one
257
        or a few flags.
7675.754.1 by Martin Pool
Very basic stub tests for feature flags
258
        """
7675.754.33 by Martin Pool
Load feature rules from database only when needed
259
        self._needRules()
7675.754.29 by Martin Pool
FeatureController lazily checks scopes as needed
260
        return dict((f, self.getFlag(f)) for f in self._rules)
7675.754.19 by Martin Pool
FeatureController loads everything at construction time
261
7675.754.33 by Martin Pool
Load feature rules from database only when needed
262
    def _needRules(self):
263
        if self._rules is None:
7675.754.60 by Martin Pool
Support parsing rules from text; generally pass them as tuples
264
            self._rules = self.rule_source.getAllRulesAsDict()
7675.754.33 by Martin Pool
Load feature rules from database only when needed
265
7675.754.31 by Martin Pool
FeatureController exposes the known features and scopes
266
    def usedFlags(self):
7675.754.39 by Robert Collins
Tweaks from the review.
267
        """Return dict of flags used in this controller so far."""
7675.754.31 by Martin Pool
FeatureController exposes the known features and scopes
268
        return dict(self._known_flags._known)
269
270
    def usedScopes(self):
271
        """Return {scope: active} for scopes that have been used so far."""
272
        return dict(self._known_scopes._known)
7675.754.40 by Martin Pool
Add a NullFeatureController and use that in LaunchpadTestRequest
273
274
275
class NullFeatureController(FeatureController):
276
    """For use in testing: everything is turned off"""
277
278
    def __init__(self):
11539.1.6 by Robert Collins
Fix race condition with using features to control timeouts.
279
        FeatureController.__init__(self, lambda scope: None,
280
            NullFeatureRuleSource())