Skip to content

Commit 35ef0cf

Browse files
authored
New downstairs clone subcommand. (#1129)
New clone subcommand for crucible-downstairs. This enables a downstairs to "clone" another downstairs using the repair endpoint. It requires the source downstairs to be read only and will destroy whatever data exists on the destination downstairs. Support for cloning SQLite backend downstairs is supported, and I had to bring back some of the old repair code that supported the additional files for that. Found an fixed a bug where the incorrect file types were returned during a repair that contained SQLite files. A bunch more tests were added to cover the clone process, and new API endpoints were added to the downstairs repair server.
1 parent 2d7e499 commit 35ef0cf

File tree

14 files changed

+1261
-60
lines changed

14 files changed

+1261
-60
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ pub enum CrucibleError {
158158

159159
#[error("missing block context for non-empty block")]
160160
MissingBlockContext,
161+
162+
#[error("Incompatible RegionDefinition {0}")]
163+
RegionIncompatible(String),
161164
}
162165

163166
impl From<std::io::Error> for CrucibleError {
@@ -363,6 +366,7 @@ impl From<CrucibleError> for dropshot::HttpError {
363366
| CrucibleError::ModifyingReadOnlyRegion
364367
| CrucibleError::OffsetInvalid
365368
| CrucibleError::OffsetUnaligned
369+
| CrucibleError::RegionIncompatible(_)
366370
| CrucibleError::ReplaceRequestInvalid(_)
367371
| CrucibleError::SnapshotExistsAlready(_)
368372
| CrucibleError::Unsupported(_) => {

common/src/region.rs

+170-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright 2021 Oxide Computer Company
22
use anyhow::{bail, Result};
3+
use schemars::JsonSchema;
34
use serde::{Deserialize, Serialize};
45
use uuid::Uuid;
56

@@ -17,7 +18,16 @@ use super::*;
1718
* downstairs expects Block { 2, 12 }.
1819
*/
1920
#[derive(
20-
Deserialize, Serialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord,
21+
Deserialize,
22+
Serialize,
23+
Copy,
24+
Clone,
25+
Debug,
26+
PartialEq,
27+
Eq,
28+
JsonSchema,
29+
PartialOrd,
30+
Ord,
2131
)]
2232
pub struct Block {
2333
// Value could mean a size or offset
@@ -118,7 +128,7 @@ impl Block {
118128
}
119129
}
120130

121-
#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq)]
131+
#[derive(Deserialize, Serialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
122132
pub struct RegionDefinition {
123133
/**
124134
* The size of each block in bytes. Must be a power of 2, minimum 512.
@@ -170,6 +180,55 @@ impl RegionDefinition {
170180
})
171181
}
172182

183+
// Compare two RegionDefinitions and verify they are compatible.
184+
// compatible is valid if all fields are the same, expect for the
185+
// UUID. The UUID should be different.
186+
pub fn compatible(
187+
self,
188+
other: RegionDefinition,
189+
) -> Result<(), CrucibleError> {
190+
// These fields should be the same.
191+
if self.block_size != other.block_size {
192+
return Err(CrucibleError::RegionIncompatible(
193+
"block_size".to_string(),
194+
));
195+
}
196+
if self.extent_size != other.extent_size {
197+
return Err(CrucibleError::RegionIncompatible(
198+
"extent_size".to_string(),
199+
));
200+
}
201+
if self.extent_count != other.extent_count {
202+
return Err(CrucibleError::RegionIncompatible(
203+
"extent_count".to_string(),
204+
));
205+
}
206+
if self.encrypted != other.encrypted {
207+
return Err(CrucibleError::RegionIncompatible(
208+
"encrypted".to_string(),
209+
));
210+
}
211+
if self.database_read_version != other.database_read_version {
212+
return Err(CrucibleError::RegionIncompatible(
213+
"database_read_version".to_string(),
214+
));
215+
}
216+
if self.database_write_version != other.database_write_version {
217+
return Err(CrucibleError::RegionIncompatible(
218+
"database_write_version".to_string(),
219+
));
220+
}
221+
222+
// If the UUIDs are the same, this is invalid.
223+
if self.uuid == other.uuid {
224+
return Err(CrucibleError::RegionIncompatible(
225+
"UUIDs are the same".to_string(),
226+
));
227+
}
228+
229+
Ok(())
230+
}
231+
173232
pub fn database_read_version(&self) -> usize {
174233
self.database_read_version
175234
}
@@ -489,4 +548,113 @@ mod test {
489548
*/
490549
assert!(rd.validate_io(Block::new(1, 9), 2048).is_err());
491550
}
551+
552+
fn test_rd() -> RegionDefinition {
553+
RegionDefinition {
554+
block_size: 512,
555+
extent_size: Block::new(10, 9),
556+
extent_count: 8,
557+
uuid: Uuid::new_v4(),
558+
encrypted: false,
559+
database_read_version: DATABASE_READ_VERSION,
560+
database_write_version: DATABASE_WRITE_VERSION,
561+
}
562+
}
563+
564+
#[test]
565+
fn test_region_compare_block() {
566+
let mut rd1 = test_rd();
567+
let rd2 = test_rd();
568+
569+
// Basic positive test first.
570+
assert_eq!(rd1.compatible(rd2), Ok(()));
571+
572+
rd1.block_size = 4096;
573+
assert!(rd1.compatible(rd2).is_err());
574+
575+
let rd1 = test_rd();
576+
let mut rd2 = test_rd();
577+
rd2.block_size = 4096;
578+
assert!(rd1.compatible(rd2).is_err());
579+
}
580+
581+
#[test]
582+
fn test_region_compare_extent_size() {
583+
let mut rd1 = test_rd();
584+
let rd2 = test_rd();
585+
586+
rd1.extent_size = Block::new(2, 9);
587+
assert!(rd1.compatible(rd2).is_err());
588+
589+
let rd1 = test_rd();
590+
let mut rd2 = test_rd();
591+
rd2.extent_size = Block::new(2, 9);
592+
assert!(rd1.compatible(rd2).is_err());
593+
}
594+
595+
#[test]
596+
fn test_region_compare_extent_count() {
597+
let mut rd1 = test_rd();
598+
let rd2 = test_rd();
599+
600+
rd1.extent_count = 9;
601+
assert!(rd1.compatible(rd2).is_err());
602+
603+
let rd1 = test_rd();
604+
let mut rd2 = test_rd();
605+
rd2.extent_count = 9;
606+
assert!(rd1.compatible(rd2).is_err());
607+
}
608+
609+
#[test]
610+
fn test_region_compare_uuid() {
611+
// Verify region compare, UUIDs must be different
612+
let mut rd1 = test_rd();
613+
let rd2 = test_rd();
614+
615+
rd1.uuid = rd2.uuid;
616+
assert!(rd1.compatible(rd2).is_err());
617+
}
618+
619+
#[test]
620+
fn test_region_compare_encrypted() {
621+
let mut rd1 = test_rd();
622+
let rd2 = test_rd();
623+
624+
rd1.encrypted = true;
625+
assert!(rd1.compatible(rd2).is_err());
626+
627+
let rd1 = test_rd();
628+
let mut rd2 = test_rd();
629+
rd2.encrypted = true;
630+
assert!(rd1.compatible(rd2).is_err());
631+
}
632+
633+
#[test]
634+
fn test_region_compare_db_read_version() {
635+
let mut rd1 = test_rd();
636+
let rd2 = test_rd();
637+
638+
rd1.database_read_version = DATABASE_READ_VERSION + 1;
639+
assert!(rd1.compatible(rd2).is_err());
640+
641+
let rd1 = test_rd();
642+
let mut rd2 = test_rd();
643+
rd2.database_read_version = DATABASE_READ_VERSION + 1;
644+
assert!(rd1.compatible(rd2).is_err());
645+
}
646+
647+
#[test]
648+
fn test_region_compare_db_write_version() {
649+
let mut rd1 = test_rd();
650+
let rd2 = test_rd();
651+
652+
rd1.database_write_version = DATABASE_WRITE_VERSION + 1;
653+
assert!(rd1.compatible(rd2).is_err());
654+
655+
let rd1 = test_rd();
656+
let mut rd2 = test_rd();
657+
rd2.database_write_version = DATABASE_WRITE_VERSION + 1;
658+
assert!(rd1.compatible(rd2).is_err());
659+
}
492660
}

downstairs/src/extent.rs

+35-8
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ impl ExtentMeta {
143143
}
144144
}
145145

