-
Notifications
You must be signed in to change notification settings - Fork 157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Database versioning and migration #3255
Changes from 28 commits
654a5f1
19abb27
92d7391
6ffb5e7
83a5b0e
9d75b30
aa7e4de
d087781
dd1888a
85ba181
9b5cc33
cefd847
1cbedd2
91d2b62
6ea25cf
9e1fd70
3948dbc
19592d0
707e512
256cde7
38351aa
57e0f15
5f6f7a8
92e3eec
151458a
2bd40f8
77d453a
5282c56
94dbd37
cbfc575
3056580
583cb8d
18a6140
a7d126a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: Migration Check | ||
|
||
# Cancel workflow if there is a new change to the branch. | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | ||
|
||
on: | ||
workflow_dispatch: | ||
pull_request: | ||
branches: | ||
- main | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
migration-check: | ||
name: Forest database migration checks | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout sources | ||
uses: actions/checkout@v3 | ||
- name: Setup sccache | ||
uses: mozilla-actions/[email protected] | ||
timeout-minutes: ${{ fromJSON(env.CACHE_TIMEOUT_MINUTES) }} | ||
continue-on-error: true | ||
- name: migration check | ||
run: ./scripts/migration_check.sh |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Steps to add support for a new database version: | ||
|
||
- Add a new enum variant for new database version in `DBVersion`. | ||
- Update `get_db_version` to include newly added enum variant. | ||
- Add version transition for each DBVersion in `migrate_db` method. | ||
- Add steps required for new migration in `migrate` method. In each migration | ||
step, you can either do in place migration or use temp_db/ to migrate data | ||
from existing db but finally it must atomically rename temp_db/ back to | ||
existing db name. | ||
- Update `LATEST_DB_VERSION` to latest database version. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
#!/usr/bin/env bash | ||
set -euo pipefail | ||
|
||
# Function to sync using a specific tag | ||
function sync_with_tag() { | ||
local tag=$1 | ||
echo "Syncing using tag ($tag)..." | ||
|
||
# Create the download URL for the forest release | ||
URL="https://github.com/ChainSafe/forest/releases/download/${tag}/forest-${tag}-linux-amd64.zip" | ||
# Download the release using curl | ||
curl -LJO "${URL}" | ||
|
||
# Unzip the downloaded file | ||
unzip "forest-${tag}-linux-amd64.zip" | ||
cd "forest-${tag}" | ||
|
||
# Run forest daemon | ||
./forest --chain calibnet --encrypt-keystore false --auto-download-snapshot --detach | ||
|
||
# Check if the sync succeeded for the tag | ||
if ./forest-cli --chain calibnet sync wait; then | ||
echo "Sync successful for tag: $tag" | ||
pkill -9 forest | ||
# clean up | ||
cd .. | ||
rm "forest-${tag}-linux-amd64.zip" "forest-${tag}" -rf | ||
sleep 5s | ||
else | ||
echo "Sync failed for tag: $tag" | ||
exit 1 | ||
fi | ||
} | ||
|
||
# DB Migration are supported v0.11.1 onwards | ||
START_TAG="v0.11.1" | ||
|
||
# Fetch the latest tags from the remote repository | ||
git fetch --tags | ||
|
||
# Get a list of all tags sorted chronologically | ||
tags=$(git tag --sort=creatordate) | ||
# Get latest tag | ||
LATEST_TAG="" | ||
|
||
# Database migration are not supported for forest version below `v0.11.1` | ||
is_tag_valid=false | ||
|
||
echo "Testing db migrations from v0.11.1 to latest, one by one" | ||
# Loop through each tag and sync with corresponding version | ||
for tag in $tags; do | ||
# Check if the current tag matches the start tag | ||
if [ "$tag" = "$START_TAG" ]; then | ||
is_tag_valid=true | ||
fi | ||
if $is_tag_valid; then | ||
# Run sync check with the current tag | ||
sync_with_tag "$tag" | ||
fi | ||
LATEST_TAG="$tag" | ||
done | ||
|
||
echo "Testing db migration from v0.11.1 to latest, at once" | ||
# Sync calibnet with Forest `V0.11.1` | ||
sync_with_tag "$START_TAG" | ||
# Sync calibnet with latest version of Forest | ||
sync_with_tag "$LATEST_TAG" | ||
|
||
echo "Migration check completed successfully." |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Copyright 2019-2023 ChainSafe Systems | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
use super::db_engine::{db_root, open_proxy_db}; | ||
use crate::chain::ChainStore; | ||
use crate::cli_shared::{chain_path, cli::Config}; | ||
use crate::fil_cns::composition as cns; | ||
use crate::genesis::read_genesis_header; | ||
use crate::state_manager::StateManager; | ||
use crate::utils::proofs_api::paramfetch::{ | ||
ensure_params_downloaded, set_proofs_parameter_cache_dir_env, | ||
}; | ||
use std::fs; | ||
use std::path::{Path, PathBuf}; | ||
use std::sync::Arc; | ||
use tracing::info; | ||
|
||
pub const LATEST_DB_VERSION: DBVersion = DBVersion::V11; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why manually define this? It could be inferred from the version number of Forest. |
||
|
||
/// Database version for each forest version which supports db migration | ||
#[derive(Debug, Eq, PartialEq)] | ||
pub enum DBVersion { | ||
V0, // Default DBVersion for any unknown db | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the version is unknown, let's call it |
||
V11, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this version 11? Forest is at version |
||
} | ||
|
||
/// Check to verify database migrations | ||
async fn migration_check(config: &Config, existing_chain_data_root: &Path) -> anyhow::Result<()> { | ||
info!( | ||
"Running database migration checks for: {}", | ||
existing_chain_data_root.display() | ||
); | ||
// Set proof param dir env path, required for running validations | ||
if cns::FETCH_PARAMS { | ||
set_proofs_parameter_cache_dir_env(&config.client.data_dir); | ||
} | ||
ensure_params_downloaded().await?; | ||
|
||
// Open db | ||
let db = Arc::new(open_proxy_db( | ||
db_root(existing_chain_data_root), | ||
config.db_config().clone(), | ||
)?); | ||
let genesis = read_genesis_header(None, config.chain.genesis_bytes(), &db).await?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably want |
||
let chain_store = Arc::new(ChainStore::new( | ||
db, | ||
Arc::clone(&config.chain), | ||
genesis, | ||
existing_chain_data_root, | ||
)?); | ||
let state_manager = Arc::new(StateManager::new(chain_store, Arc::clone(&config.chain))?); | ||
|
||
let ts = state_manager.chain_store().heaviest_tipset(); | ||
let height = ts.epoch(); | ||
assert!(height.is_positive()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand the purpose of this assertion. More documentation is needed here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not required, removed |
||
// re-compute 100 tipsets only | ||
state_manager.validate_range((height - 100)..=height)?; | ||
Ok(()) | ||
} | ||
|
||
/// Migrate database to latest version | ||
pub async fn migrate_db( | ||
config: &Config, | ||
db_path: PathBuf, | ||
target_version: DBVersion, | ||
) -> anyhow::Result<()> { | ||
info!("Running database migrations..."); | ||
// Get DBVersion from existing db path | ||
let mut current_version = get_db_version(&db_path); | ||
// Run pre-migration checks, which includes: | ||
// - re-compute 100 tipsets | ||
migration_check(config, &db_path).await?; | ||
|
||
// Iterate over all DBVersion's until database is migrated to lastest version | ||
while current_version != target_version { | ||
let next_version = match current_version { | ||
DBVersion::V0 => DBVersion::V11, | ||
_ => break, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need help understanding the details of this code. The list of Forest versions is static, and we shouldn't have to specify the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am maintaining the list of versions now |
||
// Execute the migration steps for itermediate version | ||
migrate(&db_path, &next_version)?; | ||
current_version = next_version; | ||
} | ||
// Run post-migration checks, which includes: | ||
// - re-compute 100 tipsets | ||
migration_check(config, &db_path).await?; | ||
|
||
// Rename db to latest versioned db | ||
fs::rename(db_path.as_path(), chain_path(config))?; | ||
|
||
info!("Database Successfully Migrated to {:?}", target_version); | ||
Ok(()) | ||
} | ||
|
||
// TODO: Add Steps required for new migration | ||
/// Migrate to an intermediate db version | ||
fn migrate(_existing_db_path: &Path, next_version: &DBVersion) -> anyhow::Result<()> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like you want the migrations to happen inside the database folder. That's not safe. If something goes wrong, we want to revert back to the last known good state. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am currently using |
||
match next_version { | ||
DBVersion::V11 => Ok(()), | ||
_ => Ok(()), | ||
} | ||
} | ||
|
||
/// Checks if another database already exist | ||
pub fn check_if_another_db_exist(config: &Config) -> Option<PathBuf> { | ||
let dir = PathBuf::from(&config.client.data_dir).join(config.chain.network.to_string()); | ||
let paths = fs::read_dir(&dir).ok()?; | ||
for dir in paths.flatten() { | ||
let path = dir.path(); | ||
if path.is_dir() && path != chain_path(config) { | ||
return Some(path); | ||
} | ||
} | ||
None | ||
} | ||
|
||
/// Returns `DBVersion` for the given database path. | ||
fn get_db_version(db_path: &Path) -> DBVersion { | ||
match db_path | ||
.parent() | ||
.and_then(|parent_path| parent_path.file_name()) | ||
{ | ||
Some(dir_name) => match dir_name.to_str() { | ||
Some(name) if name.starts_with("0.11") => DBVersion::V11, | ||
_ => DBVersion::V0, // Defaults to V0 | ||
}, | ||
None => DBVersion::V0, // Defaults to V0 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When is the database ever put in the
dev
folder?