Show More
@@ -46,6 +46,9 b' pub enum HgError {' | |||
|
46 | 46 | |
|
47 | 47 | /// Censored revision data. |
|
48 | 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 | 54 | /// Details about where an I/O error happened |
@@ -111,6 +114,9 b' impl fmt::Display for HgError {' | |||
|
111 | 114 | write!(f, "encountered a censored node") |
|
112 | 115 | } |
|
113 | 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 | 24 | use std::io::Write as IoWrite; |
|
25 | 25 | use std::path::{Path, PathBuf}; |
|
26 | 26 | |
|
27 | const V2_MAX_READ_ATTEMPTS: usize = 5; | |
|
28 | ||
|
27 | 29 | /// A repository on disk |
|
28 | 30 | pub struct Repo { |
|
29 | 31 | working_directory: PathBuf, |
@@ -307,14 +309,49 b' impl Repo {' | |||
|
307 | 309 | |
|
308 | 310 | fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> { |
|
309 | 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 | 348 | } else { |
|
312 | 349 | debug_wait_for_file_or_print( |
|
313 | 350 | self.config(), |
|
314 | 351 | "dirstate.pre-read-file", |
|
315 | 352 | ); |
|
316 | 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 | 355 | self.dirstate_parents.set(DirstateParents::NULL); |
|
319 | 356 | Ok(OwningDirstateMap::new_empty(Vec::new())) |
|
320 | 357 | } else { |
@@ -322,7 +359,7 b' impl Repo {' | |||
|
322 | 359 | OwningDirstateMap::new_v1(dirstate_file_contents)?; |
|
323 | 360 | self.dirstate_parents.set(parents); |
|
324 | 361 | Ok(map) |
|
325 | } | |
|
362 | }; | |
|
326 | 363 | } |
|
327 | 364 | } |
|
328 | 365 | |
@@ -347,22 +384,54 b' impl Repo {' | |||
|
347 | 384 | self.dirstate_data_file_uuid |
|
348 | 385 | .set(Some(docket.uuid.to_owned())); |
|
349 | 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 | 390 | let metadata = docket.tree_metadata(); |
|
391 | ||
|
351 | 392 | let mut map = if crate::vfs::is_on_nfs_mount(docket.data_filename()) { |
|
352 | 393 | // Don't mmap on NFS to prevent `SIGBUS` error on deletion |
|
353 | OwningDirstateMap::new_v2( | |
|
354 | self.hg_vfs().read(docket.data_filename())?, | |
|
355 |
|
|
|
356 | metadata, | |
|
357 | ) | |
|
358 | } else if let Some(data_mmap) = self | |
|
359 | .hg_vfs() | |
|
360 | .mmap_open(docket.data_filename()) | |
|
361 | .io_not_found_as_none()? | |
|
362 | { | |
|
363 | OwningDirstateMap::new_v2(data_mmap, data_size, metadata) | |
|
394 | let contents = self.hg_vfs().read(docket.data_filename()); | |
|
395 | let contents = match contents { | |
|
396 | Ok(c) => c, | |
|
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() | |
|
412 | ) | |
|
413 | } | |
|
414 | } | |
|
415 | } | |
|
416 | Err(e) => return Err(e.into()), | |
|
417 | }; | |
|
418 | OwningDirstateMap::new_v2(contents, data_size, metadata) | |
|
364 | 419 | } else { |
|
365 | OwningDirstateMap::new_v2(Vec::new(), data_size, metadata) | |
|
420 | match self | |
|
421 | .hg_vfs() | |
|
422 | .mmap_open(docket.data_filename()) | |
|
423 | .io_not_found_as_none() | |
|
424 | { | |
|
425 | Ok(Some(data_mmap)) => { | |
|
426 | OwningDirstateMap::new_v2(data_mmap, data_size, metadata) | |
|
427 | } | |
|
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 | 437 | let write_mode_config = self |
@@ -196,8 +196,12 b' The status process should return a consi' | |||
|
196 | 196 | $ cat $TESTTMP/status-race-lock.log |
|
197 | 197 | #else |
|
198 | 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 |
$ |
|
200 | abort: dirstate-v2 parse error: not enough bytes on disk | |
|
201 | 205 | #endif |
|
202 | 206 | #endif |
|
203 | 207 | #else |
@@ -305,8 +309,10 b' The status process should return a consi' | |||
|
305 | 309 | $ cat $TESTTMP/status-race-lock.log |
|
306 | 310 | #else |
|
307 | 311 | $ cat $TESTTMP/status-race-lock.out |
|
312 | ? dir/n | |
|
313 | ? p | |
|
314 | ? q | |
|
308 | 315 |
$ |
|
309 | abort: dirstate-v2 parse error: not enough bytes on disk | |
|
310 | 316 | #endif |
|
311 | 317 | #endif |
|
312 | 318 | #else |
@@ -441,8 +447,11 b' The status process should return a consi' | |||
|
441 | 447 | $ cat $TESTTMP/status-race-lock.log |
|
442 | 448 | #else |
|
443 | 449 | $ cat $TESTTMP/status-race-lock.out |
|
450 | A dir/o | |
|
451 | ? dir/n | |
|
452 | ? p | |
|
453 | ? q | |
|
444 | 454 |
$ |
|
445 | abort: dirstate-v2 parse error: not enough bytes on disk | |
|
446 | 455 | #endif |
|
447 | 456 | #endif |
|
448 | 457 | #else |
@@ -543,8 +552,12 b' The status process should return a consi' | |||
|
543 | 552 | $ cat $TESTTMP/status-race-lock.log |
|
544 | 553 | #else |
|
545 | 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 |
$ |
|
547 | abort: dirstate-v2 parse error: not enough bytes on disk | |
|
548 | 561 | #endif |
|
549 | 562 | #endif |
|
550 | 563 | #else |
General Comments 0
You need to be logged in to leave comments.
Login now