Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit fca44b7

Browse files
mergify[bot]steviez
and
steviez
authored
v1.17: Allow Blockstore to open unknown columns (backport of #34174) (#34287)
* Allow Blockstore to open unknown columns (#34174) As we develop new features or modifications, we occassionally need to introduce new columns to the Blockstore. Adding a new column introduces a compatibility break given that opening the database in Primary mode (R/W access) requires opening all columns. Reverting to an old software version that is unaware of the new column is obviously problematic. In the past, we have addressed by backporting minimal "stub" PR's to older versions. This is annoying, and only allow compatibility for the single version or two that we backport to. This PR adds a change to automatically detect all columns, and create default column descriptors for columns we were unaware of. As a result, older software versions can open a Blockstore that was modified by a newer software version, even if that new version added columns that the old version is unaware of. (cherry picked from commit 71c1782) # Conflicts: # ledger/src/blockstore_db.rs * Merge conflicts --------- Co-authored-by: steviez <[email protected]>
1 parent 4abd18f commit fca44b7

File tree

1 file changed

+109
-11
lines changed

1 file changed

+109
-11
lines changed

ledger/src/blockstore_db.rs

+109-11
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use {
3434
},
3535
solana_storage_proto::convert::generated,
3636
std::{
37-
collections::HashMap,
37+
collections::{HashMap, HashSet},
3838
ffi::{CStr, CString},
3939
fs,
4040
marker::PhantomData,
@@ -376,15 +376,12 @@ impl Rocks {
376376
}
377377
let oldest_slot = OldestSlot::default();
378378
let column_options = options.column_options.clone();
379+
let cf_descriptors = Self::cf_descriptors(path, &options, &oldest_slot);
379380

380381
// Open the database
381382
let db = match access_type {
382383
AccessType::Primary | AccessType::PrimaryForMaintenance => Rocks {
383-
db: DB::open_cf_descriptors(
384-
&db_options,
385-
path,
386-
Self::cf_descriptors(&options, &oldest_slot),
387-
)?,
384+
db: DB::open_cf_descriptors(&db_options, path, cf_descriptors)?,
388385
access_type,
389386
oldest_slot,
390387
column_options,
@@ -404,7 +401,7 @@ impl Rocks {
404401
&db_options,
405402
path,
406403
&secondary_path,
407-
Self::cf_descriptors(&options, &oldest_slot),
404+
cf_descriptors,
408405
)?,
409406
access_type,
410407
oldest_slot,
@@ -418,15 +415,25 @@ impl Rocks {
418415
Ok(db)
419416
}
420417

418+
/// Create the column family (CF) descriptors necessary to open the database.
419+
///
420+
/// In order to open a RocksDB database with Primary access, all columns must be opened. So,
421+
/// in addition to creating descriptors for all of the expected columns, also create
422+
/// descriptors for columns that were discovered but are otherwise unknown to the software.
423+
///
424+
/// One case where columns could be unknown is if a RocksDB database is modified with a newer
425+
/// software version that adds a new column, and then also opened with an older version that
426+
/// did not have knowledge of that new column.
421427
fn cf_descriptors(
428+
path: &Path,
422429
options: &BlockstoreOptions,
423430
oldest_slot: &OldestSlot,
424431
) -> Vec<ColumnFamilyDescriptor> {
425432
use columns::*;
426433

427434
let (cf_descriptor_shred_data, cf_descriptor_shred_code) =
428435
new_cf_descriptor_pair_shreds::<ShredData, ShredCode>(options, oldest_slot);
429-
vec![
436+
let mut cf_descriptors = vec![
430437
new_cf_descriptor::<SlotMeta>(options, oldest_slot),
431438
new_cf_descriptor::<DeadSlots>(options, oldest_slot),
432439
new_cf_descriptor::<DuplicateSlots>(options, oldest_slot),
@@ -447,7 +454,52 @@ impl Rocks {
447454
new_cf_descriptor::<BlockHeight>(options, oldest_slot),
448455
new_cf_descriptor::<ProgramCosts>(options, oldest_slot),
449456
new_cf_descriptor::<OptimisticSlots>(options, oldest_slot),
450-
]
457+
];
458+
459+
// If the access type is Secondary, we don't need to open all of the
460+
// columns so we can just return immediately.
461+
match options.access_type {
462+
AccessType::Secondary => {
463+
return cf_descriptors;
464+
}
465+
AccessType::Primary | AccessType::PrimaryForMaintenance => {}
466+
}
467+
468+
// Attempt to detect the column families that are present. It is not a
469+
// fatal error if we cannot, for example, if the Blockstore is brand
470+
// new and will be created by the call to Rocks::open().
471+
let detected_cfs = match DB::list_cf(&Options::default(), path) {
472+
Ok(detected_cfs) => detected_cfs,
473+
Err(err) => {
474+
warn!("Unable to detect Rocks columns: {err:?}");
475+
vec![]
476+
}
477+
};
478+
// The default column is handled automatically, we don't need to create
479+
// a descriptor for it
480+
const DEFAULT_COLUMN_NAME: &str = "default";
481+
let known_cfs: HashSet<_> = cf_descriptors
482+
.iter()
483+
.map(|cf_descriptor| cf_descriptor.name().to_string())
484+
.chain(std::iter::once(DEFAULT_COLUMN_NAME.to_string()))
485+
.collect();
486+
detected_cfs.iter().for_each(|cf_name| {
487+
if known_cfs.get(cf_name.as_str()).is_none() {
488+
info!("Detected unknown column {cf_name}, opening column with basic options");
489+
// This version of the software was unaware of the column, so
490+
// it is fair to assume that we will not attempt to read or
491+
// write the column. So, set some bare bones settings to avoid
492+
// using extra resources on this unknown column.
493+
let mut options = Options::default();
494+
// Lower the default to avoid unnecessary allocations
495+
options.set_write_buffer_size(1024 * 1024);
496+
// Disable compactions to avoid any modifications to the column
497+
options.set_disable_auto_compactions(true);
498+
cf_descriptors.push(ColumnFamilyDescriptor::new(cf_name, options));
499+
}
500+
});
501+
502+
cf_descriptors
451503
}
452504

453505
fn columns() -> Vec<&'static str> {
@@ -1982,7 +2034,9 @@ fn should_enable_compression<C: 'static + Column + ColumnName>() -> bool {
19822034

19832035
#[cfg(test)]
19842036
pub mod tests {
1985-
use {super::*, crate::blockstore_db::columns::ShredData};
2037+
use {
2038+
super::*, crate::blockstore_db::columns::ShredData, std::path::PathBuf, tempfile::tempdir,
2039+
};
19862040

19872041
#[test]
19882042
fn test_compaction_filter() {
@@ -2034,14 +2088,15 @@ pub mod tests {
20342088

20352089
#[test]
20362090
fn test_cf_names_and_descriptors_equal_length() {
2091+
let path = PathBuf::default();
20372092
let options = BlockstoreOptions::default();
20382093
let oldest_slot = OldestSlot::default();
20392094
// The names and descriptors don't need to be in the same order for our use cases;
20402095
// however, there should be the same number of each. For example, adding a new column
20412096
// should update both lists.
20422097
assert_eq!(
20432098
Rocks::columns().len(),
2044-
Rocks::cf_descriptors(&options, &oldest_slot).len()
2099+
Rocks::cf_descriptors(&path, &options, &oldest_slot).len()
20452100
);
20462101
}
20472102

@@ -2065,4 +2120,47 @@ pub mod tests {
20652120
});
20662121
assert!(!should_enable_cf_compaction("something else"));
20672122
}
2123+
2124+
#[test]
2125+
fn test_open_unknown_columns() {
2126+
solana_logger::setup();
2127+
2128+
let temp_dir = tempdir().unwrap();
2129+
let db_path = temp_dir.path();
2130+
2131+
// Open with Primary to create the new database
2132+
{
2133+
let options = BlockstoreOptions {
2134+
access_type: AccessType::Primary,
2135+
enforce_ulimit_nofile: false,
2136+
..BlockstoreOptions::default()
2137+
};
2138+
let mut rocks = Rocks::open(db_path, options).unwrap();
2139+
2140+
// Introduce a new column that will not be known
2141+
rocks
2142+
.db
2143+
.create_cf("new_column", &Options::default())
2144+
.unwrap();
2145+
}
2146+
2147+
// Opening with either Secondary or Primary access should succeed,
2148+
// even though the Rocks code is unaware of "new_column"
2149+
{
2150+
let options = BlockstoreOptions {
2151+
access_type: AccessType::Secondary,
2152+
enforce_ulimit_nofile: false,
2153+
..BlockstoreOptions::default()
2154+
};
2155+
let _ = Rocks::open(db_path, options).unwrap();
2156+
}
2157+
{
2158+
let options = BlockstoreOptions {
2159+
access_type: AccessType::Primary,
2160+
enforce_ulimit_nofile: false,
2161+
..BlockstoreOptions::default()
2162+
};
2163+
let _ = Rocks::open(db_path, options).unwrap();
2164+
}
2165+
}
20682166
}

0 commit comments

Comments
 (0)