Skip to content

Commit fadebd1

Browse files
author
Alan Hanson
committed
Verify extent under repair is valid after copying files
Added a call to the downstairs repair API that will verify that an extent is closed (or the region is read only), meaning the extent is okay to copy. This prevents a rare but possible condition where an extent is closed when a remote side contacts it for repair, but is re-opened before the extent file copy starts and the resulting copy is invalid.
1 parent 9b3f366 commit fadebd1

File tree

8 files changed

+227
-12
lines changed

8 files changed

+227
-12
lines changed

Cargo.lock

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

downstairs/src/lib.rs

+64-9
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ mod stats;
3939
mod extent_inner_raw;
4040
mod extent_inner_sqlite;
4141

42+
use extent::ExtentState;
4243
use region::Region;
44+
use repair::RepairReadyRequest;
4345

4446
pub use admin::run_dropshot;
4547
pub use dump::dump_region;
@@ -1595,6 +1597,40 @@ where
15951597
.await
15961598
}
15971599

1600+
// Repair info task
1601+
// listen for the repair API to request if a specific extent is closed
1602+
// or if the entire region is read only.
1603+
async fn repair_info(
1604+
ads: &Arc<Mutex<Downstairs>>,
1605+
mut repair_request_rx: mpsc::Receiver<RepairReadyRequest>,
1606+
) -> Result<()> {
1607+
info!(ads.lock().await.log, "Started repair info task");
1608+
while let Some(repair_request) = repair_request_rx.recv().await {
1609+
let ds = ads.lock().await;
1610+
let state = if ds.read_only {
1611+
true
1612+
} else {
1613+
matches!(ds.region.extents[repair_request.eid], ExtentState::Closed)
1614+
};
1615+
drop(ds);
1616+
1617+
if let Err(e) = repair_request.tx.send(state) {
1618+
info!(
1619+
ads.lock().await.log,
1620+
"Error {e} sending repair info for extent {}",
1621+
repair_request.eid,
1622+
);
1623+
} else {
1624+
info!(
1625+
ads.lock().await.log,
1626+
"Repair info sent {state} for extent {}", repair_request.eid,
1627+
);
1628+
}
1629+
}
1630+
warn!(ads.lock().await.log, "Repair info task ends");
1631+
Ok(())
1632+
}
1633+
15981634
async fn reply_task<WT>(
15991635
mut resp_channel_rx: mpsc::Receiver<Message>,
16001636
mut fw: FramedWrite<WT, CrucibleEncoder>,
@@ -3255,23 +3291,42 @@ pub async fn start_downstairs(
32553291
let dss = d.clone();
32563292
let repair_log = d.lock().await.log.new(o!("task" => "repair".to_string()));
32573293

3258-
let repair_listener =
3259-
match repair::repair_main(&dss, repair_address, &repair_log).await {
3260-
Err(e) => {
3261-
// TODO tear down other things if repair server can't be
3262-
// started?
3263-
bail!("got {:?} from repair main", e);
3264-
}
3294+
let (repair_request_tx, repair_request_rx) = mpsc::channel(10);
3295+
let repair_listener = match repair::repair_main(
3296+
&dss,
3297+
repair_address,
3298+
&repair_log,
3299+
repair_request_tx,
3300+
)
3301+
.await
3302+
{
3303+
Err(e) => {
3304+
// TODO tear down other things if repair server can't be
3305+
// started?
3306+
bail!("got {:?} from repair main", e);
3307+
}
32653308

3266-
Ok(socket_addr) => socket_addr,
3267-
};
3309+
Ok(socket_addr) => socket_addr,
3310+
};
32683311

32693312
{
32703313
let mut ds = d.lock().await;
32713314
ds.repair_address = Some(repair_listener);
32723315
}
32733316
info!(log, "Using repair address: {:?}", repair_listener);
32743317

3318+
let rd = d.clone();
3319+
let _repair_info_handle = tokio::spawn(async move {
3320+
if let Err(e) = repair_info(&rd, repair_request_rx).await {
3321+
error!(
3322+
rd.lock().await.log,
3323+
"repair info exits with error: {:?}", e
3324+
);
3325+
} else {
3326+
info!(rd.lock().await.log, "repair info connection all done");
3327+
}
3328+
});
3329+
32753330
// Optionally require SSL connections
32763331
let ssl_acceptor = if let Some(cert_pem_path) = cert_pem {
32773332
let key_pem_path = key_pem.unwrap();

downstairs/src/region.rs

+26-1
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,9 @@ impl Region {
507507
*
508508
* Let us assume we are repairing extent 012
509509
* 1. Make new 012.copy dir (extent name plus: .copy)
510-
* 2. Get all extent files from source side, put in 012.copy directory
510+
* 2. Get all extent files from source side, put in 012.copy directory.
511+
* Verify after the copy completes that the remote side still has
512+
* the extent closed (or the region is read only).
511513
* 3. fsync files we just downloaded
512514
* 4. Rename 012.copy dir to 012.replace dir
513515
* 5. fsync extent directory ( 00/000/ where the extent files live)
@@ -670,6 +672,29 @@ impl Region {
670672
count += 1;
671673
}
672674

675+
// We have copied over the extent. Verify that the remote side
676+
// still believes that it is valid for repair so we know we have
677+
// received a valid copy.
678+
info!(self.log, "Verify extent {eid} still ready for copy");
679+
let rc = match repair_server.extent_repair_ready(eid as u32).await {
680+
Ok(rc) => rc.into_inner(),
681+
Err(e) => {
682+
crucible_bail!(
683+
RepairRequestError,
684+
"Failed to verify extent is still valid for repair {:?}",
685+
e,
686+
);
687+
}
688+
};
689+
690+
if !rc {
691+
warn!(self.log, "The repair of {eid} is being aborted.");
692+
crucible_bail!(
693+
RepairRequestError,
694+
"Extent {eid} on remote repair server is in incorrect state",
695+
);
696+
}
697+
673698
// After we have all files: move the repair dir.
674699
info!(
675700
self.log,

downstairs/src/repair.rs

+27
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub struct FileServerContext {
2525
region_dir: PathBuf,
2626
read_only: bool,
2727
region_definition: RegionDefinition,
28+
repair_request_tx: mpsc::Sender<RepairReadyRequest>,
2829
}
2930

3031
pub fn write_openapi<W: Write>(f: &mut W) -> Result<()> {
@@ -39,6 +40,7 @@ fn build_api() -> ApiDescription<Arc<FileServerContext>> {
3940
api.register(get_files_for_extent).unwrap();
4041
api.register(get_region_info).unwrap();
4142
api.register(get_region_mode).unwrap();
43+
api.register(extent_repair_ready).unwrap();
4244

4345
api
4446
}
@@ -48,6 +50,7 @@ pub async fn repair_main(
4850
ds: &Mutex<Downstairs>,
4951
addr: SocketAddr,
5052
log: &Logger,
53+
repair_request_tx: mpsc::Sender<RepairReadyRequest>,
5154
) -> Result<SocketAddr, String> {
5255
/*
5356
* We must specify a configuration with a bind address.
@@ -78,6 +81,7 @@ pub async fn repair_main(
7881
region_dir,
7982
read_only,
8083
region_definition,
84+
repair_request_tx,
8185
};
8286

8387
/*
@@ -186,6 +190,29 @@ async fn get_a_file(path: PathBuf) -> Result<Response<Body>, HttpError> {
186190
}
187191
}
188192

193+
pub struct RepairReadyRequest {
194+
pub tx: oneshot::Sender<bool>,
195+
pub eid: usize,
196+
}
197+
198+
/// Return true if the provided extent is closed or the region is read only
199+
#[endpoint {
200+
method = GET,
201+
path = "/extent/{eid}/repair-ready",
202+
}]
203+
async fn extent_repair_ready(
204+
rqctx: RequestContext<Arc<FileServerContext>>,
205+
path: Path<Eid>,
206+
) -> Result<HttpResponseOk<bool>, HttpError> {
207+
let eid: usize = path.into_inner().eid as usize;
208+
209+
let (tx, rx) = oneshot::channel();
210+
let rrr = RepairReadyRequest { tx, eid };
211+
rqctx.context().repair_request_tx.send(rrr).await.unwrap();
212+
let res = rx.await.unwrap();
213+
Ok(HttpResponseOk(res))
214+
}
215+
189216
/**
190217
* Get the list of files related to an extent.
191218
*

integration_tests/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ futures.workspace = true
2626
hex.workspace = true
2727
httptest.workspace = true
2828
rand.workspace = true
29+
repair-client.workspace = true
2930
reqwest.workspace = true
3031
serde.workspace = true
3132
serde_json.workspace = true

integration_tests/src/lib.rs

+69
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod test {
1616
use futures::lock::Mutex;
1717
use httptest::{matchers::*, responders::*, Expectation, Server};
1818
use rand::Rng;
19+
use repair_client::Client;
1920
use sha2::Digest;
2021
use slog::{info, o, warn, Drain, Logger};
2122
use tempfile::*;
@@ -2818,6 +2819,74 @@ mod test {
28182819
Ok(())
28192820
}
28202821

2822+
#[tokio::test]
2823+
async fn integration_test_repair_ready_not_closed() -> Result<()> {
2824+
// Create a new downstairs.
2825+
// Verify repair ready returns false when an extent is open
2826+
2827+
// Create a downstairs
2828+
let new_ds = TestDownstairs::new(
2829+
"127.0.0.1".parse().unwrap(),
2830+
true, // encrypted
2831+
false, // read only
2832+
5,
2833+
2,
2834+
false,
2835+
Backend::RawFile,
2836+
)
2837+
.await
2838+
.unwrap();
2839+
2840+
let repair_addr = new_ds.repair_address().await;
2841+
2842+
let url = format!("http://{:?}", repair_addr);
2843+
let repair_server = Client::new(&url);
2844+
2845+
// Extent are open, so the repair ready request should return false.
2846+
let rc = repair_server.extent_repair_ready(0).await;
2847+
assert!(!rc.unwrap().into_inner());
2848+
2849+
let rc = repair_server.extent_repair_ready(1).await;
2850+
assert!(!rc.unwrap().into_inner());
2851+
2852+
Ok(())
2853+
}
2854+
2855+
#[tokio::test]
2856+
async fn integration_test_repair_ready_read_only() -> Result<()> {
2857+
// Create a new downstairs.
2858+
// Verify repair ready returns true for read only downstairs,
2859+
// even when extents are open.
2860+
2861+
// Create a downstairs
2862+
let new_ds = TestDownstairs::new(
2863+
"127.0.0.1".parse().unwrap(),
2864+
true, // encrypted
2865+
true, // read only
2866+
5,
2867+
2,
2868+
false,
2869+
Backend::RawFile,
2870+
)
2871+
.await
2872+
.unwrap();
2873+
2874+
let repair_addr = new_ds.repair_address().await;
2875+
2876+
let url = format!("http://{:?}", repair_addr);
2877+
let repair_server = Client::new(&url);
2878+
2879+
// Extent are not open, but the region is read only, so the requests
2880+
// should all return true.
2881+
let rc = repair_server.extent_repair_ready(0).await;
2882+
assert!(rc.unwrap().into_inner());
2883+
2884+
let rc = repair_server.extent_repair_ready(1).await;
2885+
assert!(rc.unwrap().into_inner());
2886+
2887+
Ok(())
2888+
}
2889+
28212890
#[tokio::test]
28222891
async fn integration_test_clone_diff_ec() -> Result<()> {
28232892
// Test downstairs region clone.

openapi/downstairs-repair.json

+37
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,43 @@
4646
}
4747
}
4848
},
49+
"/extent/{eid}/repair-ready": {
50+
"get": {
51+
"summary": "Return true if the provided extent is closed or the region is read only",
52+
"operationId": "extent_repair_ready",
53+
"parameters": [
54+
{
55+
"in": "path",
56+
"name": "eid",
57+
"required": true,
58+
"schema": {
59+
"type": "integer",
60+
"format": "uint32",
61+
"minimum": 0
62+
}
63+
}
64+
],
65+
"responses": {
66+
"200": {
67+
"description": "successful operation",
68+
"content": {
69+
"application/json": {
70+
"schema": {
71+
"title": "Boolean",
72+
"type": "boolean"
73+
}
74+
}
75+
}
76+
},
77+
"4XX": {
78+
"$ref": "#/components/responses/Error"
79+
},
80+
"5XX": {
81+
"$ref": "#/components/responses/Error"
82+
}
83+
}
84+
}
85+
},
4986
"/newextent/{eid}/{file_type}": {
5087
"get": {
5188
"operationId": "get_extent_file",

upstairs/src/client.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1247,8 +1247,8 @@ impl DownstairsClient {
12471247
if old_state != IOState::InProgress {
12481248
// This job is in an unexpected state.
12491249
panic!(
1250-
"[{}] Job {} completed while not InProgress: {:?}",
1251-
self.client_id, ds_id, old_state
1250+
"[{}] Job {} completed while not InProgress: {:?} {:?}",
1251+
self.client_id, ds_id, old_state, job
12521252
);
12531253
}
12541254

0 commit comments

Comments
 (0)