repoview: discard filtered changelog if index isn't shared with unfiltered
Before this patch, revisions rollbacked at failure of previous
transaction might be visible at subsequent operations unintentionally,
if repoview object is reused even after failure of transaction:
e.g. command server and HTTP server are typical cases.
'repoview' uses the tuple of values below of unfiltered changelog as
"the key" to examine validity of filtered changelog cache.
- length
- tip node
- filtered revisions (as hashed value)
- '_delayed' field
'repoview' compares between "the key" of unfiltered changelog at
previous caching and now, and reuses filtered changelog cache if no
change is detected.
But this comparison indicates only that there is no change between
unfiltered 'repo.changelog' at last caching and now, but not that
filtered changelog cache is valid for current unfiltered one.
'repoview' uses "shallow copy" of unfiltered changelog to create
filtered changelog cache. In this case, 'index' buffer of unfiltered
changelog is also referred by filtered changelog.
At failure of transaction, unfiltered changelog itself is invalidated
(= un-referred) on the 'repo' side (see
0a7610758c42 also). But
'index' of it still contains revisions to be rollbacked at this
failure, and is referred by filtered changelog.
Therefore, even if there is no change between unfiltered
'repo.changelog' at last caching and now, steps below makes rollbacked
revisions visible via filtered changelog unintentionally.
1. instantiate unfiltered changelog as 'repo.changelog'
(call it CL1)
2. make filtered (= shallow copy of) CL1
(call it FCL1)
3. cache FCL1 with "the key" of CL1
4. revisions are appended to 'index', which is shared by CL1 and FCL1
5. invalidate 'repo.changelog' (= CL1) at failure of transaction
6. instantiate 'repo.changelog' again at next operation
(call it CL2)
CL2 doesn't have revisions added at (4), because it is
instantiated from '00changelog.i', which isn't changed while
failed transaction.
7. compare between "the key" of CL1 and CL2
8. FCL1 cached at (3) is reused, because comparison at (7) doesn't
detect change between CL1 at (1) and CL2
9. revisions rollbacked at (5) are visible via FCL1 unintentionally,
because FCL1 still refers 'index' changed at (4)
The root cause of this issue is that there is no examination about
validity of filtered changelog cache against current unfiltered one.
This patch discards filtered changelog cache, if its 'index' object
isn't shared with unfiltered one.
BTW, at the time of this patch, redundant truncation of
'00changelog.i' at failure of transaction (see
0a7610758c42 for
detail) often prevents "hg serve" from making already rollbacked
revisions visible, because updating timestamps of '00changelog.i' by
truncation makes "hg serve" discard old repoview object with invalid
filtered changelog cache.
This is reason why this issue is overlooked before this patch, even
though test-bundle2-exchange.t has tests in similar situation: failure
of "hg push" via HTTP by pretxnclose hook on server side doesn't
prevent subsequent commands from looking up outgoing revisions
correctly.
But timestamp on the filesystem doesn't have enough resolution for
recent computation power, and it can't be assumed that this avoidance
always works as expected.
Therefore, without this patch, this issue might appear occasionally.
#!/usr/bin/env python
"""This does HTTP GET requests given a host:port and path and returns
a subset of the headers plus the body of the result."""
from __future__ import absolute_import
import httplib
import json
import os
import sys
try:
import msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
except ImportError:
pass
twice = False
if '--twice' in sys.argv:
sys.argv.remove('--twice')
twice = True
headeronly = False
if '--headeronly' in sys.argv:
sys.argv.remove('--headeronly')
headeronly = True
formatjson = False
if '--json' in sys.argv:
sys.argv.remove('--json')
formatjson = True
tag = None
def request(host, path, show):
assert not path.startswith('/'), path
global tag
headers = {}
if tag:
headers['If-None-Match'] = tag
conn = httplib.HTTPConnection(host)
conn.request("GET", '/' + path, None, headers)
response = conn.getresponse()
print response.status, response.reason
if show[:1] == ['-']:
show = sorted(h for h, v in response.getheaders()
if h.lower() not in show)
for h in [h.lower() for h in show]:
if response.getheader(h, None) is not None:
print "%s: %s" % (h, response.getheader(h))
if not headeronly:
print
data = response.read()
# Pretty print JSON. This also has the beneficial side-effect
# of verifying emitted JSON is well-formed.
if formatjson:
# json.dumps() will print trailing newlines. Eliminate them
# to make tests easier to write.
data = json.loads(data)
lines = json.dumps(data, sort_keys=True, indent=2).splitlines()
for line in lines:
print line.rstrip()
else:
sys.stdout.write(data)
if twice and response.getheader('ETag', None):
tag = response.getheader('ETag')
return response.status
status = request(sys.argv[1], sys.argv[2], sys.argv[3:])
if twice:
status = request(sys.argv[1], sys.argv[2], sys.argv[3:])
if 200 <= status <= 305:
sys.exit(0)
sys.exit(1)