persistent-nodemap-race-ext.py
294 lines
| 10.2 KiB
| text/x-python
|
PythonLexer
r48852 | """Create the race condition for issue6554 | |||
The persistent nodemap issues had an issue where a second writer could | ||||
overwrite the data that a previous write just wrote. The would break the append | ||||
only garantee of the persistent nodemap and could confuse reader. This | ||||
extensions create all the necessary synchronisation point to the race condition | ||||
to happen. | ||||
It involves 3 process <LEFT> (a writer) <RIGHT> (a writer) and <READER> | ||||
[1] <LEFT> take the lock and start a transaction | ||||
[2] <LEFT> updated `00changelog.i` with the new data | ||||
[3] <RIGHT> reads: | ||||
- the new changelog index `00changelog.i` | ||||
- the old `00changelog.n` | ||||
[4] <LEFT> update the persistent nodemap: | ||||
- writing new data from the last valid offset | ||||
- updating the docket (00changelog.n) | ||||
[5] <LEFT> release the lock | ||||
[6] <RIGHT> grab the lock and run `repo.invalidate` | ||||
[7] <READER> reads: | ||||
- the changelog index after <LEFT> write | ||||
- the nodemap docket after <LEFT> write | ||||
[8] <RIGHT> reload the changelog since `00changelog.n` changed | ||||
/!\ This is the faulty part in issue 6554, the outdated docket is kept | ||||
[9] <RIGHT> write: | ||||
- the changelog index (00changelog.i) | ||||
- the nodemap data (00changelog*.nd) | ||||
/!\ if the outdated docket is used, the write starts from the same ofset | ||||
/!\ as in [4], overwriting data that <LEFT> wrote in step [4]. | ||||
- the nodemap docket (00changelog.n) | ||||
[10] <READER> reads the nodemap data from `00changelog*.nd` | ||||
/!\ if step [9] was wrong, the data matching the docket that <READER> | ||||
/!\ loaded have been overwritten and the expected root-nodes is no longer | ||||
/!\ valid. | ||||
""" | ||||
from __future__ import print_function | ||||
import os | ||||
from mercurial.revlogutils.constants import KIND_CHANGELOG | ||||
from mercurial import ( | ||||
changelog, | ||||
encoding, | ||||
extensions, | ||||
localrepo, | ||||
node, | ||||
pycompat, | ||||
registrar, | ||||
testing, | ||||
util, | ||||
) | ||||
from mercurial.revlogutils import ( | ||||
nodemap as nodemaputil, | ||||
) | ||||
configtable = {} | ||||
configitem = registrar.configitem(configtable) | ||||
configitem(b'devel', b'nodemap-race.role', default=None) | ||||
cmdtable = {} | ||||
command = registrar.command(cmdtable) | ||||
LEFT = b'left' | ||||
RIGHT = b'right' | ||||
READER = b'reader' | ||||
SYNC_DIR = os.path.join(encoding.environ[b'TESTTMP'], b'sync-files') | ||||
# mark the end of step [1] | ||||
FILE_LEFT_LOCKED = os.path.join(SYNC_DIR, b'left-locked') | ||||
# mark that step [3] is ready to run. | ||||
FILE_RIGHT_READY_TO_LOCK = os.path.join(SYNC_DIR, b'right-ready-to-lock') | ||||
# mark the end of step [2] | ||||
FILE_LEFT_CL_DATA_WRITE = os.path.join(SYNC_DIR, b'left-data') | ||||
# mark the end of step [4] | ||||
FILE_LEFT_CL_NODEMAP_WRITE = os.path.join(SYNC_DIR, b'left-nodemap') | ||||
# mark the end of step [3] | ||||
FILE_RIGHT_CL_NODEMAP_READ = os.path.join(SYNC_DIR, b'right-nodemap') | ||||
# mark that step [9] is read to run | ||||
FILE_RIGHT_CL_NODEMAP_PRE_WRITE = os.path.join( | ||||
SYNC_DIR, b'right-pre-nodemap-write' | ||||
) | ||||
# mark that step [9] has run. | ||||
FILE_RIGHT_CL_NODEMAP_POST_WRITE = os.path.join( | ||||
SYNC_DIR, b'right-post-nodemap-write' | ||||
) | ||||
# mark that step [7] is ready to run | ||||
FILE_READER_READY = os.path.join(SYNC_DIR, b'reader-ready') | ||||
# mark that step [7] has run | ||||
FILE_READER_READ_DOCKET = os.path.join(SYNC_DIR, b'reader-read-docket') | ||||
def _print(*args, **kwargs): | ||||
print(*args, **kwargs) | ||||
def _role(repo): | ||||
"""find the role associated with the process""" | ||||
return repo.ui.config(b'devel', b'nodemap-race.role') | ||||
def wrap_changelog_finalize(orig, cl, tr): | ||||
"""wrap the update of `00changelog.i` during transaction finalization | ||||
This is useful for synchronisation before or after the file is updated on disk. | ||||
""" | ||||
role = getattr(tr, '_race_role', None) | ||||
if role == RIGHT: | ||||
print('right ready to write, waiting for reader') | ||||
testing.wait_file(FILE_READER_READY) | ||||
testing.write_file(FILE_RIGHT_CL_NODEMAP_PRE_WRITE) | ||||
testing.wait_file(FILE_READER_READ_DOCKET) | ||||
print('right proceeding with writing its changelog index and nodemap') | ||||
ret = orig(cl, tr) | ||||
print("finalized changelog write") | ||||
if role == LEFT: | ||||
testing.write_file(FILE_LEFT_CL_DATA_WRITE) | ||||
return ret | ||||
def wrap_persist_nodemap(orig, tr, revlog, *args, **kwargs): | ||||
"""wrap the update of `00changelog.n` and `*.nd` during tr finalization | ||||
This is useful for synchronisation before or after the files are updated on | ||||
disk. | ||||
""" | ||||
is_cl = revlog.target[0] == KIND_CHANGELOG | ||||
role = getattr(tr, '_race_role', None) | ||||
if is_cl: | ||||
if role == LEFT: | ||||
testing.wait_file(FILE_RIGHT_CL_NODEMAP_READ) | ||||
if is_cl: | ||||
print("persisting changelog nodemap") | ||||
print(" new data start at", revlog._nodemap_docket.data_length) | ||||
ret = orig(tr, revlog, *args, **kwargs) | ||||
if is_cl: | ||||
print("persisted changelog nodemap") | ||||
print_nodemap_details(revlog) | ||||
if role == LEFT: | ||||
testing.write_file(FILE_LEFT_CL_NODEMAP_WRITE) | ||||
elif role == RIGHT: | ||||
testing.write_file(FILE_RIGHT_CL_NODEMAP_POST_WRITE) | ||||
return ret | ||||
def print_nodemap_details(cl): | ||||
"""print relevant information about the nodemap docket currently in memory""" | ||||
dkt = cl._nodemap_docket | ||||
print('docket-details:') | ||||
if dkt is None: | ||||
print(' <no-docket>') | ||||
return | ||||
print(' uid: ', pycompat.sysstr(dkt.uid)) | ||||
print(' actual-tip: ', cl.tiprev()) | ||||
print(' tip-rev: ', dkt.tip_rev) | ||||
print(' data-length:', dkt.data_length) | ||||
def wrap_persisted_data(orig, revlog): | ||||
"""print some information about the nodemap information we just read | ||||
Used by the <READER> process only. | ||||
""" | ||||
ret = orig(revlog) | ||||
if ret is not None: | ||||
docket, data = ret | ||||
file_path = nodemaputil._rawdata_filepath(revlog, docket) | ||||
file_path = revlog.opener.join(file_path) | ||||
file_size = os.path.getsize(file_path) | ||||
print('record-data-length:', docket.data_length) | ||||
print('actual-data-length:', len(data)) | ||||
print('file-actual-length:', file_size) | ||||
return ret | ||||
def sync_read(orig): | ||||
"""used by <READER> to force the race window | ||||
This make sure we read the docker from <LEFT> while reading the datafile | ||||
after <RIGHT> write. | ||||
""" | ||||
orig() | ||||
testing.write_file(FILE_READER_READ_DOCKET) | ||||
print('reader: nodemap docket read') | ||||
testing.wait_file(FILE_RIGHT_CL_NODEMAP_POST_WRITE) | ||||
def uisetup(ui): | ||||
class RacedRepo(localrepo.localrepository): | ||||
def lock(self, wait=True): | ||||
# make sure <RIGHT> as the "Wrong" information in memory before | ||||
# grabbing the lock | ||||
newlock = self._currentlock(self._lockref) is None | ||||
if newlock and _role(self) == LEFT: | ||||
cl = self.unfiltered().changelog | ||||
print_nodemap_details(cl) | ||||
elif newlock and _role(self) == RIGHT: | ||||
testing.write_file(FILE_RIGHT_READY_TO_LOCK) | ||||
print('nodemap-race: right side start of the locking sequence') | ||||
testing.wait_file(FILE_LEFT_LOCKED) | ||||
testing.wait_file(FILE_LEFT_CL_DATA_WRITE) | ||||
self.invalidate(clearfilecache=True) | ||||
print('nodemap-race: right side reading changelog') | ||||
cl = self.unfiltered().changelog | ||||
tiprev = cl.tiprev() | ||||
tip = cl.node(tiprev) | ||||
tiprev2 = cl.rev(tip) | ||||
if tiprev != tiprev2: | ||||
raise RuntimeError( | ||||
'bad tip -round-trip %d %d' % (tiprev, tiprev2) | ||||
) | ||||
testing.write_file(FILE_RIGHT_CL_NODEMAP_READ) | ||||
print('nodemap-race: right side reading of changelog is done') | ||||
print_nodemap_details(cl) | ||||
testing.wait_file(FILE_LEFT_CL_NODEMAP_WRITE) | ||||
print('nodemap-race: right side ready to wait for the lock') | ||||
ret = super(RacedRepo, self).lock(wait=wait) | ||||
if newlock and _role(self) == LEFT: | ||||
print('nodemap-race: left side locked and ready to commit') | ||||
testing.write_file(FILE_LEFT_LOCKED) | ||||
testing.wait_file(FILE_RIGHT_READY_TO_LOCK) | ||||
cl = self.unfiltered().changelog | ||||
print_nodemap_details(cl) | ||||
elif newlock and _role(self) == RIGHT: | ||||
print('nodemap-race: right side locked and ready to commit') | ||||
cl = self.unfiltered().changelog | ||||
print_nodemap_details(cl) | ||||
return ret | ||||
def transaction(self, *args, **kwargs): | ||||
# duck punch the role on the transaction to help other pieces of code | ||||
tr = super(RacedRepo, self).transaction(*args, **kwargs) | ||||
tr._race_role = _role(self) | ||||
return tr | ||||
localrepo.localrepository = RacedRepo | ||||
extensions.wrapfunction( | ||||
nodemaputil, 'persist_nodemap', wrap_persist_nodemap | ||||
) | ||||
extensions.wrapfunction( | ||||
changelog.changelog, '_finalize', wrap_changelog_finalize | ||||
) | ||||
def reposetup(ui, repo): | ||||
if _role(repo) == READER: | ||||
extensions.wrapfunction( | ||||
nodemaputil, 'persisted_data', wrap_persisted_data | ||||
) | ||||
extensions.wrapfunction(nodemaputil, 'test_race_hook_1', sync_read) | ||||
class ReaderRepo(repo.__class__): | ||||
@util.propertycache | ||||
def changelog(self): | ||||
print('reader ready to read the changelog, waiting for right') | ||||
testing.write_file(FILE_READER_READY) | ||||
testing.wait_file(FILE_RIGHT_CL_NODEMAP_PRE_WRITE) | ||||
return super(ReaderRepo, self).changelog | ||||
repo.__class__ = ReaderRepo | ||||
@command(b'check-nodemap-race') | ||||
def cmd_check_nodemap_race(ui, repo): | ||||
"""Run proper <READER> access in the race Windows and check nodemap content""" | ||||
repo = repo.unfiltered() | ||||
print('reader: reading changelog') | ||||
cl = repo.changelog | ||||
print('reader: changelog read') | ||||
print_nodemap_details(cl) | ||||
tip_rev = cl.tiprev() | ||||
tip_node = cl.node(tip_rev) | ||||
print('tip-rev: ', tip_rev) | ||||
print('tip-node:', node.short(tip_node).decode('ascii')) | ||||
print('node-rev:', cl.rev(tip_node)) | ||||
for r in cl.revs(): | ||||
n = cl.node(r) | ||||
try: | ||||
r2 = cl.rev(n) | ||||
except ValueError as exc: | ||||
print('error while checking revision:', r) | ||||
print(' ', exc) | ||||
return 1 | ||||
else: | ||||
if r2 != r: | ||||
print('revision %d is missing from the nodemap' % r) | ||||
return 1 | ||||