Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/walrus-service/node_config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ db_config:
per_object_pooled_blob_info: null
storage_pool_info: null
event_cursor: null
pending_recover_blobs: null
shard: null
shard_status: null
shard_sync_progress: null
Expand Down Expand Up @@ -150,6 +151,8 @@ blob_recovery:
node_connect_timeout_secs: 1
experimental_sliver_recovery_additional_symbols: 0
monitor_interval_secs: 60
max_concurrent_pending_recoveries: 100
pending_recovery_drain_interval_secs: 60
tls:
disable_tls: false
certificate_path: null
Expand Down
12 changes: 11 additions & 1 deletion crates/walrus-service/src/node/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,13 @@ pub struct BlobRecoveryConfig {
#[serde_as(as = "DurationSeconds")]
#[serde(rename = "monitor_interval_secs")]
pub monitor_interval: Duration,
/// The maximum number of concurrent blob syncs started by the pending-recovery executor.
pub max_concurrent_pending_recoveries: usize,
/// The fallback interval at which the pending-recovery executor re-scans the
/// pending-recovery table (it is additionally woken whenever a record is inserted).
#[serde_as(as = "DurationSeconds")]
#[serde(rename = "pending_recovery_drain_interval_secs")]
pub pending_recovery_drain_interval: Duration,
}

impl Default for BlobRecoveryConfig {
Expand All @@ -1048,16 +1055,19 @@ impl Default for BlobRecoveryConfig {
max_proof_cache_elements: 7_500,
committee_service_config: CommitteeServiceConfig::default(),
monitor_interval: Duration::from_mins(1),
max_concurrent_pending_recoveries: 100,
pending_recovery_drain_interval: Duration::from_mins(1),
}
}
}

