Skip to content

Commit

Permalink
migrate event table primary keys from integer to bigint
Browse files Browse the repository at this point in the history
see: #6010
  • Loading branch information
ryanpetrello committed Feb 21, 2020
1 parent a42ff98 commit bcb050b
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
62 changes: 62 additions & 0 deletions awx/main/migrations/0109_v370_event_bigint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Generated by Django 2.2.8 on 2020-02-21 16:31

from django.db import migrations, models, connection


def migrate_event_data(apps, schema_editor):
# see: https://github.com/ansible/awx/issues/6010
#
# the goal of this function is to end with event tables (e.g., main_jobevent)
# that have a bigint primary key (because the old usage of an integer
# numeric isn't enough, as its range is about 2.1B, see:
# https://www.postgresql.org/docs/9.1/datatype-numeric.html)

# unfortunately, we can't do this with a simple ALTER TABLE, because
# for tables with hundreds of millions or billions of rows, the ALTER TABLE
# can take *hours* on modest hardware.
#
# the approach in this migration means that post-migration, event data will
# *not* immediately show up, but will be repopulated over time progressively
# the trade-off here is not having to wait hours for the full data migration
# before you can start and run AWX again (including new playbook runs)
for tblname in (
'main_jobevent', 'main_inventoryupdateevent',
'main_projectupdateevent', 'main_adhoccommandevent'
):
with connection.cursor() as cursor:
# rename the current event table
cursor.execute(
f'ALTER TABLE {tblname} RENAME TO _old_{tblname};'
)
# create a *new* table with the same schema
cursor.execute(
f'CREATE TABLE {tblname} (LIKE _old_{tblname} INCLUDING ALL);'
)
# alter the *new* table so that the primary key is a big int
cursor.execute(
f'ALTER TABLE {tblname} ALTER COLUMN id TYPE bigint USING id::bigint;'
)

# recreate counter for the new table's primary key to
# start where the *old* table left off (we have to do this because the
# counter changed from an int to a bigint)
cursor.execute(f'DROP SEQUENCE IF EXISTS "{tblname}_id_seq" CASCADE;')
cursor.execute(f'CREATE SEQUENCE "{tblname}_id_seq";')
cursor.execute(
f'ALTER TABLE "{tblname}" ALTER COLUMN "id" '
f"SET DEFAULT nextval('{tblname}_id_seq');"
)
cursor.execute(
f"SELECT setval('{tblname}_id_seq', (SELECT MAX(id) FROM _old_{tblname}), true);"
)


class Migration(migrations.Migration):

dependencies = [
('main', '0108_v370_unifiedjob_dependencies_processed'),
]

operations = [
migrations.RunPython(migrate_event_data)
]
21 changes: 21 additions & 0 deletions awx/main/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Django
from django.conf import settings # noqa
from django.db import connection, ProgrammingError
from django.db.models.signals import pre_delete # noqa

# AWX
Expand Down Expand Up @@ -80,6 +81,26 @@
User.add_to_class('accessible_objects', user_accessible_objects)


def enforce_bigint_pk_migration():
# see: https://github.com/ansible/awx/issues/6010
# look at all the event tables and verify that they have been fully migrated
# from the *old* int primary key table to the replacement bigint table
# if not, attempt to migrate them in the background
for tblname in (
'main_jobevent', 'main_inventoryupdateevent',
'main_projectupdateevent', 'main_adhoccommandevent'
):
with connection.cursor() as cursor:
try:
cursor.execute(f'SELECT MAX(id) FROM _old_{tblname}')
if cursor.fetchone():
from awx.main.tasks import migrate_legacy_event_data
migrate_legacy_event_data.apply_async([tblname])
except ProgrammingError:
# the table is gone (migration is unnecessary)
pass


def cleanup_created_modified_by(sender, **kwargs):
# work around a bug in django-polymorphic that doesn't properly
# handle cascades for reverse foreign keys on the polymorphic base model
Expand Down
4 changes: 4 additions & 0 deletions awx/main/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ class Meta:
('job', 'parent_uuid'),
]

id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
job = models.ForeignKey(
'Job',
related_name='job_events',
Expand Down Expand Up @@ -518,6 +519,7 @@ class Meta:
('project_update', 'end_line'),
]

id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
project_update = models.ForeignKey(
'ProjectUpdate',
related_name='project_update_events',
Expand Down Expand Up @@ -669,6 +671,7 @@ class Meta:
FAILED_EVENTS = [x[0] for x in EVENT_TYPES if x[2]]
EVENT_CHOICES = [(x[0], x[1]) for x in EVENT_TYPES]

id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
event = models.CharField(
max_length=100,
choices=EVENT_CHOICES,
Expand Down Expand Up @@ -731,6 +734,7 @@ class Meta:
('inventory_update', 'end_line'),
]

id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
inventory_update = models.ForeignKey(
'InventoryUpdate',
related_name='inventory_update_events',
Expand Down
42 changes: 40 additions & 2 deletions awx/main/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

# Django
from django.conf import settings
from django.db import transaction, DatabaseError, IntegrityError
from django.db import transaction, DatabaseError, IntegrityError, connection
from django.db.models.fields.related import ForeignKey
from django.utils.timezone import now, timedelta
from django.utils.encoding import smart_str
Expand Down Expand Up @@ -59,7 +59,7 @@
Inventory, InventorySource, SmartInventoryMembership,
Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob,
JobEvent, ProjectUpdateEvent, InventoryUpdateEvent, AdHocCommandEvent, SystemJobEvent,
build_safe_env
build_safe_env, enforce_bigint_pk_migration
)
from awx.main.constants import ACTIVE_STATES
from awx.main.exceptions import AwxTaskError
Expand Down Expand Up @@ -135,6 +135,12 @@ def dispatch_startup():
if Instance.objects.me().is_controller():
awx_isolated_heartbeat()

# at process startup, detect the need to migrate old event records from int
# to bigint; at *some point* in the future, once certain versions of AWX
# and Tower fall out of use/support, we can probably just _assume_ that
# everybody has moved to bigint, and remove this code entirely
enforce_bigint_pk_migration()


def inform_cluster_of_shutdown():
try:
Expand Down Expand Up @@ -660,6 +666,38 @@ def update_host_smart_inventory_memberships():
smart_inventory.update_computed_fields()


@task()
def migrate_legacy_event_data(tblname):
if 'event' not in tblname:
return
with advisory_lock(f'bigint_migration_{tblname}', wait=False) as acquired:
if acquired is False:
return
chunk = 10000

def _remaining():
cursor.execute(f'SELECT MAX(id) FROM _old_{tblname};')
return cursor.fetchone()[0]

with connection.cursor() as cursor:
total_rows = _remaining()
if total_rows is None:
return
while total_rows is not None:
sql = f'INSERT INTO {tblname} SELECT * FROM _old_{tblname} ORDER BY id DESC LIMIT {chunk};'
logger.warn(sql)
cursor.execute(sql)
cursor.execute(
f'DELETE FROM _old_{tblname} WHERE id IN (SELECT id FROM _old_{tblname} ORDER BY id DESC LIMIT {chunk});'
)
connection.commit()
total_rows = _remaining()

if total_rows is None:
cursor.execute(f'DROP TABLE _old_{tblname}')
logger.warn(f'{tblname} primary key migration to BIGINT has finished')


@task()
def delete_inventory(inventory_id, user_id, retries=5):
# Delete inventory as user
Expand Down

0 comments on commit bcb050b

Please sign in to comment.