38
37
from canonical.config import config
39
38
from canonical.database import postgresql
40
from canonical.database.constants import UTC_NOW
41
39
from canonical.database.sqlbase import (
44
from canonical.launchpad.database.emailaddress import EmailAddress
45
from canonical.launchpad.database.librarian import TimeLimitedToken
46
from canonical.launchpad.database.oauth import OAuthNonce
47
from canonical.launchpad.database.openidconsumer import OpenIDConsumerNonce
48
from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus
49
from canonical.launchpad.interfaces.lpstorm import IMasterStore
50
from canonical.launchpad.utilities.looptuner import TunableLoop
46
51
from canonical.launchpad.webapp.interfaces import (
51
from lp.answers.model.answercontact import AnswerContact
52
56
from lp.bugs.interfaces.bug import IBugSet
53
57
from lp.bugs.model.bug import Bug
54
58
from lp.bugs.model.bugattachment import BugAttachment
59
from lp.bugs.model.bugmessage import BugMessage
55
60
from lp.bugs.model.bugnotification import BugNotification
56
61
from lp.bugs.model.bugwatch import BugWatchActivity
57
62
from lp.bugs.scripts.checkwatches.scheduler import (
68
73
from lp.hardwaredb.model.hwdb import HWSubmission
69
74
from lp.registry.model.person import Person
70
from lp.services.database.lpstorm import IMasterStore
71
from lp.services.identity.interfaces.account import AccountStatus
72
from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
73
from lp.services.identity.model.emailaddress import EmailAddress
74
75
from lp.services.job.model.job import Job
75
from lp.services.librarian.model import TimeLimitedToken
76
76
from lp.services.log.logger import PrefixFilter
77
from lp.services.looptuner import TunableLoop
78
from lp.services.oauth.model import OAuthNonce
79
from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce
80
from lp.services.propertycache import cachedproperty
81
77
from lp.services.scripts.base import (
82
78
LaunchpadCronScript,
84
80
SilentLaunchpadScriptFailure,
86
82
from lp.services.session.model import SessionData
87
from lp.services.verification.model.logintoken import LoginToken
88
83
from lp.translations.interfaces.potemplate import IPOTemplateSet
89
from lp.translations.model.potmsgset import POTMsgSet
90
84
from lp.translations.model.potranslation import POTranslation
91
from lp.translations.model.translationmessage import TranslationMessage
92
from lp.translations.model.translationtemplateitem import (
93
TranslationTemplateItem,
97
ONE_DAY_IN_SECONDS = 24 * 60 * 60
87
ONE_DAY_IN_SECONDS = 24*60*60
100
90
class BulkPruner(TunableLoop):
189
179
self.store.execute("CLOSE %s" % self.cursor_name)
192
class LoginTokenPruner(BulkPruner):
193
"""Remove old LoginToken rows.
195
After 1 year, they are useless even for archaeology.
197
target_table_class = LoginToken
198
ids_to_prune_query = """
199
SELECT id FROM LoginToken WHERE
200
created < CURRENT_TIMESTAMP - CAST('1 year' AS interval)
204
182
class POTranslationPruner(BulkPruner):
205
183
"""Remove unlinked POTranslation entries.
313
class BugSummaryJournalRollup(TunableLoop):
314
"""Rollup BugSummaryJournal rows into BugSummary."""
315
maximum_chunk_size = 5000
317
def __init__(self, log, abort_time=None):
318
super(BugSummaryJournalRollup, self).__init__(log, abort_time)
319
self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
322
has_more = self.store.execute(
323
"SELECT EXISTS (SELECT TRUE FROM BugSummaryJournal LIMIT 1)"
327
def __call__(self, chunk_size):
328
chunk_size = int(chunk_size + 0.5)
330
"SELECT bugsummary_rollup_journal(%s)", (chunk_size,),
335
293
class OpenIDConsumerNoncePruner(TunableLoop):
336
294
"""An ITunableLoop to prune old OpenIDConsumerNonce records.
338
296
We remove all OpenIDConsumerNonce records older than 1 day.
340
maximum_chunk_size = 6 * 60 * 60 # 6 hours in seconds.
298
maximum_chunk_size = 6*60*60 # 6 hours in seconds.
342
300
def __init__(self, log, abort_time=None):
343
301
super(OpenIDConsumerNoncePruner, self).__init__(log, abort_time)
643
601
self.max_offset = self.store.execute(
644
602
"SELECT MAX(id) FROM UnlinkedPeople").get_one()[0]
645
603
if self.max_offset is None:
646
self.max_offset = -1 # Trigger isDone() now.
604
self.max_offset = -1 # Trigger isDone() now.
647
605
self.log.debug("No Person records to remove.")
649
607
self.log.info("%d Person records to remove." % self.max_offset)
712
class AnswerContactPruner(BulkPruner):
713
"""Remove old answer contacts which are no longer required.
715
Remove a person as an answer contact if:
716
their account has been deactivated for more than one day, or
717
suspended for more than one week.
719
target_table_class = AnswerContact
720
ids_to_prune_query = """
721
SELECT DISTINCT AnswerContact.id
722
FROM AnswerContact, Person, Account
724
AnswerContact.person = Person.id
725
AND Person.account = Account.id
727
(Account.date_status_set <
728
CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
729
- CAST('1 day' AS interval)
730
AND Account.status = %s)
732
(Account.date_status_set <
733
CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
734
- CAST('7 days' AS interval)
735
AND Account.status = %s)
737
""" % (AccountStatus.DEACTIVATED.value, AccountStatus.SUSPENDED.value)
740
670
class BranchJobPruner(BulkPruner):
741
671
"""Prune `BranchJob`s that are in a final state and more than a month old.
687
class MirrorBugMessageOwner(TunableLoop):
688
"""Mirror BugMessage.owner from Message.
690
Only needed until they are all set, after that triggers will maintain it.
693
# Test migration did 3M in 2 hours, so 5000 is ~ 10 seconds - and thats the
694
# max we want to hold a DB lock open for.
695
minimum_chunk_size = 1000
696
maximum_chunk_size = 5000
698
def __init__(self, log, abort_time=None):
699
super(MirrorBugMessageOwner, self).__init__(log, abort_time)
700
self.store = IMasterStore(BugMessage)
701
self.isDone = IMasterStore(BugMessage).find(
702
BugMessage, BugMessage.ownerID==None).is_empty
704
def __call__(self, chunk_size):
705
"""See `ITunableLoop`."""
707
updated = self.store.execute("""update bugmessage set
708
owner=message.owner from message where
709
bugmessage.message=message.id and bugmessage.id in
710
(select id from bugmessage where owner is NULL limit %s);"""
713
self.log.debug("Updated %s bugmessages." % updated)
757
717
class BugHeatUpdater(TunableLoop):
758
718
"""A `TunableLoop` for bug heat calculations."""
760
maximum_chunk_size = 5000
720
maximum_chunk_size = 1000
762
722
def __init__(self, log, abort_time=None, max_heat_age=None):
763
723
super(BugHeatUpdater, self).__init__(log, abort_time)
792
752
See `ITunableLoop`.
794
chunk_size = int(chunk_size + 0.5)
754
# We multiply chunk_size by 1000 for the sake of doing updates
756
chunk_size = int(chunk_size * 1000)
795
759
outdated_bugs = self._outdated_bugs[:chunk_size]
796
# We don't use outdated_bugs.set() here to work around
798
outdated_bug_ids = [bug.id for bug in outdated_bugs]
799
self.log.debug("Updating heat for %s bugs", len(outdated_bug_ids))
800
IMasterStore(Bug).find(
801
Bug, Bug.id.is_in(outdated_bug_ids)).set(
802
heat=SQL('calculate_bug_heat(Bug.id)'),
803
heat_last_updated=UTC_NOW)
760
self.log.debug("Updating heat for %s bugs" % outdated_bugs.count())
762
heat=SQL('calculate_bug_heat(Bug.id)'),
763
heat_last_updated=datetime.now(pytz.utc))
804
765
transaction.commit()
841
802
class OldTimeLimitedTokenDeleter(TunableLoop):
842
803
"""Delete expired url access tokens from the session DB."""
844
maximum_chunk_size = 24 * 60 * 60 # 24 hours in seconds.
805
maximum_chunk_size = 24*60*60 # 24 hours in seconds.
846
807
def __init__(self, log, abort_time=None):
847
808
super(OldTimeLimitedTokenDeleter, self).__init__(log, abort_time)
901
class UnusedPOTMsgSetPruner(TunableLoop):
902
"""Cleans up unused POTMsgSets."""
906
maximum_chunk_size = 50000
909
"""See `TunableLoop`."""
910
return self.offset >= len(self.msgset_ids_to_remove)
913
def msgset_ids_to_remove(self):
914
"""Return the IDs of the POTMsgSets to remove."""
916
-- Get all POTMsgSet IDs which are obsolete (sequence == 0)
917
-- and are not used (sequence != 0) in any other template.
919
FROM TranslationTemplateItem tti
923
FROM TranslationTemplateItem
924
WHERE potmsgset = tti.potmsgset AND sequence != 0)
926
-- Get all POTMsgSet IDs which are not referenced
927
-- by any of the templates (they must have TTI rows for that).
930
LEFT OUTER JOIN TranslationTemplateItem
931
ON TranslationTemplateItem.potmsgset = POTMsgSet.id
933
TranslationTemplateItem.potmsgset IS NULL);
935
store = IMasterStore(POTMsgSet)
936
results = store.execute(query)
937
ids_to_remove = [id for (id,) in results.get_all()]
940
def __call__(self, chunk_size):
941
"""See `TunableLoop`."""
942
# We cast chunk_size to an int to avoid issues with slicing
943
# (DBLoopTuner passes in a float).
944
chunk_size = int(chunk_size)
945
msgset_ids_to_remove = (
946
self.msgset_ids_to_remove[self.offset:][:chunk_size])
947
# Remove related TranslationTemplateItems.
948
store = IMasterStore(POTMsgSet)
949
related_ttis = store.find(
950
TranslationTemplateItem,
951
In(TranslationTemplateItem.potmsgsetID, msgset_ids_to_remove))
952
related_ttis.remove()
953
# Remove related TranslationMessages.
954
related_translation_messages = store.find(
956
In(TranslationMessage.potmsgsetID, msgset_ids_to_remove))
957
related_translation_messages.remove()
959
POTMsgSet, In(POTMsgSet.id, msgset_ids_to_remove)).remove()
960
self.offset = self.offset + chunk_size
964
862
class BaseDatabaseGarbageCollector(LaunchpadCronScript):
965
863
"""Abstract base class to run a collection of TunableLoops."""
966
script_name = None # Script name for locking and database user. Override.
967
tunable_loops = None # Collection of TunableLoops. Override.
968
continue_on_failure = False # If True, an exception in a tunable loop
969
# does not cause the script to abort.
864
script_name = None # Script name for locking and database user. Override.
865
tunable_loops = None # Collection of TunableLoops. Override.
866
continue_on_failure = False # If True, an exception in a tunable loop
867
# does not cause the script to abort.
971
869
# Default run time of the script in seconds. Override.
972
870
default_abort_script_time = None
1017
915
for count in range(0, self.options.threads):
1018
916
thread = threading.Thread(
1019
917
target=self.run_tasks_in_thread,
1020
name='Worker-%d' % (count + 1,),
918
name='Worker-%d' % (count+1,),
1021
919
args=(tunable_loops,))
1023
921
threads.add(thread)
1175
1073
transaction.abort()
1178
class FrequentDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
1179
"""Run every 5 minutes.
1181
This may become even more frequent in the future.
1183
Jobs with low overhead can go here to distribute work more evenly.
1185
script_name = 'garbo-frequently'
1076
class HourlyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
1077
script_name = 'garbo-hourly'
1186
1078
tunable_loops = [
1187
BugSummaryJournalRollup,
1079
MirrorBugMessageOwner,
1188
1080
OAuthNoncePruner,
1189
1081
OpenIDConsumerNoncePruner,
1190
1082
OpenIDConsumerAssociationPruner,
1191
AntiqueSessionPruner,
1193
experimental_tunable_loops = []
1195
# 5 minmutes minus 20 seconds for cleanup. This helps ensure the
1196
# script is fully terminated before the next scheduled hourly run
1198
default_abort_script_time = 60 * 5 - 20
1201
class HourlyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
1204
Jobs we want to run fairly often but have noticable overhead go here.
1206
script_name = 'garbo-hourly'
1208
1083
RevisionCachePruner,
1209
1084
BugWatchScheduler,
1085
AntiqueSessionPruner,
1210
1086
UnusedSessionPruner,
1211
1087
DuplicateSessionPruner,
1212
1088
BugHeatUpdater,
1221
1097
class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
1224
Jobs that don't need to be run frequently.
1226
If there is low overhead, consider putting these tasks in more
1227
frequently invoked lists to distribute the work more evenly.
1229
1098
script_name = 'garbo-daily'
1230
1099
tunable_loops = [
1231
AnswerContactPruner,
1232
1100
BranchJobPruner,
1233
1101
BugNotificationPruner,
1234
1102
BugWatchActivityPruner,
1235
1103
CodeImportEventPruner,
1236
1104
CodeImportResultPruner,
1237
1105
HWSubmissionEmailLinker,
1239
1106
ObsoleteBugAttachmentPruner,
1240
1107
OldTimeLimitedTokenDeleter,
1241
1108
RevisionAuthorEmailLinker,
1242
1109
SuggestiveTemplatesCacheUpdater,
1243
1110
POTranslationPruner,
1244
UnusedPOTMsgSetPruner,
1246
1112
experimental_tunable_loops = [