Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style |
| 3 | # license that can be found in the LICENSE file or at |
| 4 | # https://developers.google.com/open-source/licenses/bsd |
| 5 | |
| 6 | """A class to handle cron requests to expunge doomed and deletable projects.""" |
| 7 | from __future__ import print_function |
| 8 | from __future__ import division |
| 9 | from __future__ import absolute_import |
| 10 | |
| 11 | import logging |
| 12 | import time |
| 13 | |
| 14 | from framework import jsonfeed |
| 15 | |
| 16 | RUN_DURATION_LIMIT = 50 * 60 # 50 minutes |
| 17 | |
| 18 | |
Adrià Vilanova Martínez | 9f9ade5 | 2022-10-10 23:20:11 +0200 | [diff] [blame^] | 19 | class Reap(jsonfeed.FlaskInternalTask): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 20 | """Look for doomed and deletable projects and delete them.""" |
| 21 | |
| 22 | def HandleRequest(self, mr): |
| 23 | """Update/Delete doomed and deletable projects as needed. |
| 24 | |
| 25 | Args: |
| 26 | mr: common information parsed from the HTTP request. |
| 27 | |
| 28 | Returns: |
| 29 | Results dictionary in JSON format. The JSON will look like this: |
| 30 | { |
| 31 | 'doomed_project_ids': <int>, |
| 32 | 'expunged_project_ids': <int> |
| 33 | } |
| 34 | doomed_project_ids are the projects which have been marked as deletable. |
| 35 | expunged_project_ids are the projects that have either been completely |
| 36 | expunged or are in the midst of being expunged. |
| 37 | """ |
| 38 | doomed_project_ids = self._MarkDoomedProjects(mr.cnxn) |
| 39 | expunged_project_ids = self._ExpungeDeletableProjects(mr.cnxn) |
| 40 | return { |
| 41 | 'doomed_project_ids': doomed_project_ids, |
| 42 | 'expunged_project_ids': expunged_project_ids, |
| 43 | } |
| 44 | |
| 45 | def _MarkDoomedProjects(self, cnxn): |
| 46 | """No longer needed projects get doomed, and this marks them deletable.""" |
| 47 | now = int(time.time()) |
| 48 | doomed_project_rows = self.services.project.project_tbl.Select( |
| 49 | cnxn, cols=['project_id'], |
| 50 | # We only match projects with real timestamps and not delete_time = 0. |
| 51 | where=[('delete_time < %s', [now]), ('delete_time != %s', [0])], |
| 52 | state='archived', limit=1000) |
| 53 | doomed_project_ids = [row[0] for row in doomed_project_rows] |
| 54 | for project_id in doomed_project_ids: |
| 55 | # Note: We go straight to services layer because this is an internal |
| 56 | # request, not a request from a user. |
| 57 | self.services.project.MarkProjectDeletable( |
| 58 | cnxn, project_id, self.services.config) |
| 59 | |
| 60 | return doomed_project_ids |
| 61 | |
| 62 | def _ExpungeDeletableProjects(self, cnxn): |
| 63 | """Chip away at deletable projects until they are gone.""" |
| 64 | request_deadline = time.time() + RUN_DURATION_LIMIT |
| 65 | |
| 66 | deletable_project_rows = self.services.project.project_tbl.Select( |
| 67 | cnxn, cols=['project_id'], state='deletable', limit=100) |
| 68 | deletable_project_ids = [row[0] for row in deletable_project_rows] |
| 69 | # expunged_project_ids will contain projects that have either been |
| 70 | # completely expunged or are in the midst of being expunged. |
| 71 | expunged_project_ids = set() |
| 72 | for project_id in deletable_project_ids: |
| 73 | for _part in self._ExpungeParts(cnxn, project_id): |
| 74 | expunged_project_ids.add(project_id) |
| 75 | if time.time() > request_deadline: |
| 76 | return list(expunged_project_ids) |
| 77 | |
| 78 | return list(expunged_project_ids) |
| 79 | |
| 80 | def _ExpungeParts(self, cnxn, project_id): |
| 81 | """Delete all data from the specified project, one part at a time. |
| 82 | |
| 83 | This method purges all data associated with the specified project. The |
| 84 | following is purged: |
| 85 | * All issues of the project. |
| 86 | * Project config. |
| 87 | * Saved queries. |
| 88 | * Filter rules. |
| 89 | * Former locations. |
| 90 | * Local ID counters. |
| 91 | * Quick edit history. |
| 92 | * Item stars. |
| 93 | * Project from the DB. |
| 94 | |
| 95 | Returns a generator whose return values can be either issue |
| 96 | ids or the specified project id. The returned values are intended to be |
| 97 | iterated over and not read. |
| 98 | """ |
| 99 | # Purge all issues of the project. |
| 100 | while True: |
| 101 | issue_id_rows = self.services.issue.issue_tbl.Select( |
| 102 | cnxn, cols=['id'], project_id=project_id, limit=1000) |
| 103 | issue_ids = [row[0] for row in issue_id_rows] |
| 104 | for issue_id in issue_ids: |
| 105 | self.services.issue_star.ExpungeStars(cnxn, issue_id) |
| 106 | self.services.issue.ExpungeIssues(cnxn, issue_ids) |
| 107 | yield issue_ids |
| 108 | break |
| 109 | |
| 110 | # All project purge functions are called with cnxn and project_id. |
| 111 | project_purge_functions = ( |
| 112 | self.services.config.ExpungeConfig, |
| 113 | self.services.template.ExpungeProjectTemplates, |
| 114 | self.services.features.ExpungeSavedQueriesExecuteInProject, |
| 115 | self.services.features.ExpungeFilterRules, |
| 116 | self.services.issue.ExpungeFormerLocations, |
| 117 | self.services.issue.ExpungeLocalIDCounters, |
| 118 | self.services.features.ExpungeQuickEditHistory, |
| 119 | self.services.project_star.ExpungeStars, |
| 120 | self.services.project.ExpungeProject, |
| 121 | ) |
| 122 | |
| 123 | for f in project_purge_functions: |
| 124 | f(cnxn, project_id) |
| 125 | yield project_id |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 126 | |
Adrià Vilanova Martínez | 9f9ade5 | 2022-10-10 23:20:11 +0200 | [diff] [blame^] | 127 | def GetReap(self, **kwargs): |
| 128 | return self.handler(**kwargs) |