code/pallets/subtensor/src/migrations/migrate_storage_bloat_v2.rs

migrate_storage_bloat_v2.rs

226 lines · 7,452 bytes · 71136ad109RawGitHub
use super::*;
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::{storage_alias, traits::Get, weights::Weight};
use scale_info::TypeInfo;
use scale_info::prelude::string::String;
use sp_io::{hashing::twox_128, storage};
use sp_std::vec::Vec;

const MIGRATION_NAME: &[u8] = b"migrate_storage_bloat_v2";

#[derive(Clone, Copy, PartialEq, Eq)]
enum CleanupMode {
    Clear,
    ClearIfZero,
}

#[derive(Clone, Copy)]
struct CleanupTarget {
    pallet: &'static str,
    storage: &'static str,
    mode: CleanupMode,
}

// The undeclared prefixes are dead pre-dTAO state. The remaining targets use ValueQuery (or an
// OptionQuery whose consumer treats a missing value as zero), so deleting a zero/default row does
// not change the value observed by runtime callers.
const TARGETS: &[CleanupTarget] = &[
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "TotalHotkeyStake",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "PendingdHotkeyEmission",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "PendingdHotkeyEmissionUntouchable",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "LastHotkeyEmissionDrain",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "StakeDeltaSinceLastEmissionDrain",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "TotalColdkeyStake",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "LastAddStakeIncrease",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "ColdkeyArbitrationBlock",
        mode: CleanupMode::Clear,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "Alpha",
        mode: CleanupMode::ClearIfZero,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "TotalHotkeyShares",
        mode: CleanupMode::ClearIfZero,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "TotalHotkeyAlpha",
        mode: CleanupMode::ClearIfZero,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "TotalHotkeyAlphaLastEpoch",
        mode: CleanupMode::ClearIfZero,
    },
    CleanupTarget {
        pallet: "SubtensorModule",
        storage: "StakingHotkeys",
        mode: CleanupMode::ClearIfZero,
    },
];

/// Persistent progress for the bounded storage cleanup.
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo)]
pub struct StorageBloatCleanupProgress {
    /// Index into [`TARGETS`].
    pub target: u16,
    /// Last raw key visited in the current prefix. An empty cursor starts at the prefix.
    pub cursor: Vec<u8>,
    /// Rows inspected across all passes.
    pub scanned: u64,
    /// Rows removed across all passes.
    pub removed: u64,
}

#[storage_alias]
pub type StorageBloatCleanupMigration<T: Config> =
    StorageValue<Pallet<T>, StorageBloatCleanupProgress, OptionQuery>;

fn storage_prefix(pallet: &str, item: &str) -> Vec<u8> {
    [twox_128(pallet.as_bytes()), twox_128(item.as_bytes())].concat()
}

fn is_zero_value(value: &[u8]) -> bool {
    !value.is_empty() && value.iter().all(|byte| *byte == 0)
}

fn scan_item_weight<T: Config>(mode: CleanupMode) -> Weight {
    match mode {
        // next_key + clear
        CleanupMode::Clear => T::DbWeight::get().reads_writes(1, 1),
        // next_key + get + a conservatively charged clear
        CleanupMode::ClearIfZero => T::DbWeight::get().reads_writes(2, 1),
    }
}

/// Starts the cleanup without doing any unbounded work in the runtime-upgrade block.
pub fn kickoff_storage_bloat_cleanup<T: Config>() -> Weight {
    let mut weight = T::DbWeight::get().reads(2);
    if HasMigrationRun::<T>::get(MIGRATION_NAME) || StorageBloatCleanupMigration::<T>::exists() {
        return weight;
    }

    StorageBloatCleanupMigration::<T>::put(StorageBloatCleanupProgress {
        target: 0,
        cursor: Vec::new(),
        scanned: 0,
        removed: 0,
    });
    weight.saturating_accrue(T::DbWeight::get().writes(1));
    log::info!(
        "Migration '{}' scheduled for bounded on_idle execution",
        String::from_utf8_lossy(MIGRATION_NAME)
    );
    weight
}
/// Continues the cleanup using no more than the supplied remaining block weight.
pub fn continue_storage_bloat_cleanup<T: Config>(limit: Weight) -> Weight {
    // Cursor read plus either a cursor write, or the completion marker and cursor removal.
    let pass_overhead = T::DbWeight::get().reads_writes(1, 2);
    if !pass_overhead.all_lte(limit) {
        return Weight::zero();
    }

    let Some(mut progress) = StorageBloatCleanupMigration::<T>::get() else {
        return T::DbWeight::get().reads(1);
    };
    let work_limit = limit.saturating_sub(pass_overhead);
    let mut work_weight = Weight::zero();

    while let Some(target) = TARGETS.get(usize::from(progress.target)).copied() {
        let item_weight = scan_item_weight::<T>(target.mode);
        if !work_weight.saturating_add(item_weight).all_lte(work_limit) {
            break;
        }

        let prefix = storage_prefix(target.pallet, target.storage);
        let start = if progress.cursor.is_empty() {
            &prefix
        } else {
            &progress.cursor
        };
        let Some(next_key) = storage::next_key(start) else {
            work_weight.saturating_accrue(T::DbWeight::get().reads(1));
            progress.target = progress.target.saturating_add(1);
            progress.cursor.clear();
            continue;
        };

        if !next_key.starts_with(&prefix) {
            work_weight.saturating_accrue(T::DbWeight::get().reads(1));
            log::info!(
                "Migration '{}' finished {}::{} (scanned {}, removed {} total)",
                String::from_utf8_lossy(MIGRATION_NAME),
                target.pallet,
                target.storage,
                progress.scanned,
                progress.removed,
            );
            progress.target = progress.target.saturating_add(1);
            progress.cursor.clear();
            continue;
        }

        let should_clear = match target.mode {
            CleanupMode::Clear => true,
            CleanupMode::ClearIfZero => storage::get(&next_key)
                .as_deref()
                .is_some_and(is_zero_value),
        };
        if should_clear {
            storage::clear(&next_key);
            progress.removed = progress.removed.saturating_add(1);
        }
        progress.cursor = next_key;
        progress.scanned = progress.scanned.saturating_add(1);
        work_weight.saturating_accrue(item_weight);
    }

    if usize::from(progress.target) == TARGETS.len() {
        HasMigrationRun::<T>::insert(MIGRATION_NAME, true);
        StorageBloatCleanupMigration::<T>::kill();
        log::info!(
            "Migration '{}' completed: scanned {}, removed {} rows",
            String::from_utf8_lossy(MIGRATION_NAME),
            progress.scanned,
            progress.removed,
        );
    } else {
        StorageBloatCleanupMigration::<T>::put(progress);
    }

    pass_overhead.saturating_add(work_weight)
}