From 587b8167937edce6affe0f1b4abe7d7dcd4dc77d Mon Sep 17 00:00:00 2001 From: Dirk Julich Date: Thu, 13 Aug 2026 19:41:48 +0200 Subject: [PATCH] =?UTF-8?q?AAP-87549=20=E2=80=94=20Add=20management=20comm?= =?UTF-8?q?and=20to=20create=20JobHostSummary=20composite=20index=20(#1658?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `create_host_summary_index` management command that creates the composite index (host_id, id DESC) on main_jobhostsummary using CREATE INDEX CONCURRENTLY. The command is idempotent: it checks for existing valid/invalid indexes, drops invalid indexes from failed prior attempts, and skips if the index already exists. Disables statement_timeout for the session to avoid killing the index build on large tables where ALTER ROLE timeout is set. Forward-port of tower#7893 (stable-2.6). Co-authored-by: Claude Opus 4.6 --- .../commands/create_host_summary_index.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 awx/main/management/commands/create_host_summary_index.py diff --git a/awx/main/management/commands/create_host_summary_index.py b/awx/main/management/commands/create_host_summary_index.py new file mode 100644 index 0000000000..ba39fbdd1b --- /dev/null +++ b/awx/main/management/commands/create_host_summary_index.py @@ -0,0 +1,59 @@ +from django.core.management.base import BaseCommand +from django.db import connection + + +class Command(BaseCommand): + help = 'Creates composite index (host_id, id DESC) on main_jobhostsummary to improve host list query performance.' + + INDEX_NAME = 'main_jobhostsumm_host_id_desc' + TABLE_NAME = 'main_jobhostsummary' + + def _index_status(self): + """Check whether the index exists in pg_class and whether it is valid. + + Returns: + 'valid' - index exists and is usable; no action needed. + 'invalid' - index exists but was left in an invalid state by a + previous failed CREATE INDEX CONCURRENTLY; must be + dropped before retrying. + None - index does not exist; safe to create. + """ + with connection.cursor() as cursor: + cursor.execute( + "SELECT pg_index.indisvalid " "FROM pg_class JOIN pg_index ON pg_class.oid = pg_index.indexrelid " "WHERE pg_class.relname = %s", + [self.INDEX_NAME], + ) + row = cursor.fetchone() + if row is None: + return None + return 'valid' if row[0] else 'invalid' + + def handle(self, *args, **options): + status = self._index_status() + + if status == 'valid': + self.stdout.write(f"Index {self.INDEX_NAME} already exists, nothing to do.") + return + + if status == 'invalid': + self.stdout.write(f"Dropping invalid index {self.INDEX_NAME} from a previous failed attempt...") + with connection.cursor() as cursor: + cursor.execute(f"DROP INDEX IF EXISTS {self.INDEX_NAME}") + + self.stdout.write(f"Creating index {self.INDEX_NAME} on {self.TABLE_NAME} (host_id, id DESC) concurrently...") + + # CREATE INDEX CONCURRENTLY cannot run inside a transaction. + connection.ensure_connection() + old_autocommit = connection.connection.autocommit + connection.connection.autocommit = True + try: + with connection.cursor() as cursor: + # Disable statement_timeout for this session — the customer may + # have set ALTER ROLE ... SET statement_timeout which would kill + # the index build on large tables. + cursor.execute("SET statement_timeout = 0") + cursor.execute(f"CREATE INDEX CONCURRENTLY {self.INDEX_NAME} " f"ON {self.TABLE_NAME} (host_id, id DESC)") + finally: + connection.connection.autocommit = old_autocommit + + self.stdout.write(f"Index {self.INDEX_NAME} created successfully.")