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