Show More
@@ -46,6 +46,9 b' pub enum HgError {' | |||||
46 |
|
46 | |||
47 | /// Censored revision data. |
|
47 | /// Censored revision data. | |
48 | CensoredNodeError, |
|
48 | CensoredNodeError, | |
|
49 | /// A race condition has been detected. This *must* be handled locally | |||
|
50 | /// and not directly surface to the user. | |||
|
51 | RaceDetected(String), | |||
49 | } |
|
52 | } | |
50 |
|
53 | |||
51 | /// Details about where an I/O error happened |
|
54 | /// Details about where an I/O error happened | |
@@ -111,6 +114,9 b' impl fmt::Display for HgError {' | |||||
111 | write!(f, "encountered a censored node") |
|
114 | write!(f, "encountered a censored node") | |
112 | } |
|
115 | } | |
113 | HgError::ConfigValueParseError(error) => error.fmt(f), |
|
116 | HgError::ConfigValueParseError(error) => error.fmt(f), | |
|
117 | HgError::RaceDetected(context) => { | |||
|
118 | write!(f, "encountered a race condition {context}") | |||
|
119 | } | |||
114 | } |
|
120 | } | |
115 | } |
|
121 | } | |
116 | } |
|
122 | } |
@@ -24,6 +24,8 b' use std::io::SeekFrom;' | |||||
24 | use std::io::Write as IoWrite; |
|
24 | use std::io::Write as IoWrite; | |
25 | use std::path::{Path, PathBuf}; |
|
25 | use std::path::{Path, PathBuf}; | |
26 |
|
26 | |||
|
27 | const V2_MAX_READ_ATTEMPTS: usize = 5; | |||
|
28 | ||||
27 | /// A repository on disk |
|
29 | /// A repository on disk | |
28 | pub struct Repo { |
|
30 | pub struct Repo { | |
29 | working_directory: PathBuf, |
|
31 | working_directory: PathBuf, | |
@@ -307,14 +309,49 b' impl Repo {' | |||||
307 |
|
309 | |||
308 | fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> { |
|
310 | fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> { | |
309 | if self.has_dirstate_v2() { |
|
311 | if self.has_dirstate_v2() { | |
310 | self.read_docket_and_data_file() |
|
312 | // The v2 dirstate is split into a docket and a data file. | |
|
313 | // Since we don't always take the `wlock` to read it | |||
|
314 | // (like in `hg status`), it is susceptible to races. | |||
|
315 | // A simple retry method should be enough since full rewrites | |||
|
316 | // only happen when too much garbage data is present and | |||
|
317 | // this race is unlikely. | |||
|
318 | let mut tries = 0; | |||
|
319 | ||||
|
320 | while tries < V2_MAX_READ_ATTEMPTS { | |||
|
321 | tries += 1; | |||
|
322 | match self.read_docket_and_data_file() { | |||
|
323 | Ok(m) => { | |||
|
324 | return Ok(m); | |||
|
325 | } | |||
|
326 | Err(e) => match e { | |||
|
327 | DirstateError::Common(HgError::RaceDetected( | |||
|
328 | context, | |||
|
329 | )) => { | |||
|
330 | log::info!( | |||
|
331 | "dirstate read race detected {} (retry {}/{})", | |||
|
332 | context, | |||
|
333 | tries, | |||
|
334 | V2_MAX_READ_ATTEMPTS, | |||
|
335 | ); | |||
|
336 | continue; | |||
|
337 | } | |||
|
338 | _ => return Err(e.into()), | |||
|
339 | }, | |||
|
340 | } | |||
|
341 | } | |||
|
342 | let error = HgError::abort( | |||
|
343 | format!("dirstate read race happened {tries} times in a row"), | |||
|
344 | 255, | |||
|
345 | None, | |||
|
346 | ); | |||
|
347 | return Err(DirstateError::Common(error)); | |||
311 | } else { |
|
348 | } else { | |
312 | debug_wait_for_file_or_print( |
|
349 | debug_wait_for_file_or_print( | |
313 | self.config(), |
|
350 | self.config(), | |
314 | "dirstate.pre-read-file", |
|
351 | "dirstate.pre-read-file", | |
315 | ); |
|
352 | ); | |
316 | let dirstate_file_contents = self.dirstate_file_contents()?; |
|
353 | let dirstate_file_contents = self.dirstate_file_contents()?; | |
317 | if dirstate_file_contents.is_empty() { |
|
354 | return if dirstate_file_contents.is_empty() { | |
318 | self.dirstate_parents.set(DirstateParents::NULL); |
|
355 | self.dirstate_parents.set(DirstateParents::NULL); | |
319 | Ok(OwningDirstateMap::new_empty(Vec::new())) |
|
356 | Ok(OwningDirstateMap::new_empty(Vec::new())) | |
320 | } else { |
|
357 | } else { | |
@@ -322,7 +359,7 b' impl Repo {' | |||||
322 | OwningDirstateMap::new_v1(dirstate_file_contents)?; |
|
359 | OwningDirstateMap::new_v1(dirstate_file_contents)?; | |
323 | self.dirstate_parents.set(parents); |
|
360 | self.dirstate_parents.set(parents); | |
324 | Ok(map) |
|
361 | Ok(map) | |
325 | } |
|
362 | }; | |
326 | } |
|
363 | } | |
327 | } |
|
364 | } | |
328 |
|
365 | |||
@@ -347,22 +384,54 b' impl Repo {' | |||||
347 | self.dirstate_data_file_uuid |
|
384 | self.dirstate_data_file_uuid | |
348 | .set(Some(docket.uuid.to_owned())); |
|
385 | .set(Some(docket.uuid.to_owned())); | |
349 | let data_size = docket.data_size(); |
|
386 | let data_size = docket.data_size(); | |
|
387 | ||||
|
388 | let context = "between reading dirstate docket and data file"; | |||
|
389 | let race_error = HgError::RaceDetected(context.into()); | |||
350 | let metadata = docket.tree_metadata(); |
|
390 | let metadata = docket.tree_metadata(); | |
|
391 | ||||
351 | let mut map = if crate::vfs::is_on_nfs_mount(docket.data_filename()) { |
|
392 | let mut map = if crate::vfs::is_on_nfs_mount(docket.data_filename()) { | |
352 | // Don't mmap on NFS to prevent `SIGBUS` error on deletion |
|
393 | // Don't mmap on NFS to prevent `SIGBUS` error on deletion | |
353 | OwningDirstateMap::new_v2( |
|
394 | let contents = self.hg_vfs().read(docket.data_filename()); | |
354 | self.hg_vfs().read(docket.data_filename())?, |
|
395 | let contents = match contents { | |
355 |
|
|
396 | Ok(c) => c, | |
356 | metadata, |
|
397 | Err(HgError::IoError { error, context }) => { | |
|
398 | match error.raw_os_error().expect("real os error") { | |||
|
399 | // 2 = ENOENT, No such file or directory | |||
|
400 | // 116 = ESTALE, Stale NFS file handle | |||
|
401 | // | |||
|
402 | // TODO match on `error.kind()` when | |||
|
403 | // `ErrorKind::StaleNetworkFileHandle` is stable. | |||
|
404 | 2 | 116 => { | |||
|
405 | // Race where the data file was deleted right after | |||
|
406 | // we read the docket, try again | |||
|
407 | return Err(race_error.into()); | |||
|
408 | } | |||
|
409 | _ => { | |||
|
410 | return Err( | |||
|
411 | HgError::IoError { error, context }.into() | |||
357 | ) |
|
412 | ) | |
358 | } else if let Some(data_mmap) = self |
|
413 | } | |
|
414 | } | |||
|
415 | } | |||
|
416 | Err(e) => return Err(e.into()), | |||
|
417 | }; | |||
|
418 | OwningDirstateMap::new_v2(contents, data_size, metadata) | |||
|
419 | } else { | |||
|
420 | match self | |||
359 | .hg_vfs() |
|
421 | .hg_vfs() | |
360 | .mmap_open(docket.data_filename()) |
|
422 | .mmap_open(docket.data_filename()) | |
361 |
.io_not_found_as_none() |
|
423 | .io_not_found_as_none() | |
362 | { |
|
424 | { | |
|
425 | Ok(Some(data_mmap)) => { | |||
363 | OwningDirstateMap::new_v2(data_mmap, data_size, metadata) |
|
426 | OwningDirstateMap::new_v2(data_mmap, data_size, metadata) | |
364 | } else { |
|
427 | } | |
365 | OwningDirstateMap::new_v2(Vec::new(), data_size, metadata) |
|
428 | Ok(None) => { | |
|
429 | // Race where the data file was deleted right after we | |||
|
430 | // read the docket, try again | |||
|
431 | return Err(race_error.into()); | |||
|
432 | } | |||
|
433 | Err(e) => return Err(e.into()), | |||
|
434 | } | |||
366 | }?; |
|
435 | }?; | |
367 |
|
436 | |||
368 | let write_mode_config = self |
|
437 | let write_mode_config = self |
@@ -196,8 +196,12 b' The status process should return a consi' | |||||
196 | $ cat $TESTTMP/status-race-lock.log |
|
196 | $ cat $TESTTMP/status-race-lock.log | |
197 | #else |
|
197 | #else | |
198 | $ cat $TESTTMP/status-race-lock.out |
|
198 | $ cat $TESTTMP/status-race-lock.out | |
|
199 | A dir/n | |||
|
200 | A dir/o | |||
|
201 | R dir/nested/m | |||
|
202 | ? p | |||
|
203 | ? q | |||
199 |
$ |
|
204 | $ cat $TESTTMP/status-race-lock.log | |
200 | abort: dirstate-v2 parse error: not enough bytes on disk |
|
|||
201 | #endif |
|
205 | #endif | |
202 | #endif |
|
206 | #endif | |
203 | #else |
|
207 | #else | |
@@ -305,8 +309,10 b' The status process should return a consi' | |||||
305 | $ cat $TESTTMP/status-race-lock.log |
|
309 | $ cat $TESTTMP/status-race-lock.log | |
306 | #else |
|
310 | #else | |
307 | $ cat $TESTTMP/status-race-lock.out |
|
311 | $ cat $TESTTMP/status-race-lock.out | |
|
312 | ? dir/n | |||
|
313 | ? p | |||
|
314 | ? q | |||
308 |
$ |
|
315 | $ cat $TESTTMP/status-race-lock.log | |
309 | abort: dirstate-v2 parse error: not enough bytes on disk |
|
|||
310 | #endif |
|
316 | #endif | |
311 | #endif |
|
317 | #endif | |
312 | #else |
|
318 | #else | |
@@ -441,8 +447,11 b' The status process should return a consi' | |||||
441 | $ cat $TESTTMP/status-race-lock.log |
|
447 | $ cat $TESTTMP/status-race-lock.log | |
442 | #else |
|
448 | #else | |
443 | $ cat $TESTTMP/status-race-lock.out |
|
449 | $ cat $TESTTMP/status-race-lock.out | |
|
450 | A dir/o | |||
|
451 | ? dir/n | |||
|
452 | ? p | |||
|
453 | ? q | |||
444 |
$ |
|
454 | $ cat $TESTTMP/status-race-lock.log | |
445 | abort: dirstate-v2 parse error: not enough bytes on disk |
|
|||
446 | #endif |
|
455 | #endif | |
447 | #endif |
|
456 | #endif | |
448 | #else |
|
457 | #else | |
@@ -543,8 +552,12 b' The status process should return a consi' | |||||
543 | $ cat $TESTTMP/status-race-lock.log |
|
552 | $ cat $TESTTMP/status-race-lock.log | |
544 | #else |
|
553 | #else | |
545 | $ cat $TESTTMP/status-race-lock.out |
|
554 | $ cat $TESTTMP/status-race-lock.out | |
|
555 | A dir/o | |||
|
556 | R dir/nested/m | |||
|
557 | ? dir/n | |||
|
558 | ? p | |||
|
559 | ? q | |||
546 |
$ |
|
560 | $ cat $TESTTMP/status-race-lock.log | |
547 | abort: dirstate-v2 parse error: not enough bytes on disk |
|
|||
548 | #endif |
|
561 | #endif | |
549 | #endif |
|
562 | #endif | |
550 | #else |
|
563 | #else |
General Comments 0
You need to be logged in to leave comments.
Login now