Database migrations
Problem
Storing data is one of the prime purposes of Forest, and we often play around with different layouts and database settings.
Up until now, we've been okay with deleting the database and starting from a fresh snapshot. This was annoying but acceptable since we never had more than 100GiB of data. Going forward, we'll be setting up nodes with ~14 TiB of data, and we want them to not require any maintenance when new versions of Forest are released.
Proposed solution
High-level flowchart:
flowchart TB
RunForest[/Forest run/] --> DevMode{Development mode?}
DevMode ==>|no| SameDbVersionExists{Same DB version already exists?}
DevMode -->|yes| DevDbExists{Development DB exists?}
subgraph development mode
DevDbExists -->|no| CreateDevDb[Create development DB]
DevDbExists -->|yes| OpenDevDb[Open development DB]
end
SameDbVersionExists -->|yes| Finish[Finish]
SameDbVersionExists ==>|no| OlderDbVersionExists{Older DB version exists and migration possible?}:::Yellow
OlderDbVersionExists -->|no| CreateVersionedDb
OlderDbVersionExists ==>|yes| RunMigration[Run migration]:::Yellow
RunMigration ==> RunChecks[Run checks]:::Yellow
RunChecks ==> ChecksPassing{Checks passing?}:::Green
ChecksPassing -->|yes| Finish
ChecksPassing -->|no| CreateVersionedDb
CreateVersionedDb --> Finish
CreateDevDb --> Finish
OpenDevDb --> Finish[/Run daemon/]
classDef Green stroke:#0f0,stroke-width:2px;
classDef Yellow stroke:#ff0,stroke-width:2px;
Expected migration path is marked in bold.
Scenarios to cover
Scenario 1: No DB exists
A new DB is created and the daemon is started. If the development environment
variable, the database is created under <DATA_DIR>/<NETWORK>/paritydb-dev
.
Otherwise, it is created under <DATA_DIR>/<NETWORK>/paritydb-vX.Y.Z
.
Scenario 2: DB exists, but is not the latest version (migration!)
An attempt to migrate the database is made, provided that the migration is defined. If the migration is not defined, a warning is displayed and Forest will start under a new database. Alternatively, we can choose to fail the migration and not start the daemon.
If migration succeeds and the checks are passing, the database is atomically
renamed to <DATA_DIR>/<NETWORK>/paritydb-vX.Y.Z
and the daemon is started. If
the checks are not passing, the migration is cancelled, and we start the daemon
under a new database.
Scenario 3: DB exists and is the latest version
The daemon is started and the database path is not changed.
Scenario 4: DB exists and is newer than the daemon version
The daemon is started with a new database as migrating down is not supported.
Use cases
Developer, switching to newer branch (development mode)
An attempt will be made to open the database, it may succeed or not.
Developer, switching to older branch (development mode)
We don't support migrating down. An attempt will be made to open the database,
it may succeed or not. A possible development database is also the
<DATA_DIR>/<NETWORK>/paritydb
database (what we currently use).
CI, accepting PR with a breaking change
To test that a breaking change is detected and handled correctly, we can use
Forest image with the latest
tag.
- Run Forest with the
latest
tag, sync up to the HEAD. We can use volumes to share the database between runs. - Run Forest from the current branch (re-using the database), sync up to the HEAD.
If the database is compatible or a migration is successful, the second run should be able to sync up to the HEAD. Otherwise, the second run should fail.
CI, sync check
Sync check is constantly checking new edge
versions of Forest. It may be that
there are several breaking changes before we release a new version. Given that
supporting this may be a lot of work (reverts are permitted on main
) and that
we are not sure if we want to support this, we can simply use the development
mode database for this.
User, upgrading to a new version
See scenario 2.
User, starting a new node from scratch
See Scenario 4.
Migration
flowchart TB
MigrationExists{Migration exists?} -->|no| Fail[/Fail/]
MigrationExists -->|yes| RunMigration[Run migration]
RunMigration --> RunChecks[Run checks]
RunChecks --> ChecksPassing{Checks passing?}
ChecksPassing -->|yes| PersistDb[Persist DB] --> Finish[/Finish/]
ChecksPassing -->|no| Fail
Note: migration is run on a temporary database. If the checks are passing, the result is persisted.
Checking if migration exists
We can use a graph to check if a migration exists. There doesn't have to be a direct path between two versions.
flowchart TB
subgraph Basic with one version
Version1 -.- Version2
Version1 ==>|migration| Version2
end
subgraph Multiple versions direct migration
Version3 -.- Version4 -.- Version5
Version3 ==>|migration| Version5
end
subgraph Multiple versions indirect migration
Version6 -.- Version7 -.- Version8
Version6 ==>|migration| Version7 ==>|migration| Version8
end
linkStyle 1,4,7,8 stroke:#0f0,stroke-width:4px
Note: a migration may not exist by design.
Example migration
Let's consider the most basic change, a modification in the column settings. A possible algorithm for the migration is:
for each column in the current database:
for each entry in the column:
save the entry to a temporary database
if all entries are saved successfully:
rename the temporary database to the new database
remove the old database
else:
fail the migration
Performance considerations
The migration is run on a temporary database. This means that it requires twice the regular disk space.
Potential improvements
In development mode, we could potentially try to use the existing versioned database.