~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

__all__ = [
    'FeatureController',
    'flag_info',
    'NullFeatureController',
    'undocumented_flags',
    'value_domain_info',
    ]


from lp.services.features.rulesource import (
    NullFeatureRuleSource,
    StormFeatureRuleSource,
    )


__metaclass__ = type


value_domain_info = sorted([
    ('boolean',
     'Any non-empty value is true; an empty value is false.'),
    ('float',
     'The flag value is set to the given floating point number.'),
    ('int',
     "An integer."),
    ('space delimited',
     'Space-delimited strings.')
    ])

# Data for generating web-visible feature flag documentation.
#
# Entries for each flag are:
# flag name, value domain, prose documentation, default behaviour.
#
# Value domain as in value_domain_info above.
#
# NOTE: "default behaviour" does not specify a default value.  It
# merely documents the code's behaviour if no value is specified.
flag_info = sorted([
    ('bugs.bugtracker_components.enabled',
     'boolean',
     ('Enables the display of bugtracker components.'),
     ''),
    ('code.ajax_revision_diffs.enabled',
     'boolean',
     ("Offer expandable inline diffs for branch revisions."),
     ''),
    ('code.branchmergequeue',
     'boolean',
     'Enables merge queue pages and lists them on branch pages.',
     ''),
    ('code.incremental_diffs.enabled',
     'boolean',
     'Shows incremental diffs on merge proposals.',
     ''),
    ('hard_timeout',
     'float',
     'Sets the hard request timeout in milliseconds.',
     ''),
    ('mail.dkim_authentication.disabled',
     'boolean',
     'Disable DKIM authentication checks on incoming mail.',
     ''),
    ('malone.disable_targetnamesearch',
     'boolean',
     'If true, disables consultation of target names during bug text search.',
     ''),
    ('memcache',
     'boolean',
     'Enables use of memcached where it is supported.',
     'enabled'),
    ('profiling.enabled',
     'boolean',
     'Overrides config.profiling.profiling_allowed to permit profiling.',
     ''),
    ('soyuz.derived_series.max_synchronous_syncs',
     'int',
     "How many package syncs may be done directly in a web request.",
     '100'),
    ('soyuz.derived_series_ui.enabled',
     'boolean',
     'Enables derivative distributions pages.',
     ''),
    ('soyuz.derived_series_sync.enabled',
     'boolean',
     'Enables syncing of packages on derivative distributions pages.',
     ''),
    ('soyuz.derived_series_upgrade.enabled',
     'boolean',
     'Enables mass-upgrade of packages on derivative distributions pages.',
     ''),
    ('soyuz.derived_series_jobs.enabled',
     'boolean',
     "Compute package differences for derived distributions.",
     ''),
    ('translations.sharing_information.enabled',
     'boolean',
     'Enables display of sharing information on translation pages.',
     ''),
    ('visible_render_time',
     'boolean',
     'Shows the server-side page render time in the login widget.',
     ''),
    ('bugs.private_notification.enabled',
     'boolean',
     'Changes the appearance of notifications on private bugs.',
     ''),
    ('disclosure.picker_enhancements.enabled',
     'boolean',
     ('Enables the display of extra details in the person picker.'),
     ''),
    ('disclosure.person_affiliation_rank.enabled',
     'boolean',
     ('Enables ranking by pillar affiliation in the person picker.'),
     ''),
    ('bugs.autoconfirm.enabled_distribution_names',
     'space delimited',
     ('Enables auto-confirming bugtasks for distributions (and their '
      'series and packages).  Use the default domain.  Specify a single '
      'asterisk ("*") to enable for all distributions.'),
     'None are enabled'),
    ('bugs.autoconfirm.enabled_product_names',
     'space delimited',
     ('Enables auto-confirming bugtasks for products (and their '
      'series).  Use the default domain.  Specify a single '
      'asterisk ("*") to enable for all products.'),
     'None are enabled'),
    ])

# The set of all flag names that are documented.
documented_flags = set(info[0] for info in flag_info)
# The set of all the flags names that have been used during the process
# lifetime, but were not documented in flag_info.
undocumented_flags = set()


