~launchpad-pqm/launchpad/devel

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