Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/framework/reap.py b/framework/reap.py
new file mode 100644
index 0000000..6bc5cf0
--- /dev/null
+++ b/framework/reap.py
@@ -0,0 +1,125 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A class to handle cron requests to expunge doomed and deletable projects."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+from framework import jsonfeed
+
+RUN_DURATION_LIMIT = 50 * 60  # 50 minutes
+
+
+class Reap(jsonfeed.InternalTask):
+  """Look for doomed and deletable projects and delete them."""
+
+  def HandleRequest(self, mr):
+    """Update/Delete doomed and deletable projects as needed.
+
+    Args:
+      mr: common information parsed from the HTTP request.
+
+    Returns:
+      Results dictionary in JSON format. The JSON will look like this:
+      {
+        'doomed_project_ids': <int>,
+        'expunged_project_ids': <int>
+      }
+      doomed_project_ids are the projects which have been marked as deletable.
+      expunged_project_ids are the projects that have either been completely
+      expunged or are in the midst of being expunged.
+    """
+    doomed_project_ids = self._MarkDoomedProjects(mr.cnxn)
+    expunged_project_ids = self._ExpungeDeletableProjects(mr.cnxn)
+    return {
+        'doomed_project_ids': doomed_project_ids,
+        'expunged_project_ids': expunged_project_ids,
+        }
+
+  def _MarkDoomedProjects(self, cnxn):
+    """No longer needed projects get doomed, and this marks them deletable."""
+    now = int(time.time())
+    doomed_project_rows = self.services.project.project_tbl.Select(
+        cnxn, cols=['project_id'],
+        # We only match projects with real timestamps and not delete_time = 0.
+        where=[('delete_time < %s', [now]), ('delete_time != %s', [0])],
+        state='archived', limit=1000)
+    doomed_project_ids = [row[0] for row in doomed_project_rows]
+    for project_id in doomed_project_ids:
+      # Note: We go straight to services layer because this is an internal
+      # request, not a request from a user.
+      self.services.project.MarkProjectDeletable(
+          cnxn, project_id, self.services.config)
+
+    return doomed_project_ids
+
+  def _ExpungeDeletableProjects(self, cnxn):
+    """Chip away at deletable projects until they are gone."""
+    request_deadline = time.time() + RUN_DURATION_LIMIT
+
+    deletable_project_rows = self.services.project.project_tbl.Select(
+        cnxn, cols=['project_id'], state='deletable', limit=100)
+    deletable_project_ids = [row[0] for row in deletable_project_rows]
+    # expunged_project_ids will contain projects that have either been
+    # completely expunged or are in the midst of being expunged.
+    expunged_project_ids = set()
+    for project_id in deletable_project_ids:
+      for _part in self._ExpungeParts(cnxn, project_id):
+        expunged_project_ids.add(project_id)
+        if time.time() > request_deadline:
+          return list(expunged_project_ids)
+
+    return list(expunged_project_ids)
+
+  def _ExpungeParts(self, cnxn, project_id):
+    """Delete all data from the specified project, one part at a time.
+
+    This method purges all data associated with the specified project. The
+    following is purged:
+    * All issues of the project.
+    * Project config.
+    * Saved queries.
+    * Filter rules.
+    * Former locations.
+    * Local ID counters.
+    * Quick edit history.
+    * Item stars.
+    * Project from the DB.
+
+    Returns a generator whose return values can be either issue
+    ids or the specified project id. The returned values are intended to be
+    iterated over and not read.
+    """
+    # Purge all issues of the project.
+    while True:
+      issue_id_rows = self.services.issue.issue_tbl.Select(
+          cnxn, cols=['id'], project_id=project_id, limit=1000)
+      issue_ids = [row[0] for row in issue_id_rows]
+      for issue_id in issue_ids:
+        self.services.issue_star.ExpungeStars(cnxn, issue_id)
+      self.services.issue.ExpungeIssues(cnxn, issue_ids)
+      yield issue_ids
+      break
+
+    # All project purge functions are called with cnxn and project_id.
+    project_purge_functions = (
+      self.services.config.ExpungeConfig,
+      self.services.template.ExpungeProjectTemplates,
+      self.services.features.ExpungeSavedQueriesExecuteInProject,
+      self.services.features.ExpungeFilterRules,
+      self.services.issue.ExpungeFormerLocations,
+      self.services.issue.ExpungeLocalIDCounters,
+      self.services.features.ExpungeQuickEditHistory,
+      self.services.project_star.ExpungeStars,
+      self.services.project.ExpungeProject,
+    )
+
+    for f in project_purge_functions:
+      f(cnxn, project_id)
+      yield project_id