@@ -4407,6 +4407,9 @@ def _archive_deleted_rows_for_table(
44074407 select = select .order_by (column ).limit (max_rows )
44084408 with conn .begin ():
44094409 rows = conn .execute (select ).fetchall ()
4410+
4411+ # This is a list of IDs of rows that should be archived from this table,
4412+ # limited to a length of max_rows.
44104413 records = [r [0 ] for r in rows ]
44114414
44124415 # We will archive deleted rows for this table and also generate insert and
@@ -4419,51 +4422,103 @@ def _archive_deleted_rows_for_table(
44194422
44204423 # Keep track of any extra tablenames to number of rows that we archive by
44214424 # following FK relationships.
4422- # {tablename: extra_rows_archived}
4425+ #
4426+ # extras = {tablename: number_of_extra_rows_archived}
44234427 extras = collections .defaultdict (int )
4424- if records :
4425- insert = shadow_table .insert ().from_select (
4426- columns , sql .select (table ).where (column .in_ (records ))
4427- ).inline ()
4428- delete = table .delete ().where (column .in_ (records ))
4428+
4429+ if not records :
4430+ # Nothing to archive, so return.
4431+ return rows_archived , deleted_instance_uuids , extras
4432+
4433+ # Keep track of how many rows we accumulate for the insert+delete database
4434+ # transaction and cap it as soon as it is >= max_rows. Because we will
4435+ # archive all child rows of a parent row along with the parent at the same
4436+ # time, we end up with extra rows to archive in addition to len(records).
4437+ num_rows_in_batch = 0
4438+ # The sequence of query statements we will execute in a batch. These are
4439+ # ordered: [child1, child1, parent1, child2, child2, child2, parent2, ...]
4440+ # Parent + child "trees" are kept together to avoid FK constraint
4441+ # violations.
4442+ statements_in_batch = []
4443+ # The list of records in the batch. This is used for collecting deleted
4444+ # instance UUIDs in the case of the 'instances' table.
4445+ records_in_batch = []
4446+
4447+ # (melwitt): We will gather rows related by foreign key relationship for
4448+ # each deleted row, one at a time. We do it this way to keep track of and
4449+ # limit the total number of rows that will be archived in a single database
4450+ # transaction. In a large scale database with potentially hundreds of
4451+ # thousands of deleted rows, if we don't limit the size of the transaction
4452+ # based on max_rows, we can get into a situation where we get stuck not
4453+ # able to make much progress. The value of max_rows has to be 1) small
4454+ # enough to not exceed the database's max packet size limit or timeout with
4455+ # a deadlock but 2) large enough to make progress in an environment with a
4456+ # constant high volume of create and delete traffic. By archiving each
4457+ # parent + child rows tree one at a time, we can ensure meaningful progress
4458+ # can be made while allowing the caller to predictably control the size of
4459+ # the database transaction with max_rows.
4460+ for record in records :
44294461 # Walk FK relationships and add insert/delete statements for rows that
44304462 # refer to this table via FK constraints. fk_inserts and fk_deletes
44314463 # will be prepended to by _get_fk_stmts if referring rows are found by
44324464 # FK constraints.
44334465 fk_inserts , fk_deletes = _get_fk_stmts (
4434- metadata , conn , table , column , records )
4435-
4436- # NOTE(tssurya): In order to facilitate the deletion of records from
4437- # instance_mappings, request_specs and instance_group_member tables in
4438- # the nova_api DB, the rows of deleted instances from the instances
4439- # table are stored prior to their deletion. Basically the uuids of the
4440- # archived instances are queried and returned.
4441- if tablename == "instances" :
4442- query_select = sql .select (table .c .uuid ).where (
4443- table .c .id .in_ (records )
4444- )
4445- with conn .begin ():
4446- rows = conn .execute (query_select ).fetchall ()
4447- deleted_instance_uuids = [r [0 ] for r in rows ]
4466+ metadata , conn , table , column , [record ])
4467+ statements_in_batch .extend (fk_inserts + fk_deletes )
4468+ # statement to add parent row to shadow table
4469+ insert = shadow_table .insert ().from_select (
4470+ columns , sql .select (table ).where (column .in_ ([record ]))).inline ()
4471+ statements_in_batch .append (insert )
4472+ # statement to remove parent row from main table
4473+ delete = table .delete ().where (column .in_ ([record ]))
4474+ statements_in_batch .append (delete )
44484475
4449- try :
4450- # Group the insert and delete in a transaction.
4451- with conn .begin ():
4452- for fk_insert in fk_inserts :
4453- conn .execute (fk_insert )
4454- for fk_delete in fk_deletes :
4455- result_fk_delete = conn .execute (fk_delete )
4456- extras [fk_delete .table .name ] += result_fk_delete .rowcount
4457- conn .execute (insert )
4458- result_delete = conn .execute (delete )
4459- rows_archived += result_delete .rowcount
4460- except db_exc .DBReferenceError as ex :
4461- # A foreign key constraint keeps us from deleting some of
4462- # these rows until we clean up a dependent table. Just
4463- # skip this table for now; we'll come back to it later.
4464- LOG .warning ("IntegrityError detected when archiving table "
4465- "%(tablename)s: %(error)s" ,
4466- {'tablename' : tablename , 'error' : str (ex )})
4476+ records_in_batch .append (record )
4477+
4478+ # Check whether were have a full batch >= max_rows. Rows are counted as
4479+ # the number of rows that will be moved in the database transaction.
4480+ # So each insert+delete pair represents one row that will be moved.
4481+ # 1 parent + its fks
4482+ num_rows_in_batch += 1 + len (fk_inserts )
4483+
4484+ if max_rows is not None and num_rows_in_batch >= max_rows :
4485+ break
4486+
4487+ # NOTE(tssurya): In order to facilitate the deletion of records from
4488+ # instance_mappings, request_specs and instance_group_member tables in the
4489+ # nova_api DB, the rows of deleted instances from the instances table are
4490+ # stored prior to their deletion. Basically the uuids of the archived
4491+ # instances are queried and returned.
4492+ if tablename == "instances" :
4493+ query_select = sql .select (table .c .uuid ).where (
4494+ table .c .id .in_ (records_in_batch ))
4495+ with conn .begin ():
4496+ rows = conn .execute (query_select ).fetchall ()
4497+ # deleted_instance_uuids = ['uuid1', 'uuid2', ...]
4498+ deleted_instance_uuids = [r [0 ] for r in rows ]
4499+
4500+ try :
4501+ # Group the insert and delete in a transaction.
4502+ with conn .begin ():
4503+ for statement in statements_in_batch :
4504+ result = conn .execute (statement )
4505+ result_tablename = statement .table .name
4506+ # Add to archived row counts if not a shadow table.
4507+ if not result_tablename .startswith (_SHADOW_TABLE_PREFIX ):
4508+ if result_tablename == tablename :
4509+ # Number of tablename (parent) rows archived.
4510+ rows_archived += result .rowcount
4511+ else :
4512+ # Number(s) of child rows archived.
4513+ extras [result_tablename ] += result .rowcount
4514+
4515+ except db_exc .DBReferenceError as ex :
4516+ # A foreign key constraint keeps us from deleting some of these rows
4517+ # until we clean up a dependent table. Just skip this table for now;
4518+ # we'll come back to it later.
4519+ LOG .warning ("IntegrityError detected when archiving table "
4520+ "%(tablename)s: %(error)s" ,
4521+ {'tablename' : tablename , 'error' : str (ex )})
44674522
44684523 conn .close ()
44694524
0 commit comments