~launchpad-pqm/launchpad/devel

10637.3.1 by Guilherme Salgado
Use the default python version instead of a hard-coded version
1
#!/usr/bin/python -S
8687.15.7 by Karl Fogel
Add the copyright header block to more files.
2
#
14027.3.2 by Jeroen Vermeulen
Merge devel, resolve conflicts.
3
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.7 by Karl Fogel
Add the copyright header block to more files.
4
# GNU Affero General Public License version 3 (see the file LICENSE).
5
4935.3.7 by Curtis Hovey
Added bad name suppression to cronscripts.
6
# pylint: disable-msg=C0103,W0403
2002 by Canonical.com Patch Queue Manager
Implements the karma framework (according to the KarmaImplementation spec) and hook it into Malone events. r=spiv,stub
7
2125 by Canonical.com Patch Queue Manager
[r=bjornt] Cronscript refactorings
8
import _pythonpath
14606.3.4 by William Grant
Replace canonical.database usage everywhere, and format-imports.
9
3691.9.40 by Guilherme Salgado
Fix a bug in the karmacache updater which was causing the KarmaCache table to bloat absurdly
10
from zope.component import getUtility
11
14606.3.4 by William Grant
Replace canonical.database usage everywhere, and format-imports.
12
from lp.app.errors import NotFoundError
13
from lp.registry.interfaces.karma import IKarmaCacheManager
14605.1.1 by Curtis Hovey
Moved canonical.config to lp.services.
14
from lp.services.config import config
14606.3.4 by William Grant
Replace canonical.database usage everywhere, and format-imports.
15
from lp.services.database.sqlbase import (
12415.1.2 by William Grant
Fix foaf-update-karma-cache.py to get a cursor without using the condemned ZTM.
16
    cursor,
17
    flush_database_updates,
18
    )
