The hidden Django anti-pattern preventing your prod rollback

codereviewdoctor

Code Review Doctor

Posted on November 14, 2020

The hidden Django anti-pattern preventing your prod rollback

Picture the scene: it's 11pm and production is down. Stress. You're about to roll back to the last good release. All you need is to reverse the database migrations and then deploy and...wait what?

$ python manage.py migrate hounds 0002
Operations to perform:
  Target specific migration: 0002_auto, from hounds
Running migrations:
  Rendering model states... DONE
  Unapplying hounds.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunPython > in hounds.0003_auto is not reversible
Enter fullscreen mode Exit fullscreen mode

Migrations forwards and back

Django data migrations are two parters: forwards and backwards, but the cause of this failed migration is...for reasons...the develop only added the forwards handler:

# 0003_auto.py
def forwards(apps, schema_editor): 
    ...

class Migration(migrations.Migration):
    dependencies = [("cases", "0009_auto_20200320_1210")]
    operations = [migrations.RunPython(forwards)]
Enter fullscreen mode Exit fullscreen mode

Django requires forwards, but backwards is technically optional. While the above is valid, below is infinitely better:

# 0003_auto.py
def forwards(apps, schema_editor): 
    ...

def backwards(apps, schema_editor):
    ...

class Migration(migrations.Migration):
    dependencies = [("cases", "0009_auto_20200320_1210")]
    operations = [migrations.RunPython(forwards, backwards)]
Enter fullscreen mode Exit fullscreen mode

So backwards is technically optional in the same way that wearing a parachute while skydiving is optional: given enough time you will eventually hit the ground. Hard. Indeed, if you want the option of rolling back your production website then no, backwards is not optional. Omitting backwards means you only have the option of rolling forwards, which is a bit more daunting compared with safely rolling back to a release we know works.

The fix is a super simple too: just specifying migrations.RunPython.noop can be enough. As the name implies, Django will do nothing for the reverse, simply skip over the data migration, without throwing IrreversibleError:

# 0003_auto.py
class Migration(migrations.Migration):
    dependencies = [("cases", "0009_auto_20200320_1210")]
    operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]
Enter fullscreen mode Exit fullscreen mode

So in practice, follow this advice:

I'm a GitHub code review bot, and I automatically suggest this fix

Does your codebase have a IrreversibleError waiting to happen?

Over time it's easy for tech debt to slip into your codebase. You might have a irreversible migration in your codebase. I can check that for you at django.doctor, or can review your GitHub PRs:

Alt Text

Or try out Django refactor challenges.

💖 💪 🙅 🚩
codereviewdoctor
Code Review Doctor

Posted on November 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related