impl BlobRecoveryConfig {
/// Returns a default configuration with a shorter monitor interval for testing.
/// Returns a default configuration with shorter intervals for testing.
#[cfg(any(test, feature = "test-utils"))]
pub fn default_for_test() -> Self {
Self {
monitor_interval: Duration::from_secs(5),
pending_recovery_drain_interval: Duration::from_secs(1),
..Default::default()
}
}
Expand Down
70 changes: 70 additions & 0 deletions crates/walrus-service/src/node/dbtool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ use crate::{
pending_cf_name,
},
storage::{
PendingRecoverBlob,
PrimarySliverData,
SecondarySliverData,
blob_info::{
Expand All @@ -74,6 +75,7 @@ use crate::{
garbage_collector_table_cf_name,
metadata_cf_name,
node_status_cf_name,
pending_recover_blobs_cf_name,
per_object_blob_info_cf_name,
per_object_pooled_blob_info_cf_name,
primary_slivers_column_family_name,
Expand Down Expand Up @@ -208,6 +210,20 @@ pub enum DbToolCommands {
count: usize,
},

/// Read pending-recovery records from the RocksDB database.
ReadPendingRecoverBlobs {
/// Path to the RocksDB database directory.
#[arg(long)]
db_path: PathBuf,
/// Start blob ID in URL-safe base64 format (no padding).
#[arg(long)]
#[serde_as(as = "Option<DisplayFromStr>")]
start_blob_id: Option<BlobId>,
/// Number of entries to scan.
#[arg(long, default_value = "1")]
count: usize,
},

/// Count the number of certified blobs in the RocksDB database.
CountCertifiedBlobs {
/// Path to the RocksDB database directory.
Expand Down Expand Up @@ -420,6 +436,11 @@ impl DbToolCommands {
start_object_id,
count,
} => read_object_blob_info(db_path, start_object_id, count),
Self::ReadPendingRecoverBlobs {
db_path,
start_blob_id,
count,
} => read_pending_recover_blobs(db_path, start_blob_id, count),
Self::CountCertifiedBlobs { db_path, epoch } => count_certified_blobs(db_path, epoch),
Self::DropColumnFamilies {
db_path,
Expand Down Expand Up @@ -750,6 +771,53 @@ fn read_blob_info(db_path: PathBuf, start_blob_id: Option<BlobId>, count: usize)
Ok(())
}

fn read_pending_recover_blobs(
db_path: PathBuf,
start_blob_id: Option<BlobId>,
count: usize,
) -> Result<()> {
let pending_recover_blobs_options =
DatabaseTableOptionsFactory::new(DatabaseConfig::default(), false).pending_recover_blobs();
let db = DB::open_cf_with_opts_for_read_only(
&RocksdbOptions::default(),
db_path,
[(
pending_recover_blobs_cf_name(),
pending_recover_blobs_options,
)],
false,
)?;

let cf = db
.cf_handle(pending_recover_blobs_cf_name())
.expect("pending recover blobs column family should exist");

let iter = if let Some(blob_id) = start_blob_id {
db.iterator_cf(
&cf,
rocksdb::IteratorMode::From(&be_fix_int_ser(&blob_id)?, rocksdb::Direction::Forward),
)
} else {
db.iterator_cf(&cf, rocksdb::IteratorMode::Start)
};

for result in iter.take(count) {
match result {
Ok((key, value)) => {
let blob_id: BlobId = bcs::from_bytes(&key)?;
let record: PendingRecoverBlob = bcs::from_bytes(&value)?;
println!("Blob ID: {blob_id}, PendingRecoverBlob: {record:?}");
}
Err(e) => {
println!("Error: {e:?}");
return Err(e.into());
}
}
}

Ok(())
}

fn read_object_blob_info(
db_path: PathBuf,
start_object_id: Option<ObjectID>,
Expand Down Expand Up @@ -1272,6 +1340,7 @@ fn report_storage_probe(db: &DB, column_families: &[String], exact_counts: bool)
node_status_cf_name(),
event_cursor_cf_name(),
event_index_cf_name(),
pending_recover_blobs_cf_name(),
garbage_collector_table_cf_name(),
] {
if column_families.iter().any(|name| name == cf_name) {
Expand Down Expand Up @@ -1517,6 +1586,7 @@ fn cf_options_for_name(
name if name == metadata_cf_name() => factory.metadata(),
name if name == node_status_cf_name() => factory.node_status(),
name if name == event_cursor_cf_name() => event_cursor_cf_options(factory),
name if name == pending_recover_blobs_cf_name() => factory.pending_recover_blobs(),
name if name == garbage_collector_table_cf_name() => factory.garbage_collector(),
_ => RocksdbOptions::default(),
}
Expand Down
10 changes: 10 additions & 0 deletions crates/walrus-service/src/node/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ walrus_utils::metrics::define_metric_set! {
#[help = "The number of blob recoveries currently pending"]
recover_blob_backlog: IntGaugeVec["state"],

#[help = "The number of records in the pending-recovery table"]
pending_recover_blob_count: U64Gauge[],

#[help = "The event index of the oldest record in the pending-recovery table, or 0 if \
the table is empty"]
pending_recover_blob_oldest_event_index: U64Gauge[],

#[help = "The number of pending-recovery records handled by the executor, by outcome"]
pending_recovery_executor_total: IntCounterVec["outcome"],

#[help = "Time (in seconds) spent processing events"]
event_process_duration_seconds: HistogramVec["event_type"],

Expand Down
52 changes: 52 additions & 0 deletions crates/walrus-service/src/node/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ pub(crate) use event_cursor_table::event_cursor_cf_options;

mod event_sequencer;
mod metrics;
mod pending_recover_blobs;
pub(crate) use pending_recover_blobs::PendingRecoverBlob;
use pending_recover_blobs::PendingRecoverBlobsTable;

mod shard;

pub(crate) use shard::{PrimarySliverData, SecondarySliverData, ShardStatus, ShardStorage};
Expand Down Expand Up @@ -342,6 +346,7 @@ pub struct Storage {
metadata: DBMap<BlobId, BlobMetadata>,
blob_info: BlobInfoTable,
event_cursor: EventCursorTable,
pending_recover_blobs: PendingRecoverBlobsTable,
garbage_collector_table: DBMap<String, Epoch>,
shards: Arc<RwLock<HashMap<ShardIndex, Arc<ShardStorage>>>>,
db_table_opts_factory: DatabaseTableOptionsFactory,
Expand Down Expand Up @@ -440,6 +445,8 @@ impl Storage {
let blob_info_column_families = BlobInfoTable::options(&db_table_opts_factory);
let (event_cursor_cf_name, event_cursor_options) =
EventCursorTable::options(&db_table_opts_factory);
let (pending_recover_blobs_cf_name, pending_recover_blobs_options) =
PendingRecoverBlobsTable::options(&db_table_opts_factory);
let garbage_collector_table_cf_name = garbage_collector_table_cf_name();
let garbage_collector_table_options = db_table_opts_factory.garbage_collector();

Expand All @@ -450,6 +457,7 @@ impl Storage {
(node_status_cf_name, node_status_options),
(metadata_cf_name, metadata_options),
(event_cursor_cf_name, event_cursor_options),
(pending_recover_blobs_cf_name, pending_recover_blobs_options),
(
garbage_collector_table_cf_name,
garbage_collector_table_options,
Expand Down Expand Up @@ -515,6 +523,7 @@ impl Storage {
)?;

let event_cursor = EventCursorTable::reopen(&database)?;
let pending_recover_blobs = PendingRecoverBlobsTable::reopen(&database)?;
let blob_info = BlobInfoTable::reopen(&database)?;
let shards = Arc::new(RwLock::new(
existing_shards_ids
Expand Down Expand Up @@ -543,6 +552,7 @@ impl Storage {
metadata,
blob_info,
event_cursor,
pending_recover_blobs,
garbage_collector_table,
shards,
db_table_opts_factory,
Expand Down Expand Up @@ -1508,6 +1518,48 @@ impl Storage {
self.blob_info.get_latest_handled_event_index()
}

/// Durably records that the blob needs to be recovered, overwriting any existing record, and
/// returns the number of pending-recovery records.
///
/// Must be called before the certify event is marked as complete: if the node crashes in
/// between, the event is replayed and the record is written again.
#[allow(dead_code)] // The callers land in a follow-up change.
pub(crate) fn insert_pending_recover_blob(
&self,
blob_id: &BlobId,
event_index: u64,
certified_epoch: Epoch,
) -> Result<u64, TypedStoreError> {
self.pending_recover_blobs.insert(
blob_id,
&PendingRecoverBlob::new(event_index, certified_epoch),
)
}

/// Deletes the pending-recovery record for the blob, if any, and returns the number of
/// remaining pending-recovery records.
#[allow(dead_code)] // The callers land in a follow-up change.
pub(crate) fn delete_pending_recover_blob(
&self,
blob_id: &BlobId,
) -> Result<u64, TypedStoreError> {
self.pending_recover_blobs.delete(blob_id)
}

/// Returns all pending-recovery records.
#[allow(dead_code)] // The callers land in a follow-up change.
pub(crate) fn scan_pending_recover_blobs(
&self,
) -> Result<Vec<(BlobId, PendingRecoverBlob)>, TypedStoreError> {
self.pending_recover_blobs.scan_all()
}

/// Returns the number of pending-recovery records.
#[allow(dead_code)] // The callers land in a follow-up change.
pub(crate) fn pending_recover_blob_count(&self) -> u64 {
self.pending_recover_blobs.count()
}

/// Clears the metadata in the storage for testing purposes.
#[cfg(test)]
pub fn clear_metadata_in_test(&self) -> Result<(), TypedStoreError> {
Expand Down
7 changes: 7 additions & 0 deletions crates/walrus-service/src/node/storage/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const EVENT_CURSOR_KEY: [u8; 6] = *b"cursor";
const GARBAGE_COLLECTOR_TABLE_COLUMN_FAMILY_NAME: &str = "garbage_collector_last_completed_epoch";
const GARBAGE_COLLECTOR_LAST_STARTED_EPOCH_KEY: &str = "started";
const GARBAGE_COLLECTOR_LAST_COMPLETED_EPOCH_KEY: &str = "completed";
const PENDING_RECOVER_BLOBS_COLUMN_FAMILY_NAME: &str = "pending_recover_blobs";

// Base name for shard-related column families
const SHARD_BASE_COLUMN_FAMILY_NAME: &str = "shard";
Expand Down Expand Up @@ -87,6 +88,11 @@ pub fn garbage_collector_last_completed_epoch_key() -> String {
GARBAGE_COLLECTOR_LAST_COMPLETED_EPOCH_KEY.to_string()
}

/// Returns the name of the pending recover blobs column family.
pub fn pending_recover_blobs_cf_name() -> &'static str {
PENDING_RECOVER_BLOBS_COLUMN_FAMILY_NAME
}

/// Returns the column family name for primary slivers of a shard.
pub fn primary_slivers_column_family_name(id: ShardIndex) -> String {
format!(
Expand Down Expand Up @@ -144,6 +150,7 @@ mod tests {
assert_eq!(per_object_blob_info_cf_name(), "per_object_blob_info");
assert_eq!(node_status_cf_name(), "node_status");
assert_eq!(event_index_cf_name(), "latest_handled_event_index");
assert_eq!(pending_recover_blobs_cf_name(), "pending_recover_blobs");

let shard = ShardIndex(900);
assert_eq!(base_column_family_name(shard), "shard-900");
Expand Down
13 changes: 13 additions & 0 deletions crates/walrus-service/src/node/storage/database_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ pub struct DatabaseConfig {
pub(super) storage_pool_info: Option<DatabaseTableOptions>,
/// Event cursor database options.
pub(super) event_cursor: Option<DatabaseTableOptions>,
/// Pending recover blobs database options.
pub(super) pending_recover_blobs: Option<DatabaseTableOptions>,
/// Shard database options.
pub(super) shard: Option<DatabaseTableOptions>,
/// Shard status database options.
Expand Down Expand Up @@ -468,6 +470,11 @@ impl DatabaseConfig {
Self::inherit_from_or_use_template(&self.event_cursor, self.standard())
}

/// Returns the pending recover blobs database option.
pub fn pending_recover_blobs(&self) -> DatabaseTableOptions {
Self::inherit_from_or_use_template(&self.pending_recover_blobs, self.standard())
}

/// Returns the shard database option.
pub fn shard(&self) -> DatabaseTableOptions {
Self::inherit_from_or_use_template(&self.shard, self.optimized_for_blobs())
Expand Down Expand Up @@ -549,6 +556,7 @@ impl Default for DatabaseConfig {
per_object_pooled_blob_info: None,
storage_pool_info: None,
event_cursor: None,
pending_recover_blobs: None,
shard: None,
shard_status: None,
shard_sync_progress: None,
Expand Down Expand Up @@ -771,6 +779,11 @@ impl DatabaseTableOptionsFactory {
self.to_options(&self.config.event_cursor(), false)
}

/// Returns the pending recover blobs database option.
pub fn pending_recover_blobs(&self) -> Options {
self.to_options(&self.config.pending_recover_blobs(), false)
}

// Below 4 options are for shard storage column families.

/// Returns the shard database option with shared cache.
Expand Down
Loading
Loading