Idea to backport migrations

Goal

Introduce a bugfix migration on master and also backport that bugfix to an earlier z-stream of Pulp. To keep it simple, let’s say it’s the most recent z-stream. So for example let’s introduce a bugfix migration on master (not-yet-released 3.16.0) and try to backport to 3.15.next also, in this case 3.15.3 would be the next z-stream for 3.15.

Let’s say this new migration is migration 0077_bmbouter_test to give a tangible name.

We want all migrations to run once, so for all these upgrade paths the migration 0077_bmbouter_test needs to run once.

  • 3.15.2 → 3.15.3 → 3.16.0
  • 3.15.2 → 3.16.0
  • fresh install of 3.16.0

Problem

If you’re not careful Django complains for some of the upgrade paths. For fresh installs you could get a “multiple leaf node” errors. Or for some upgrade paths you could receive a InconsistentMigrationHistory exception.

Possible Solution

Here are two branches I made:

The master migration

1.Checkout master
2. Create the migration of interest. In this case a dummy migration: pulpcore-manager makemigrations --name bmbouter_test --empty core. This made 0077_bmbouter_test.
3. Edit the migration to depend on the latest available migration from the 3.15 branch. So instead of depending on 0076_remove_reserved_resource have it depend on 0075_rbaccontentguard. You can see that edit in the migration here.
4. This creates two leaf nodes which Django doesn’t like and it will tell you this if you try to run migrations fresh. Solve this by creating a merge migration: pulpcore-manager makemigrations --merge. This made migration 0078_merge_20211001_2019. which is also in the PR against master.

The backport migration

These are in separate commits, and only the migration 0077_bmbouter_test would be the one to backport to 3.15. So I made the 3.15 backport PR with just that migration only.

3.15.2 → 3.15.3 → 3.16.0 Upgrade Verification

At this point you can checkout pulpcore:3.15 and run pclean. Then checkout the branch simulating the PR to the 3.15 branch and run pulpcore-manager migrate and you’ll see 0077_bmbouter_test apply. Then checkout the PR to master and run pulpcore-manager migrate and it’ll just run the additional master migrations and the merge migration.

3.15.2 → 3.16.0 Upgrade Verification

At this point you can checkout pulpcore:3.15 and run pclean. Then checkout the PR to master and run pulpcore-manager migrate and it’ll run 0077_bmbouter_test, the additional master migrations, and the merge migration.

Fresh install of 3.16.0 Verification

Checkout the PR to master and run pclean. You’ll see it run run 0077_bmbouter_test, the additional master migrations, and the merge migration.

Conclusion

The trick of it is to do two things:

  1. Have the migration on master declare dependency on the oldest migration of the z-stream you want to backport to.
  2. Make a merge migration

Open Questions

How to backport to multiple z-streams? In that case you can’t just have one z-stream that is the “lastest one migration” to declare a dependency on. Maybe we could keep the migration name the same, and just have it declare dependency on the one it needs to for that z-stream? I haven’t tested this but I could when I get back.

In the case of multiple z-streams, I’m a little unclear if we have to backport and release to every z-stream in between.

1 Like

I have several thoughts about this:

  1. Great care must be taken when migration dependencies are rewritten. This basically allows django to reorder migrations, and on the z-stream they will run in a different order.
  2. The process is naturally limited by the youngest migration that is not commutative with the one in question.
  3. When cherry-picking migrations from master, we must treat all migrations on master as released and may not amend them anymore.
  4. With this process in mind, you must “design” the migration to be backported to a specific y-release branch in the first place.
  5. The merge migration for the sole purpose of being backportable feels odd on the master branch.
  6. It is basically impossible to backport the same migration to multiple y-release branches.
  7. After a migration is merged to master (in the usual way) it’s to late to decide, you need a backport.
  8. We need to make sure, the backported migration is avaliable on all in-between y-branches. (Upgrade path 3.14.6 → 3.15.3 → 3.16.0 should never “loose” an applied migration).

I would like to add to the goal, that development on master is not visibly affected in any way by whatever process we come up. That way cherry-picking to mutiple y-branches will all look the same.
To my knowledge, Django only records the name of a migration, that was applied, and not their dependencies. So a solution may be to adjust the dependency while cherry-picking. This needs experimenting and verifying obviously. So the process would be:

  1. Switch to 3.15.
  2. Migrate and populate pulp.
  3. Cherry-pick the commit with the migration from master to 3.15.
  4. Adjust the dependency to the latest prior migration on 3.15.
  5. Verify it migrates cleanly.
  6. Switch to master and migrate to verify reordering was not a problem.

(Thinking about it now, this last step is where the inconsistent history my be raised.)

Another even more complex variant would be the following:

  1. Every new migration should depend on the oldest possible prior migration.
  2. We maintain a floating merge-migration on top of every branch.
  3. The merge migration will be fixed in the release process.
  4. Only merge migrations may depend on merge migrations (this actually follows from 1.).

The nice part of this approach is, that the whole thinking about commutativity (which translates to backportability) takes place the first time the migration is introduced.