class Memoize():

    def __init__(self, calc):
        self._known = {}
        self._calc = calc

    def lookup(self, key):
        if key in self._known:
            return self._known[key]
        v = self._calc(key)
        self._known[key] = v
        return v


class ScopeDict():
    """Allow scopes to be looked up by getitem"""

    def __init__(self, features):
        self.features = features

    def __getitem__(self, scope_name):
        return self.features.isInScope(scope_name)


class FeatureController():
    """A FeatureController tells application code what features are active.

    It does this by meshing together two sources of data:

      - feature flags, typically set by an administrator into the database

      - feature scopes, which would typically be looked up based on attributes
      of the current web request, or the user for whom a job is being run, or
      something similar.

    FeatureController presents a high level interface for application code to
    query flag values, without it needing to know that they are stored in the
    database.

    At this level flag names and scope names are presented as strings for
    easier use in Python code, though the values remain unicode.  They
    should always be ascii like Python identifiers.

    One instance of FeatureController should be constructed for the lifetime
    of code that has consistent configuration values.  For instance there will
    be one per web app request.

    Intended performance: when this object is first asked about a flag, it
    will read the whole feature flag table from the database.  It is expected
    to be reasonably small.  The scopes may be expensive to compute (eg
    checking team membership) so they are checked at most once when
    they are first needed.

    The controller is then supposed to be held in a thread-local and reused
    for the duration of the request.

    @see: U{https://dev.launchpad.net/LEP/FeatureFlags}
    """

    def __init__(self, scope_check_callback, rule_source=None):
        """Construct a new view of the features for a set of scopes.

        :param scope_check_callback: Given a scope name, says whether
            it's active or not.

        :param rule_source: Instance of StormFeatureRuleSource or similar.
        """
        self._known_scopes = Memoize(scope_check_callback)
        self._known_flags = Memoize(self._checkFlag)
        # rules are read from the database the first time they're needed
        self._rules = None
        self.scopes = ScopeDict(self)
        if rule_source is None:
            rule_source = StormFeatureRuleSource()
        self.rule_source = rule_source

    def getFlag(self, flag):
        """Get the value of a specific flag.

        :param flag: A name to lookup. e.g. 'recipes.enabled'

        :return: The value of the flag determined by the highest priority rule
        that matched.
        """
        # If this is an undocumented flag, record it.
        if flag not in documented_flags:
            undocumented_flags.add(flag)
        return self._known_flags.lookup(flag)

    def _checkFlag(self, flag):
        self._needRules()
        if flag in self._rules:
            for scope, priority, value in self._rules[flag]:
                if self._known_scopes.lookup(scope):
                    return value

    def isInScope(self, scope):
        return self._known_scopes.lookup(scope)

    def __getitem__(self, flag_name):
        """FeatureController can be indexed.

        This is to support easy zope traversal through eg
        "request/features/a.b.c".  We don't support other collection
        protocols.

        Note that calling this the first time for any key may cause
        arbitrarily large amounts of work to be done to determine if the
        controller is in any scopes relevant to this flag.
        """
        return self.getFlag(flag_name)

    def getAllFlags(self):
        """Return a dict of all active flags.

        This may be expensive because of evaluating many scopes, so it
        shouldn't normally be used by code that only wants to know about one
        or a few flags.
        """
        self._needRules()
        return dict((f, self.getFlag(f)) for f in self._rules)

    def _needRules(self):
        if self._rules is None:
            self._rules = self.rule_source.getAllRulesAsDict()

    def usedFlags(self):
        """Return dict of flags used in this controller so far."""
        return dict(self._known_flags._known)

    def usedScopes(self):
        """Return {scope: active} for scopes that have been used so far."""
        return dict(self._known_scopes._known)


class NullFeatureController(FeatureController):
    """For use in testing: everything is turned off"""

    def __init__(self):
        FeatureController.__init__(self, lambda scope: None,
            NullFeatureRuleSource())