@@ -4407,6 +4407,9 @@ def _archive_deleted_rows_for_table(
4407
4407
select = select .order_by (column ).limit (max_rows )
4408
4408
with conn .begin ():
4409
4409
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.
4410
4413
records = [r [0 ] for r in rows ]
4411
4414
4412
4415
# We will archive deleted rows for this table and also generate insert and
@@ -4419,51 +4422,103 @@ def _archive_deleted_rows_for_table(
4419
4422
4420
4423
# Keep track of any extra tablenames to number of rows that we archive by
4421
4424
# following FK relationships.
4422
- # {tablename: extra_rows_archived}
4425
+ #
4426
+ # extras = {tablename: number_of_extra_rows_archived}
4423
4427
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 :
4429
4461
# Walk FK relationships and add insert/delete statements for rows that
4430
4462
# refer to this table via FK constraints. fk_inserts and fk_deletes
4431
4463
# will be prepended to by _get_fk_stmts if referring rows are found by
4432
4464
# FK constraints.
4433
4465
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 )
4448
4475
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 )})
4467
4522
4468
4523
conn .close ()
4469
4524
0 commit comments