146-
#[derive(Debug, Clone)]
146+
#[derive(Debug, Copy, Clone)]
147147
#[allow(clippy::enum_variant_names)]
148148
pub enum ExtentType {
149149
Data,
@@ -168,7 +168,7 @@ impl fmt::Display for ExtentType {
168168
* FileType from the repair client.
169169
*/
170170
impl ExtentType {
171-
pub fn to_file_type(&self) -> FileType {
171+
pub fn to_file_type(self) -> FileType {
172172
match self {
173173
ExtentType::Data => FileType::Data,
174174
ExtentType::Db => FileType::Db,
@@ -307,7 +307,7 @@ impl Extent {
307307
"Extent {} found replacement dir, finishing replacement",
308308
number
309309
);
310-
move_replacement_extent(dir, number as usize, log)?;
310+
move_replacement_extent(dir, number as usize, log, false)?;
311311
}
312312

313313
// We will migrate every read-write extent with a SQLite file present.
@@ -616,6 +616,7 @@ pub(crate) fn move_replacement_extent<P: AsRef<Path>>(
616616
region_dir: P,
617617
eid: usize,
618618
log: &Logger,
619+
clone: bool,
619620
) -> Result<(), CrucibleError> {
620621
let destination_dir = extent_dir(&region_dir, eid as u32);
621622
let extent_file_name = extent_file_name(eid as u32, ExtentType::Data);
@@ -651,13 +652,31 @@ pub(crate) fn move_replacement_extent<P: AsRef<Path>>(
651652
sync_path(&original_file, log)?;
652653

653654
// We distinguish between SQLite-backend and raw-file extents based on the
654-
// presence of the `.db` file. We should never do live migration across
655-
// different extent formats; in fact, we should never live-migrate
656-
// SQLite-backed extents at all, but must still handle the case of
657-
// unfinished migrations.
655+
// presence of the `.db` file. We should never do extent repair across
656+
// different extent formats; it must be SQLite-to-SQLite or raw-to-raw.
657+
//
658+
// It is uncommon to perform extent repair on SQLite-backed extents at all,
659+
// because they are automatically migrated into raw file extents or
660+
// read-only. However, it must be supported for two cases:
661+
//
662+
// - If there was an unfinished replacement, we must finish that replacement
663+
// before migrating from SQLite -> raw file backend, which happens
664+
// automatically later in startup.
665+
// - We use this same code path to perform clones of read-only regions,
666+
// which may be SQLite-backed (and will never migrate to raw files,
667+
// because they are read only). This is only the case when the `clone`
668+
// argument is `true`.
669+
//
670+
// In the first case, we know that we are repairing an SQLite-based extent
671+
// because the target (original) extent includes a `.db` file.
672+
//
673+
// In the second case, the target (original) extent is not present, so we
674+
// check whether the new files include a `.db` file.
675+
658676
new_file.set_extension("db");
659677
original_file.set_extension("db");
660-
if original_file.exists() {
678+
679+
if original_file.exists() || (new_file.exists() && clone) {
661680
if let Err(e) = std::fs::copy(new_file.clone(), original_file.clone()) {
662681
crucible_bail!(
663682
IoError,
@@ -688,6 +707,10 @@ pub(crate) fn move_replacement_extent<P: AsRef<Path>>(
688707
}
689708
sync_path(&original_file, log)?;
690709
} else if original_file.exists() {
710+
// If we are cloning, then our new region will have been
711+
// created with Backend::RawFile, and we should have no SQLite
712+
// files.
713+
assert!(!clone);
691714
info!(
692715
log,
693716
"Remove old file {:?} as there is no replacement",
@@ -712,6 +735,10 @@ pub(crate) fn move_replacement_extent<P: AsRef<Path>>(
712735
}
713736
sync_path(&original_file, log)?;
714737
} else if original_file.exists() {
738+
// If we are cloning, then our new region will have been
739+
// created with Backend::RawFile, and we should have no SQLite
740+
// files.
741+
assert!(!clone);
715742
info!(
716743
log,
717744
"Remove old file {:?} as there is no replacement",

0 commit comments

Comments
 (0)