8356.1.1 by Leonard Richardson
Partial move.
19
from lp.services.scripts.base import LaunchpadCronScript
4264.2.1 by James Henstridge
add a LaunchpadCronScript subclass, and make cronscripts/*.py use it
20
21
22
class KarmaCacheUpdater(LaunchpadCronScript):
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
23
    def main(self):
24
        """Update the KarmaCache table for all valid Launchpad users.
25
26
        For each Launchpad user with a preferred email address, calculate his
27
        karmavalue for each category of actions we have and update his entry
28
        in the KarmaCache table. If a user doesn't have an entry for that
29
        category in KarmaCache a new one will be created.
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
30
31
        Entries in the KarmaTotalCache table will also be created/updated for
32
        each user which has entries in the KarmaCache table. Any user which
33
        doesn't have any entries in the KarmaCache table has its entries
34
        removed from the KarmaTotalCache table as well.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
35
        """
36
        self.logger.info("Updating Launchpad karma caches")
37
12415.1.2 by William Grant
Fix foaf-update-karma-cache.py to get a cursor without using the condemned ZTM.
38
        self.cur = cursor()
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
39
        self.karmacachemanager = getUtility(IKarmaCacheManager)
40
41
        # This method ordering needs to be preserved. In particular,
42
        # C_add_summed_totals method is called last because we don't want to
43
        # include the values added in our calculation in A_update_karmacache.
44
        self.A_update_karmacache()
45
        self.B_update_karmatotalcache()
46
        self.C_add_karmacache_sums()
47
48
        self.logger.info("Finished updating Launchpad karma caches")
49
50
    def A_update_karmacache(self):
51
        self.logger.info("Step A: Calculating individual KarmaCache entries")
52
53
        # Calculate everyones karma. Karma degrades each day, becoming
54
        # worthless after karma_expires_after. This query produces odd results
55
        # when datecreated is in the future, but there is really no point
56
        # adding the extra WHEN clause.
57
        karma_expires_after = '1 year'
58
        self.cur.execute("""
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
59
            SELECT person, category, product, distribution,
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
60
                ROUND(SUM(
61
                CASE WHEN karma.datecreated + %s::interval
62
                    <= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' THEN 0
63
                ELSE points * (1 - extract(
64
                    EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC' -
65
                    karma.datecreated
66
                    ) / extract(EPOCH FROM %s::interval))
67
                END
3691.88.7 by Stuart Bishop
Enforce max scaling factor for karma calculations
68
                ))
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
69
            FROM Karma
70
            JOIN KarmaAction ON action = KarmaAction.id
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
71
            GROUP BY person, category, product, distribution
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
72
            """, (karma_expires_after, karma_expires_after))
73
74
        # Suck into RAM to avoid tieing up resources on the DB.
75
        results = list(self.cur.fetchall())
76
        self.logger.debug("Got %d (person, category) scores", len(results))
77
78
        # Note that we don't need to commit each iteration because we are
79
        # running in autocommit mode.
80
        scaling = self.calculate_scaling(results)
81
        for entry in results:
82
            self.update_one_karma_cache_entry(entry, scaling)
7675.357.20 by Stuart Bishop
flush database updates before issuing raw SQL deletes to avoid losing data
83
        flush_database_updates()
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
84
85
        # Delete the entries we're going to replace.
86
        self.cur.execute("DELETE FROM KarmaCache WHERE category IS NULL")
87
        self.cur.execute("""
88
            DELETE FROM KarmaCache
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
89
            WHERE project IS NOT NULL AND product IS NULL""")
90
        self.cur.execute("""
91
            DELETE FROM KarmaCache
92
            WHERE category IS NOT NULL AND project IS NULL AND product IS NULL
93
                  AND distribution IS NULL AND sourcepackagename IS NULL""")
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
94
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
95
        # Don't allow our table to bloat with inactive users.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
96
        self.cur.execute("DELETE FROM KarmaCache WHERE karmavalue <= 0")
97
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
98
        # VACUUM KarmaCache since we have just touched every record in it.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
99
        self.cur.execute("""VACUUM KarmaCache""")
100
101
    def B_update_karmatotalcache(self):
102
        self.logger.info("Step B: Rebuilding KarmaTotalCache")
103
        # Trash old records
104
        self.cur.execute("""
105
            DELETE FROM KarmaTotalCache
106
            WHERE person NOT IN (SELECT person FROM KarmaCache)
107
            """)
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
108
        # Update existing records.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
109
        self.cur.execute("""
110
            UPDATE KarmaTotalCache SET karma_total=sum_karmavalue
111
            FROM (
112
                SELECT person AS sum_person, SUM(karmavalue) AS sum_karmavalue
113
                FROM KarmaCache
114
                GROUP BY person
115
                ) AS sums
116
            WHERE KarmaTotalCache.person = sum_person
117
            """)
118
119
        # VACUUM KarmaTotalCache since we have just touched every row in it.
120
        self.cur.execute("""VACUUM KarmaTotalCache""")
121
10303.1.1 by Gary Poster
use newest version of zc.buildout
122
        # Insert new records into the KarmaTotalCache table.
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
123
124
        # XXX: salgado 2007-02-06:
125
        # If deadlocks ever become a problem, first LOCK the
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
126
        # corresponding rows in the Person table so the bulk insert cannot
127
        # fail. We don't bother at the moment as this would involve granting
128
        # UPDATE rights on the Person table to the karmacacheupdater user.
129
        ## cur.execute("BEGIN")
130
        ## cur.execute("""
131
        ##     SELECT * FROM Person
132
        ##     WHERE id NOT IN (SELECT person FROM KarmaTotalCache)
133
        ##     FOR UPDATE
134
        ##     """)
135
136
        self.cur.execute("""
137
            INSERT INTO KarmaTotalCache (person, karma_total)
138
            SELECT person, SUM(karmavalue) FROM KarmaCache
139
            WHERE person NOT IN (SELECT person FROM KarmaTotalCache)
140
            GROUP BY person
141
            """)
142
143
        ## self.cur.execute("COMMIT")
144
145
    def C_add_karmacache_sums(self):
146
        self.logger.info("Step C: Calculating KarmaCache sums")
10303.1.1 by Gary Poster
use newest version of zc.buildout
147
        # We must issue some SUM queries to insert the karma totals for:
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
148
        # - All actions of a person on a given product.
149
        # - All actions of a person on a given distribution.
150
        # - All actions of a person on a given project.
151
        # - All actions with a specific category of a person on a given
152
        #   project.
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
153
        # - All actions with a specific category of a person.
154
155
        # - All actions with a specific category of a person.
156
        self.cur.execute("""
10303.1.1 by Gary Poster
use newest version of zc.buildout
157
            INSERT INTO KarmaCache
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
158
                (person, category, karmavalue, product, distribution,
159
                 sourcepackagename, project)
160
            SELECT person, category, SUM(karmavalue), NULL, NULL, NULL, NULL
161
            FROM KarmaCache
162
            WHERE category IS NOT NULL
163
            GROUP BY person, category
164
            """)
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
165
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
166
        # - All actions of a person on a given product.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
167
        self.cur.execute("""
10303.1.1 by Gary Poster
use newest version of zc.buildout
168
            INSERT INTO KarmaCache
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
169
                (person, category, karmavalue, product, distribution,
170
                 sourcepackagename, project)
171
            SELECT person, NULL, SUM(karmavalue), product, NULL, NULL, NULL
172
            FROM KarmaCache
173
            WHERE product IS NOT NULL
174
            GROUP BY person, product
175
            """)
176
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
177
        # - All actions of a person on a given distribution.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
178
        self.cur.execute("""
10303.1.1 by Gary Poster
use newest version of zc.buildout
179
            INSERT INTO KarmaCache
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
180
                (person, category, karmavalue, product, distribution,
181
                 sourcepackagename, project)
14050.3.1 by Jeroen Vermeulen
Lint.
182
            SELECT
183
                person, NULL, SUM(karmavalue), NULL, distribution, NULL, NULL
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
184
            FROM KarmaCache
185
            WHERE distribution IS NOT NULL
186
            GROUP BY person, distribution
187
            """)
188
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
189
        # - All actions of a person on a given project.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
190
        self.cur.execute("""
10303.1.1 by Gary Poster
use newest version of zc.buildout
191
            INSERT INTO KarmaCache
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
192
                (person, category, karmavalue, product, distribution,
193
                 sourcepackagename, project)
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
194
            SELECT person, NULL, SUM(karmavalue), NULL, NULL, NULL,
195
                   Product.project
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
196
            FROM KarmaCache
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
197
            JOIN Product ON product = Product.id
198
            WHERE Product.project IS NOT NULL AND product IS NOT NULL
199
                  AND category IS NOT NULL
200
            GROUP BY person, Product.project
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
201
            """)
202
14050.3.1 by Jeroen Vermeulen
Lint.
203
        # - All actions with a specific category of a person on a given
204
        # project.
3691.336.11 by Guilherme Salgado
Some changes suggested by Francis.
205
        # IMPORTANT: This has to be the latest step; otherwise the rows
206
        # inserted here will be included in the calculation of the overall
207
        # karma of a person on a given project.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
208
        self.cur.execute("""
10303.1.1 by Gary Poster
use newest version of zc.buildout
209
            INSERT INTO KarmaCache
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
210
                (person, category, karmavalue, product, distribution,
211
                 sourcepackagename, project)
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
212
            SELECT person, category, SUM(karmavalue), NULL, NULL, NULL,
213
                   Product.project
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
214
            FROM KarmaCache
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
215
            JOIN Product ON product = Product.id
216
            WHERE Product.project IS NOT NULL AND product IS NOT NULL
217
                  AND category IS NOT NULL
218
            GROUP BY person, category, Product.project
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
219
            """)
220
221
    def calculate_scaling(self, results):
222
        """Return a dict of scaling factors keyed on category ID"""
223
224
        # Get a list of categories, which we will need shortly.
225
        categories = {}
226
        self.cur.execute("SELECT id, name from KarmaCategory")
227
        for id, name in self.cur.fetchall():
228
            categories[id] = name
229
230
        # Calculate normalization factor for each category. We currently have
231
        # category bloat, where translators dominate the top karma rankings.
232
        # By calculating a scaling factor automatically, this slant will be
233
        # removed even as more events are added or scoring tweaked.
234
        points_per_category = {}
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
235
        for dummy, category, dummy, dummy, points in results:
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
236
            if category not in points_per_category:
237
                points_per_category[category] = 0
238
            points_per_category[category] += points
239
        largest_total = max(points_per_category.values())
240
241
        scaling = {}
242
        for category, points in points_per_category.items():
243
            if points == 0:
244
                scaling[category] = 1
245
            else:
246
                scaling[category] = float(largest_total) / float(points)
247
            max_scaling = config.karmacacheupdater.max_scaling
248
            if scaling[category] > max_scaling:
7675.357.20 by Stuart Bishop
flush database updates before issuing raw SQL deletes to avoid losing data
249
                self.logger.info(
250
                    'Scaling %s by a factor of %0.4f (capped to %0.4f)'
251
                    % (categories[category], scaling[category], max_scaling))
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
252
                scaling[category] = max_scaling
7675.357.20 by Stuart Bishop
flush database updates before issuing raw SQL deletes to avoid losing data
253
            else:
254
                self.logger.info(
255
                    'Scaling %s by a factor of %0.4f'
256
                    % (categories[category], scaling[category]))
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
257
        return scaling
10303.1.1 by Gary Poster
use newest version of zc.buildout
258
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
259
    def update_one_karma_cache_entry(self, entry, scaling):
260
        """Updates an individual (non-summed) KarmaCache entry.
261
262
        KarmaCache has individual entries, and then it has the summed entries
263
        that correspond to overall contributions across all categories. Look
264
        at C_add_summed_totals to see how the summed entries are generated.
265
        """
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
266
        (person_id, category_id, product_id, distribution_id, points) = entry
14050.3.1 by Jeroen Vermeulen
Lint.
267
        points *= scaling[category_id]  # Scaled. wow.
10303.1.1 by Gary Poster
use newest version of zc.buildout
268
        self.logger.debug("Setting person_id=%d, category_id=%d, points=%d"
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
269
                          % (person_id, category_id, points))
3149.1.4 by Stuart Bishop
Make foaf-update-karma-cache friendlier and less likely to trigger deadlocks
270
3691.9.40 by Guilherme Salgado
Fix a bug in the karmacache updater which was causing the KarmaCache table to bloat absurdly
271
        points = int(points)
3691.9.41 by Guilherme Salgado
some changes suggested by kiko
272
        context = {'product_id': product_id,
3691.336.7 by Guilherme Salgado
Change KarmaCache to store other levels of caches so that we don't need to issue SUM queries on it.
273
                   'distribution_id': distribution_id}
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
274
3691.336.7 by Guilherme Salgado
Change KarmaCache to store other levels of caches so that we don't need to issue SUM queries on it.
275
        try:
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
276
            self.karmacachemanager.updateKarmaValue(
3691.336.7 by Guilherme Salgado
Change KarmaCache to store other levels of caches so that we don't need to issue SUM queries on it.
277
                points, person_id, category_id, **context)
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
278
            self.logger.debug(
279
                "Updated karmacache for person=%s, points=%s, category=%s, "
280
                "context=%s" % (person_id, points, category_id, context))
3691.336.7 by Guilherme Salgado
Change KarmaCache to store other levels of caches so that we don't need to issue SUM queries on it.
281
        except NotFoundError:
282
            # Row didn't exist; do an insert.
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
283
            self.karmacachemanager.new(
3691.336.7 by Guilherme Salgado
Change KarmaCache to store other levels of caches so that we don't need to issue SUM queries on it.
284
                points, person_id, category_id, **context)
3691.336.15 by Guilherme Salgado
Bunch of code changes to cope with new db patch provided by stub
285
            self.logger.debug(
286
                "Created karmacache for person=%s, points=%s, category=%s, "
287
                "context=%s" % (person_id, points, category_id, context))
3691.336.7 by Guilherme Salgado
Change KarmaCache to store other levels of caches so that we don't need to issue SUM queries on it.
288
2976.4.2 by Stuart Bishop
Drop person.karma, replacing with new KarmaTotalCache table
289
2002 by Canonical.com Patch Queue Manager
Implements the karma framework (according to the KarmaImplementation spec) and hook it into Malone events. r=spiv,stub
290
if __name__ == '__main__':
12415.1.1 by William Grant
Stop using txn.set_isolation_level in scripts... run() has an argument for that purpose.
291
    script = KarmaCacheUpdater(
292
        'karma-update',
3691.336.8 by Guilherme Salgado
Convert karma script updater to LaunchpadScript, kill all the bizarre vars(), factor ugliness into nice little boxes, and keep one priceless comment.
293
        dbuser=config.karmacacheupdater.dbuser)
12415.1.1 by William Grant
Stop using txn.set_isolation_level in scripts... run() has an argument for that purpose.
294
    # We use the autocommit transaction isolation level to minimize
295
    # contention. It also allows us to not bother explicitly calling
296
    # COMMIT all the time. However, if we interrupt this script mid-run
297
    # it will need to be re-run as the data will be inconsistent (only
298
    # part of the caches will have been recalculated).
14022.3.2 by William Grant
LaunchpadScript no longer uses initZopeless.
299
    script.lock_and_run(isolation='autocommit')