mod rebuild;
pub mod report;
use std::path::PathBuf;
use std::time::Instant;
use crate::tree::TreePolicy;
use super::config::{
DEFAULT_INTERNAL_TARGET_BYTES, DEFAULT_LEAF_TARGET_BYTES, FenceView, MigrationState,
finalize_v1, finalize_v2, flip_to_abort, install_forward_fence, read_state,
};
use super::lock::DataDirLock;
use super::{DatabaseConfig, DatabaseError};
pub use report::{MigrationDirection, MigrationOutcome, MigrationReport};
use rebuild::{BranchRebuild, ShardRebuild};
#[cfg(test)]
mod test_support;
#[cfg(test)]
#[path = "migrate_tests.rs"]
mod migrate_tests;
#[cfg(test)]
#[path = "crash_tests.rs"]
mod crash_tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MigrateMode {
Migrate,
Abort,
}
#[derive(Debug, Clone)]
pub struct MigrateOptions {
pub data_dir: PathBuf,
pub mode: MigrateMode,
}
impl MigrateOptions {
#[must_use]
pub const fn migrate(data_dir: PathBuf) -> Self {
Self {
data_dir,
mode: MigrateMode::Migrate,
}
}
#[must_use]
pub const fn abort(data_dir: PathBuf) -> Self {
Self {
data_dir,
mode: MigrateMode::Abort,
}
}
}
pub fn migrate_chunking(options: &MigrateOptions) -> Result<MigrationReport, DatabaseError> {
run(options, None)?.map_or_else(
|| {
Err(DatabaseError::MigrationFailed(
"internal invariant: the migration stopped early with no crash injection"
.to_owned(),
))
},
Ok,
)
}
fn v2_targets(state: &MigrationState) -> (u64, u64) {
state
.v2_targets
.unwrap_or((DEFAULT_LEAF_TARGET_BYTES, DEFAULT_INTERNAL_TARGET_BYTES))
}
enum Plan {
NoOp(MigrationDirection),
Forward { install_fence: bool },
Abort { flip: bool },
}
fn plan(mode: MigrateMode, state: &MigrationState) -> Plan {
match (&state.fence, mode) {
(
Some(FenceView {
target_is_v2: true, ..
}),
MigrateMode::Migrate,
) => Plan::Forward {
install_fence: false,
},
(
Some(FenceView {
target_is_v2: true, ..
}),
MigrateMode::Abort,
) => Plan::Abort { flip: true },
(
Some(FenceView {
target_is_v2: false,
..
}),
_,
) => Plan::Abort { flip: false },
(None, MigrateMode::Migrate) => {
if is_v2(state) {
Plan::NoOp(MigrationDirection::Forward)
} else {
Plan::Forward {
install_fence: true,
}
}
}
(None, MigrateMode::Abort) => {
if is_v2(state) {
Plan::Abort { flip: true }
} else {
Plan::NoOp(MigrationDirection::Abort)
}
}
}
}
fn is_v2(state: &MigrationState) -> bool {
state.format_version == Some(super::config::ON_DISK_FORMAT_VERSION)
&& state.v2_targets.is_some()
}
fn run(
options: &MigrateOptions,
crash_after: Option<&str>,
) -> Result<Option<MigrationReport>, DatabaseError> {
let started = Instant::now();
let _lock = DataDirLock::acquire(&options.data_dir)?;
let state = read_state(&options.data_dir)?;
match plan(options.mode, &state) {
Plan::NoOp(direction) => Ok(Some(no_op_report(options, &state, direction, started))),
Plan::Forward { install_fence } => {
run_forward(options, &state, install_fence, crash_after, started)
}
Plan::Abort { flip } => run_abort(options, &state, flip, crash_after, started),
}
}
fn no_op_report(
options: &MigrateOptions,
state: &MigrationState,
direction: MigrationDirection,
started: Instant,
) -> MigrationReport {
base_report(
options,
state,
direction,
MigrationOutcome::AlreadyInTargetFormat,
&Tally::default(),
started,
)
}
#[derive(Default)]
struct Tally {
shards_rebuilt: usize,
shards_unchanged: usize,
shards_empty: usize,
branch_heads_rebuilt: usize,
branch_heads_unchanged: usize,
}
fn base_report(
options: &MigrateOptions,
state: &MigrationState,
direction: MigrationDirection,
outcome: MigrationOutcome,
tally: &Tally,
started: Instant,
) -> MigrationReport {
let (source_policy, target_policy) = match direction {
MigrationDirection::Forward => ("count-target-v1", "bytes-v2"),
MigrationDirection::Abort => ("bytes-v2", "count-target-v1"),
};
MigrationReport {
data_dir: options.data_dir.clone(),
outcome,
direction,
source_policy: source_policy.to_owned(),
target_policy: target_policy.to_owned(),
shard_count: state.config.shard_count,
shards_rebuilt: tally.shards_rebuilt,
shards_unchanged: tally.shards_unchanged,
shards_empty: tally.shards_empty,
branch_heads_rebuilt: tally.branch_heads_rebuilt,
branch_heads_unchanged: tally.branch_heads_unchanged,
duration_ms: started.elapsed().as_millis(),
vacuum_before_migrate: report::VACUUM_BEFORE_MIGRATE,
manifest_bracket: report::MANIFEST_BRACKET,
}
}
fn rebuild_all(
config: &DatabaseConfig,
policy: TreePolicy,
crash_after: Option<&str>,
tally: &mut Tally,
shard_label: impl Fn(usize) -> String,
all_shards_label: &str,
) -> Result<Option<()>, DatabaseError> {
let data_dir = &config.data_dir;
for shard_id in 0..config.shard_count {
match rebuild::rebuild_shard(data_dir, shard_id, policy)? {
ShardRebuild::Rebuilt => tally.shards_rebuilt += 1,
ShardRebuild::Unchanged => tally.shards_unchanged += 1,
ShardRebuild::Empty => tally.shards_empty += 1,
}
if crash_after == Some(shard_label(shard_id).as_str()) {
return Ok(None);
}
}
if crash_after == Some(all_shards_label) {
return Ok(None);
}
for (index, name) in rebuild::branch_names(data_dir)?.into_iter().enumerate() {
match rebuild::rebuild_one_branch(data_dir, &name, policy)? {
BranchRebuild::Rebuilt => tally.branch_heads_rebuilt += 1,
BranchRebuild::Unchanged => tally.branch_heads_unchanged += 1,
}
if crash_after == Some(format!("after_branch_head_{index}").as_str()) {
return Ok(None);
}
}
Ok(Some(()))
}
fn run_forward(
options: &MigrateOptions,
state: &MigrationState,
install_fence: bool,
crash_after: Option<&str>,
started: Instant,
) -> Result<Option<MigrationReport>, DatabaseError> {
let config = &state.config;
let (leaf, internal) = v2_targets(state);
if install_fence {
install_forward_fence(config, leaf, internal)?;
if crash_after == Some("after_forward_fence") {
return Ok(None);
}
}
let policy = TreePolicy::v2(leaf, internal);
let mut tally = Tally::default();
if rebuild_all(
config,
policy,
crash_after,
&mut tally,
|shard_id| format!("after_shard_{shard_id}"),
"after_all_shards",
)?
.is_none()
{
return Ok(None);
}
if crash_after == Some("before_fence_removal") {
return Ok(None);
}
finalize_v2(config, leaf, internal)?;
Ok(Some(base_report(
options,
state,
MigrationDirection::Forward,
MigrationOutcome::MigratedToV2,
&tally,
started,
)))
}
fn run_abort(
options: &MigrateOptions,
state: &MigrationState,
flip: bool,
crash_after: Option<&str>,
started: Instant,
) -> Result<Option<MigrationReport>, DatabaseError> {
let config = &state.config;
let (leaf, internal) = v2_targets(state);
if flip {
flip_to_abort(config, leaf, internal)?;
if crash_after == Some("after_abort_flip") {
return Ok(None);
}
}
let mut tally = Tally::default();
if rebuild_all(
config,
TreePolicy::V1_DEFAULT,
crash_after,
&mut tally,
|shard_id| format!("after_abort_shard_{shard_id}"),
"after_all_abort_shards",
)?
.is_none()
{
return Ok(None);
}
if crash_after == Some("before_v1_finalize") {
return Ok(None);
}
finalize_v1(config)?;
Ok(Some(base_report(
options,
state,
MigrationDirection::Abort,
MigrationOutcome::AbortedToV1,
&tally,
started,
)))
}
#[cfg(test)]
fn migrate_with_crash(
options: &MigrateOptions,
crash_after: &str,
) -> Result<Option<MigrationReport>, DatabaseError> {
run(options, Some(crash_after))
}