From 88d92d62312859a90d7ea6d5a73aacf7b1f55df8 Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Fri, 17 Jul 2026 12:08:03 +0200 Subject: [PATCH] fix: delete inventory hosts in batches to avoid memory exhaustion --- awx/main/tasks/system.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/awx/main/tasks/system.py b/awx/main/tasks/system.py index 569dd86afa..a0e2a36b82 100644 --- a/awx/main/tasks/system.py +++ b/awx/main/tasks/system.py @@ -1022,6 +1022,33 @@ def update_host_smart_inventory_memberships(): smart_inventory.update_computed_fields() +def _batched_delete_inventory(inventory, batch_size=500): + """Delete inventory hosts in batches to avoid high memory usage. + + With ansible facts, loading thousands of hosts at once can use a lot of memory. To avoid + this, we delete them in batches (of 500). + + Safe to retry after a crash because inventory.pending_deletion + is already set and each batch is its own transaction. + """ + from awx.main.models.inventory import Host + + # first delete all hosts in batches + total_deleted = 0 + while True: + pks = list(Host.objects.filter(inventory_id=inventory.id).values_list('pk', flat=True)[:batch_size]) + if not pks: + break + with transaction.atomic(): + deleted_count, _ = Host.objects.filter(pk__in=pks).delete() + total_deleted += deleted_count + logger.debug('Batch-deleted %d hosts from inventory %d (%d total so far)', len(pks), inventory.id, total_deleted) + + # then delete the inventory itself + inventory.delete() + logger.info('Batched deletion of inventory %d complete (%d hosts removed)', inventory.id, total_deleted) + + @task(queue=get_task_queuename, timeout=3600 * 5) def delete_inventory(inventory_id, user_id, retries=5): # Delete inventory as user @@ -1034,12 +1061,14 @@ def delete_inventory(inventory_id, user_id, retries=5): user = None with ignore_inventory_computed_fields(), ignore_inventory_group_removal(), impersonate(user): try: - Inventory.objects.get(id=inventory_id).delete() - emit_channel_notification('inventories-status_changed', {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'}) - logger.debug('Deleted inventory {} as user {}.'.format(inventory_id, user_id)) + inv = Inventory.objects.get(id=inventory_id) except Inventory.DoesNotExist: logger.exception("Delete Inventory failed due to missing inventory: " + str(inventory_id)) return + try: + _batched_delete_inventory(inv) + emit_channel_notification('inventories-status_changed', {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'}) + logger.debug('Deleted inventory {} as user {}.'.format(inventory_id, user_id)) except DatabaseError: logger.exception('Database error deleting inventory {}, but will retry.'.format(inventory_id)) if retries > 0: