Files
galaxy/scripts/cleanup_datasets/pgcleanup.py
T

1151 lines
52 KiB
Python
Executable File

#!/usr/bin/env python
"""
pgcleanup.py - A script for cleaning up datasets in Galaxy efficiently, by
bypassing the Galaxy model and operating directly on the database.
PostgreSQL 9.1 or greater is required.
"""
from __future__ import print_function
import argparse
import datetime
import inspect
import logging
import os
import shutil
import string
import sys
from collections import namedtuple
from functools import partial
import psycopg2
from psycopg2.extras import NamedTupleCursor
from six import string_types
from sqlalchemy.engine.url import make_url
galaxy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
sys.path.insert(1, os.path.join(galaxy_root, 'lib'))
import galaxy.config
from galaxy.exceptions import ObjectNotFound
from galaxy.objectstore import build_object_store_from_config
from galaxy.util.bunch import Bunch
from galaxy.util.script import app_properties_from_args, populate_config_args
DEFAULT_LOG_DIR = os.path.join(galaxy_root, 'scripts', 'cleanup_datasets')
log = logging.getLogger(__name__)
MetadataFile = namedtuple('MetadataFile', ['id', 'object_store_id'])
Dataset = namedtuple('Dataset', ['id', 'object_store_id'])
class RemovesMetadataFiles(object):
pass
class RemovesDatasets(object):
pass
class Action(object):
update_time_sql = ", update_time = NOW()"
force_retry_sql = " AND NOT purged"
causals = ()
_action_sql = ""
_action_sql_args = {}
# these shouldn't be overridden by subclasses
@classmethod
def name_c(cls):
name = [cls.__name__[0].lower()]
for c in cls.__name__[1:]:
if c in string.ascii_uppercase:
c = '_' + c.lower()
name.append(c)
return ''.join(name)
@classmethod
def doc_iter(cls):
for line in cls.__doc__.splitlines():
yield line.replace(' ', '', 4)
def __init__(self, args):
self.__log_dir = args.log_dir
self.__dry_run = args.dry_run
self.__debug = args.debug
self.__update_time = args.update_time
self.__force_retry = args.force_retry
self.__days = args.days
self.__log = None
self._init()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.__log is not None:
self.__close_log()
def __open_log(self):
logf = os.path.join(self.__log_dir, self.name + '.log')
if self.__dry_run:
log.info('--dry-run specified, logging changes to stderr instead of log file: %s' % logf)
h = logging.StreamHandler()
else:
log.info('Opening log file: %s' % logf)
h = logging.FileHandler(logf)
h.setLevel(logging.DEBUG if self.__debug else logging.INFO)
f = logging.Formatter('%(message)s')
h.setFormatter(f)
l = logging.getLogger(self.name)
l.addHandler(h)
l.propagate = False
m = ('==== Log opened: %s ' % datetime.datetime.now().isoformat()).ljust(72, '=')
l.info(m)
self.__log = l
def __close_log(self):
m = ('==== Log closed: %s ' % datetime.datetime.now().isoformat()).ljust(72, '=')
self.log.info(m)
self.__log = None
@property
def log(self):
if self.__log is None:
self.__open_log()
return self.__log
@property
def name(self):
return self.__class__.name_c()
# subclasses can override these as needed
@property
def _update_time_sql(self):
# only update time if not doing force retry (otherwise a lot of things would have their update times reset that
# were actually purged a long time ago)
if self.__update_time and not self.__force_retry:
return self.update_time_sql
return ""
@property
def _force_retry_sql(self):
# a misnomer, the value of force_retry_sql is actually the SQL that should be used if *not* forcibly retrying
if not self.__force_retry:
return self.force_retry_sql
return ""
@property
def sql(self):
return self._action_sql.format(
update_time_sql=self._update_time_sql,
force_retry_sql=self._force_retry_sql,
)
@property
def sql_args(self):
args = self._action_sql_args.copy()
if 'days' not in args:
args['days'] = self.__days
return args
def _collect_row_results(self, row, results, primary_key):
primary_key = row._fields[0]
primary = getattr(row, primary_key)
if primary not in results:
results[primary] = [set() for x in range(len(self.causals))]
rowgetter = partial(getattr, row)
for i, causal in enumerate(self.causals):
vals = tuple(map(rowgetter, causal))
if any(vals[1:]):
results[primary][i].add(vals)
def _collect_row_actions(self, row):
if hasattr(row, 'disk_accounting_user_id'):
self.disk_accounting_user_ids.add(row.disk_accounting_user_id)
object_store_id = getattr(row, 'object_store_id', None)
if hasattr(row, 'deleted_metadata_file_id'):
self.metadata_files.add((row.deleted_metadata_file_id, object_store_id))
def _log_results(self, results, primary_key):
for primary in sorted(results.keys()):
self._log('%s: %s' % (primary_key, primary))
for causal, s in zip(self.causals, results[primary]):
for r in sorted(s):
secondaries = ', '.join(['%s: %s' % x for x in zip(causal[1:], r[1:])])
self._log('%s %s caused %s' % (causal[0], r[0], secondaries))
# FIXME: can you do this better than with causals?
def handle_results(self, cur):
"""Log the results of an action where results are joined across many tables and may contain duplicates, and
handle any actions that should be taken on those results.
:param row: cursor object to iterate
:type row: :class:`psycopg2.cursor`
XX :param causals: names of columns that caused actions to be performed on other columns
XX :type causals: tuple of tuples of strs
The first column is considered the "primary" column upon which actions are being taken. If disk usage should be
recalculated for users, those user IDs should be in a column named ``disk_accounting_user_id``.
"""
results = {}
primary_key = None
for row in cur:
if primary_key is None:
primary_key = row._fields[0]
self._collect_row_results(row, results, primary_key)
self._collect_row_actions(row)
self._log_results(results, primary_key)
# subclasses can implement these
def _init(self):
pass
def handle_row(self, row):
pass
def post_action(self):
pass
class PurgeDatasets(RemovesDatasets, Action):
"""
Mark purged all Datasets marked deleted that are older than the specified number of days.
"""
sql = """
WITH purged_dataset_ids
AS ( UPDATE dataset
SET purged = true{update_time_sql}
WHERE deleted{force_retry_sql}
AND update_time < (NOW() - interval '%(days)s days')
RETURNING id,
object_store_id),
dataset_events
AS (INSERT INTO cleanup_event_dataset_association
(create_time, cleanup_event_id, dataset_id)
SELECT NOW(), %(event_id)s, id
FROM purged_dataset_ids)
SELECT id AS purged_dataset_id,
object_store_id AS object_store_id
FROM purged_dataset_ids
ORDER BY id
"""
def _init(self):
self.datasets = set()
def handle_row(self, row):
self.datasets.add(Dataset(row.purged_dataset_id, row.object_store_id))
def post_action(self):
pass
class Cleanup(object):
def __init__(self):
self.args = None
self.config = None
self.__conn = None
self.__actions = None
self.disk_accounting_user_ids = set()
self.remove_metadata_files = set()
self.remove_datasets = set()
self.object_store = None
self.current_action = None
self.action_logger = None
self.__parse_args()
self.__setup_logging()
self.__validate_actions()
self.__load_config()
self.__load_object_store()
@property
def actions(self):
if self.__actions is None:
self.__actions = {}
for name, value in inspect.getmembers(sys.modules[__name__]):
if not name.startswith('_') and inspect.isclass(value) and value != Action and issubclass(value, Action):
self.__actions[value.name_c()] = value
return self.__actions
@property
def conn(self):
if self.__conn is None:
url = make_url(galaxy.config.get_database_url(self.config))
log.info('Connecting to database with URL: %s' % url)
args = url.translate_connect_args(username='user')
args.update(url.query)
assert url.get_dialect().name == 'postgresql', 'This script can only be used with PostgreSQL.'
self.__conn = psycopg2.connect(cursor_factory=NamedTupleCursor, **args)
return self.__conn
def __parse_args(self):
parser = argparse.ArgumentParser()
populate_config_args(parser)
parser.add_argument(
'-d', '--debug',
action='store_true',
default=False,
help='Enable debug logging (SQL queries)')
parser.add_argument(
'--dry-run',
action='store_true',
default=False,
help="Dry run (rollback all transactions)")
parser.add_argument(
'--force-retry',
action='store_true',
default=False,
help="Retry file removals (on applicable actions)")
parser.add_argument(
'-o', '--older-than',
dest='days',
type=int,
default=14,
help='Only perform action(s) on objects that have not been updated since the specified number of days')
parser.add_argument(
'-U', '--no-update-time',
action='store_false',
dest='update_time',
default=True,
help="Don't set update_time on updated objects")
parser.add_argument(
'-s', '--sequence',
dest='sequence',
default='',
help='DEPRECATED: Comma-separated sequence of actions')
parser.add_argument(
'-w', '--work-mem',
dest='work_mem',
default=None,
help='Set PostgreSQL work_mem for this connection')
parser.add_argument(
'-l', '--log-dir',
default=DEFAULT_LOG_DIR,
help='Log file directory')
parser.add_argument(
'actions',
nargs='*',
metavar='ACTION',
default=[],
help='Action(s) to perform, chosen from: %s' % ', '.join(sorted(self.actions.keys())))
self.args = parser.parse_args()
# add deprecated sequence arg to actions
self.args.sequence = [x.strip() for x in self.args.sequence.split(',')]
if self.args.sequence != ['']:
self.args.actions.extend(self.args.sequence)
def __setup_logging(self):
format = "%(asctime)s %(levelname)-5s %(funcName)s(): %(message)s"
if self.args.debug:
logging.basicConfig(level=logging.DEBUG, format=format)
else:
logging.basicConfig(level=logging.INFO, format=format)
def __validate_actions(self):
ok = True
for name in self.args.actions:
if name not in self.actions.keys():
log.error('Unknown action in sequence: %s' % name)
ok = False
if not ok:
log.critical('Exiting due to previous error(s)')
sys.exit(1)
def __load_config(self):
app_properties = app_properties_from_args(self.args)
self.config = galaxy.config.Configuration(**app_properties)
def __load_object_store(self):
self.object_store = build_object_store_from_config(self.config)
def run(self):
for name in self.args.actions:
cls = self.actions[name]
log.info("Running action '%s':", name)
map(log.info, cls.doc_iter())
self.current_action = name
with cls(self.args) as action:
self._run_action(action)
#self.__getattribute__(name)()
log.info('Finished %s' % name)
if self.disk_accounting_user_ids:
self.update_user_disk_usage()
def _run_action(self, action):
self._update(action.sql, action.sql_args, force_retry_sql=action.force_retry_sql)
def _create_event(self, message=None):
"""
Create a new event in the cleanup_event table.
"""
if message is None:
message = self.current_action
sql = """
INSERT INTO cleanup_event
(create_time, message)
VALUES (NOW(), %(message)s)
RETURNING id;
"""
args = {'message': message}
log.debug("SQL is: %s", sql % args_for_print(args))
cur = self.conn.cursor()
if self.args.dry_run:
sql = "SELECT MAX(id) FROM cleanup_event;"
cur.execute(sql)
max_id = cur.fetchone()[0]
if max_id is None:
# there has to be at least one event in the table, if there are none just create a fake one.
sql = "INSERT INTO cleanup_event (create_time, message) VALUES (NOW(), 'dry_run_event') RETURNING id;"
cur.execute(sql)
max_id = cur.fetchone()[0]
self.conn.commit()
log.info("An event must exist for the subsequent query to succeed, so a dummy event has been created")
else:
log.info("Not executing event creation (increments sequence even when rolling back), using an old event ID (%i) for dry run" % max_id)
return max_id
log.info("Executing SQL")
cur.execute(sql, args)
log.info('Database status: %s' % cur.statusmessage)
return cur.fetchone()[0]
def _update(self, sql, args, add_event=True, event_message=None):
if add_event and isinstance(args, dict) and 'event_id' not in args:
args['event_id'] = self._create_event(message=event_message)
if args is not None:
log.debug('SQL is: %s', sql % args_for_print(args))
else:
log.debug('SQL is: %s', sql)
cur = self.conn.cursor()
if self.args.work_mem is not None:
log.info('Setting work_mem to %s' % self.args.work_mem)
cur.execute('SET work_mem TO %s', (self.args.work_mem,))
log.info('Executing SQL')
cur.execute(sql, args)
log.info('Database status: %s' % cur.statusmessage)
log.info('Flushing transaction')
self._flush()
return cur
def _flush(self):
if self.args.dry_run:
self.conn.rollback()
log.info("--dry-run specified, all changes rolled back")
else:
self.conn.commit()
log.info("All changes committed")
def _remove_metadata_file(self, id, object_store_id):
metadata_file = MetadataFile(id=id, object_store_id=object_store_id)
try:
filename = self.object_store.get_filename(metadata_file, extra_dir='_metadata_files', extra_dir_at_root=True, alt_name="metadata_%d.dat" % id)
self._log('Removing from disk: %s' % filename)
except (ObjectNotFound, AttributeError) as e:
log.error('Unable to get MetadataFile %s filename: %s' % (id, e))
return
if not self.args.dry_run:
try:
os.unlink(filename)
except Exception as e:
self._log('Removal of %s failed with error: %s' % (filename, e))
def update_user_disk_usage(self):
"""
Any operation that purges a HistoryDatasetAssociation may require
updating a user's disk usage. Rather than attempt to resolve dataset
copies at purge-time, simply maintain a list of users that have had
HDAs purged, and update their usages once all updates are complete.
This could probably be done more efficiently.
"""
log.info('Recalculating disk usage for users whose data were purged')
for user_id in sorted(self.disk_accounting_user_ids):
# TODO: h.purged = false should be unnecessary once all hdas in purged histories are purged.
sql = """
UPDATE galaxy_user
SET disk_usage = (SELECT COALESCE(SUM(total_size), 0)
FROM ( SELECT d.total_size
FROM history_dataset_association hda
JOIN history h ON h.id = hda.history_id
JOIN dataset d ON hda.dataset_id = d.id
WHERE h.user_id = %(user_id)s
AND h.purged = false
AND hda.purged = false
AND d.purged = false
AND d.id NOT IN (SELECT dataset_id
FROM library_dataset_dataset_association)
GROUP BY d.id) sizes)
WHERE id = %(user_id)s
RETURNING disk_usage;
"""
args = {'user_id': user_id}
cur = self._update(sql, args, add_event=False)
for tup in cur:
# disk_usage might be None (e.g. user has purged all data)
log.info('Updated disk usage for user id %i to %s bytes' % (user_id, tup[0]))
def _shutdown(self):
self.object_store.shutdown()
self.conn.close()
if self.action_logger is not None:
m = ('==== Log closed at shutdown: %s ' % datetime.datetime.now().isoformat()).ljust(72, '=')
self.action_logger.info(m)
#
# actions
#
# the _update method automatically:
# - calls the sql argument's (string) format() method to replace '{update_time_sql}' and '{force_retry_sql}'
# - creates an event and adds its id to args with key `event_id` (if args is a dict)
# - adds `days` to args (if args is a dict)
def update_hda_purged_flag(self):
"""
The old cleanup script does not mark HistoryDatasetAssociations as purged when deleted Histories are purged. This method can be used to rectify that situation.
"""
log.info('Marking purged all HistoryDatasetAssociations associated with purged Datasets')
# update_time is intentionally left unmodified.
sql = """
WITH purged_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true
FROM dataset
WHERE history_dataset_association.dataset_id = dataset.id
AND dataset.purged
AND NOT history_dataset_association.purged
RETURNING history_dataset_association.id),
hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, id
FROM purged_hda_ids)
SELECT id AS purged_hda_id
FROM purged_hda_ids
ORDER BY id;
"""
cur = self._update(sql, {})
self._handle_results(cur)
def delete_inactive_users(self):
"""
Mark deleted all users that are older than the specified number of days.
Mark deleted (state = 'deleted') all Jobs whose user_ids are deleted in this step.
"""
log.info('Marking deleted all inactive GalaxyUsers older than %i days' % self.args.days)
sql = """
WITH deleted_user_ids
AS ( UPDATE galaxy_user
SET deleted = true{update_time_sql}
WHERE NOT active{force_retry_sql}
AND update_time < (NOW() - interval '%(days)s days')
RETURNING id),
deleted_job_ids
AS ( UPDATE job
SET state = 'deleted'{update_time_sql}
FROM deleted_user_ids
WHERE deleted_user_ids.id = job.user_id
AND job.state = 'new'
RETURNING job.user_id AS user_id,
job.id AS id),
user_events
AS (INSERT INTO cleanup_event_user_association
(create_time, cleanup_event_id, user_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_user_ids)
SELECT deleted_user_ids.id AS deleted_user_id,
deleted_job_ids.id AS deleted_job_id
FROM deleted_user_ids
LEFT OUTER JOIN deleted_job_ids
ON deleted_job_ids.user_id = deleted_user_ids.id
ORDER BY deleted_user_ids.id
"""
force_retry_sql = " AND NOT deleted"
cur = self._update(sql, {}, force_retry_sql=force_retry_sql)
causals = (
('purged_user_id', 'purged_job_id'),
)
self._handle_results(cur, causals)
def purge_deleted_users(self):
"""
Mark purged all users that are older than the specified number of days.
Mark purged all Histories whose user_ids are purged in this step.
Mark purged all HistoryDatasetAssociations whose history_ids are purged in this step.
Delete all UserGroupAssociations whose user_ids are purged in this step.
Delete all UserRoleAssociations whose user_ids are purged in this step EXCEPT FOR THE PRIVATE ROLE.
Delete all UserAddresses whose user_ids are purged in this step.
"""
log.info('Marking purged all deleted GalaxyUsers older than %i days' % self.args.days)
sql = """
WITH purged_user_ids
AS ( UPDATE galaxy_user
SET purged = true{update_time_sql}
WHERE deleted{force_retry_sql}
AND update_time < (NOW() - interval '%(days)s days')
RETURNING id),
purged_history_ids
AS ( UPDATE history
SET purged = true{update_time_sql}
FROM purged_user_ids
WHERE purged_user_ids.id = history.user_id
AND NOT history.purged
RETURNING history.user_id AS user_id,
history.id AS id),
purged_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM purged_history_ids
WHERE purged_history_ids.id = history_dataset_association.history_id
AND NOT history_dataset_association.purged
RETURNING history_dataset_association.history_id AS history_id,
history_dataset_association.id AS id),
deleted_metadata_file_ids
AS ( UPDATE metadata_file
SET deleted = true{update_time_sql}
FROM purged_hda_ids
WHERE purged_hda_ids.id = metadata_file.hda_id
RETURNING metadata_file.hda_id AS hda_id,
metadata_file.id AS id,
metadata_file.object_store_id AS object_store_id),
deleted_icda_ids
AS ( UPDATE implicitly_converted_dataset_association
SET deleted = true{update_time_sql}
FROM purged_hda_ids
WHERE purged_hda_ids.id = implicitly_converted_dataset_association.hda_parent_id
RETURNING implicitly_converted_dataset_association.hda_id AS hda_id,
implicitly_converted_dataset_association.hda_parent_id AS hda_parent_id,
implicitly_converted_dataset_association.id AS id),
deleted_icda_purged_child_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM deleted_icda_ids
WHERE deleted_icda_ids.hda_id = history_dataset_association.id),
deleted_uga_ids
AS (DELETE FROM user_group_association
USING purged_user_ids
WHERE user_group_association.user_id = purged_user_ids.id
RETURNING user_group_association.user_id AS user_id,
user_group_association.id AS id),
deleted_ura_ids
AS (DELETE FROM user_role_association
USING role
WHERE role.id = user_role_association.role_id
AND role.type != 'private'
AND user_role_association.user_id IN
( SELECT id
FROM purged_user_ids)
RETURNING user_role_association.user_id AS user_id,
user_role_association.id AS id),
deleted_ua_ids
AS (DELETE FROM user_address
USING purged_user_ids
WHERE user_address.user_id = purged_user_ids.id
RETURNING user_address.user_id AS user_id,
user_address.id AS id),
user_events
AS (INSERT INTO cleanup_event_user_association
(create_time, cleanup_event_id, user_id)
SELECT NOW(), %(event_id)s, id
FROM purged_user_ids),
history_events
AS (INSERT INTO cleanup_event_history_association
(create_time, cleanup_event_id, history_id)
SELECT NOW(), %(event_id)s, id
FROM purged_history_ids),
hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, id
FROM purged_hda_ids),
metadata_file_events
AS (INSERT INTO cleanup_event_metadata_file_association
(create_time, cleanup_event_id, metadata_file_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_metadata_file_ids),
icda_events
AS (INSERT INTO cleanup_event_icda_association
(create_time, cleanup_event_id, icda_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_icda_ids),
icda_hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, hda_id
FROM deleted_icda_ids)
SELECT purged_user_ids.id AS purged_user_id,
purged_user_ids.id AS disk_accounting_user_id,
purged_history_ids.id AS purged_history_id,
purged_hda_ids.id AS purged_hda_id,
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
deleted_metadata_file_ids.object_store_id AS object_store_id,
deleted_icda_ids.id AS deleted_icda_id,
deleted_icda_ids.hda_id AS deleted_icda_hda_id,
deleted_uga_ids.id AS deleted_uga_id,
deleted_ura_ids.id AS deleted_ura_id,
deleted_ua_ids.id AS deleted_ua_id
FROM purged_user_ids
LEFT OUTER JOIN purged_history_ids
ON purged_user_ids.id = purged_history_ids.user_id
LEFT OUTER JOIN purged_hda_ids
ON purged_history_ids.id = purged_hda_ids.history_id
LEFT OUTER JOIN deleted_metadata_file_ids
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
LEFT OUTER JOIN deleted_icda_ids
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
LEFT OUTER JOIN deleted_uga_ids
ON purged_user_ids.id = deleted_uga_ids.user_id
LEFT OUTER JOIN deleted_ura_ids
ON purged_user_ids.id = deleted_ura_ids.user_id
LEFT OUTER JOIN deleted_ua_ids
ON purged_user_ids.id = deleted_ua_ids.user_id
ORDER BY purged_user_ids.id
"""
cur = self._update(sql, {})
causals = (
('purged_user_id', 'purged_history_id'),
('purged_history_id', 'purged_hda_id'),
('purged_hda_id', 'deleted_metadata_file_id', 'object_store_id'),
('purged_hda_id', 'deleted_icda_id', 'deleted_icda_hda_id'),
('purged_user_id', 'deleted_uga_id'),
('purged_user_id', 'deleted_ura_id'),
('purged_user_id', 'deleted_ua_id'),
)
self._handle_results(cur, causals)
def delete_userless_histories(self):
"""
Mark deleted all "anonymous" Histories (not owned by a registered user) that are older than the specified number of days.
"""
log.info('Marking deleted all userless Histories older than %i days' % self.args.days)
sql = """
WITH deleted_history_ids
AS ( UPDATE history
SET deleted = true{update_time_sql}
WHERE user_id is null
AND NOT deleted
AND update_time < (NOW() - interval '%(days)s days')
RETURNING id),
history_events
AS (INSERT INTO cleanup_event_history_association
(create_time, cleanup_event_id, history_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_history_ids)
SELECT id AS deleted_history_id
FROM deleted_history_ids
ORDER BY id
"""
cur = self._update(sql, {})
self._handle_results(cur)
def purge_error_hdas(self):
"""
Mark purged all HistoryDatasetAssociations whose dataset_id is state = 'error' that are older than the specified
number of days.
"""
log.info('Marking purged all error state HistoryDatasetAssociations older than %i days' % self.args.days)
sql = """
WITH purged_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM dataset
WHERE history_dataset_association.dataset_id = dataset.id
AND dataset.state = 'error'
AND history_dataset_association.update_time < (NOW() - interval '%(days)s days')
RETURNING history_dataset_association.id as id,
history_dataset_association.history_id as history_id),
hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, id
FROM purged_hda_ids)
SELECT purged_hda_ids.id AS purged_hda_id,
history.user_id AS disk_accounting_user_id
FROM purged_hda_ids
LEFT OUTER JOIN history
ON purged_hda_ids.history_id = history.id
ORDER BY purged_hda_ids.id
"""
cur = self._update(sql, {})
self._handle_results(cur)
def purge_hdas_of_purged_histories(self):
"""
Mark purged all HistoryDatasetAssociations in histories that are purged and older than the specified number of days.
"""
log.info('Marking purged all HistoryDatasetAssociations in purged Histories older than %i days' % self.args.days)
sql = """
WITH purged_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM history
WHERE history_dataset_association.history_id = history.id
AND NOT history_dataset_association.purged
AND history.purged
AND history.update_time < (NOW() - interval '%(days)s days')
RETURNING history_dataset_association.id as id,
history_dataset_association.history_id as history_id),
hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, id
FROM purged_hda_ids)
SELECT purged_hda_ids.id AS purged_hda_id,
history.user_id AS disk_accounting_user_id
FROM purged_hda_ids
LEFT OUTER JOIN history
ON purged_hda_ids.history_id = history.id
ORDER BY purged_hda_ids.id
"""
cur = self._update(sql, {})
self._handle_results(cur)
def purge_deleted_hdas(self):
"""
Mark purged all HistoryDatasetAssociations currently marked deleted that are older than the specified number of days.
Mark deleted all MetadataFiles whose hda_id is purged in this step.
Mark deleted all ImplicitlyConvertedDatasetAssociations whose hda_parent_id is purged in this step.
Mark purged all HistoryDatasetAssociations for which an ImplicitlyConvertedDatasetAssociation with matching hda_id is deleted in this step.
"""
log.info('Marking purged all deleted HistoryDatasetAssociations older than %i days' % self.args.days)
sql = """
WITH purged_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
WHERE deleted{force_retry_sql}
AND update_time < (NOW() - interval '%(days)s days')
RETURNING id,
history_id),
deleted_metadata_file_ids
AS ( UPDATE metadata_file
SET deleted = true{update_time_sql}
FROM purged_hda_ids
WHERE purged_hda_ids.id = metadata_file.hda_id
RETURNING metadata_file.hda_id AS hda_id,
metadata_file.id AS id,
metadata_file.object_store_id AS object_store_id),
deleted_icda_ids
AS ( UPDATE implicitly_converted_dataset_association
SET deleted = true{update_time_sql}
FROM purged_hda_ids
WHERE purged_hda_ids.id = implicitly_converted_dataset_association.hda_parent_id
RETURNING implicitly_converted_dataset_association.hda_id AS hda_id,
implicitly_converted_dataset_association.hda_parent_id AS hda_parent_id,
implicitly_converted_dataset_association.id AS id),
deleted_icda_purged_child_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM deleted_icda_ids
WHERE deleted_icda_ids.hda_id = history_dataset_association.id),
hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, id
FROM purged_hda_ids),
metadata_file_events
AS (INSERT INTO cleanup_event_metadata_file_association
(create_time, cleanup_event_id, metadata_file_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_metadata_file_ids),
icda_events
AS (INSERT INTO cleanup_event_icda_association
(create_time, cleanup_event_id, icda_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_icda_ids),
icda_hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, hda_id
FROM deleted_icda_ids)
SELECT purged_hda_ids.id,
history.user_id,
deleted_metadata_file_ids.id,
deleted_metadata_file_ids.object_store_id,
deleted_icda_ids.id,
deleted_icda_ids.hda_id
FROM purged_hda_ids
LEFT OUTER JOIN deleted_metadata_file_ids
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
LEFT OUTER JOIN deleted_icda_ids
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
LEFT OUTER JOIN history
ON purged_hda_ids.history_id = history.id
"""
cur = self._update(sql, {})
self._open_logfile()
for tup in cur:
self._log('Marked HistoryDatasetAssociations purged: %s' % tup[0])
if tup[1] is not None and tup[1] not in self.disk_accounting_user_ids:
self.disk_accounting_user_ids.append(int(tup[1]))
if tup[2] is not None:
self._log('Purge of HDA %s caused deletion of MetadataFile: %s in Object Store: %s' % (tup[0], tup[2], tup[3]))
self._remove_metadata_file(tup[2], tup[3], self.current_action)
if tup[4] is not None:
self._log('Purge of HDA %s caused deletion of ImplicitlyConvertedDatasetAssociation: %s and converted HistoryDatasetAssociation: %s' % (tup[0], tup[4], tup[5]))
self._close_logfile()
def purge_deleted_histories(self):
"""
Mark purged all Histories marked deleted that are older than the specified number of days.
Mark purged all HistoryDatasetAssociations in Histories marked purged in this step (if not already purged).
"""
log.info('Marking purged all deleted histories that are older than the specified number of days.')
sql = """
WITH purged_history_ids
AS ( UPDATE history
SET purged = true{update_time_sql}
WHERE deleted{force_retry_sql}
AND update_time < (NOW() - interval '%(days)s days')
RETURNING id,
user_id),
purged_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM purged_history_ids
WHERE purged_history_ids.id = history_dataset_association.history_id
AND NOT history_dataset_association.purged
RETURNING history_dataset_association.history_id AS history_id,
history_dataset_association.id AS id),
deleted_metadata_file_ids
AS ( UPDATE metadata_file
SET deleted = true{update_time_sql}
FROM purged_hda_ids
WHERE purged_hda_ids.id = metadata_file.hda_id
RETURNING metadata_file.hda_id AS hda_id,
metadata_file.id AS id,
metadata_file.object_store_id AS object_store_id),
deleted_icda_ids
AS ( UPDATE implicitly_converted_dataset_association
SET deleted = true{update_time_sql}
FROM purged_hda_ids
WHERE purged_hda_ids.id = implicitly_converted_dataset_association.hda_parent_id
RETURNING implicitly_converted_dataset_association.hda_id AS hda_id,
implicitly_converted_dataset_association.hda_parent_id AS hda_parent_id,
implicitly_converted_dataset_association.id AS id),
deleted_icda_purged_child_hda_ids
AS ( UPDATE history_dataset_association
SET purged = true{update_time_sql}
FROM deleted_icda_ids
WHERE deleted_icda_ids.hda_id = history_dataset_association.id),
history_events
AS (INSERT INTO cleanup_event_history_association
(create_time, cleanup_event_id, history_id)
SELECT NOW(), %(event_id)s, id
FROM purged_history_ids),
hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, id
FROM purged_hda_ids),
metadata_file_events
AS (INSERT INTO cleanup_event_metadata_file_association
(create_time, cleanup_event_id, metadata_file_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_metadata_file_ids),
icda_events
AS (INSERT INTO cleanup_event_icda_association
(create_time, cleanup_event_id, icda_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_icda_ids),
icda_hda_events
AS (INSERT INTO cleanup_event_hda_association
(create_time, cleanup_event_id, hda_id)
SELECT NOW(), %(event_id)s, hda_id
FROM deleted_icda_ids)
SELECT purged_history_ids.id,
purged_history_ids.user_id,
purged_hda_ids.id,
deleted_metadata_file_ids.id,
deleted_metadata_file_ids.object_store_id,
deleted_icda_ids.id,
deleted_icda_ids.hda_id
FROM purged_history_ids
LEFT OUTER JOIN purged_hda_ids
ON purged_history_ids.id = purged_hda_ids.history_id
LEFT OUTER JOIN deleted_metadata_file_ids
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
LEFT OUTER JOIN deleted_icda_ids
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
ORDER BY purged_history_ids.id
"""
cur = self._update(sql, {})
self._open_logfile()
for tup in cur:
self._log('Marked History purged: %s' % tup[0])
if tup[1] is not None and tup[1] not in self.disk_accounting_user_ids:
self.disk_accounting_user_ids.append(int(tup[1]))
if tup[2] is not None:
self._log('Purge of History %s caused deletion of HistoryDatasetAssociation: %s' % (tup[0], tup[2]))
if tup[3] is not None:
self._log('Purge of HDA %s caused deletion of MetadataFile: %s in Object Store: %s' % (tup[1], tup[3], tup[4]))
self._remove_metadata_file(tup[3], tup[4], self.current_action)
if tup[5] is not None:
self._log('Purge of HDA %s caused deletion of ImplicitlyConvertedDatasetAssociation: %s and converted HistoryDatasetAssociation: %s' % (tup[1], tup[5], tup[6]))
self._close_logfile()
def delete_exported_histories(self):
"""
Mark deleted all Datasets that are derivative of JobExportHistoryArchives that are older than the specified number of days.
"""
log.info('Marking deleted all Datasets that are derivative of JobExportHistoryArchives that are older than the specified number of days.')
sql = """
WITH deleted_dataset_ids
AS ( UPDATE dataset
SET deleted = true{update_time_sql}
FROM job_export_history_archive
WHERE job_export_history_archive.dataset_id = dataset.id
AND NOT deleted
AND dataset.update_time <= (NOW() - interval '%(days)s days')
RETURNING dataset.id),
dataset_events
AS (INSERT INTO cleanup_event_dataset_association
(create_time, cleanup_event_id, dataset_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_dataset_ids)
SELECT id
FROM deleted_dataset_ids
ORDER BY id
"""
cur = self._update(sql, {})
self._open_logfile()
for tup in cur:
self._log('Marked Dataset deleted: %s' % tup[0])
self._close_logfile()
def delete_datasets(self):
"""
Mark deleted all Datasets whose associations are all marked as deleted (LDDA) or purged (HDA) that are older than the specified number of days.
"""
log.info('Marking deleted all Datasets whose associations are all marked as deleted/purged that are older than the specified number of days.')
sql = """
WITH deleted_dataset_ids
AS ( UPDATE dataset
SET deleted = true{update_time_sql}
WHERE NOT deleted
AND NOT EXISTS (SELECT true
FROM library_dataset_dataset_association
WHERE (NOT deleted
OR update_time >= (NOW() - interval '%(days)s days'))
AND dataset.id = dataset_id)
AND NOT EXISTS (SELECT true
FROM history_dataset_association
WHERE (NOT purged
OR update_time >= (NOW() - interval '%(days)s days'))
AND dataset.id = dataset_id)
RETURNING id),
dataset_events
AS (INSERT INTO cleanup_event_dataset_association
(create_time, cleanup_event_id, dataset_id)
SELECT NOW(), %(event_id)s, id
FROM deleted_dataset_ids)
SELECT id
FROM deleted_dataset_ids
ORDER BY id
"""
cur = self._update(sql, {})
self._open_logfile()
for tup in cur:
self._log('Marked Dataset deleted: %s' % tup[0])
self._close_logfile()
def purge_datasets(self):
log.info('Marking purged all Datasets marked deleted that are older than the specified number of days.')
sql = """
"""
cur = self._update(sql, {})
self._open_logfile()
for tup in cur:
self._log('Marked Dataset purged: %s in Object Store: %s' % (tup[0], tup[1]))
# always try to remove the "object store path" - if it's at an external_filename, that file will be untouched anyway (which is what we want)
dataset = Dataset(id=tup[0], object_store_id=tup[1])
try:
filename = self.object_store.get_filename(dataset)
except (ObjectNotFound, AttributeError) as e:
log.error('Unable to get Dataset %s filename: %s' % (tup[0], e))
continue
try:
extra_files_dir = self.object_store.get_filename(dataset, dir_only=True, extra_dir="dataset_%d_files" % tup[0])
except (ObjectNotFound, AttributeError):
extra_files_dir = None
# don't check for existence of the dataset, it should exist
self._log('Removing from disk: %s' % filename)
if not self.args.dry_run:
try:
os.unlink(filename)
except Exception as e:
self._log('Removal of %s failed with error: %s' % (filename, e))
# extra_files_dir is optional so it's checked first
if extra_files_dir is not None and os.path.exists(extra_files_dir):
self._log('Removing from disk: %s' % extra_files_dir)
if not self.args.dry_run:
try:
shutil.rmtree(extra_files_dir)
except Exception as e:
self._log('Removal of %s failed with error: %s' % (extra_files_dir, e))
self._close_logfile()
def quote_for_print(v):
if isinstance(v, string_types):
return "'" + v.replace("'", "\\'") + "'"
else:
return v
def args_for_print(args):
if isinstance(args, dict):
r = {}
for k, v in args.items():
r[k] = quote_for_print(v)
elif isinstance(args, (tuple, list)):
r = []
for v in args:
r.append(quote_for_print(v))
if isinstance(args, tuple):
r = tuple(r)
else:
r = args
return r
if __name__ == '__main__':
cleanup = Cleanup()
try:
cleanup.run()
except Exception:
log.exception('Caught exception in run sequence:')
cleanup._shutdown()