34
34
} ,
35
35
solana_storage_proto:: convert:: generated,
36
36
std:: {
37
- collections:: HashMap ,
37
+ collections:: { HashMap , HashSet } ,
38
38
ffi:: { CStr , CString } ,
39
39
fs,
40
40
marker:: PhantomData ,
@@ -376,15 +376,12 @@ impl Rocks {
376
376
}
377
377
let oldest_slot = OldestSlot :: default ( ) ;
378
378
let column_options = options. column_options . clone ( ) ;
379
+ let cf_descriptors = Self :: cf_descriptors ( path, & options, & oldest_slot) ;
379
380
380
381
// Open the database
381
382
let db = match access_type {
382
383
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) ?,
388
385
access_type,
389
386
oldest_slot,
390
387
column_options,
@@ -404,7 +401,7 @@ impl Rocks {
404
401
& db_options,
405
402
path,
406
403
& secondary_path,
407
- Self :: cf_descriptors ( & options , & oldest_slot ) ,
404
+ cf_descriptors,
408
405
) ?,
409
406
access_type,
410
407
oldest_slot,
@@ -418,15 +415,25 @@ impl Rocks {
418
415
Ok ( db)
419
416
}
420
417
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.
421
427
fn cf_descriptors (
428
+ path : & Path ,
422
429
options : & BlockstoreOptions ,
423
430
oldest_slot : & OldestSlot ,
424
431
) -> Vec < ColumnFamilyDescriptor > {
425
432
use columns:: * ;
426
433
427
434
let ( cf_descriptor_shred_data, cf_descriptor_shred_code) =
428
435
new_cf_descriptor_pair_shreds :: < ShredData , ShredCode > ( options, oldest_slot) ;
429
- vec ! [
436
+ let mut cf_descriptors = vec ! [
430
437
new_cf_descriptor:: <SlotMeta >( options, oldest_slot) ,
431
438
new_cf_descriptor:: <DeadSlots >( options, oldest_slot) ,
432
439
new_cf_descriptor:: <DuplicateSlots >( options, oldest_slot) ,
@@ -447,7 +454,52 @@ impl Rocks {
447
454
new_cf_descriptor:: <BlockHeight >( options, oldest_slot) ,
448
455
new_cf_descriptor:: <ProgramCosts >( options, oldest_slot) ,
449
456
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
451
503
}
452
504
453
505
fn columns ( ) -> Vec < & ' static str > {
@@ -1982,7 +2034,9 @@ fn should_enable_compression<C: 'static + Column + ColumnName>() -> bool {
1982
2034
1983
2035
#[ cfg( test) ]
1984
2036
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
+ } ;
1986
2040
1987
2041
#[ test]
1988
2042
fn test_compaction_filter ( ) {
@@ -2034,14 +2088,15 @@ pub mod tests {
2034
2088
2035
2089
#[ test]
2036
2090
fn test_cf_names_and_descriptors_equal_length ( ) {
2091
+ let path = PathBuf :: default ( ) ;
2037
2092
let options = BlockstoreOptions :: default ( ) ;
2038
2093
let oldest_slot = OldestSlot :: default ( ) ;
2039
2094
// The names and descriptors don't need to be in the same order for our use cases;
2040
2095
// however, there should be the same number of each. For example, adding a new column
2041
2096
// should update both lists.
2042
2097
assert_eq ! (
2043
2098
Rocks :: columns( ) . len( ) ,
2044
- Rocks :: cf_descriptors( & options, & oldest_slot) . len( )
2099
+ Rocks :: cf_descriptors( & path , & options, & oldest_slot) . len( )
2045
2100
) ;
2046
2101
}
2047
2102
@@ -2065,4 +2120,47 @@ pub mod tests {
2065
2120
} ) ;
2066
2121
assert ! ( !should_enable_cf_compaction( "something else" ) ) ;
2067
2122
}
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
+ }
2068
2166
}
0 commit comments