##// END OF EJS Templates
dirstate: deal with read-race for pure python code...
marmoute -
r51132:a3b1ab5f stable
parent child Browse files
Show More
@@ -1,715 +1,733 b''
1 1 # dirstatemap.py
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6
7 7 from .i18n import _
8 8
9 9 from . import (
10 10 error,
11 11 pathutil,
12 12 policy,
13 13 testing,
14 14 txnutil,
15 15 util,
16 16 )
17 17
18 18 from .dirstateutils import (
19 19 docket as docketmod,
20 20 v2,
21 21 )
22 22
23 23 parsers = policy.importmod('parsers')
24 24 rustmod = policy.importrust('dirstate')
25 25
26 26 propertycache = util.propertycache
27 27
28 28 if rustmod is None:
29 29 DirstateItem = parsers.DirstateItem
30 30 else:
31 31 DirstateItem = rustmod.DirstateItem
32 32
33 33 rangemask = 0x7FFFFFFF
34 34
35 35 WRITE_MODE_AUTO = 0
36 36 WRITE_MODE_FORCE_NEW = 1
37 37 WRITE_MODE_FORCE_APPEND = 2
38 38
39 39
40 V2_MAX_READ_ATTEMPTS = 5
41
42
40 43 class _dirstatemapcommon:
41 44 """
42 45 Methods that are identical for both implementations of the dirstatemap
43 46 class, with and without Rust extensions enabled.
44 47 """
45 48
46 49 # please pytype
47 50
48 51 _map = None
49 52 copymap = None
50 53
51 54 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
52 55 self._use_dirstate_v2 = use_dirstate_v2
53 56 self._nodeconstants = nodeconstants
54 57 self._ui = ui
55 58 self._opener = opener
56 59 self._root = root
57 60 self._filename = b'dirstate'
58 61 self._nodelen = 20 # Also update Rust code when changing this!
59 62 self._parents = None
60 63 self._dirtyparents = False
61 64 self._docket = None
62 65 write_mode = ui.config(b"devel", b"dirstate.v2.data_update_mode")
63 66 if write_mode == b"auto":
64 67 self._write_mode = WRITE_MODE_AUTO
65 68 elif write_mode == b"force-append":
66 69 self._write_mode = WRITE_MODE_FORCE_APPEND
67 70 elif write_mode == b"force-new":
68 71 self._write_mode = WRITE_MODE_FORCE_NEW
69 72 else:
70 73 # unknown value, fallback to default
71 74 self._write_mode = WRITE_MODE_AUTO
72 75
73 76 # for consistent view between _pl() and _read() invocations
74 77 self._pendingmode = None
75 78
76 79 def preload(self):
77 80 """Loads the underlying data, if it's not already loaded"""
78 81 self._map
79 82
80 83 def get(self, key, default=None):
81 84 return self._map.get(key, default)
82 85
83 86 def __len__(self):
84 87 return len(self._map)
85 88
86 89 def __iter__(self):
87 90 return iter(self._map)
88 91
89 92 def __contains__(self, key):
90 93 return key in self._map
91 94
92 95 def __getitem__(self, item):
93 96 return self._map[item]
94 97
95 98 ### disk interaction
96 99
97 100 def _opendirstatefile(self):
98 101 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
99 102 if self._pendingmode is not None and self._pendingmode != mode:
100 103 fp.close()
101 104 raise error.Abort(
102 105 _(b'working directory state may be changed parallelly')
103 106 )
104 107 self._pendingmode = mode
105 108 return fp
106 109
107 110 def _readdirstatefile(self, size=-1):
108 111 try:
109 112 with self._opendirstatefile() as fp:
110 113 return fp.read(size)
111 114 except FileNotFoundError:
112 115 # File doesn't exist, so the current state is empty
113 116 return b''
114 117
115 118 @property
116 119 def docket(self):
117 120 if not self._docket:
118 121 if not self._use_dirstate_v2:
119 122 raise error.ProgrammingError(
120 123 b'dirstate only has a docket in v2 format'
121 124 )
122 125 self._docket = docketmod.DirstateDocket.parse(
123 126 self._readdirstatefile(), self._nodeconstants
124 127 )
125 128 return self._docket
126 129
127 130 def _read_v2_data(self):
131 data = None
132 attempts = 0
133 while attempts < V2_MAX_READ_ATTEMPTS:
134 attempts += 1
135 try:
136 data = self._opener.read(self.docket.data_filename())
137 except FileNotFoundError:
138 # read race detected between docket and data file
139 # reload the docket and retry
140 self._docket = None
141 if data is None:
142 assert attempts >= V2_MAX_READ_ATTEMPTS
143 msg = b"dirstate read race happened %d times in a row"
144 msg %= attempts
145 raise error.Abort(msg)
128 146 return self._opener.read(self.docket.data_filename())
129 147
130 148 def write_v2_no_append(self, tr, st, meta, packed):
131 149 old_docket = self.docket
132 150 new_docket = docketmod.DirstateDocket.with_new_uuid(
133 151 self.parents(), len(packed), meta
134 152 )
135 153 if old_docket.uuid == new_docket.uuid:
136 154 raise error.ProgrammingError(b'dirstate docket name collision')
137 155 data_filename = new_docket.data_filename()
138 156 self._opener.write(data_filename, packed)
139 157 # Write the new docket after the new data file has been
140 158 # written. Because `st` was opened with `atomictemp=True`,
141 159 # the actual `.hg/dirstate` file is only affected on close.
142 160 st.write(new_docket.serialize())
143 161 st.close()
144 162 # Remove the old data file after the new docket pointing to
145 163 # the new data file was written.
146 164 if old_docket.uuid:
147 165 data_filename = old_docket.data_filename()
148 166 unlink = lambda _tr=None: self._opener.unlink(data_filename)
149 167 if tr:
150 168 category = b"dirstate-v2-clean-" + old_docket.uuid
151 169 tr.addpostclose(category, unlink)
152 170 else:
153 171 unlink()
154 172 self._docket = new_docket
155 173
156 174 ### reading/setting parents
157 175
158 176 def parents(self):
159 177 if not self._parents:
160 178 if self._use_dirstate_v2:
161 179 self._parents = self.docket.parents
162 180 else:
163 181 read_len = self._nodelen * 2
164 182 st = self._readdirstatefile(read_len)
165 183 l = len(st)
166 184 if l == read_len:
167 185 self._parents = (
168 186 st[: self._nodelen],
169 187 st[self._nodelen : 2 * self._nodelen],
170 188 )
171 189 elif l == 0:
172 190 self._parents = (
173 191 self._nodeconstants.nullid,
174 192 self._nodeconstants.nullid,
175 193 )
176 194 else:
177 195 raise error.Abort(
178 196 _(b'working directory state appears damaged!')
179 197 )
180 198
181 199 return self._parents
182 200
183 201
184 202 class dirstatemap(_dirstatemapcommon):
185 203 """Map encapsulating the dirstate's contents.
186 204
187 205 The dirstate contains the following state:
188 206
189 207 - `identity` is the identity of the dirstate file, which can be used to
190 208 detect when changes have occurred to the dirstate file.
191 209
192 210 - `parents` is a pair containing the parents of the working copy. The
193 211 parents are updated by calling `setparents`.
194 212
195 213 - the state map maps filenames to tuples of (state, mode, size, mtime),
196 214 where state is a single character representing 'normal', 'added',
197 215 'removed', or 'merged'. It is read by treating the dirstate as a
198 216 dict. File state is updated by calling various methods (see each
199 217 documentation for details):
200 218
201 219 - `reset_state`,
202 220 - `set_tracked`
203 221 - `set_untracked`
204 222 - `set_clean`
205 223 - `set_possibly_dirty`
206 224
207 225 - `copymap` maps destination filenames to their source filename.
208 226
209 227 The dirstate also provides the following views onto the state:
210 228
211 229 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
212 230 form that they appear as in the dirstate.
213 231
214 232 - `dirfoldmap` is a dict mapping normalized directory names to the
215 233 denormalized form that they appear as in the dirstate.
216 234 """
217 235
218 236 ### Core data storage and access
219 237
220 238 @propertycache
221 239 def _map(self):
222 240 self._map = {}
223 241 self.read()
224 242 return self._map
225 243
226 244 @propertycache
227 245 def copymap(self):
228 246 self.copymap = {}
229 247 self._map
230 248 return self.copymap
231 249
232 250 def clear(self):
233 251 self._map.clear()
234 252 self.copymap.clear()
235 253 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
236 254 util.clearcachedproperty(self, b"_dirs")
237 255 util.clearcachedproperty(self, b"_alldirs")
238 256 util.clearcachedproperty(self, b"filefoldmap")
239 257 util.clearcachedproperty(self, b"dirfoldmap")
240 258
241 259 def items(self):
242 260 return self._map.items()
243 261
244 262 # forward for python2,3 compat
245 263 iteritems = items
246 264
247 265 def debug_iter(self, all):
248 266 """
249 267 Return an iterator of (filename, state, mode, size, mtime) tuples
250 268
251 269 `all` is unused when Rust is not enabled
252 270 """
253 271 for (filename, item) in self.items():
254 272 yield (filename, item.state, item.mode, item.size, item.mtime)
255 273
256 274 def keys(self):
257 275 return self._map.keys()
258 276
259 277 ### reading/setting parents
260 278
261 279 def setparents(self, p1, p2, fold_p2=False):
262 280 self._parents = (p1, p2)
263 281 self._dirtyparents = True
264 282 copies = {}
265 283 if fold_p2:
266 284 for f, s in self._map.items():
267 285 # Discard "merged" markers when moving away from a merge state
268 286 if s.p2_info:
269 287 source = self.copymap.pop(f, None)
270 288 if source:
271 289 copies[f] = source
272 290 s.drop_merge_data()
273 291 return copies
274 292
275 293 ### disk interaction
276 294
277 295 def read(self):
278 296 # ignore HG_PENDING because identity is used only for writing
279 297 self.identity = util.filestat.frompath(
280 298 self._opener.join(self._filename)
281 299 )
282 300
283 301 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
284 302 if self._use_dirstate_v2:
285 303
286 304 if not self.docket.uuid:
287 305 return
288 306 testing.wait_on_cfg(self._ui, b'dirstate.post-docket-read-file')
289 307 st = self._read_v2_data()
290 308 else:
291 309 st = self._readdirstatefile()
292 310
293 311 if not st:
294 312 return
295 313
296 314 # TODO: adjust this estimate for dirstate-v2
297 315 if util.safehasattr(parsers, b'dict_new_presized'):
298 316 # Make an estimate of the number of files in the dirstate based on
299 317 # its size. This trades wasting some memory for avoiding costly
300 318 # resizes. Each entry have a prefix of 17 bytes followed by one or
301 319 # two path names. Studies on various large-scale real-world repositories
302 320 # found 54 bytes a reasonable upper limit for the average path names.
303 321 # Copy entries are ignored for the sake of this estimate.
304 322 self._map = parsers.dict_new_presized(len(st) // 71)
305 323
306 324 # Python's garbage collector triggers a GC each time a certain number
307 325 # of container objects (the number being defined by
308 326 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
309 327 # for each file in the dirstate. The C version then immediately marks
310 328 # them as not to be tracked by the collector. However, this has no
311 329 # effect on when GCs are triggered, only on what objects the GC looks
312 330 # into. This means that O(number of files) GCs are unavoidable.
313 331 # Depending on when in the process's lifetime the dirstate is parsed,
314 332 # this can get very expensive. As a workaround, disable GC while
315 333 # parsing the dirstate.
316 334 #
317 335 # (we cannot decorate the function directly since it is in a C module)
318 336 if self._use_dirstate_v2:
319 337 p = self.docket.parents
320 338 meta = self.docket.tree_metadata
321 339 parse_dirstate = util.nogc(v2.parse_dirstate)
322 340 parse_dirstate(self._map, self.copymap, st, meta)
323 341 else:
324 342 parse_dirstate = util.nogc(parsers.parse_dirstate)
325 343 p = parse_dirstate(self._map, self.copymap, st)
326 344 if not self._dirtyparents:
327 345 self.setparents(*p)
328 346
329 347 # Avoid excess attribute lookups by fast pathing certain checks
330 348 self.__contains__ = self._map.__contains__
331 349 self.__getitem__ = self._map.__getitem__
332 350 self.get = self._map.get
333 351
334 352 def write(self, tr, st):
335 353 if self._use_dirstate_v2:
336 354 packed, meta = v2.pack_dirstate(self._map, self.copymap)
337 355 self.write_v2_no_append(tr, st, meta, packed)
338 356 else:
339 357 packed = parsers.pack_dirstate(
340 358 self._map, self.copymap, self.parents()
341 359 )
342 360 st.write(packed)
343 361 st.close()
344 362 self._dirtyparents = False
345 363
346 364 @propertycache
347 365 def identity(self):
348 366 self._map
349 367 return self.identity
350 368
351 369 ### code related to maintaining and accessing "extra" property
352 370 # (e.g. "has_dir")
353 371
354 372 def _dirs_incr(self, filename, old_entry=None):
355 373 """increment the dirstate counter if applicable"""
356 374 if (
357 375 old_entry is None or old_entry.removed
358 376 ) and "_dirs" in self.__dict__:
359 377 self._dirs.addpath(filename)
360 378 if old_entry is None and "_alldirs" in self.__dict__:
361 379 self._alldirs.addpath(filename)
362 380
363 381 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
364 382 """decrement the dirstate counter if applicable"""
365 383 if old_entry is not None:
366 384 if "_dirs" in self.__dict__ and not old_entry.removed:
367 385 self._dirs.delpath(filename)
368 386 if "_alldirs" in self.__dict__ and not remove_variant:
369 387 self._alldirs.delpath(filename)
370 388 elif remove_variant and "_alldirs" in self.__dict__:
371 389 self._alldirs.addpath(filename)
372 390 if "filefoldmap" in self.__dict__:
373 391 normed = util.normcase(filename)
374 392 self.filefoldmap.pop(normed, None)
375 393
376 394 @propertycache
377 395 def filefoldmap(self):
378 396 """Returns a dictionary mapping normalized case paths to their
379 397 non-normalized versions.
380 398 """
381 399 try:
382 400 makefilefoldmap = parsers.make_file_foldmap
383 401 except AttributeError:
384 402 pass
385 403 else:
386 404 return makefilefoldmap(
387 405 self._map, util.normcasespec, util.normcasefallback
388 406 )
389 407
390 408 f = {}
391 409 normcase = util.normcase
392 410 for name, s in self._map.items():
393 411 if not s.removed:
394 412 f[normcase(name)] = name
395 413 f[b'.'] = b'.' # prevents useless util.fspath() invocation
396 414 return f
397 415
398 416 @propertycache
399 417 def dirfoldmap(self):
400 418 f = {}
401 419 normcase = util.normcase
402 420 for name in self._dirs:
403 421 f[normcase(name)] = name
404 422 return f
405 423
406 424 def hastrackeddir(self, d):
407 425 """
408 426 Returns True if the dirstate contains a tracked (not removed) file
409 427 in this directory.
410 428 """
411 429 return d in self._dirs
412 430
413 431 def hasdir(self, d):
414 432 """
415 433 Returns True if the dirstate contains a file (tracked or removed)
416 434 in this directory.
417 435 """
418 436 return d in self._alldirs
419 437
420 438 @propertycache
421 439 def _dirs(self):
422 440 return pathutil.dirs(self._map, only_tracked=True)
423 441
424 442 @propertycache
425 443 def _alldirs(self):
426 444 return pathutil.dirs(self._map)
427 445
428 446 ### code related to manipulation of entries and copy-sources
429 447
430 448 def reset_state(
431 449 self,
432 450 filename,
433 451 wc_tracked=False,
434 452 p1_tracked=False,
435 453 p2_info=False,
436 454 has_meaningful_mtime=True,
437 455 parentfiledata=None,
438 456 ):
439 457 """Set a entry to a given state, diregarding all previous state
440 458
441 459 This is to be used by the part of the dirstate API dedicated to
442 460 adjusting the dirstate after a update/merge.
443 461
444 462 note: calling this might result to no entry existing at all if the
445 463 dirstate map does not see any point at having one for this file
446 464 anymore.
447 465 """
448 466 # copy information are now outdated
449 467 # (maybe new information should be in directly passed to this function)
450 468 self.copymap.pop(filename, None)
451 469
452 470 if not (p1_tracked or p2_info or wc_tracked):
453 471 old_entry = self._map.get(filename)
454 472 self._drop_entry(filename)
455 473 self._dirs_decr(filename, old_entry=old_entry)
456 474 return
457 475
458 476 old_entry = self._map.get(filename)
459 477 self._dirs_incr(filename, old_entry)
460 478 entry = DirstateItem(
461 479 wc_tracked=wc_tracked,
462 480 p1_tracked=p1_tracked,
463 481 p2_info=p2_info,
464 482 has_meaningful_mtime=has_meaningful_mtime,
465 483 parentfiledata=parentfiledata,
466 484 )
467 485 self._map[filename] = entry
468 486
469 487 def set_tracked(self, filename):
470 488 new = False
471 489 entry = self.get(filename)
472 490 if entry is None:
473 491 self._dirs_incr(filename)
474 492 entry = DirstateItem(
475 493 wc_tracked=True,
476 494 )
477 495
478 496 self._map[filename] = entry
479 497 new = True
480 498 elif not entry.tracked:
481 499 self._dirs_incr(filename, entry)
482 500 entry.set_tracked()
483 501 self._refresh_entry(filename, entry)
484 502 new = True
485 503 else:
486 504 # XXX This is probably overkill for more case, but we need this to
487 505 # fully replace the `normallookup` call with `set_tracked` one.
488 506 # Consider smoothing this in the future.
489 507 entry.set_possibly_dirty()
490 508 self._refresh_entry(filename, entry)
491 509 return new
492 510
493 511 def set_untracked(self, f):
494 512 """Mark a file as no longer tracked in the dirstate map"""
495 513 entry = self.get(f)
496 514 if entry is None:
497 515 return False
498 516 else:
499 517 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
500 518 if not entry.p2_info:
501 519 self.copymap.pop(f, None)
502 520 entry.set_untracked()
503 521 self._refresh_entry(f, entry)
504 522 return True
505 523
506 524 def set_clean(self, filename, mode, size, mtime):
507 525 """mark a file as back to a clean state"""
508 526 entry = self[filename]
509 527 size = size & rangemask
510 528 entry.set_clean(mode, size, mtime)
511 529 self._refresh_entry(filename, entry)
512 530 self.copymap.pop(filename, None)
513 531
514 532 def set_possibly_dirty(self, filename):
515 533 """record that the current state of the file on disk is unknown"""
516 534 entry = self[filename]
517 535 entry.set_possibly_dirty()
518 536 self._refresh_entry(filename, entry)
519 537
520 538 def _refresh_entry(self, f, entry):
521 539 """record updated state of an entry"""
522 540 if not entry.any_tracked:
523 541 self._map.pop(f, None)
524 542
525 543 def _drop_entry(self, f):
526 544 """remove any entry for file f
527 545
528 546 This should also drop associated copy information
529 547
530 548 The fact we actually need to drop it is the responsability of the caller"""
531 549 self._map.pop(f, None)
532 550 self.copymap.pop(f, None)
533 551
534 552
535 553 if rustmod is not None:
536 554
537 555 class dirstatemap(_dirstatemapcommon):
538 556
539 557 ### Core data storage and access
540 558
541 559 @propertycache
542 560 def _map(self):
543 561 """
544 562 Fills the Dirstatemap when called.
545 563 """
546 564 # ignore HG_PENDING because identity is used only for writing
547 565 self.identity = util.filestat.frompath(
548 566 self._opener.join(self._filename)
549 567 )
550 568
551 569 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
552 570 if self._use_dirstate_v2:
553 571 if self.docket.uuid:
554 572 testing.wait_on_cfg(
555 573 self._ui, b'dirstate.post-docket-read-file'
556 574 )
557 575 # TODO: use mmap when possible
558 576 data = self._opener.read(self.docket.data_filename())
559 577 else:
560 578 data = b''
561 579 self._map = rustmod.DirstateMap.new_v2(
562 580 data, self.docket.data_size, self.docket.tree_metadata
563 581 )
564 582 parents = self.docket.parents
565 583 else:
566 584 self._map, parents = rustmod.DirstateMap.new_v1(
567 585 self._readdirstatefile()
568 586 )
569 587
570 588 if parents and not self._dirtyparents:
571 589 self.setparents(*parents)
572 590
573 591 self.__contains__ = self._map.__contains__
574 592 self.__getitem__ = self._map.__getitem__
575 593 self.get = self._map.get
576 594 return self._map
577 595
578 596 @property
579 597 def copymap(self):
580 598 return self._map.copymap()
581 599
582 600 def debug_iter(self, all):
583 601 """
584 602 Return an iterator of (filename, state, mode, size, mtime) tuples
585 603
586 604 `all`: also include with `state == b' '` dirstate tree nodes that
587 605 don't have an associated `DirstateItem`.
588 606
589 607 """
590 608 return self._map.debug_iter(all)
591 609
592 610 def clear(self):
593 611 self._map.clear()
594 612 self.setparents(
595 613 self._nodeconstants.nullid, self._nodeconstants.nullid
596 614 )
597 615 util.clearcachedproperty(self, b"_dirs")
598 616 util.clearcachedproperty(self, b"_alldirs")
599 617 util.clearcachedproperty(self, b"dirfoldmap")
600 618
601 619 def items(self):
602 620 return self._map.items()
603 621
604 622 # forward for python2,3 compat
605 623 iteritems = items
606 624
607 625 def keys(self):
608 626 return iter(self._map)
609 627
610 628 ### reading/setting parents
611 629
612 630 def setparents(self, p1, p2, fold_p2=False):
613 631 self._parents = (p1, p2)
614 632 self._dirtyparents = True
615 633 copies = {}
616 634 if fold_p2:
617 635 copies = self._map.setparents_fixup()
618 636 return copies
619 637
620 638 ### disk interaction
621 639
622 640 @propertycache
623 641 def identity(self):
624 642 self._map
625 643 return self.identity
626 644
627 645 def write(self, tr, st):
628 646 if not self._use_dirstate_v2:
629 647 p1, p2 = self.parents()
630 648 packed = self._map.write_v1(p1, p2)
631 649 st.write(packed)
632 650 st.close()
633 651 self._dirtyparents = False
634 652 return
635 653
636 654 # We can only append to an existing data file if there is one
637 655 write_mode = self._write_mode
638 656 if self.docket.uuid is None:
639 657 write_mode = WRITE_MODE_FORCE_NEW
640 658 packed, meta, append = self._map.write_v2(write_mode)
641 659 if append:
642 660 docket = self.docket
643 661 data_filename = docket.data_filename()
644 662 with self._opener(data_filename, b'r+b') as fp:
645 663 fp.seek(docket.data_size)
646 664 assert fp.tell() == docket.data_size
647 665 written = fp.write(packed)
648 666 if written is not None: # py2 may return None
649 667 assert written == len(packed), (written, len(packed))
650 668 docket.data_size += len(packed)
651 669 docket.parents = self.parents()
652 670 docket.tree_metadata = meta
653 671 st.write(docket.serialize())
654 672 st.close()
655 673 else:
656 674 self.write_v2_no_append(tr, st, meta, packed)
657 675 # Reload from the newly-written file
658 676 util.clearcachedproperty(self, b"_map")
659 677 self._dirtyparents = False
660 678
661 679 ### code related to maintaining and accessing "extra" property
662 680 # (e.g. "has_dir")
663 681
664 682 @propertycache
665 683 def filefoldmap(self):
666 684 """Returns a dictionary mapping normalized case paths to their
667 685 non-normalized versions.
668 686 """
669 687 return self._map.filefoldmapasdict()
670 688
671 689 def hastrackeddir(self, d):
672 690 return self._map.hastrackeddir(d)
673 691
674 692 def hasdir(self, d):
675 693 return self._map.hasdir(d)
676 694
677 695 @propertycache
678 696 def dirfoldmap(self):
679 697 f = {}
680 698 normcase = util.normcase
681 699 for name in self._map.tracked_dirs():
682 700 f[normcase(name)] = name
683 701 return f
684 702
685 703 ### code related to manipulation of entries and copy-sources
686 704
687 705 def set_tracked(self, f):
688 706 return self._map.set_tracked(f)
689 707
690 708 def set_untracked(self, f):
691 709 return self._map.set_untracked(f)
692 710
693 711 def set_clean(self, filename, mode, size, mtime):
694 712 self._map.set_clean(filename, mode, size, mtime)
695 713
696 714 def set_possibly_dirty(self, f):
697 715 self._map.set_possibly_dirty(f)
698 716
699 717 def reset_state(
700 718 self,
701 719 filename,
702 720 wc_tracked=False,
703 721 p1_tracked=False,
704 722 p2_info=False,
705 723 has_meaningful_mtime=True,
706 724 parentfiledata=None,
707 725 ):
708 726 return self._map.reset_state(
709 727 filename,
710 728 wc_tracked,
711 729 p1_tracked,
712 730 p2_info,
713 731 has_meaningful_mtime,
714 732 parentfiledata,
715 733 )
@@ -1,554 +1,569 b''
1 1 ==============================================================================
2 2 Check potential race conditions between a dirstate's read and other operations
3 3 ==============================================================================
4 4
5 5 #testcases dirstate-v1 dirstate-v2-append dirstate-v2-rewrite
6 6 #testcases pre-all-read pre-some-read
7 7
8 8 Some commands, like `hg status`, do not need to take the wlock but need to
9 9 access dirstate data.
10 10 Other commands might update the dirstate data while this happens.
11 11
12 12 This can create issues if repository data is read in the wrong order, or for
13 13 the dirstate-v2 format where the data is contained in multiple files.
14 14
15 15 This test file is meant to test various cases where such parallel operations
16 16 happen and make sure the reading process behaves fine. We do so with a `hg
17 17 status` command since it is probably the most advanced of such read-only
18 18 command.
19 19
20 20 It bears simililarity with `tests/test-dirstate-status-race.t ` but tests a
21 21 different type of race.
22 22
23 23 Setup
24 24 =====
25 25
26 26 $ cat >> $HGRCPATH << EOF
27 27 > [storage]
28 28 > dirstate-v2.slow-path=allow
29 29 > EOF
30 30
31 31 #if no-dirstate-v1
32 32 $ cat >> $HGRCPATH << EOF
33 33 > [format]
34 34 > use-dirstate-v2=yes
35 35 > EOF
36 36 #else
37 37 $ cat >> $HGRCPATH << EOF
38 38 > [format]
39 39 > use-dirstate-v2=no
40 40 > EOF
41 41 #endif
42 42
43 43 #if dirstate-v2-rewrite
44 44 $ d2args="--config devel.dirstate.v2.data_update_mode=force-new"
45 45 #endif
46 46 #if dirstate-v2-append
47 47 $ d2args="--config devel.dirstate.v2.data_update_mode=force-append"
48 48 #endif
49 49
50 50
51 51 #if dirstate-v1
52 52 $ cfg="devel.sync.dirstate.pre-read-file"
53 53 #else
54 54 #if pre-all-read
55 55 $ cfg="devel.sync.dirstate.pre-read-file"
56 56 #else
57 57 $ cfg="devel.sync.dirstate.post-docket-read-file"
58 58 #endif
59 59 #endif
60 60
61 61 $ directories="dir dir/nested dir2"
62 62 $ first_files="dir/nested/a dir/b dir/c dir/d dir2/e f"
63 63 $ second_files="g dir/nested/h dir/i dir/j dir2/k dir2/l dir/nested/m"
64 64 $ extra_files="dir/n dir/o p q"
65 65
66 66 $ hg init reference-repo
67 67 $ cd reference-repo
68 68 $ mkdir -p dir/nested dir2
69 69 $ touch -t 200001010000 $first_files $directories
70 70 $ hg commit -Aqm "recreate a bunch of files to facilitate dirstate-v2 append"
71 71 $ touch -t 200001010010 $second_files $directories
72 72 $ hg commit -Aqm "more files to have two commit"
73 73 $ hg log -G -v
74 74 @ changeset: 1:9a86dcbfb938
75 75 | tag: tip
76 76 | user: test
77 77 | date: Thu Jan 01 00:00:00 1970 +0000
78 78 | files: dir/i dir/j dir/nested/h dir/nested/m dir2/k dir2/l g
79 79 | description:
80 80 | more files to have two commit
81 81 |
82 82 |
83 83 o changeset: 0:4f23db756b09
84 84 user: test
85 85 date: Thu Jan 01 00:00:00 1970 +0000
86 86 files: dir/b dir/c dir/d dir/nested/a dir2/e f
87 87 description:
88 88 recreate a bunch of files to facilitate dirstate-v2 append
89 89
90 90
91 91 $ hg manifest
92 92 dir/b
93 93 dir/c
94 94 dir/d
95 95 dir/i
96 96 dir/j
97 97 dir/nested/a
98 98 dir/nested/h
99 99 dir/nested/m
100 100 dir2/e
101 101 dir2/k
102 102 dir2/l
103 103 f
104 104 g
105 105
106 106 Add some unknown files and refresh the dirstate
107 107
108 108 $ touch -t 200001010020 $extra_files
109 109 $ hg add dir/o
110 110 $ hg remove dir/nested/m
111 111
112 112 $ hg st --config devel.dirstate.v2.data_update_mode=force-new
113 113 A dir/o
114 114 R dir/nested/m
115 115 ? dir/n
116 116 ? p
117 117 ? q
118 118 $ hg debugstate
119 119 n 644 0 2000-01-01 00:00:00 dir/b
120 120 n 644 0 2000-01-01 00:00:00 dir/c
121 121 n 644 0 2000-01-01 00:00:00 dir/d
122 122 n 644 0 2000-01-01 00:10:00 dir/i
123 123 n 644 0 2000-01-01 00:10:00 dir/j
124 124 n 644 0 2000-01-01 00:00:00 dir/nested/a
125 125 n 644 0 2000-01-01 00:10:00 dir/nested/h
126 126 r ?????????????????????????????????? dir/nested/m (glob)
127 127 a ?????????????????????????????????? dir/o (glob)
128 128 n 644 0 2000-01-01 00:00:00 dir2/e
129 129 n 644 0 2000-01-01 00:10:00 dir2/k
130 130 n 644 0 2000-01-01 00:10:00 dir2/l
131 131 n 644 0 2000-01-01 00:00:00 f
132 132 n 644 0 2000-01-01 00:10:00 g
133 133 $ hg debugstate > ../reference
134 134 $ cd ..
135 135
136 136 Actual Testing
137 137 ==============
138 138
139 139 Race with a `hg add`
140 140 -------------------
141 141
142 142 $ cp -a reference-repo race-with-add
143 143 $ cd race-with-add
144 144
145 145 spin a `hg status` with some caches to update
146 146
147 147 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
148 148 > --config rhg.on-unsupported=abort \
149 149 > --config ${cfg}=$TESTTMP/status-race-lock \
150 150 > &
151 151 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
152 152
153 153 Add a file
154 154
155 155 $ hg $d2args add dir/n
156 156 $ touch $TESTTMP/status-race-lock
157 157 $ wait
158 158
159 159 The file should in a "added" state
160 160
161 161 $ hg status
162 162 A dir/n
163 163 A dir/o
164 164 R dir/nested/m
165 165 ? p
166 166 ? q
167 167
168 168 The status process should return a consistent result and not crash.
169 169
170 170 #if dirstate-v1
171 171 $ cat $TESTTMP/status-race-lock.out
172 172 A dir/n
173 173 A dir/o
174 174 R dir/nested/m
175 175 ? p
176 176 ? q
177 177 $ cat $TESTTMP/status-race-lock.log
178 178 #else
179 179 #if rhg
180 180 #if pre-all-read
181 181 $ cat $TESTTMP/status-race-lock.out
182 182 A dir/n
183 183 A dir/o
184 184 R dir/nested/m
185 185 ? p
186 186 ? q
187 187 $ cat $TESTTMP/status-race-lock.log
188 188 #else
189 189 #if dirstate-v2-append
190 190 $ cat $TESTTMP/status-race-lock.out
191 191 A dir/o
192 192 R dir/nested/m
193 193 ? dir/n
194 194 ? p
195 195 ? q
196 196 $ cat $TESTTMP/status-race-lock.log
197 197 #else
198 198 $ cat $TESTTMP/status-race-lock.out
199 199 $ cat $TESTTMP/status-race-lock.log
200 200 abort: dirstate-v2 parse error: not enough bytes on disk
201 201 #endif
202 202 #endif
203 203 #else
204 204 #if rust
205 205 #if dirstate-v2-rewrite
206 206 $ cat $TESTTMP/status-race-lock.out
207 207 $ cat $TESTTMP/status-race-lock.log
208 208 abort: $ENOENT$: '$TESTTMP/race-with-add/.hg/dirstate.* (glob)
209 209 #else
210 210 $ cat $TESTTMP/status-race-lock.out
211 211 A dir/o
212 212 R dir/nested/m
213 213 ? dir/n
214 214 ? p
215 215 ? q
216 216 $ cat $TESTTMP/status-race-lock.log
217 217 #endif
218 218 #else
219 219 $ cat $TESTTMP/status-race-lock.out
220 A dir/n
221 A dir/o
222 R dir/nested/m
223 ? p
224 ? q
220 225 $ cat $TESTTMP/status-race-lock.log
221 abort: $ENOENT$: '$TESTTMP/race-with-add/.hg/dirstate.* (glob)
222 226 #endif
223 227 #endif
224 228 #endif
225 229
226 230 final cleanup
227 231
228 232 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
229 233 $ cd ..
230 234
231 235 Race with a `hg commit`
232 236 -----------------------
233 237
234 238 $ cp -a reference-repo race-with-commit
235 239 $ cd race-with-commit
236 240
237 241 spin a `hg status with some cache to update
238 242
239 243 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
240 244 > --config rhg.on-unsupported=abort \
241 245 > --config ${cfg}=$TESTTMP/status-race-lock \
242 246 > &
243 247 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
244 248
245 249 Add a do a commit
246 250
247 251 $ hg status
248 252 A dir/o
249 253 R dir/nested/m
250 254 ? dir/n
251 255 ? p
252 256 ? q
253 257 $ hg $d2args commit -m 'racing commit'
254 258 $ touch $TESTTMP/status-race-lock
255 259 $ wait
256 260
257 261 commit was created, and status is now clean
258 262
259 263 $ hg log -GT '{node|short} {desc}\n'
260 264 @ 02a67a77ee9b racing commit
261 265 |
262 266 o 9a86dcbfb938 more files to have two commit
263 267 |
264 268 o 4f23db756b09 recreate a bunch of files to facilitate dirstate-v2 append
265 269
266 270 $ hg status
267 271 ? dir/n
268 272 ? p
269 273 ? q
270 274
271 275 The status process should return a consistent result and not crash.
272 276
273 277 #if dirstate-v1
274 278 $ cat $TESTTMP/status-race-lock.out
275 279 M dir/o (no-rhg !)
276 280 ? dir/n
277 281 ? p
278 282 ? q
279 283 $ cat $TESTTMP/status-race-lock.log
280 284 warning: ignoring unknown working parent 02a67a77ee9b! (no-rhg !)
281 285 #else
282 286 #if rhg
283 287 #if pre-all-read
284 288 $ cat $TESTTMP/status-race-lock.out
285 289 ? dir/n
286 290 ? p
287 291 ? q
288 292 $ cat $TESTTMP/status-race-lock.log
289 293 #else
290 294 #if dirstate-v2-append
291 295 $ cat $TESTTMP/status-race-lock.out
292 296 A dir/o
293 297 R dir/nested/m
294 298 ? dir/n
295 299 ? p
296 300 ? q
297 301 $ cat $TESTTMP/status-race-lock.log
298 302 #else
299 303 $ cat $TESTTMP/status-race-lock.out
300 304 $ cat $TESTTMP/status-race-lock.log
301 305 abort: dirstate-v2 parse error: not enough bytes on disk
302 306 #endif
303 307 #endif
304 308 #else
305 309 #if rust
306 310 #if dirstate-v2-rewrite
307 311 $ cat $TESTTMP/status-race-lock.out
308 312 $ cat $TESTTMP/status-race-lock.log
309 313 abort: $ENOENT$: '$TESTTMP/race-with-commit/.hg/dirstate.* (glob)
310 314 #else
311 315 $ cat $TESTTMP/status-race-lock.out
312 316 A dir/o
313 317 R dir/nested/m
314 318 ? dir/n
315 319 ? p
316 320 ? q
317 321 $ cat $TESTTMP/status-race-lock.log
318 322 #endif
319 323 #else
320 324 $ cat $TESTTMP/status-race-lock.out
325 M dir/o
326 ? dir/n
327 ? p
328 ? q
321 329 $ cat $TESTTMP/status-race-lock.log
322 abort: $ENOENT$: '$TESTTMP/race-with-commit/.hg/dirstate.* (glob)
330 warning: ignoring unknown working parent 02a67a77ee9b!
323 331 #endif
324 332 #endif
325 333 #endif
326 334
327 335 final cleanup
328 336
329 337 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
330 338 $ cd ..
331 339
332 340 Race with a `hg update`
333 341 -----------------------
334 342
335 343 $ cp -a reference-repo race-with-update
336 344 $ cd race-with-update
337 345
338 346 spin a `hg status` with some caches to update
339 347
340 348 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
341 349 > --config rhg.on-unsupported=abort \
342 350 > --config ${cfg}=$TESTTMP/status-race-lock \
343 351 > &
344 352 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
345 353 do an update
346 354
347 355 $ hg status
348 356 A dir/o
349 357 R dir/nested/m
350 358 ? dir/n
351 359 ? p
352 360 ? q
353 361 $ hg log -GT '{node|short} {desc}\n'
354 362 @ 9a86dcbfb938 more files to have two commit
355 363 |
356 364 o 4f23db756b09 recreate a bunch of files to facilitate dirstate-v2 append
357 365
358 366 $ hg $d2args update --merge ".~1"
359 367 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
360 368 $ touch $TESTTMP/status-race-lock
361 369 $ wait
362 370 #if rhg dirstate-v2-append pre-some-read
363 371 $ hg log -GT '{node|short} {desc}\n'
364 372 @ 9a86dcbfb938 more files to have two commit
365 373 |
366 374 o 4f23db756b09 recreate a bunch of files to facilitate dirstate-v2 append
367 375
368 376 $ hg status
369 377 A dir/o
370 378 R dir/nested/m
371 379 ! dir/i
372 380 ! dir/j
373 381 ! dir/nested/h
374 382 ! dir2/k
375 383 ! dir2/l
376 384 ! g
377 385 ? dir/n
378 386 ? p
379 387 ? q
380 388 #else
381 389 $ hg log -GT '{node|short} {desc}\n'
382 390 o 9a86dcbfb938 more files to have two commit
383 391 |
384 392 @ 4f23db756b09 recreate a bunch of files to facilitate dirstate-v2 append
385 393
386 394 $ hg status
387 395 A dir/o
388 396 ? dir/n
389 397 ? p
390 398 ? q
391 399 #endif
392 400
393 401 The status process should return a consistent result and not crash.
394 402
395 403 #if dirstate-v1
396 404 $ cat $TESTTMP/status-race-lock.out
397 405 A dir/o
398 406 ? dir/n
399 407 ? p
400 408 ? q
401 409 $ cat $TESTTMP/status-race-lock.log
402 410 #else
403 411 #if rhg
404 412 #if pre-all-read
405 413 $ cat $TESTTMP/status-race-lock.out
406 414 A dir/o
407 415 ? dir/n
408 416 ? p
409 417 ? q
410 418 $ cat $TESTTMP/status-race-lock.log
411 419 #else
412 420 #if dirstate-v2-append
413 421 $ cat $TESTTMP/status-race-lock.out
414 422 A dir/o
415 423 R dir/nested/m
416 424 ! dir/i
417 425 ! dir/j
418 426 ! dir/nested/h
419 427 ! dir2/k
420 428 ! dir2/l
421 429 ! g
422 430 ? dir/n
423 431 ? p
424 432 ? q
425 433 $ cat $TESTTMP/status-race-lock.log
426 434 #else
427 435 $ cat $TESTTMP/status-race-lock.out
428 436 $ cat $TESTTMP/status-race-lock.log
429 437 abort: dirstate-v2 parse error: not enough bytes on disk
430 438 #endif
431 439 #endif
432 440 #else
433 441 #if rust
434 442 #if dirstate-v2-rewrite
435 443 $ cat $TESTTMP/status-race-lock.out
436 444 $ cat $TESTTMP/status-race-lock.log
437 445 abort: $ENOENT$: '$TESTTMP/race-with-update/.hg/dirstate.* (glob)
438 446 #else
439 447 $ cat $TESTTMP/status-race-lock.out
440 448 A dir/o
441 449 R dir/nested/m
442 450 ! dir/i
443 451 ! dir/j
444 452 ! dir/nested/h
445 453 ! dir2/k
446 454 ! dir2/l
447 455 ! g
448 456 ? dir/n
449 457 ? p
450 458 ? q
451 459 $ cat $TESTTMP/status-race-lock.log
452 460 #endif
453 461 #else
454 462 $ cat $TESTTMP/status-race-lock.out
463 A dir/o
464 ? dir/n
465 ? p
466 ? q
455 467 $ cat $TESTTMP/status-race-lock.log
456 abort: $ENOENT$: '$TESTTMP/race-with-update/.hg/dirstate.* (glob)
457 468 #endif
458 469 #endif
459 470 #endif
460 471
461 472 final cleanup
462 473
463 474 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
464 475 $ cd ..
465 476
466 477 Race with a cache updating `hg status`
467 478 --------------------------------------
468 479
469 480 It is interesting to race with "read-only" operation (that still update its cache)
470 481
471 482 $ cp -a reference-repo race-with-status
472 483 $ cd race-with-status
473 484
474 485 spin a `hg status` with some caches to update
475 486
476 487 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
477 488 > --config rhg.on-unsupported=abort \
478 489 > --config ${cfg}=$TESTTMP/status-race-lock \
479 490 > &
480 491 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
481 492 do an update
482 493
483 494 $ touch -t 200001020006 f
484 495 $ hg $d2args status
485 496 A dir/o
486 497 R dir/nested/m
487 498 ? dir/n
488 499 ? p
489 500 ? q
490 501 $ touch $TESTTMP/status-race-lock
491 502 $ wait
492 503
493 504 The status process should return a consistent result and not crash.
494 505
495 506 #if dirstate-v1
496 507 $ cat $TESTTMP/status-race-lock.out
497 508 A dir/o
498 509 R dir/nested/m
499 510 ? dir/n
500 511 ? p
501 512 ? q
502 513 $ cat $TESTTMP/status-race-lock.log
503 514 #else
504 515 #if rhg
505 516 #if pre-all-read
506 517 $ cat $TESTTMP/status-race-lock.out
507 518 A dir/o
508 519 R dir/nested/m
509 520 ? dir/n
510 521 ? p
511 522 ? q
512 523 $ cat $TESTTMP/status-race-lock.log
513 524 #else
514 525 #if dirstate-v2-append
515 526 $ cat $TESTTMP/status-race-lock.out
516 527 A dir/o
517 528 R dir/nested/m
518 529 ? dir/n
519 530 ? p
520 531 ? q
521 532 $ cat $TESTTMP/status-race-lock.log
522 533 #else
523 534 $ cat $TESTTMP/status-race-lock.out
524 535 $ cat $TESTTMP/status-race-lock.log
525 536 abort: dirstate-v2 parse error: not enough bytes on disk
526 537 #endif
527 538 #endif
528 539 #else
529 540 #if rust
530 541 #if dirstate-v2-rewrite
531 542 $ cat $TESTTMP/status-race-lock.out
532 543 $ cat $TESTTMP/status-race-lock.log
533 544 abort: $ENOENT$: '$TESTTMP/race-with-status/.hg/dirstate.* (glob)
534 545 #else
535 546 $ cat $TESTTMP/status-race-lock.out
536 547 A dir/o
537 548 R dir/nested/m
538 549 ? dir/n
539 550 ? p
540 551 ? q
541 552 $ cat $TESTTMP/status-race-lock.log
542 553 #endif
543 554 #else
544 555 $ cat $TESTTMP/status-race-lock.out
556 A dir/o
557 R dir/nested/m
558 ? dir/n
559 ? p
560 ? q
545 561 $ cat $TESTTMP/status-race-lock.log
546 abort: $ENOENT$: '$TESTTMP/race-with-status/.hg/dirstate.* (glob)
547 562 #endif
548 563 #endif
549 564 #endif
550 565
551 566 final cleanup
552 567
553 568 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
554 569 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now