Fixed graph rep to handle multiple heads correctly. I had to change the topkey format so repositories inserted with earlier versions won't work with the new code.
diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py --- a/infocalypse/__init__.py +++ b/infocalypse/__init__.py @@ -125,6 +125,7 @@ Post to freenet group on FMS. import os +from binascii import hexlify from mercurial import commands, util from infcmds import get_config_info, execute_create, execute_pull, \ @@ -135,13 +136,16 @@ def set_target_version(ui_, repo, opts, revs = opts.get('rev') or None if not revs is None: - if len(revs) > 1: - raise util.Abort("Only a single -r version is supported. ") - rev = revs[0] - ctx = repo[rev] # Fail if we don't have the rev. - params['TO_VERSION'] = rev - if ctx != repo['tip']: - ui_.status(msg_fmt % rev) + for rev in revs: + repo.changectx(rev) # Fail if we don't have the rev. + + params['TO_VERSIONS'] = tuple(revs) + ui_.status(msg_fmt % ' '.join([ver[:12] for ver in revs])) + else: + # REDFLAG: get rid of default versions arguments? + params['TO_VERSIONS'] = tuple([hexlify(head) for head in repo.heads()]) + #print "set_target_version -- using all head" + #print params['TO_VERSIONS'] def infocalypse_create(ui_, repo, **opts): """ Create a new Infocalypse repository in Freenet. """ @@ -154,7 +158,7 @@ def infocalypse_create(ui_, repo, **opts return set_target_version(ui_, repo, opts, params, - "Only inserting to version: %s\n") + "Only inserting to version(s): %s\n") params['INSERT_URI'] = insert_uri execute_create(ui_, repo, params, stored_cfg) @@ -230,7 +234,7 @@ def infocalypse_push(ui_, repo, **opts): return set_target_version(ui_, repo, opts, params, - "Only pushing to version: %s\n") + "Only pushing to version(s): %s\n") params['INSERT_URI'] = insert_uri #if opts['requesturi'] != '': # # DOESN'T search the insert uri index. @@ -260,6 +264,7 @@ AGGRESSIVE_OPT = [('', 'aggressive', Non NOSEARCH_OPT = [('', 'nosearch', None, 'use USK version in URI'), ] # Allow mercurial naming convention for command table. # pylint: disable-msg=C0103 + cmdtable = { "fn-pull": (infocalypse_pull, [('', 'uri', '', 'request URI to pull from'),] diff --git a/infocalypse/bundlecache.py b/infocalypse/bundlecache.py --- a/infocalypse/bundlecache.py +++ b/infocalypse/bundlecache.py @@ -26,7 +26,10 @@ import random from mercurial import commands +from fcpconnection import sha1_hexdigest + from graph import FIRST_INDEX, FREENET_BLOCK_LEN, MAX_REDUNDANT_LENGTH +from graphutil import get_rollup_bounds def make_temp_file(temp_dir): """ Make a temporary file name. """ @@ -71,10 +74,17 @@ class BundleCache: def get_bundle_path(self, index_pair): """ INTERNAL: Get the full path to a bundle file for the given edge. """ - start_info = self.graph.index_table[index_pair[0]] - end_info = self.graph.index_table[index_pair[1]] - return os.path.join(self.base_dir, "_tmp_%s_%s.hg" - % (start_info[1], end_info[1])) + bundle_id = sha1_hexdigest( + ''.join(self.graph.index_table[index_pair[0]][0]) + + '|' # hmmm really needed? + +''.join(self.graph.index_table[index_pair[0]][1]) + + +''.join(self.graph.index_table[index_pair[1]][0]) + + '|' # hmmm really needed? + +''.join(self.graph.index_table[index_pair[1]][1]) + ) + + return os.path.join(self.base_dir, "_tmp_%s.hg" % bundle_id) def get_cached_bundle(self, index_pair, out_file): """ INTERNAL: Copy the cached bundle file for the edge to out_file. """ @@ -110,7 +120,7 @@ class BundleCache: if raised and os.path.exists(out_file): os.remove(out_file) - def make_bundle(self, graph, index_pair, out_file=None): + def make_bundle(self, graph, version_table, index_pair, out_file=None): """ Create an hg bundle file corresponding to the edge in graph. """ #print "INDEX_PAIR:", index_pair assert not index_pair is None @@ -125,14 +135,20 @@ class BundleCache: if out_file is None: out_file = make_temp_file(self.base_dir) try: - start_info = self.graph.index_table[index_pair[0]] - end_info = self.graph.index_table[index_pair[1]] + + parents, heads = get_rollup_bounds(self.graph, self.repo, + index_pair[0] + 1, # INCLUSIVE + index_pair[1], + version_table) # Hmmm... ok to suppress mercurial noise here. self.ui_.pushbuffer() try: + #print 'PARENTS:', list(parents) + #print 'HEADS:', list(heads) commands.bundle(self.ui_, self.repo, out_file, - None, base=[start_info[1]], rev=[end_info[1]]) + None, base=list(parents), + rev=list(heads)) finally: self.ui_.popbuffer() @@ -149,7 +165,8 @@ class BundleCache: # INTENT: Freenet stores data in 32K blocks. If we can stuff # extra changes into the bundle file under the block boundry # we get extra redundancy for free. - def make_redundant_bundle(self, graph, last_index, out_file=None): + def make_redundant_bundle(self, graph, version_table, last_index, + out_file=None): """ Make an hg bundle file including the changes in the edge and other earlier changes if it is possible to fit them under the 32K block size boundry. """ @@ -167,6 +184,7 @@ class BundleCache: pair = (earliest_index, last_index) #print "PAIR:", pair bundle = self.make_bundle(graph, + version_table, pair, out_file) diff --git a/infocalypse/choose.py b/infocalypse/choose.py --- a/infocalypse/choose.py +++ b/infocalypse/choose.py @@ -21,8 +21,10 @@ import random -from graph import MAX_PATH_LEN, block_cost, print_list +from graph import MAX_PATH_LEN, block_cost, print_list, canonical_path_itr, \ + build_version_table +from graphutil import get_rollup_bounds # This is the maximum allowed ratio of allowed path block cost # to minimum full update block cost. # It is used in low_block_cost_edges() to determine when a @@ -167,7 +169,10 @@ def low_block_cost_edges(graph, known_ed def canonical_path_edges(graph, known_edges, from_index, allowed): """ INTERNAL: Returns edges containing from_index from canonical paths. """ # Steps from canonical paths - paths = graph.canonical_paths(graph.latest_index, MAX_PATH_LEN) + # REDFLAG: fix, efficiency, bound number of path, add alternate edges + #paths = graph.canonical_paths(graph.latest_index, MAX_PATH_LEN) + paths = canonical_path_itr(graph, 0, graph.latest_index, MAX_PATH_LEN) + second = [] #print "get_update_edges -- after" for path in paths: @@ -192,7 +197,6 @@ def canonical_path_edges(graph, known_ed return second - # STEP BACK: # This function answers two questions: # 0) What should I request to update as quickly as possible? @@ -277,3 +281,109 @@ def dump_update_edges(first, second, all print_list("second choice:", second) print "---" +def get_top_key_updates(graph, repo, version_table=None): + """ Returns the update tuples needed to build the top key.""" + + graph.rep_invariant() + + edges = graph.get_top_key_edges() + + coalesced_edges = [] + ordinals = {} + for edge in edges: + assert edge[2] >= 0 and edge[2] < 2 + assert edge[2] == 0 or (edge[0], edge[1], 0) in edges + ordinal = ordinals.get(edge[:2]) + if ordinal is None: + ordinal = 0 + coalesced_edges.append(edge[:2]) + ordinals[edge[:2]] = max(ordinal, edge[2]) + + if version_table is None: + version_table = build_version_table(graph, repo) + ret = [] + for edge in coalesced_edges: + parents, latest = get_rollup_bounds(graph, repo, + edge[0] + 1, edge[1], + version_table) + + length = graph.get_length(edge) + assert len(graph.edge_table[edge][1:]) > 0 + + #(length, parent_rev, latest_rev, (CHK, ...)) + update = (length, parents, latest, + graph.edge_table[edge][1:], + True, True) + ret.append(update) + + + # Stuff additional remote heads into first update. + result = get_rollup_bounds(graph, + repo, + 0, + graph.latest_index, + version_table) + + for head in ret[0][2]: + if not head in result[1]: + print "Expected head not in all_heads!", head[:12] + assert False + + #top_update = list(ret[0]) + #top_update[2] = tuple(all_heads) + #ret[0] = tuple(top_update) + + ret[0] = list(ret[0]) + ret[0][2] = tuple(result[1]) + ret[0] = tuple(ret[0]) + + return ret + +def build_salting_table(target): + """ INTERNAL: Build table used to keep track of metadata salting. """ + def traverse_candidates(candidate_list, table): + """ INTERNAL: Helper function to traverse a single candidate list. """ + for candidate in candidate_list: + if candidate[6]: + continue + edge = candidate[3] + value = table.get(edge, []) + value.append(candidate[2]) + table[edge] = value + ret = {} + traverse_candidates(target.pending_candidates(), ret) + traverse_candidates(target.current_candidates, ret) + traverse_candidates(target.next_candidates, ret) + return ret + +# REDFLAG: get rid of unused methods. +# Hmmm... feels like coding my way out of a design problem. +class SaltingState: + """ INTERNAL: Helper class to keep track of metadata salting state. + """ + def __init__(self, target): + self.table = build_salting_table(target) + + def full_request(self, edge): + """ Return True if a full request is scheduled for the edge. """ + if not edge in self.table: + return False + + for value in self.table[edge]: + if not value: + return True + return False + + def add(self, edge, is_partial): + """ Add an entry to the table. """ + value = self.table.get(edge, []) + value.append(is_partial) + self.table[edge] = value + + def needs_full_request(self, graph, edge): + """ Returns True if a full request is required. """ + assert len(edge) == 3 + if not graph.is_redundant(edge): + return False + return not (self.full_request(edge) or + self.full_request((edge[0], edge[1], int(not edge[2])))) diff --git a/infocalypse/devnotes.txt b/infocalypse/devnotes.txt --- a/infocalypse/devnotes.txt +++ b/infocalypse/devnotes.txt @@ -1,3 +1,14 @@ +djk20090425 +I reworked the graph representation to handle multiple +heads correctly. + +Repositories inserted with the previous code won't work +with the new code because I had to change the +top key format. + +djk20090422 +pylint -fparseable --include-ids=y *.py + djk20090414 KNOWN LIMITATIONS: @@ -10,3 +21,4 @@ o 1208 SSK reinserts of same data fail w o 1208 RemoveRequest kills the FCP connection. This can cause fn-pull to fail. It should work if you run it again. + diff --git a/infocalypse/fcpconnection.py b/infocalypse/fcpconnection.py --- a/infocalypse/fcpconnection.py +++ b/infocalypse/fcpconnection.py @@ -194,9 +194,6 @@ class NonBlockingSocket(IAsyncSocket): """ data = self.socket.recv(RECV_BLOCK) if not data: - #self.close() - #ret = False - #break return None #print "FROM_WIRE:" @@ -432,8 +429,6 @@ class FileDataSource(IDataSource): assert self.file return self.file.read(READ_BLOCK) - - # MESSAGE LEVEL class FCPConnection: @@ -605,8 +600,7 @@ class FCPConnection: break time.sleep(POLL_TIME_SECS) - # Doh saw this trip 20080124. Regression from - # NonBlockingSocket changes? + # Doh saw this trip 20080124. Regression from NonBlockingSocket changes? # assert client.response if not client.response: raise IOError("No response. Maybe the socket dropped?") @@ -727,9 +721,8 @@ class FCPConnection: # Copy metadata into final message msg[1]['Metadata.ContentType'] = client.context.metadata - # Add a third entry to the msg tuple containing - # the raw data, or a comment saying where it was - # written. + # Add a third entry to the msg tuple containing the raw data, + # or a comment saying where it was written. assert len(msg) == 2 msg = list(msg) if client.context.data_sink.file_name: @@ -763,17 +756,15 @@ class FCPConnection: def closed_handler(self): """ INTERNAL: Callback called by the IAsyncSocket delegate when the socket closes. """ - # REDFLAG: DCI: Remove - def dropping(data): + def dropping(data): # REDFLAG: Harmless but remove eventually. + """ INTERNAL: Print warning when data is dropped after close. """ print "DROPPING %i BYTES OF DATA AFTER CLOSE!" % len(data) self.node_hello = None if not self.socket is None: - # REDFLAG: DCI: test! - # Ignore any subsequent data. self.socket.recv_callback = lambda x:None - self.socket.recv_callback = dropping - + self.socket.recv_callback = dropping # Ignore any subsequent data. + # Hmmmm... other info, ok to share this? fake_msg = ('ProtocolError', {'CodeDescription':'Socket closed'}) #print "NOTIFIED: CLOSED" diff --git a/infocalypse/graph.py b/infocalypse/graph.py --- a/infocalypse/graph.py +++ b/infocalypse/graph.py @@ -23,9 +23,9 @@ # REDFLAG: Document how pruning works. # REDFLAG: Remove unused crap from this file # REDFLAG: push MAX_PATH_LEN into graph class -> max_cannonical_len - +# REDFLAG: stash version map info in the graph? +# REDFLAG: DOCUMENT version sorting assumptions/requirements import copy -import mercurial import os import random @@ -37,6 +37,9 @@ FIRST_INDEX = -1 NULL_REV = '0000000000000000000000000000000000000000' PENDING_INSERT = 'pending' PENDING_INSERT1 = 'pending1' + +# Values greater than 4 won't work without fixing the implementation +# of canonical_paths(). MAX_PATH_LEN = 4 INSERT_NORMAL = 1 # Don't transform inserted data. @@ -105,31 +108,132 @@ def pull_bundle(repo, ui_, bundle_file): ############################################################ -def cmp_age_weight(path_a, path_b): - """ Comparison function used to sort paths in ascending order - of 'canonicalness'. """ - # Only works for equivalent paths! - assert path_a[0][0] == path_b[0][0] - assert path_b[-1][1] == path_b[-1][1] +def edges_containing(graph, index): + """ INTERNAL: Returns a list of edges containing index in order of + ascending 'canonicalness'. + """ + def cmp_edge(edge_a, edge_b): + """ INTERNAL: Comparison function. """ + # First, ascending final index. == Most recent. + diff = edge_a[1] - edge_b[1] + if diff == 0: + # Then, descending initial index. == Most redundant + diff = edge_b[0] - edge_a[0] + if diff == 0: + # Finally, descending 'canonicalness' + diff = edge_b[2] - edge_a[2] + return diff - # NO! path step tuples contain a third entry which keeps this - # from working. - # if path_a == path_b: - # return 0 + edges = graph.contain(index) + edges.sort(cmp_edge) # Best last so you can pop + #print "--- dumping edges_containing ---" + #print '\n'.join([str(edge) for edge in edges]) + #print "---" + return edges - index = 0 - while index < len(path_a) and index < len(path_b): - if path_a[index][1] == path_b[index][1]: - if path_a[index][2] == path_b[index][2]: - index += 1 - continue - # If the edges are the same age prefer the one - # the the lower (i.e. older) CHK ordinal. - return path_b[index][2] - path_a[index][2] - return path_a[index][1] - path_b[index][1] +def tail(list_value): + """ Returns the tail of a list. """ + return list_value[len(list_value) - 1] - #print "CMP == ", path_a, path_b - return 0 +def canonical_path_itr(graph, from_index, to_index, max_search_len): + """ A generator which returns a sequence of canonical paths in + descending order of 'canonicalness'. """ + + returned = set([]) + min_search_len = -1 + while min_search_len <= max_search_len: + visited = set([]) # Retraverse for each length! REDFLAG: Do better? + steps = [edges_containing(graph, from_index), ] + current_search_len = max_search_len + while len(steps) > 0: + while len(tail(steps)) > 0: + #candidate = [tail(paths) for paths in steps] + #print "candidate: ", candidate + #print "end: ", tail(tail(steps)) + if tail(tail(steps))[1] >= to_index: + # The edge at the bottom of every list. + value = [tail(step) for step in steps] + #print "HIT:" + #print value + + if min_search_len == -1: + min_search_len = len(steps) + + current_search_len = max(len(steps), min_search_len) + tag = str(value) + if not tag in returned: + returned.add(tag) + + # Shorter paths should already be in returned. + assert len(value) >= min_search_len + assert len(value) <= max_search_len + yield value + tail(steps).pop() + elif len(steps) < current_search_len: + tag = str([tail(step) for step in steps]) + if not tag in visited: + # Follow the path one more step. + visited.add(tag) + steps.append(edges_containing(graph, + tail(tail(steps))[1] + 1)) + else: + tail(steps).pop() + else: + # Abandon the path because it's too long. + tail(steps).pop() + + # Get rid of the empty list + assert len(tail(steps)) == 0 + steps.pop() + if min_search_len == -1: + #print "No such path." + return + min_search_len += 1 + + #print "exiting" + # Done iterating. + +def get_changes(repo, version_map, versions): + """ INTERNAL: Helper function used by UpdateGraph.update() + to determine which changes need to be added. """ + if versions == None: + versions = [hexlify(head) for head in repo.heads()] + else: + versions = list(versions) # Hmmmm... + # Normalize all versions to 40 digit hex strings. + for index, version in enumerate(versions): + versions[index] = hex_version(repo, version) + + if NULL_REV in versions: + versions.remove(NULL_REV) + + new_heads = [] + for version in versions: + if not version in version_map: + new_heads.append(version) + + if len(new_heads) == 0: + if len(versions) > 0: + versions.sort() + raise UpToDate("Already in repo: " + ' '.join([ver[:12] for + ver in versions])) + else: + raise UpToDate("Empty repository. Nothing to add.") + + if len(version_map) == 1: + return ((NULL_REV,), new_heads) + + #print "VERSION MAP:" + #print version_map + # Determine base revs. + base_revs = set([]) + traversed = set([]) + for head in new_heads: + find_latest_bases(repo, head, version_map, traversed, base_revs) + + return (base_revs, new_heads) + +############################################################ def block_cost(length): """ Return the number of Freenet blocks required to store @@ -139,79 +243,6 @@ def block_cost(length): blocks += 1 return blocks -############################################################ -# Doesn't dump FIRST_INDEX entry. -def graph_to_string(graph): - """ Returns a human readable representation of the graph. """ - lines = [] - # Indices - indices = graph.index_table.keys() - indices.sort() - for index in indices: - if index == FIRST_INDEX: - continue - - entry = graph.index_table[index] - #print entry - lines.append("I:%i:%s:%s" % (index, entry[0], entry[1])) - - # Edges - index_pairs = graph.edge_table.keys() - for index_pair in index_pairs: - edge_info = graph.edge_table[index_pair] - as_str = ':'.join(edge_info[1:]) - if as_str != '': - as_str = ':' + as_str - lines.append("E:%i:%i:%i%s" % (index_pair[0], index_pair[1], - edge_info[0], - as_str)) - - return '\n'.join(lines) + '\n' - -def parse_graph(text): - """ Returns a graph parsed from text. - text must be in the format used by graph_to_string(). - Lines starting with '#' are ignored. - """ - - graph = UpdateGraph() - lines = text.split('\n') - for line in lines: - fields = line.split(':') - if fields[0] == 'I': - if len(fields) != 4: - raise ValueError("Exception parsing index values.") - index = int(fields[1]) - if index in graph.index_table: - print "OVERWRITING INDEX: " , index - if len(tuple(fields[2:])) != 2: - raise ValueError("Error parsing index value: %i" % index) - graph.index_table[index] = tuple(fields[2:]) - elif fields[0] == 'E': - #print fields - if len(fields) < 5: - raise ValueError("Exception parsing edge values.") - index_pair = (int(fields[1]), int(fields[2])) - length = int(fields[3]) - chk_list = [] - for chk in fields[4:]: - chk_list.append(chk) - graph.edge_table[index_pair] = tuple([length, ] + chk_list) - #else: - # print "SKIPPED LINE:" - # print line - indices = graph.index_table.keys() - if len(indices) == 0: - raise ValueError("No indices?") - indices.sort() - graph.latest_index = indices[-1] - - graph.rep_invariant() - - return graph - -############################################################ - class UpdateGraphException(Exception): """ Base class for UpdateGraph exceptions. """ def __init__(self, msg): @@ -225,12 +256,16 @@ class UpToDate(UpdateGraphException): class UpdateGraph: """ A digraph representing an Infocalypse Freenet - hg repository. """ + hg repository. """ # REDFLAG: digraph of what dude? def __init__(self): # Vertices in the update digraph. - # index_ordinal -> (start_rev, end_rev) - self.index_table = {FIRST_INDEX:(NULL_REV, NULL_REV)} + # + # An indice is an encapsulation of the parameters that you + # need to bundle a collection of changes. + # + # index_ordinal -> ((base_revs, ), (tip_revs, )) + self.index_table = {FIRST_INDEX:((), (NULL_REV,))} # These are edges in the update digraph. # There can be multiple redundant edges. @@ -241,9 +276,6 @@ class UpdateGraph: # (start_index, end_index) -> (length, chk@, chk@, ...) self.edge_table = {} - # Bound path search length. - self.max_search_path = 10 - self.latest_index = -1 def clone(self): @@ -271,27 +303,6 @@ class UpdateGraph: return (index_pair[0], index_pair[1], len(self.edge_table[index_pair]) - 2) - def subgraph(self, containing_paths): - """ Return a subgraph which contains the vertices and - edges in containing_paths. """ - self.rep_invariant() - graph = UpdateGraph() - max_index = -1 - - for path in containing_paths: - for step in path: - pair = step[:2] - # REDFLAG: copies ALL redundant paths - graph.edge_table[pair] = self.edge_table[pair][:] - for index in pair: - if index not in graph.index_table: - graph.index_table[index] = self.index_table[index][:] - max_index = max(max_index, index) - - graph.latest_index = max_index - graph.rep_invariant() - return graph - ############################################################ # Helper functions used when inserting / requesting # the edge CHKs. @@ -375,7 +386,7 @@ class UpdateGraph: length = self.edge_table[edge_triple[:2]][0] - # REDFLAG: DCI. MUST DEAL WITH ==32k case + # REDFLAG: MUST DEAL WITH ==32k case, djk20080425 -- I think this is ok if length <= FREENET_BLOCK_LEN: # Made redundant path by padding. return INSERT_PADDED @@ -403,10 +414,24 @@ class UpdateGraph: ############################################################ + def add_index(self, base_revs, new_heads): + """ Add changes to the graph. """ + assert not NULL_REV in new_heads + assert len(base_revs) > 0 + assert len(new_heads) > 0 + base_revs.sort() + new_heads.sort() + if self.latest_index != FIRST_INDEX and NULL_REV in base_revs: + print "add_index -- base=null in base_revs. Really do that?" + self.latest_index += 1 + self.index_table[self.latest_index] = (tuple(base_revs), + tuple(new_heads)) + return self.latest_index + # REDFLAG: really no need for ui? if so, remove arg # Index and edges to insert # Returns index triples with new edges that need to be inserted. - def update(self, repo, dummy, version, cache): + def update(self, repo, dummy, versions, cache): """ Update the graph to include versions up to version in repo. @@ -416,49 +441,50 @@ class UpdateGraph: The client code is responsible for setting their CHKs!""" - if self.latest_index > FIRST_INDEX: - if (repo.changectx(version).rev() <= - repo.changectx(self.index_table[self.latest_index][1]).rev()): - raise UpToDate("Version: %s is already in the repo." % - hex_version(repo, version)[:12]) + version_map = build_version_table(self, repo) + base_revs, new_heads = get_changes(repo, version_map, versions) + + # IMPORTANT: Order matters. Must be after find_latest_bases above. + # Update the map. REDFLAG: required? + #for version in new_heads: + # version_map[version] = self.latest_index + 1 + + index = self.add_index(list(base_revs), new_heads) new_edges = [] - # Add changes to graph. - prev_changes = self.index_table[self.latest_index] - parent_rev = prev_changes[1] - # REDFLAG: Think. What are the implicit assumptions here? - first_rev = hex_version(repo, prev_changes[1], 1) - latest_rev = hex_version(repo, version) - - index = self._add_changes(parent_rev, first_rev, latest_rev) #print "ADDED INDEX: ", index #print self.index_table # Insert index w/ rollup if possible. - first_bundle = cache.make_redundant_bundle(self, index) + first_bundle = cache.make_redundant_bundle(self, version_map, index) new_edges.append(self.add_edge(first_bundle[2], (first_bundle[0], PENDING_INSERT))) #print "ADDED EDGE: ", new_edges[-1] + bundle = None + try: + canonical_path = self.canonical_path(index, MAX_PATH_LEN) + assert len(canonical_path) <= MAX_PATH_LEN + except UpdateGraphException: + # We need to compress the path. + short_cut = self._compress_canonical_path(index, MAX_PATH_LEN + 1) - canonical_path = self.canonical_path(index, MAX_PATH_LEN + 1) - assert len(canonical_path) <= MAX_PATH_LEN + 1 + bundle = cache.make_bundle(self, version_map, short_cut) - bundle = None - if len(canonical_path) > MAX_PATH_LEN: - print "CANNONICAL LEN: ", len(canonical_path) - short_cut = self._compress_canonical_path(index, MAX_PATH_LEN + 1) - bundle = cache.make_bundle(self, short_cut) new_edges.append(self.add_edge(bundle[2], (bundle[0], PENDING_INSERT))) + # MAX_PATH_LEN + 1 search can be very slow. canonical_path = self.canonical_path(index, MAX_PATH_LEN + 1) + assert len(canonical_path) <= MAX_PATH_LEN if bundle == None: if (first_bundle[0] <= FREENET_BLOCK_LEN and first_bundle[2][0] < index - 1): # This gives us redundancy at the cost of one 32K block. + bundle = cache.make_bundle(self, + version_map, (first_bundle[2][0] + 1, index)) new_edges.append(self.add_edge(bundle[2], @@ -564,22 +590,12 @@ class UpdateGraph: to latest_index. This is what you would use to bootstrap from hg rev -1. """ - - return self.canonical_paths(to_index, max_search_len)[-1] - - def canonical_paths(self, to_index, max_search_len): - """ Returns a list of paths from no updates to to_index in - ascending order of 'canonicalness'. i.e. so you - can pop() the candidates off the list. """ - - paths = self.enumerate_update_paths(0, to_index, max_search_len) - if len(paths) == 0: + try: + return canonical_path_itr(self, 0, to_index, max_search_len).next() + except StopIteration: raise UpdateGraphException("No such path: %s" % str((0, to_index))) - paths.sort(cmp_age_weight) - return paths - def path_cost(self, path, blocks=False): """ The sum of the lengths of the hg bundles required to update using the path. """ @@ -628,15 +644,6 @@ class UpdateGraph: # descending initial update. i.e. Most recent first. return step_b[1] - step_a[1] - # REDFLAG: add_index instead ??? - # REDFLAG: rethink parent_rev - def _add_changes(self, parent_rev, first_rev, last_rev): - """ Add changes to the graph. """ - assert parent_rev == self.index_table[self.latest_index][1] - self.latest_index += 1 - self.index_table[self.latest_index] = (first_rev, last_rev) - return self.latest_index - def _cmp_block_cost(self, path_a, path_b): """ INTERNAL: A comparison function for sorting single edge paths in order of ascending order of block count. """ @@ -720,17 +727,80 @@ class UpdateGraph: ret.append(edge) return ret - def rep_invariant(self): + def rep_invariant(self, repo=None, full=True): """ Debugging function to check invariants. """ max_index = -1 + min_index = -1 for index in self.index_table.keys(): max_index = max(index, max_index) + min_index = min(index, min_index) + assert self.latest_index == max_index + assert min_index == FIRST_INDEX - assert self.latest_index == max_index + assert self.index_table[FIRST_INDEX][0] == () + assert self.index_table[FIRST_INDEX][1] == (NULL_REV, ) + for index in range(0, self.latest_index + 1): + # Indices must be contiguous. + assert index in self.index_table + # Each index except for the empty graph sentinel + # must have at least one base and head rev. + assert len(self.index_table[index][0]) > 0 + assert len(self.index_table[index][1]) > 0 + + # All edges must be resolvable. for edge in self.edge_table.keys(): assert edge[0] in self.index_table assert edge[1] in self.index_table + assert edge[0] < edge[1] + + + if repo is None: + return + + # Slow + version_map = build_version_table(self, repo) + + values = set(version_map.values()) + values = list(values) + values.sort() + assert values[-1] == max_index + assert values[0] == FIRST_INDEX + # Indices contiguous + assert values == range(FIRST_INDEX, max_index + 1) + + if full: + # Verify that version map is complete. + copied = version_map.copy() + for rev in range(-1, repo['tip'].rev() + 1): + version = hex_version(repo, rev) + assert version in copied + del copied[version] + + assert len(copied) == 0 + + every_head = set([]) + for index in range(FIRST_INDEX, max_index + 1): + versions = set([]) + for version in (self.index_table[index][0] + + self.index_table[index][0]): + assert version in version_map + if version in versions: + continue + + assert has_version(repo, version) + versions.add(version) + + # Base versions must have a lower index. + for version in self.index_table[index][0]: + assert version_map[version] < index + + # Heads must have index == index. + for version in self.index_table[index][1]: + assert version_map[version] == index + # Each head should appear in one and only one index. + assert not version in every_head + every_head.add(version) # REDFLAG: O(n), has_index(). def latest_index(graph, repo): @@ -740,62 +810,19 @@ def latest_index(graph, repo): for index in range(graph.latest_index, FIRST_INDEX - 1, -1): if not index in graph.index_table: continue - if has_version(repo, graph.index_table[index][1]): - return index + # BUG: Dog slow for big repos? cache index -> heads map + skip = False + for head in get_heads(graph, index): + if not has_version(repo, head): + skip = True + break # Inner loop... grrr named continue? + + if skip: + continue + return index + return FIRST_INDEX -# REDFLAG: fix this so that it always includes pending edges. -def minimal_update_graph(graph, max_size=32*1024, - formatter_func=graph_to_string): - """ Returns a subgraph that can be formatted to <= max_size - bytes with formatter_func. """ - - index = graph.latest_index - assert index > FIRST_INDEX - - # All the edges that would be included in the top key. - # This includes the canonical bootstrap path and the - # two cheapest updates from the previous index. - paths = [[edge, ] for edge in graph.get_top_key_edges()] - - minimal = graph.subgraph(paths) - if len(formatter_func(minimal)) > max_size: - raise UpdateGraphException("Too big with only required paths.") - - # REDFLAG: read up on clone() - prev_minimal = minimal.clone() - - # Then add all other full bootstrap paths. - canonical_paths = graph.canonical_paths(index, MAX_PATH_LEN) - - while len(canonical_paths): - if minimal.copy_path(graph, canonical_paths.pop()): - size = len(formatter_func(minimal)) - #print "minimal_update_graph -- size: %i " % size - if size > max_size: - return prev_minimal - else: - prev_minimal = minimal.clone() - - if index == 0: - return prev_minimal - - # Favors older edges - # Then add bootstrap paths back to previous indices - for upper_index in range(index - 1, FIRST_INDEX, - 1): - canonical_paths = graph.canonical_paths(upper_index, MAX_PATH_LEN) - while len(canonical_paths): - if minimal.copy_path(graph, canonical_paths.pop()): - size = len(formatter_func(minimal)) - #print "minimal_update_graph -- size(1): %i" % size - if size > max_size: - return prev_minimal - else: - prev_minimal = minimal.clone() - - return prev_minimal - - def chk_to_edge_triple_map(graph): """ Returns a CHK -> edge triple map. """ ret = {} @@ -862,4 +889,77 @@ def print_list(msg, values): print " ", value if len(values) == 0: print +# REDFLAG: is it a version_map or a version_table? decide an fix all names +# REDFLAG: Scales to what? 10k nodes? +# Returns version -> index mapping +# REQUIRES: Every version is in an index! +def build_version_table(graph, repo): + """ INTERNAL: Build a version -> index ordinal map for all changesets + in the graph. """ + table = {NULL_REV:-1} + for index in range(0, graph.latest_index + 1): + assert index in graph.index_table + dummy, heads = graph.index_table[index] + for head in heads: + if not head in table: + assert not head in table + table[head] = index + ancestors = repo[head].ancestors() + for ancestor in ancestors: + version = hexlify(ancestor.node()) + if version in table: + continue + table[version] = index + return table + +# Find most recent ancestors for version which are already in +# the version map. REDFLAG: fix. don't make reference to time + +def find_latest_bases(repo, version, version_map, traversed, base_revs): + """ INTERNAL: Add latest known base revs for version to base_revs. """ + #print "find_latest_bases -- called: ", version[:12] + assert version_map != {NULL_REV:FIRST_INDEX} + if version in traversed: + return + traversed.add(version) + if version in version_map: + #print " find_latest_bases -- adding: ", version[:12] + base_revs.add(version) + return + parents = [hexlify(parent.node()) for parent in repo[version].parents()] + for parent in parents: + find_latest_bases(repo, parent, version_map, traversed, base_revs) + + +# REDFLAG: correct? I can't come up with a counter example. +def get_heads(graph, to_index=None): + """ Returns the 40 digit hex changeset ids of the heads. """ + if to_index is None: + to_index = graph.latest_index + + heads = set([]) + bases = set([]) + for index in range(FIRST_INDEX, to_index + 1): + for base in graph.index_table[index][0]: + bases.add(base) + for head in graph.index_table[index][1]: + heads.add(head) + heads = list(heads - bases) + heads.sort() + return tuple(heads) + +# ASSUMPTIONS: +# 0) head which don't appear in bases are tip heads. True? + +# INVARIANTS: +# o every changeset must exist "in" one and only one index +# -> contiguousness +# o the parent revs for all the changesets in every index +# must exist in a previous index (though not necessarily the +# immediate predecessor) +# o indices referenced by edges must exist +# o latest index must be set correctly +# o inices must be contiguous +# o FIRST_INDEX in index_table + diff --git a/infocalypse/graphutil.py b/infocalypse/graphutil.py new file mode 100644 --- /dev/null +++ b/infocalypse/graphutil.py @@ -0,0 +1,397 @@ +""" Functions for manipulating UpdateGraphs. + + Copyright (C) 2009 Darrell Karbott + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2.0 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks +""" + + +from binascii import hexlify + +from graph import FIRST_INDEX, MAX_PATH_LEN, UpdateGraph, \ + UpdateGraphException, canonical_path_itr + +############################################################ +# Doesn't dump FIRST_INDEX entry. +def graph_to_string(graph): + """ Returns a human readable representation of the graph. """ + lines = [] + # Indices + indices = graph.index_table.keys() + indices.sort() + for index in indices: + if index == FIRST_INDEX: + continue + + entry = graph.index_table[index] + + # Example: + # I:0:aaaaaaaaaaaa:|:bbbbbbbbbbbb:cccccccccccc + lines.append(':'.join(('I', str(index), ':'.join(entry[0]), '|', + ':'.join(entry[1])))) + + # Edges + index_pairs = graph.edge_table.keys() + # MUST sort so you get the same CHK for the same graph instance. + index_pairs.sort() + for index_pair in index_pairs: + edge_info = graph.edge_table[index_pair] + as_str = ':'.join(edge_info[1:]) + if as_str != '': + as_str = ':' + as_str + lines.append("E:%i:%i:%i%s" % (index_pair[0], index_pair[1], + edge_info[0], + as_str)) + + return '\n'.join(lines) + '\n' + +def parse_graph(text): + """ Returns a graph parsed from text. + text must be in the format used by graph_to_string(). + Lines starting with '#' are ignored. + """ + + graph = UpdateGraph() + lines = text.split('\n') + for line in lines: + fields = line.split(':') + if fields[0] == 'I': + fields.pop(0) + try: + if len(fields) == 0: + raise ValueError("HACK") + index = int(fields.pop(0)) + except ValueError: + raise ValueError("Syntax error reading index") + try: + divider = fields.index('|') + except ValueError: + raise ValueError("Syntax error reading index %i" % index) + parents = fields[:divider] + heads = fields[divider + 1:] + + if index in graph.index_table: + print "OVERWRITING INDEX: " , index + if len(parents) < 1: + raise ValueError("index %i has no parent revs" % index) + if len(heads) < 1: + raise ValueError("index %i has no head revs" % index) + + graph.index_table[index] = (tuple(parents), tuple(heads)) + elif fields[0] == 'E': + #print fields + if len(fields) < 5: + raise ValueError("Exception parsing edge values.") + index_pair = (int(fields[1]), int(fields[2])) + length = int(fields[3]) + chk_list = [] + for chk in fields[4:]: + chk_list.append(chk) + graph.edge_table[index_pair] = tuple([length, ] + chk_list) + #else: + # print "SKIPPED LINE:" + # print line + indices = graph.index_table.keys() + if len(indices) == 0: + raise ValueError("No indices?") + indices.sort() + graph.latest_index = indices[-1] + + graph.rep_invariant() + + return graph + + +def parse_v100_graph(text): + """ Returns a graph parsed from text in old format. + text must be in the format used by graph_to_string(). + Lines starting with '#' are ignored. + """ + + graph = UpdateGraph() + lines = text.split('\n') + for line in lines: + fields = line.split(':') + if fields[0] == 'I': + if len(fields) != 4: + raise ValueError("Exception parsing index values.") + index = int(fields[1]) + if index in graph.index_table: + print "OVERWRITING INDEX: " , index + if len(tuple(fields[2:])) != 2: + raise ValueError("Error parsing index value: %i" % index) + versions = tuple(fields[2:]) + graph.index_table[index] = ((versions[0], ), (versions[1], )) + elif fields[0] == 'E': + #print fields + if len(fields) < 5: + raise ValueError("Exception parsing edge values.") + index_pair = (int(fields[1]), int(fields[2])) + length = int(fields[3]) + chk_list = [] + for chk in fields[4:]: + chk_list.append(chk) + graph.edge_table[index_pair] = tuple([length, ] + chk_list) + #else: + # print "SKIPPED LINE:" + # print line + indices = graph.index_table.keys() + if len(indices) == 0: + raise ValueError("No indices?") + indices.sort() + graph.latest_index = indices[-1] + + graph.rep_invariant() + + return graph + +############################################################ + + +def should_add_head(repo, version_table, head, to_index): + """ INTERNAL: Helper function used by get_rollup_bounds. """ + children = [hexlify(child.node()) for child in repo[head].children()] + for child in children: + # Versions that we don't know about, don't count. + # REDFLAG: Think this through + if not child in version_table: + #print "head: %s %s not in version table. IGNORING" \ + # % (head[:12], child[:12]) + continue + + # REDFLAG: Check. This graph stuff makes me crazy. + child_index = version_table[child] + if child_index <= to_index: + # Has a child in or under the index we are rolling up to. + # Not a head! + #print "should_add_head -- returned False, %i:%s" % + # (child_index, child[:12]) + return False + + #print "should_add_head -- returned True" + return True + +# TRICKY: +# You need the repository changset DAG in order to determine +# the base revs. because new changes might have branched from +# a change in the middle of a previous index which doesn't +# appear explictly in the graph. +# +def get_rollup_bounds(graph, repo, from_index, to_index, version_table): + """ Return a ((parent_revs, ), (head_revs,) tuple required to + create a bundle with all the changes [from_index, to_index] (inclusive). + """ + assert from_index <= to_index + assert to_index > FIRST_INDEX + #print "get_rollup_bounds -- ", from_index, to_index + #print "version_table:", len(version_table) + + new_bases = set([]) + new_heads = set([]) + + for index in range(from_index, to_index + 1): + bases, heads = graph.index_table[index] + for base in bases: + #print " considering base ", base[:12], version_table[base] + if version_table[base] < from_index: + #print " adding base ", base[:12], version_table[base] + new_bases.add(base) + for head in heads: + if should_add_head(repo, version_table, head, to_index): + new_heads.add(head) + + new_bases = list(new_bases) + new_bases.sort() + new_heads = list(new_heads) + new_heads.sort() + + #print "get_rollup_bounds -- returning" + #print " bases: ", new_bases + #print " heads: ", new_heads + assert len(new_bases) > 0 + assert len(new_heads) > 0 + return (tuple(new_bases), tuple(new_heads)) + + +# call this from subgraph +def coalesce_indices(original_graph, graph, repo, version_table): + """ INTERNAL: Coalesce changes so that indices (and changes) + are contiguous. """ + original_graph.rep_invariant() + # graph invariants are broken ! + assert FIRST_INDEX in graph.index_table + assert len(graph.index_table) > 1 + + # Roll up info in missing indices into existing ones. + lacuna = False + prev_index = graph.latest_index + for index in range(graph.latest_index - 1, FIRST_INDEX -1, -1): + # REDFLAG: There was a bad bug here. Better testing? + if index in graph.index_table: + if lacuna: + # Rollup all changes in the missing indices into + # the latest one. + graph.index_table[prev_index] = ( + get_rollup_bounds(original_graph, + repo, + index + 1, + prev_index, + version_table)) + lacuna = False + prev_index = index + else: + lacuna = True + # Hmmm... or graph is empty + assert lacuna == False + + # Make indices contiguous. + indices = graph.index_table.keys() + indices.sort() + + assert indices[0] == FIRST_INDEX + assert FIRST_INDEX == -1 + + fixups = {} + for ordinal, value in enumerate(indices): + fixups[value] = ordinal - 1 + + new_indices = {} + for old_index in indices: + # Ok not to copy, value is immutable (a tuple). + new_indices[fixups[old_index]] = graph.index_table[old_index] + + new_edges = {} + for edge in graph.edge_table: + # Deep copy? Nothing else has a ref to the values. + new_edges[(fixups[edge[0]], fixups[edge[1]])] = graph.edge_table[edge] + + graph.index_table.clear() + graph.edge_table.clear() + graph.index_table.update(new_indices) + graph.edge_table.update(new_edges) + graph.latest_index = max(graph.index_table.keys()) + + original_graph.rep_invariant() + #print "FAILING:" + #print graph_to_string(graph) + graph.rep_invariant() + +def subgraph(graph, repo, version_table, containing_paths): + """ Return a subgraph which contains the vertices and + edges in containing_paths. """ + graph.rep_invariant() + small_graph = UpdateGraph() + max_index = -1 + + # Copy edges and indices. + for path in containing_paths: + for step in path: + pair = step[:2] + # REDFLAG: copies ALL redundant paths + small_graph.edge_table[pair] = graph.edge_table[pair][:] + for index in pair: + if index not in small_graph.index_table: + # Don't need to deep copy because index info is + # immutable. (a tuple) + small_graph.index_table[index] = graph.index_table[index] + max_index = max(max_index, index) + + small_graph.latest_index = max_index + + # Fix contiguousness. + coalesce_indices(graph, small_graph, repo, version_table) + + # Invariants should be fixed. + small_graph.rep_invariant() + graph.rep_invariant() + + return small_graph + +# REDFLAG: TERMINATE when all edges in graph have been yielded? +def important_edge_itr(graph, known_paths): + """ INTERNAL: A generator which returns a sequence of edges in order + of descending criticalness.""" + known_edges = set([]) + for path in known_paths: + for edge in path: + known_edges.add(edge) + + # Edges which are in the canonical path + index = graph.latest_index + canonical_paths = canonical_path_itr(graph, 0, index, MAX_PATH_LEN) + + for path in canonical_paths: + for edge in path: + if edge in known_edges: + continue + known_edges.add(edge) + yield edge + + if index == 0: + return + + # Then add bootstrap paths back to previous indices + # Favors older edges. + for upper_index in range(index - 1, FIRST_INDEX, - 1): + canonical_paths = canonical_path_itr(graph, 0, upper_index, + MAX_PATH_LEN) + for path in canonical_paths: + for edge in path: + if edge in known_edges: + continue + known_edges.add(edge) + yield edge + return + +# Really slow +def minimal_graph(graph, repo, version_table, max_size=32*1024, + formatter_func=graph_to_string): + """ Returns a subgraph that can be formatted to <= max_size + bytes with formatter_func. """ + + length = len(formatter_func(graph)) + if length <= max_size: + #print "minimal_update_graph -- graph fits as is: ", length + # Hmmm... must clone for consistent semantics. + return graph.clone() + + index = graph.latest_index + assert index > FIRST_INDEX + + # All the edges that would be included in the top key. + # This includes the canonical bootstrap path and the + # two cheapest updates from the previous index. + paths = [[edge, ] for edge in graph.get_top_key_edges()] + minimal = subgraph(graph, repo, version_table, paths) + length = len(formatter_func(minimal)) + if length > max_size: + raise UpdateGraphException("Too big with only required paths (%i > %i)" + % (length, max_size)) + + prev_minimal = minimal.clone() + + for edge in important_edge_itr(graph, paths): + paths.append([edge, ]) + minimal = subgraph(graph, repo, version_table, paths) + length = len(formatter_func(minimal)) + if length > max_size: + return prev_minimal + else: + prev_minimal = minimal.clone() + + return prev_minimal + diff --git a/infocalypse/infcmds.py b/infocalypse/infcmds.py --- a/infocalypse/infcmds.py +++ b/infocalypse/infcmds.py @@ -37,12 +37,12 @@ from fcpconnection import FCPConnection, get_code, FCPError from requestqueue import RequestRunner -from graph import UpdateGraph, hex_version +from graph import UpdateGraph from bundlecache import BundleCache, is_writable from updatesm import UpdateStateMachine, QUIESCENT, FINISHING, REQUESTING_URI, \ REQUESTING_GRAPH, REQUESTING_BUNDLES, INVERTING_URI, \ REQUESTING_URI_4_INSERT, INSERTING_BUNDLES, INSERTING_GRAPH, \ - INSERTING_URI, FAILING, REQUESTING_URI_4_COPY + INSERTING_URI, FAILING, REQUESTING_URI_4_COPY, CANCELING, CleaningUp from config import Config, DEFAULT_CFG_PATH @@ -52,14 +52,16 @@ DEFAULT_PARAMS = { 'PriorityClass':1, 'DontCompress':True, # hg bundles are already compressed. 'Verbosity':1023, # MUST set this to get progress messages. + #'GetCHKOnly':True, # REDFLAG: DCI! remove # Non-FCP stuff 'N_CONCURRENT':4, # Maximum number of concurrent FCP requests. 'CANCEL_TIME_SECS': 10 * 60, # Bound request time. 'POLL_SECS':0.25, # Time to sleep in the polling loop. + #'TEST_DISABLE_GRAPH': True, # Disable reading the graph. + #'TEST_DISABLE_UPDATES': True, # Don't update info in the top key. } - MSG_TABLE = {(QUIESCENT, REQUESTING_URI_4_INSERT) :"Requesting previous URI...", (QUIESCENT, REQUESTING_URI_4_COPY) @@ -115,6 +117,14 @@ class UICallbacks: def monitor_callback(self, update_sm, client, msg): """ FCP message status callback which writes to a ui. """ + # REDFLAG: remove when 1209 comes out. + if (msg[0] == 'PutFailed' and get_code(msg) == 9 and + update_sm.params['FREENET_BUILD'] == '1208' and + update_sm.ctx.get('REINSERT', 0) > 0): + self.ui_.warn('There is a KNOWN BUG in 1208 which ' + + 'causes code==9 failures for re-inserts.\n' + + 'The re-insert might actually have succeeded.\n' + + 'Who knows???\n') if self.verbosity < 2: return @@ -152,13 +162,10 @@ class UICallbacks: self.ui_.status("%s%s:%s\n" % (prefix, str(client.tag), text)) # REDFLAG: re-add full dumping of FCP errors at debug level? #if msg[0].find('Failed') != -1 or msg[0].find('Error') != -1: - # print client.in_params.pretty() - # print msg - # print "FINISHED:" , client.is_finished(), - #bool(client.is_finished()) + #print client.in_params.pretty() + #print msg + #print "FINISHED:" , bool(client.is_finished()) - -# Paranoia? Just stat? I'm afraid of problems/differences w/ Windoze. # Hmmmm... SUSPECT. Abuse of mercurial ui design intent. # ISSUE: I don't just want to suppress/include output. # I use this value to keep from running code which isn't @@ -192,7 +199,8 @@ def get_config_info(ui_, opts): params['TMP_DIR'] = cfg.defaults['TMP_DIR'] params['VERBOSITY'] = get_verbosity(ui_) params['NO_SEARCH'] = (bool(opts.get('nosearch')) and - (opts.get('uri', None) or opts.get('requesturi', None))) + (opts.get('uri', None) or + opts.get('requesturi', None))) request_uri = opts.get('uri') or opts.get('requesturi') if bool(opts.get('nosearch')) and not request_uri: @@ -228,6 +236,31 @@ def check_uri(ui_, uri): + "\nUse --aggressive instead. \n") raise util.Abort("Negative USK %s\n" % uri) + +def disable_cancel(updatesm, disable=True): + """ INTERNAL: Hack to work around 1208 cancel kills FCP connection bug. """ + if disable: + if not hasattr(updatesm.runner, 'old_cancel_request'): + updatesm.runner.old_cancel_request = updatesm.runner.cancel_request + msg = ("RequestRunner.cancel_request() disabled to work around " + + "1208 bug\n") + updatesm.runner.cancel_request = ( + lambda dummy : updatesm.ctx.ui_.status(msg)) + else: + if hasattr(updatesm.runner, 'old_cancel_request'): + updatesm.runner.cancel_request = updatesm.runner.old_cancel_request + updatesm.ctx.ui_.status("Re-enabled canceling so that " + + "shutdown works.\n") +class PatchedCleaningUp(CleaningUp): + """ INTERNAL: 1208 bug work around to re-enable canceling. """ + def __init__(self, parent, name, finished_state): + CleaningUp.__init__(self, parent, name, finished_state) + + def enter(self, from_state): + """ Override to back out 1208 cancel hack. """ + disable_cancel(self.parent, False) + CleaningUp.enter(self, from_state) + # REDFLAG: remove store_cfg def setup(ui_, repo, params, stored_cfg): """ INTERNAL: Setup to run an Infocalypse extension command. """ @@ -273,12 +306,31 @@ def setup(ui_, repo, params, stored_cfg) raise err runner = RequestRunner(connection, params['N_CONCURRENT']) - update_sm = UpdateStateMachine(runner, repo, ui_, cache) update_sm.params = params.copy() update_sm.transition_callback = callbacks.transition_callback update_sm.monitor_callback = callbacks.monitor_callback + # Modify only after copy. + update_sm.params['FREENET_BUILD'] = runner.connection.node_hello[1]['Build'] + + # REDFLAG: Hack to work around 1208 cancel bug. Remove. + if update_sm.params['FREENET_BUILD'] == '1208': + ui_.warn("DISABLING request canceling to work around 1208 FCP bug.\n" + "This may cause requests to hang. :-(\n") + disable_cancel(update_sm) + + # Patch state machine to re-enable canceling on shutdown. + #CANCELING:CleaningUp(self, CANCELING, QUIESCENT), + #FAILING:CleaningUp(self, FAILING, QUIESCENT), + #FINISHING:CleaningUp(self, FINISHING, QUIESCENT), + update_sm.states[CANCELING] = PatchedCleaningUp(update_sm, + CANCELING, QUIESCENT) + update_sm.states[FAILING] = PatchedCleaningUp(update_sm, + FAILING, QUIESCENT) + update_sm.states[FINISHING] = PatchedCleaningUp(update_sm, + FINISHING, QUIESCENT) + return update_sm def run_until_quiescent(update_sm, poll_secs, close_socket=True): @@ -300,12 +352,12 @@ def run_until_quiescent(update_sm, poll_ # Indirectly nudge the state machine. update_sm.runner.kick() except socket.error: # Not an IOError until 2.6. - update_sm.ui_.warn("Exiting because of an error on " - + "the FCP socket.\n") + update_sm.ctx.ui_.warn("Exiting because of an error on " + + "the FCP socket.\n") raise except IOError: # REDLAG: better message. - update_sm.ui_.warn("Exiting because of an IO error.\n") + update_sm.ctx.ui_.warn("Exiting because of an IO error.\n") raise # Rest :-) time.sleep(poll_secs) @@ -460,7 +512,7 @@ def execute_create(ui_, repo, params, st #ui_.status("Current tip: %s\n" % hex_version(repo)[:12]) update_sm.start_inserting(UpdateGraph(), - params.get('TO_VERSION', 'tip'), + params.get('TO_VERSIONS', ('tip',)), params['INSERT_URI']) run_until_quiescent(update_sm, params['POLL_SECS']) @@ -568,7 +620,7 @@ def execute_push(ui_, repo, params, stor #ui_.status("Current tip: %s\n" % hex_version(repo)[:12]) update_sm.start_pushing(params['INSERT_URI'], - params.get('TO_VERSION', 'tip'), + params.get('TO_VERSIONS', ('tip',)), request_uri, # None is allowed is_keypair) run_until_quiescent(update_sm, params['POLL_SECS']) @@ -601,7 +653,7 @@ def execute_pull(ui_, repo, params, stor ui_.status("Pulled from:\n%s\n" % update_sm.get_state('REQUESTING_URI'). get_latest_uri()) - ui_.status("New tip: %s\n" % hex_version(repo)[:12]) + #ui_.status("New tip: %s\n" % hex_version(repo)[:12]) else: ui_.status("Pull failed.\n") diff --git a/infocalypse/insertingbundles.py b/infocalypse/insertingbundles.py --- a/infocalypse/insertingbundles.py +++ b/infocalypse/insertingbundles.py @@ -20,8 +20,9 @@ Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks """ -from graph import graph_to_string, UpToDate, INSERT_SALTED_METADATA, \ - FREENET_BLOCK_LEN +from graph import UpToDate, INSERT_SALTED_METADATA, \ + FREENET_BLOCK_LEN, build_version_table, get_heads +from graphutil import graph_to_string from bundlecache import BundleException from statemachine import RequestQueueState @@ -67,13 +68,16 @@ class InsertingBundles(RequestQueueState self.parent.ctx.ui_.status("--- Initial Graph ---\n") self.parent.ctx.ui_.status(graph_to_string(graph) +'\n') - latest_rev = graph.index_table[graph.latest_index][1] - self.parent.ctx.ui_.warn("Latest version in Freenet: %s\n" - % latest_rev[:12]) - if not self.parent.ctx.has_version(latest_rev): - self.parent.ctx.ui_.warn("The latest version in Freenet isn't in " - "the local repository.\n" - "Try doing an fn-pull to update.\n") + + latest_revs = get_heads(graph) + + self.parent.ctx.ui_.status("Latest heads(s) in Freenet: %s\n" + % ' '.join([ver[:12] for ver in latest_revs])) + + if not self.parent.ctx.has_versions(latest_revs): + self.parent.ctx.ui_.warn("The local repository isn't up " + + "to date.\n" + + "Try doing an fn-pull.\n") self.parent.transition(FAILING) # Hmmm... hard coded state name return @@ -240,11 +244,17 @@ class InsertingBundles(RequestQueueState def set_new_edges(self, graph): """ INTERNAL: Set the list of new edges to insert. """ + + # REDFLAG: Think this through. + self.parent.ctx.version_table = build_version_table(graph, + self.parent.ctx. + repo) if self.parent.ctx.get('REINSERT', 0) == 0: self.new_edges = graph.update(self.parent.ctx.repo, self.parent.ctx.ui_, - self.parent.ctx['TARGET_VERSION'], + self.parent.ctx['TARGET_VERSIONS'], self.parent.ctx.bundle_cache) + return # Hmmmm... later support different int values of REINSERT? diff --git a/infocalypse/requestingbundles.py b/infocalypse/requestingbundles.py --- a/infocalypse/requestingbundles.py +++ b/infocalypse/requestingbundles.py @@ -28,15 +28,14 @@ import random # Hmmm... good enough? from fcpmessage import GET_DEF from bundlecache import make_temp_file -from graph import parse_graph, latest_index, \ +from graph import latest_index, \ FREENET_BLOCK_LEN, chk_to_edge_triple_map, \ - dump_paths, MAX_PATH_LEN - -from choose import get_update_edges, dump_update_edges + dump_paths, MAX_PATH_LEN, get_heads, canonical_path_itr +from graphutil import parse_graph +from choose import get_update_edges, dump_update_edges, SaltingState from statemachine import RetryingRequestList, CandidateRequest -#from topkey import dump_top_key_tuple from chk import clear_control_bytes # REDFLAG: Make sure that you are copying lists. eg. updates @@ -45,7 +44,7 @@ from chk import clear_control_bytes # 1) Single block fetch alternate keys. # 2) Choose between primary and alternate keys "fairly" # 3) transition from no graph to graph case. -# 4) deal with padding hack +# 4) deal with padding hacks # 5) Optionally disable alternate single block fetching? # ?6) serialize? Easier to write from scratch? @@ -56,58 +55,7 @@ from chk import clear_control_bytes # Can unwind padding hack w/o graph # len < 32K done # len == 32K re-request DONE -# -# REDFLAG: get rid of pending_candidates by subclassing StatefulRequest -# to include a .candidate attribute. ??? -def build_salting_table(target): - """ INTERNAL: Build table used to keep track of metadata salting. """ - def traverse_candidates(candidate_list, table): - """ INTERNAL: Helper function to traverse a single candidate list. """ - for candidate in candidate_list: - if candidate[6]: - continue - edge = candidate[3] - value = table.get(edge, []) - value.append(candidate[2]) - table[edge] = value - ret = {} - traverse_candidates(target.pending_candidates(), ret) - traverse_candidates(target.current_candidates, ret) - traverse_candidates(target.next_candidates, ret) - return ret - -# REDFLAG: get rid of unused methods. -# Hmmm... feels like coding my way out of a design problem. -class SaltingState: - """ INTERNAL: Helper class to keep track of metadata salting state. - """ - def __init__(self, target): - self.table = build_salting_table(target) - - def full_request(self, edge): - """ Return True if a full request is scheduled for the edge. """ - if not edge in self.table: - return False - - for value in self.table[edge]: - if not value: - return True - return False - - def add(self, edge, is_partial): - """ Add an entry to the table. """ - value = self.table.get(edge, []) - value.append(is_partial) - self.table[edge] = value - - def needs_full_request(self, graph, edge): - """ Returns True if a full request is required. """ - assert len(edge) == 3 - if not graph.is_redundant(edge): - return False - return not (self.full_request(edge) or - self.full_request((edge[0], edge[1], int(not edge[2])))) # What this does: # 0) Fetches graph(s) # 1) Fetches early bundles in parallel with graphs @@ -126,6 +74,7 @@ class RequestingBundles(RetryingRequestL self.success_state = success_state self.failure_state = failure_state self.top_key_tuple = None # FNA sskdata + self.freenet_heads = None ############################################################ # State implementation @@ -165,7 +114,8 @@ class RequestingBundles(RetryingRequestL # Catch state machine stalls. if (self.parent.current_state == self and self.is_stalled()): - print "STALLED, BAILING OUT!" + self.parent.ctx.ui_.warn("Giving up because the state " + + "machine stalled.\n") self.parent.transition(self.failure_state) # DONT add to pending. Base clase does that. @@ -209,6 +159,9 @@ class RequestingBundles(RetryingRequestL ############################################################ + + # DEALING: With partial heads, partial bases? + # REDFLAG: deal with optional request serialization? # REDFLAG: Move # ASSUMPTION: Keys are in descenting order of latest_rev. @@ -229,6 +182,12 @@ class RequestingBundles(RetryingRequestL if index < start_index: # REDFLAG: do better? continue + if not update[4] or not update[5]: + # Don't attempt to queue updates if we don't know + # full parent/head info. + # REDFLAG: remove test code + print "_queue_from_updates -- bailing out", update[4], update[5] + break if only_latest and update[0] > 5 * FREENET_BLOCK_LEN: # Short circuit (-1, top_index) rollup case. @@ -238,7 +197,7 @@ class RequestingBundles(RetryingRequestL # Only full updates. break - if not self.parent.ctx.has_version(update[1]): + if not self.parent.ctx.has_versions(update[1]): # Only updates we can pull. if only_latest: # Don't want big bundles from the canonical path. @@ -246,7 +205,7 @@ class RequestingBundles(RetryingRequestL else: continue - if self.parent.ctx.has_version(update[2]): + if self.parent.ctx.has_versions(update[2]): # Only updates we need. continue @@ -266,6 +225,42 @@ class RequestingBundles(RetryingRequestL return last_queued + + def _handle_testing_hacks(self): + """ INTERNAL: Helper function to implement TEST_DISABLE_UPDATES + and TEST_DISABLE_GRAPH testing params. """ + if self.top_key_tuple is None: + return + if self.parent.params.get("TEST_DISABLE_UPDATES", False): + updates = list(self.top_key_tuple[1]) + for index in range(0, len(updates)): + update = list(updates[index]) + update[4] = False + update[5] = False + updates[index] = tuple(update) + top = list(self.top_key_tuple) + top[1] = tuple(updates) + self.top_key_tuple = tuple(top) + self.parent.ctx.ui_.warn("TEST_DISABLE_UPDATES == True\n" + + "Disabled updating w/o graph\n") + + if self.parent.params.get("TEST_DISABLE_GRAPH", False): + top = list(self.top_key_tuple) + # REDFLAG: Fix post 1208 + # Using bad keys is a more realistic test but there's + # an FCP bug in 1208 that kills the connection on + # cancel. Go back to this when 1209 comes out. + top[0] = ('CHK@badroutingkeyA55JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8', + 'CHK@badroutingkeyB55JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8', + ) + top[0] = () + self.top_key_tuple = tuple(top) + self.parent.ctx.ui_.warn("TEST_DISABLE_GRAPH == True\n" + + "Disabled graph by removing graph " + + "chks.\n") + # Hack special case code to add the graph. def _initialize(self, top_key_tuple=None): """ INTERNAL: Initialize. @@ -279,6 +274,7 @@ class RequestingBundles(RetryingRequestL """ self.top_key_tuple = top_key_tuple + self._handle_testing_hacks() ############################################################ # Hack used to test graph request failure. #bad_chk = ('CHK@badroutingkeyA55JblbGup0yNSpoDJgVPnL8E5WXoc,' @@ -297,28 +293,41 @@ class RequestingBundles(RetryingRequestL if self.top_key_tuple is None: raise Exception("No top key data.") - #if self.parent.params.get('DUMP_TOP_KEY', False): - # dump_top_key_tuple(top_key_tuple) + updates = self.top_key_tuple[1] + if updates[0][5]: + self.freenet_heads = updates[0][2] + self.parent.ctx.ui_.status('Freenet heads: %s\n' % + ' '.join([ver[:12] for ver in + updates[0][2]])) - updates = self.top_key_tuple[1] + if self.parent.ctx.has_versions(updates[0][2]): + self.parent.ctx.ui_.warn("All remote heads are already " + + "in the local repo.\n") + self.parent.transition(self.success_state) + return - if self.parent.ctx.has_version(updates[0][2]): - self.parent.ctx.ui_.warn(("Version: %s is already in the " - + "local repo.\n") - % updates[0][2][:12]) - self.parent.transition(self.success_state) - return + # INTENT: Improve throughput for most common update case. + # If it is possible to update fully in one fetch, queue the + # (possibly redundant) key(s) BEFORE graph requests. + latest_queued = self._queue_from_updates(self. + current_candidates, + -1, False, True) - # INTENT: Improve throughput for most common update case. - # If it is possible to update fully in one fetch, queue the - # (possibly redundant) key(s) BEFORE graph requests. - latest_queued = self._queue_from_updates(self.current_candidates, - -1, False, True) + if latest_queued != -1: + self.parent.ctx.ui_.status("Full update is possible in a " + + "single FCP fetch. :-)\n") - if latest_queued != -1: - self.parent.ctx.ui_.status("Full update is possible in a " - + "single FCP fetch. :-)\n") + else: + self.parent.ctx.ui_.warn("Couldn't read all Freenet heads from " + + "top key.\n" + + "Dunno if you're up to date :-(\n" + + "Waiting for graph...\n") + if len(self.top_key_tuple[0]) == 0: + self.parent.ctx.ui_.warn("No graph CHKs in top key! " + + "Giving up...\n") + self.parent.transition(self.failure_state) + return # Kick off the fetch(es) for the full update graph. # REDFLAG: make a parameter parallel_graph_fetch = True @@ -333,6 +342,11 @@ class RequestingBundles(RetryingRequestL if not parallel_graph_fetch: break + + if self.freenet_heads is None: + # Need to wait for the graph. + return + # Queue remaining fetchable keys in the NEXT pass. # INTENT: # The graph might have a better update path. So we don't try these @@ -392,7 +406,11 @@ class RequestingBundles(RetryingRequestL text += " BAD TOP KEY DATA!" + ":" + chk + "\n" self.parent.ctx.ui_.status(text) + all_heads = get_heads(graph) + assert (self.freenet_heads is None or + self.freenet_heads == all_heads) + self.freenet_heads = all_heads self.parent.ctx.graph = graph self.rep_invariant() @@ -404,6 +422,7 @@ class RequestingBundles(RetryingRequestL #break_edges(graph, kill_prob, skip_chks) # "fix" (i.e. break) pending good chks. + # REDFLAG: comment this out too? for candidate in self.current_candidates + self.next_candidates: if candidate[6]: continue @@ -435,6 +454,25 @@ class RequestingBundles(RetryingRequestL self.parent.ctx.ui_.warn("Couldn't read graph from Freenet!\n") self.parent.transition(self.failure_state) + def _handle_dump_canonical_paths(self, graph): + """ INTERNAL: Dump the top 20 canonical paths. """ + if not self.parent.params.get('DUMP_CANONICAL_PATHS', False): + return + + paths = canonical_path_itr(graph, 0, graph.latest_index, + MAX_PATH_LEN) + first_paths = [] + # REDFLAG: Magick number + while len(first_paths) < 20: + try: + first_paths.append(paths.next()) + except StopIteration: + break + + dump_paths(graph, + first_paths, + "Canonical paths") + def _graph_request_done(self, client, msg, candidate): """ INTERNAL: Handle requests for the graph. """ #print "CANDIDATE:", candidate @@ -456,13 +494,7 @@ class RequestingBundles(RetryingRequestL self.parent.ctx.ui_.status(data) self.parent.ctx.ui_.status("\n---\n") graph = parse_graph(data) - if self.parent.params.get('DUMP_CANONICAL_PATHS', False): - paths = graph.canonical_paths(graph.latest_index, - MAX_PATH_LEN)[-20:] - paths.reverse() - dump_paths(graph, - paths, - "Canonical paths") + self._handle_dump_canonical_paths(graph) self._set_graph(graph) self._reevaluate() finally: @@ -582,23 +614,20 @@ class RequestingBundles(RetryingRequestL candidate[5] = msg self.finished_candidates.append(candidate) #print "_handle_success -- pulling!" + name = str(candidate[3]) + if name == 'None': + name = "%s:%s" % (','.join([ver[:12] for ver in candidate[4][1]]), + ','.join([ver[:12] for ver in candidate[4][2]])) + + #print "Trying to pull: ", name self._pull_bundle(client, msg, candidate) #print "_handle_success -- pulled bundle ", candidate[3] - name = str(candidate[3]) - if name == 'None': - name = "%s:%s" % (candidate[4][1][:12], candidate[4][2][:12]) self.parent.ctx.ui_.status("Pulled bundle: %s\n" % name) - graph = self.parent.ctx.graph - if graph is None: - latest_version = self.top_key_tuple[1][0][2] - else: - latest_version = graph.index_table[graph.latest_index][1] - - if self.parent.ctx.has_version(latest_version): + if self.parent.ctx.has_versions(self.freenet_heads): # Done and done! - print "DONE, UP TO DATE!" + #print "SUCCEEDED!" self.parent.transition(self.success_state) return @@ -691,11 +720,11 @@ class RequestingBundles(RetryingRequestL could be pulled and contains changes that we don't already have. """ versions = self._get_versions(candidate) #print "_needs_bundle -- ", versions - if not self.parent.ctx.has_version(versions[0]): + if not self.parent.ctx.has_versions(versions[0]): #print "Doesn't have parent ", versions return False # Doesn't have parent. - return not self.parent.ctx.has_version(versions[1]) + return not self.parent.ctx.has_versions(versions[1]) # REDFLAGE: remove msg arg? def _pull_bundle(self, client, dummy_msg, candidate): @@ -730,12 +759,15 @@ class RequestingBundles(RetryingRequestL all_chks = pending.union(current).union(next).union(finished) for update in self.top_key_tuple[1]: - if not self.parent.ctx.has_version(update[1]): + if not self.parent.ctx.has_versions(update[1]): + # Still works with incomplete base. continue # Don't have parent. - if self.parent.ctx.has_version(update[2]): + if self.parent.ctx.has_versions(update[2]): + # Not guaranteed to work with incomplete heads. continue # Already have the update's changes. + new_chks = [] for chk in update[3]: if not chk in all_chks: @@ -849,14 +881,14 @@ class RequestingBundles(RetryingRequestL if candidate[6]: continue # Skip graph requests. versions = self._get_versions(candidate) - if self.parent.ctx.has_version(versions[1]): + if self.parent.ctx.has_versions(versions[1]): self.parent.runner.cancel_request(client) # "finish" requests which are no longer required. victims = [] for candidate in self.current_candidates: versions = self._get_versions(candidate) - if self.parent.ctx.has_version(versions[1]): + if self.parent.ctx.has_versions(versions[1]): victims.append(candidate) for victim in victims: self.current_candidates.remove(victim) @@ -866,7 +898,7 @@ class RequestingBundles(RetryingRequestL victims = [] for candidate in self.next_candidates: versions = self._get_versions(candidate) - if self.parent.ctx.has_version(versions[1]): + if self.parent.ctx.has_versions(versions[1]): victims.append(candidate) for victim in victims: self.next_candidates.remove(victim) @@ -875,7 +907,7 @@ class RequestingBundles(RetryingRequestL def _get_versions(self, candidate): """ Return the mercurial 40 digit hex version strings for the - parent version and latest version of the candidate's edge. """ + parent versions and latest versions of the candidate's edge. """ assert not candidate[6] # graph request! graph = self.parent.ctx.graph if graph is None: @@ -891,7 +923,7 @@ class RequestingBundles(RetryingRequestL #print "_get_versions -- ", step, graph.index_table[step[0]][1], \ # graph.index_table[step[1]][2] - return (graph.index_table[step[0]][1], + return (graph.index_table[step[0] + 1][0], graph.index_table[step[1]][1]) def _known_chks(self): @@ -958,6 +990,3 @@ class RequestingBundles(RetryingRequestL print_list("pending_candidates", self.pending_candidates()) print_list("current_candidates", self.current_candidates) print_list("next_candidates", self.next_candidates) - - -# LocalWords: requeueing diff --git a/infocalypse/test_graph.py b/infocalypse/test_graph.py new file mode 100644 --- /dev/null +++ b/infocalypse/test_graph.py @@ -0,0 +1,620 @@ +""" Smoke test graph. + + Copyright (C) 2009 Darrell Karbott + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2.0 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks +""" + + +# We can be a little sloppy for test code. +# pylint: disable-msg=R0915, R0914 +import binascii +import os +import random +import shutil + +from binascii import unhexlify + +from mercurial import hg, ui +from bundlecache import BundleCache +from graph import UpdateGraph, \ + build_version_table, UpdateGraphException, \ + pull_bundle, FIRST_INDEX, hex_version, UpToDate +from graphutil import parse_graph, graph_to_string, get_rollup_bounds, \ + minimal_graph +from chk import bytes_to_chk, CHK_SIZE + +# Fix these paths as necessary +CACHE_DIR = '/tmp/bundle_cache' # MUST exist +TST_REPO_DIR = '/tmp/TST_REPO' # MUST not exist + +# Must exist and contain the hg repo unbundled from: +# CHK@5fdjppBySiW2uFGd1nZNFD6U9xaadSPYTut9C3CdZa0, +# 33fQQwKXcjnhpAqI3nJw9kvjXYL~R9kVckAoFqbQ4IY,AAIC--8 +# +# (Inserted from hgsvn_freenet_9f093d2c85c3.hg) +HGSVN_FREENET_REPO = '/tmp/HGSVN_FREENET' + +VER_1 = '0000000000000000000000000000000000000000' +VER_2 = '1111111111111111111111111111111111111111' +VER_3 = '2222222222222222222222222222222222222222' +VER_4 = '3333333333333333333333333333333333333333' +VER_5 = '4444444444444444444444444444444444444444' +VER_6 = '5555555555555555555555555555555555555555' + +def fake_chks(): + """ Return a random CHK. """ + size = CHK_SIZE - 5 + while True: + #yield bytes_to_chk('\x00\x02\x02\xff\xff' + # + ''.join(map(chr, map(random.randrange, + # [0] * size, [256] * size)))) + yield bytes_to_chk('\x00\x02\x02\xff\xff' + + ''.join([chr(random.randrange(0, 256)) for dummy + in range(0, size)])) + + + +def set_chks(graph, edges, chks): + """ Set the chks for edges to random values. """ + for edge in edges: + length = graph.get_length(edge) + graph.set_chk(edge[:2], edge[2], length, chks.next()) + +def test_presentation(): + """ Smoke test graph_to_string and parse_graph. """ + graph = UpdateGraph() + print "EMPTY:" + print graph_to_string(graph) + print "Adding index: ", graph.add_index([VER_1, ], [VER_2, ]) + print "Adding index: ", graph.add_index([VER_2, ], [VER_3, VER_4]) + print "Adding index: ", graph.add_index([VER_3, VER_2], [VER_5, ]) + chks = fake_chks() + graph.add_edge((-1, 0), (100, chks.next())) + graph.add_edge((1, 2), (200, chks.next())) + graph.add_edge((-1, 2), (500, chks.next())) + text = graph_to_string(graph) + print + print text + print + graph1 = parse_graph(text) + print + text1 = graph_to_string(graph1) + print "Round trip:" + print text1 + assert text == text1 + +def test_update(repo_dir): + """ OBSOLETE? """ + ui_ = ui.ui() + repo = hg.repository(ui_, repo_dir) + cache = BundleCache(repo, ui_, CACHE_DIR) + cache.remove_files() + graph = UpdateGraph() + graph.update(repo, ui, [1, 2], cache) + print graph_to_string(graph) + print + print + graph.update(repo, ui, [3, 4], cache) + + print graph_to_string(graph) + print + print + graph.update(repo, ui, [6, ], cache) + + print graph_to_string(graph) + +def test_update_real(repo_dir, version_list=None, full=False): + """ Smoke test graph.update(). """ + ui_ = ui.ui() + repo = hg.repository(ui_, repo_dir) + cache = BundleCache(repo, ui_, CACHE_DIR) + cache.remove_files() + graph = UpdateGraph() + if version_list is None: + latest = repo['tip'].rev() + version_list = [[ordinal, ] for ordinal in range(0, latest + 1)] + + chks = fake_chks() + for vers in version_list: + print "UPDATING TO: ", vers + new_edges = graph.update(repo, ui, vers, cache) + for edge in new_edges: + length = graph.get_length(edge) + graph.set_chk(edge[:2], edge[2], length, chks.next()) + + # REDFLAG: should call minimal_graph for "real" behavior + text = graph_to_string(graph) + print "GRAPH_LEN: ", len(text) + print text + + if full: + print "UPDATING TO: latest heads" + try: + new_edges = graph.update(repo, ui, None, cache) + for edge in new_edges: + length = graph.get_length(edge) + graph.set_chk(edge[:2], edge[2], length, chks.next()) + + # REDFLAG: should call minimal_graph for "real" behavior + text = graph_to_string(graph) + print "GRAPH_LEN: ", len(text) + print text + except UpToDate: + print "Already has the head revs." + + return (graph, repo, cache) + +def test_minimal_graph(repo_dir, version_list, file_name=None): + """ Smoke test minimal_graph(). """ + ui_ = ui.ui() + if file_name is None: + graph, repo, cache = test_update_real(repo_dir, version_list, True) + open('/tmp/latest_graph.txt', 'wb').write(graph_to_string(graph)) + else: + repo = hg.repository(ui_, repo_dir) + cache = BundleCache(repo, ui_, CACHE_DIR) + cache.remove_files() + graph = parse_graph(open(file_name, 'rb').read()) + print "--- from file: %s ---" % file_name + print graph_to_string(graph) + version_map = build_version_table(graph, repo) + + # Incomplete, but better than nothing. + # Verify that the chk bounds are the same after shrinking. + chk_bounds = {} + initial_edges = graph.get_top_key_edges() + for edge in initial_edges: + chk_bounds[graph.get_chk(edge)] = ( + get_rollup_bounds(graph, repo, edge[0] + 1, edge[1], version_map)) + + print "CHK BOUNDS:" + for value in chk_bounds: + print value + print " ", chk_bounds[value] + print + sizes = (512, 1024, 2048, 4096, 16 * 1024) + for max_size in sizes: + try: + print "MAX:", max(version_map.values()) + small = minimal_graph(graph, repo, version_map, max_size) + print "--- size == %i" % max_size + print graph_to_string(small) + + small.rep_invariant(repo, True) # Full check + chks = chk_bounds.keys() + path = small.get_top_key_edges() + print "TOP KEY EDGES:" + print path + for edge in path: + # MUST rebuild the version map because the indices changed. + new_map = build_version_table(small, repo) + bounds = get_rollup_bounds(small, repo, edge[0] + 1, + edge[1], new_map) + print "CHK:", small.get_chk(edge) + print "BOUNDS: ", bounds + assert chk_bounds[small.get_chk(edge)] == bounds + print "DELETING: ", edge, small.get_chk(edge) + chks.remove(small.get_chk(edge)) + assert len(chks) == 0 + except UpdateGraphException, err: + print "IGNORED: ", err + +def versions_str(version_list): + """ Format a list of 40 digit hex versions for humans. """ + return ' '.join([version[:12] for version in version_list]) + +# def test_rollup(repo_dir): +# version_list = [[1, ], [2, 3], [4, ], [7, ], [8, ], [9, 10]] +# graph, repo, dummy = test_update_real(repo_dir, version_list) +# print graph_to_string(graph) +# print +# version_map = build_version_table(graph, repo) +# reverse_map = {} +# for version in version_map: +# index = version_map[version] +# entry = reverse_map.get(index, set([])) +# entry.add(version) +# reverse_map[index] = entry + +# indices = reverse_map.keys() +# indices.sort() +# print "VERSION MAP:" +# for index in indices: +# print "%i:" % index +# for version in reverse_map[index]: +# print " ", version + +# for index in range(0, graph.latest_index + 1): +# parents, heads = get_rollup_bounds(graph, repo, 0, index, version_map) +# print "%i: %s" % (index, versions_str(heads)) +# print " ", versions_str(parents) + +# # Final roll +# parents, heads = get_rollup_bounds(graph, repo, 3, 5, +# version_map) + +# print "edge: %s" % versions_str(heads) +# print " ", versions_str(parents) + +# for index in range(0, graph.latest_index + 1): +# heads = get_heads(graph, index) +# print "[%i]:%s" % (index, versions_str(heads)) + + +def hexlify_file(in_file): + """ Dump the hex rep of a file. """ + data = binascii.hexlify(open(in_file, 'rb').read()) + while len(data): + chunk = data[:64] + print '+ "%s"' % chunk + data = data[len(chunk):] + + +HGSVN_FREENET_REVS = ( + '1c33735d20b6', + '815ca7018bf6', + '3e2436fe3928', + 'ae954303d1dd', + '723bd8953983', + '40c11333daf8', + '12eb1c81a1ec', + 'b02a0c6a859a', + 'a754303da0e5', + '20c6ae2419bf', + '2f8a676bb4c3', + 'c27794351de7', + '1ef183fa59f6', + '6479bf5a3eb9', + '3f912b103873', + '3a2204acb83c', + '3aaad6480514', + '76419812d2b7', + 'b5ba8a35c801', + 'ffcad749bed9', + '31acbabecc22', + 'f5dd66d8e676', + '3726fc577853', + 'efe0d89e6e8f', + '87a25acc07e3', + 'ba9f937fed0e', + '28f5a21c8e6e', + 'b0797db2ab16', + 'bc2e0f5d4a90', + '3c9bb5cf63f6', + 'ffd58f1accde', + '39ed8f43e65d', + 'b574671511ab', + 'b01c9e1811a8', + '158e1087d72b', + 'ac5cfb495ef7', + '32af1ded78d6', + '950457d1f557', + '42552b6d3b36', +# '8ec2ceb1545d', + ) + +ROLLUP_TEST_HG = ( + "48473130425a6839314159265359fb23d3670000be7fffffffffffdfffffffff" + + "fff7effffffffffffffffffffffffffffffffdffffffdfe00b7d97c6d8346f2a" + + "a8001806d52a9dc0000000200000000000000000000000000000000d34000000" + + "000000000000000000000000d034929fa9a9a3d53c934d3f5469903ca6268c46" + + "8da9934d1a00686468d34d18434d0699068c0134000d34321a6101880d0c4610" + + "d01a61346801a34683201a1a0000d4d026d0264429a4f5004c04c03400013000" + + "02600069300000000130046000d0046269800000013469800000000d1000a604" + + "3553daa68d03d400343401a07ea80034000d34d068d000034001a06800000190" + + "00d001a0001a0001a00d00f500200000000000000000000000000000000d3400" + + "0000000000000000000000000000d0152a44533406129a69ed1368536a9fa4f0" + + "9a60a78984d354fc14f29a9fea9e9a9ea329fa0d0c53268d3c149ec93c990f48" + + "69a1a69a53f53349e933527e493d3689a6a6d3c93d0a7b53d32989a793d4d26d" + + "340d29b6a9f88694f1a9e8d2901f6ddff8f5ec575ff465bd3fab2dc5755caa32" + + "4b441ed5c323427a9a4c5af0fc4860d6dd17fb7cf99b3ccc2aeda2c7cb39d4aa" + + "9bc89edd7f5125aa4b7ddf5f586c3698ead8d358835139365eef4d1735cdea5c" + + "af2dc4408cd4825724082fdc126cde4c81320dc5dd8258c8b53b8a6bcc400212" + + "24254c08ccab995733a68ab9578801b19002531c250dbf70e6411833bc444350" + + "33304a049e3881247361014a4958d6929430744a56992df40a747d7ac91248c0" + + "2510914e144945122850851424a2502461a50d6adf15ae6d5118ba05144e3c5f" + + "d49551449181675497595bb8ecea27d54e896f62e364a95e4719d1c904e21ac2" + + "23860c1883ab4215072d98bb108921a0ccb22220f3d0add0bb4d09984c6da8c9" + + "e4f0fc0ba79c886089ccc8cc806add24224f0a4aa7766862ea8982c43924308f" + + "022a618464dd6535f99b6bde4ba37c33d09915a45b57eb9ff28cc52d43d58cb2" + + "c1ea2d5066aad487e8a09858d6292166e0ad1773c09e2c6d1908ddd5374b5b25" + + "1b754fc0e6b0b43ff6a97477bc2f428a4a14605997e28a8a9a9e8631332af6a3" + + "1eb634aea0c3b3d18df493571b672adb57451a0f2ea9a48fbadb2c3b9516b538" + + "e89e0bf8ba7c2f329808f7d6e744af7223bd77ca738d1cec77f1f82db6530702" + + "d737c6f60dd6401e02f0822eb9444ea0e32d43345ac5e54640113682c0108f0b" + + "46bad9addf8b11af8dadafb4daaea64dc79faa2d1d9455beb3f447e43a68dd2e" + + "31ae4b679aa32d16b5575d179544170adba450770cc522786609698eadfd23ef" + + "3a5a393c6356e9dd64dd3c2788c6689f4380f9d3fbad64f2f492455ceff86f29" + + "d7631273541b382feaa080bfa911e201986a8101abdf6dfbfc1a34111ad98788" + + "704e3049274ae3308ae664c838da4b88e103cd6c047c605590a08b542f730ea1" + + "eeb5771ba2919b8e5c23f3408d8f44d246730ad9f24eb5c6cc4eceb2807605d8" + + "91121052327622bf04e080a6f50cbaaad69442b0f4c90e4de935f531180d1158" + + "b8c997712e4538c1fb10f9c2dfcf4b00955b5b056fcb1d64ed6402ef840dd962" + + "a6805f1c81e28a4a546428db81c848028320f0715b904402e46a118acec8bc06" + + "a0c3db4850b05d09cdb1745cc71b2f1122178b5ac9134828312f11508b66b7a6" + + "427687aa78e1b0691cfa6ad4c87f51ada6f84a6a96c5db654e734dad72cd3761" + + "70dc1a64834037a508761100a8b5a86e0c01c64d7d5d0719e28b4390642022b1" + + "2be5c30f2d02f865009d3230080181004eb862d7285ed6b1958b9e61ed144e03" + + "4b81138200269601355eb4605ba660dd28a58c7cbc7908bdd5a4e13456db50f9" + + "1b570685151ac3532c704c0ab93c5166aa10e9ac848bc60d0f0a136d812915ad" + + "81980303d4b27285182d2a0c2e11b0cd2d4b85c85899bcb60709ae01a3575ca0" + + "f0aa08d1ef18f5163242cc4a4259434c6a1587212c740d606a7408aff741dd24" + + "e352dc24b4b6554816a405ff59330e2c6504e2b155a3272113785872570a21ec" + + "8624ba27494109d1c232da401df528ce41b91baa24a7581a707d62cc8bfbf1fc" + + "d037d1de2063a06ee1f7ba3702408cef175cc75e09589ebc641270125830b641" + + "6a0a8131817062a16038444403c68c04cb6fded01ae0f11091443941816608d3" + + "26d968b08467032c7032b24232a3e6c6ec738f5822b5e20e556eadcf585d984a" + + "fc8442af283048c961acb138084b2fb178d4345a8a2a8da4196ce80d36295182" + + "c1b858525499005156173a792ebc4a61a5729269baac9dac8242afbe0a6d2362" + + "430192df6c6f58ac6260a25646513689f80129388708404ea2aeba9554a9e536" + + "8e71ecc6fe3dd7931f65ec46363f83db6aa8a28aa5852fdef2231b1e2ba5b9e3" + + "948df4d96ef4abab1ca8c2db549cfcf22340d0d8bebbd3e5af0f156c7dbe1ba8" + + "68debafce13ab61df95ce641d4af6b8ffcf0fdc984d0c5a97dd1e0a39e8f477b" + + "dc47389e52ad94a3417de43d49a88d4daa36f1b758464a2959b90b434aca64b9" + + "88f10d82dfbb649d9bc13f3b08c4b3cde3b73ca7b0d0ab5725939cf1edd1b85c" + + "fa4526ce38abf7abf4c5d9848f7b4ec239575fcef82fd98c8ac75397374d4c79" + + "2b04ebe74b3d5699a97aefc33b499ced958c3b1bc66815eab8d23471b8f9e363" + + "de5ef573fc3daec18e535d22eb06e5ee69a5094508a28f3e306ccee9d4c5c0af" + + "756c57637c63eefd852bb52fadc90dff920ecf52d0c6a8f1691c2a16b9a23063" + + "06ad8a4aec588bad4b80f007f21b6a66bb0d3a7202a6db7004dce201244d2b7b" + + "a1c72076e812a120385279dff4a4db913b45d5634d6cc015b025cb707899b013" + + "1c6c19255e906a18c48bb3b05569eab42cfd48596ae36024a1ec80b6b3887324" + + "bb4b1cde2d5cd1c4846e23fb164d68ba62e355114d4cb4b558bb2926ad95ada9" + + "10b6906faadc902dc8c26d396279d39a10a278a1ac25a1b4c9c9639413033429" + + "1140a4293086281018913e64325594d55e4517134b2f524d8757bacaa42ce0b0" + + "ba026b2f0e09174db4b6618d8328811888b92504b2d3187441b011adaf5331de" + + "bfbc308a3509f3822215de9595158a7d018930ba41e1747d609887d8e6b53786" + + "b5e22420983160a4af3073e1cd2186db0e85717366d152d89d8a0c5c2a44c24c" + + "5328c0c890f60ac02dc29172abb4188d5431ed70a2b63058ba90aaa4b7456624" + + "264209517b658e126ccd74d6d46c625f4043142415dad8541980ea9256113491" + + "110864b38431af70934444633e107a77209c02c8b12ea59e48cce1fb542f97fd" + + "dc7975ee1cd726b9c9fe6cbed294bbb948e6bbb3b24c7491ef15a861e311476f" + + "1d3bea29f2ce510f390bff329654d0d5e21f0b6b1cec6c18043ee626463e8a0a" + + "b89631b8c6dc6a1669f246a57ad5cfb7f458768f58f4ac582b775ffa663a9f52" + + "a0280d345118d53e657e9d37af7f6fb276bca7fa5f2973dcc54e4792bc2c5555" + + "79c9238ceea308f9d709aef77e57fc7a6aa50a4a350de25ce2a6a6b1cccf59ac" + + "5276b17d665ad996923cc5eb6b30e7fbb3687f0db3e68e42e05b63f54ff2b279" + + "b174772ca3191e333a9edf0238876d5df69c452f7dbf7eb54ef22ca79f2e6a88" + + "c7c7f77865f55113e659c7eb763fe741868a3ecc53c6c5f5b53efbe07f67ee97" + + "0a162ad3871b454f09cc392d5ad8decdff7564c12bd8f79ea51a78f6985534e0" + + "ba658caf8be354eea3bb8c84e5be35af6370b2ee4cb7033549d3f54ae6fa3e1d" + + "ce61b759ac951cbeb766ad8eecfeb1c0564d67e9e145fcf65b2b357a97239d6b" + + "566d33f2c7d335ed92bdf14584f398a9b964e3d1fa89472f61f61fb65f7511e8" + + "297e78d4b3714e2ef1477d44dac50cdb41c64d9c6466d1dcad26ce3fc2a5954f" + + "18e025ee3f036cfcd2b3731a73d96763bcc8de721728f1d9f4e856a9776a9a97" + + "dd57e89c556fb77cb7df13d57612f050ab6763111c18bb3f0ba4748cc3ca5b63" + + "1ce24d0b68e146fdcfbee1cdc6fe2be6563dc8f53afb7e2d817bd97a3b88cc3f" + + "13ce2d4d84705e7a77d612f4cd38eeb22edd6fb5170609f62324b5c67a3091cf" + + "9c752ef23b2705d69736963d1bc3e2958db5de9e93fa1cbc53bdb03819bb65d5" + + "a6bcf9b8bc5b7706328a9e7be552b6bb19965d9df3ed278cae741ad2895945a6" + + "2e4cb33f15115b1948a53191bd2f4dc9c568a5b12c6366efdbceae28c79896d2" + + "39b619dc3dc6196e9969dbc67de9be48e8e3a337ef9d62cf9f41ef35cdc3ad9d" + + "b4699f1aa52caf3fb93026ca38ae3aa79f5c5e229ec5ab53cd6565d63fed9d76" + + "f55bde61c88f95f5638ce55716bfe38c44d74ece2c716af8c84790a73932bd81" + + "3a469f492fef816f786e99d42af7d9e71e69de5bf1de6d11b88b645c9fd5e39d" + + "e9c8981786ae8f357b9eeb44c9baaabcd9d725a3b1736e4350d5c5e9fb2338ce" + + "342bc6be8e3cfbca75cd645a5ac8fa57b378a3f9c5ba799729e8b4f197f09d09" + + "efadb1a397460cfc897466a5628b8c652297b1314d3b62d546e990decd547fe4" + + "ab798f0a7f26f8bf2ff191b4a603f6b79332b6be28f972af9188c3ede7271a47" + + "df658ff68d8bb58c44c51d2baece4c5a6fcf53151e93d66a5c4564725a5eb70d" + + "a23a05f16733f1805ee2eecf4de33297168e94a3dd755167388aef6ebb60a783" + + "a14fe98bfc6c1eb1e03a0f3b3f146858964cfc6f49aa7ff8bb9229c28487d91e" + + "9b38") + +def setup_rollup_test_repo(dir_name): + """ INTERNAL: Setup for rollup test. """ + assert dir_name.endswith('TST_REPO') + if os.path.exists(dir_name): + shutil.rmtree(dir_name) + + assert not os.path.exists(dir_name) + os.makedirs(dir_name) + ui_ = ui.ui() + repo = hg.repository(ui_, dir_name, True) + bundle_path = os.path.join(dir_name, 'bundle.hg') + bundle_file = open(bundle_path, 'wb') + bundle_file.write(unhexlify(ROLLUP_TEST_HG)) + bundle_file.close() + pull_bundle(repo, ui_, bundle_path) + return (repo, ui_) + +def dump_version_map(version_map): + """ Print a version map table in a human readable format. """ + reverse_map = {} + for version in version_map: + index = version_map[version] + entry = reverse_map.get(index, set([])) + entry.add(version) + reverse_map[index] = entry + + indices = reverse_map.keys() + indices.sort() + print "---Version map---" + for index in indices: + print "%i:" % index + for version in reverse_map[index]: + print " ", version + +# Only compares first 12 digits so full ids can be compared +# against short ones. +def check_result(result_a, result_b): + """ INTERNAL: Helper function. """ + assert len(result_a) == 2 + assert len(result_b) == 2 + assert len(result_a[0]) == len(result_b[0]) + assert len(result_a[1]) == len(result_b[1]) + for outer in range(0, 2): + for inner in range(0, len(result_a[outer])): + if result_a[outer][inner][:12] != result_b[outer][inner][:12]: + print "MISMATCH:" + print result_a + print result_b + assert False + +def dump_changesets(repo): + """ Print all the changesets in a repo. """ + print "---" + max_rev = repo['tip'].rev() + for rev in range(-1, max_rev + 1): + print hex_version(repo, rev) + print "---" +# There are many, many ways to fail. +# More testing would be good. + +EXPECTED_VERSION_MAP = { + '0000000000000000000000000000000000000000':-1, + '716c293192c7b2e26860ade38e1b279e905cd197':0, + '2aa9c462481a05287e7b97d7abc48ca53b24b33c':1, + '4636fd812094cf54aeb58c7f5edf35d19ebe79e3':1, + '076aec9f34c96c62a3069a98af1927bf710430b4':1, + '62a72a238ffc748c11d115d8ceab44daf517fd76':2, + '4409936ef21f3cb20e443b6ec37110978fccb484':2, + '90374996e95f994e8925301fb91252fe509661e6':3, + '75a57040197d15bde53148157403284727bcaba4':3, + 'a2c749d99d546c3db4f1fdb8c5c77ec9bef30aeb':3, + '138466bcf8027a765b77b13a456598e74fe65115':4, + '8ddce595000df42d9abbc81c7654fa36457b2081':4, + 'fd1e6832820b1a7a33f6e69d3ca561c09af2e015':5, + '7429bf7b11f56d016a1cd7b7ec5cf130e48108b7':6, + 'f6248cd464e3f8fc8bd9a03dcc78943615d0e148':4, + 'fcc2e90dbf0dc2736c119c6ae995edf092f3f6cb':7, + '03c047d036ca0f3aab3a47451fbde2b02026ee99':8, + '9eaabc277b994c299aa4341178b416728fd279ff':9, + '2f6c65f64ce59060c08dae82b7dcbeb8e4d2d976':9, +} + +def test_rollup(): + """ Smoke test get_rollup_bounds(). """ + repo, ui_ = setup_rollup_test_repo(TST_REPO_DIR) + dump_changesets(repo) + cache = BundleCache(repo, ui_, CACHE_DIR) + cache.remove_files() + graph = UpdateGraph() + + chks = fake_chks() + # 0 Single changeset + edges = graph.update(repo, ui_, ['716c293192c7', ], cache) + set_chks(graph, edges, chks) + # 1 Multiple changesets + edges = graph.update(repo, ui_, ['076aec9f34c9', ], cache) + set_chks(graph, edges, chks) + # 2 Multiple heads, single base + edges = graph.update(repo, ui_, ['62a72a238ffc', '4409936ef21f'], cache) + set_chks(graph, edges, chks) + # 3 Multiple bases, single head + edges = graph.update(repo, ui_, ['a2c749d99d54', ], cache) + set_chks(graph, edges, chks) + # 4 + edges = graph.update(repo, ui_, ['f6248cd464e3', ], cache) + set_chks(graph, edges, chks) + # 5 + edges = graph.update(repo, ui_, ['fd1e6832820b', ], cache) + set_chks(graph, edges, chks) + # 6 + edges = graph.update(repo, ui_, ['7429bf7b11f5', ], cache) + set_chks(graph, edges, chks) + # 7 + edges = graph.update(repo, ui_, ['fcc2e90dbf0d', ], cache) + set_chks(graph, edges, chks) + # 8 + edges = graph.update(repo, ui_, ['03c047d036ca', ], cache) + set_chks(graph, edges, chks) + + # 9 + edges = graph.update(repo, ui_, ['2f6c65f64ce5', ], cache) + set_chks(graph, edges, chks) + + print + print graph_to_string(graph) + version_map = build_version_table(graph, repo) + + dump_version_map(version_map) + assert version_map == EXPECTED_VERSION_MAP + + graph.rep_invariant(repo, True) # Verify contiguousness. + + print "From earliest..." + for index in range(0, graph.latest_index + 1): + parents, heads = get_rollup_bounds(graph, repo, 0, index, version_map) + print "(%i->%i): %s" % (0, index, versions_str(heads)) + print " ", versions_str(parents) + + + print "To latest..." + for index in range(0, graph.latest_index + 1): + parents, heads = get_rollup_bounds(graph, repo, index, + graph.latest_index, + version_map) + print "(%i->%i): %s" % (index, graph.latest_index, versions_str(heads)) + print " ", versions_str(parents) + + + # Empty + try: + get_rollup_bounds(graph, repo, FIRST_INDEX, FIRST_INDEX, + version_map) + except AssertionError: + # Asserted as expected for to_index == FIRST_INDEX + print "Got expected assertion." + + # Rollup of one changeset index. + result = get_rollup_bounds(graph, repo, 0, 0, version_map) + check_result(result, (('000000000000', ), ('716c293192c7',))) + + # Rollup of multiple changeset index. + result = get_rollup_bounds(graph, repo, 1, 1, version_map) + check_result(result, (('716c293192c7', ), ('076aec9f34c9',))) + + # Rollup of with multiple heads. + result = get_rollup_bounds(graph, repo, 1, 2, version_map) + check_result(result, (('716c293192c7', ), ('4409936ef21f','62a72a238ffc'))) + + # Rollup of with multiple bases. + result = get_rollup_bounds(graph, repo, 3, 4, version_map) + check_result(result, (('4409936ef21f', '62a72a238ffc', ), + ('f6248cd464e3',))) + + # Rollup with head pulled in from earlier base. + result = get_rollup_bounds(graph, repo, 3, 8, version_map) + print result + check_result(result, (('4409936ef21f', '62a72a238ffc', ), + ('03c047d036ca', '7429bf7b11f5'))) + + # Rollup after remerge to a single head. + result = get_rollup_bounds(graph, repo, 0, 9, version_map) + print result + check_result(result, (('000000000000', ), ('2f6c65f64ce5', ))) + +if __name__ == "__main__": + test_presentation() + VERSIONS = [(ver, ) for ver in HGSVN_FREENET_REVS] + test_minimal_graph(HGSVN_FREENET_REPO, VERSIONS) + test_rollup() + +# Testing minimal graph. +# - contains the right edges [Inspection. not aut] +# x revision bounds of edges don't change +# - backmap from chks +# ? indexes are contiguous [Lean on graph.rep_invariant)()] +# a) ordinals -- easy +# b) changesets -- hard +# ?? depend on version map +# CONCERNS: +# - creating real use test cases +# - so hard that I will "waste" a lot of time without finding bugs + diff --git a/infocalypse/test_topkey.py b/infocalypse/test_topkey.py new file mode 100644 --- /dev/null +++ b/infocalypse/test_topkey.py @@ -0,0 +1,69 @@ +""" Smoke test topkey. + + Copyright (C) 2009 Darrell Karbott + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2.0 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks +""" + + +from topkey import top_key_tuple_to_bytes, bytes_to_top_key_tuple, \ + dump_top_key_tuple + +BAD_CHK1 = ('CHK@badroutingkey155JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') +BAD_CHK2 = ('CHK@badroutingkey255JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') +BAD_CHK3 = ('CHK@badroutingkey355JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') +BAD_CHK4 = ('CHK@badroutingkey455JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') +BAD_CHK5 = ('CHK@badroutingkey555JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') +BAD_CHK6 = ('CHK@badroutingkey655JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') +BAD_CHK7 = ('CHK@badroutingkey755JblbGup0yNSpoDJgVPnL8E5WXoc,' + +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') + +TOP = ((BAD_CHK6,), + ((10, ('0' * 40, '1' * 40, '2' * 40), ('a' * 40, 'b' * 40,), + (BAD_CHK1,), True, True), + (20, ('3' * 40,), ('c' * 40,), + (BAD_CHK2,), False, True), + (30, ('3' * 40,), ('d' * 40,), + (BAD_CHK3, BAD_CHK4), True, False), + (40, ('2' * 40,), ('e' * 40,), + (BAD_CHK5,), False, False), + )) + +def smoke_test_topkey(): + """ Smoke test top key functions. """ + # To binary rep... + bytes0 = top_key_tuple_to_bytes(TOP) + bytes1 = top_key_tuple_to_bytes(TOP, 0xcc) + + # Check salting. + assert bytes0 != bytes1 + assert len(bytes0) == len(bytes1) + + # ... and back + assert bytes_to_top_key_tuple(bytes0)[0] == TOP + assert bytes_to_top_key_tuple(bytes1)[0] == TOP + + dump_top_key_tuple(TOP) + +if __name__ == "__main__": + smoke_test_topkey() diff --git a/infocalypse/topkey.py b/infocalypse/topkey.py --- a/infocalypse/topkey.py +++ b/infocalypse/topkey.py @@ -24,7 +24,11 @@ ((graph_a_chk, graph_b_chk), (<update>,...)) Where: - <update> := (length, parent_rev, latest_rev, (CHK, ...)) + <update> := (length, (parent_rev, ...), (head_revs, ...), (CHK, ...), + all_parent_revs, all_head_revs) + + all_parent_revs is True iff all parent revs are included. + all_head_revs is True iff all parent revs are included. top_key_data_to_bytes() converts from the tuple format to a compact binary rep. @@ -38,15 +42,22 @@ import struct from binascii import hexlify, unhexlify +from fcpconnection import sha1_hexdigest + from chk import CHK_SIZE, bytes_to_chk, chk_to_bytes -from fcpconnection import sha1_hexdigest +# Known versions: +# 1.00 -- Initial release. +# 2.00 -- Support for multiple head and parent versions and incomplete lists. -MAJOR_VERSION = '1' +HDR_V1 = 'HGINF100' # Obsolete + +MAJOR_VERSION = '2' MINOR_VERSION = '00' HDR_VERSION = MAJOR_VERSION + MINOR_VERSION -HDR_BYTES = 'HGINF%s' % HDR_VERSION +HDR_PREFIX = 'HGINF' +HDR_BYTES = HDR_PREFIX + HDR_VERSION # Header length 'HGINF100' HDR_SIZE = 8 @@ -55,75 +66,137 @@ assert len(HDR_BYTES) == HDR_SIZE # Length of the binary rep of an hg version HGVER_SIZE = 20 -# <header bytes><salt byte><num graph chks> -BASE_FMT = "!%isBB" % HDR_SIZE -# bundle_len:parent_rev:latest_rev:CHK [:CHK] -BASE_UPDATE_FMT = "!q%is%isB" % (HGVER_SIZE, HGVER_SIZE) -# Binary rep of a single CHK -CHK_FMT = "!%is" % CHK_SIZE +# <header bytes><salt byte><num graph chks><num updates> +BASE_FMT = "!%isBBB" % HDR_SIZE +# <bundle_len><flags><parent_count><head_count><chk_count> \ +# [parent data][head data][chk data] +BASE_UPDATE_FMT = "!qBBBB" BASE_LEN = struct.calcsize(BASE_FMT) BASE_UPDATE_LEN = struct.calcsize(BASE_UPDATE_FMT) +# More pythonic way? +# Hmmm... why are you using bit bashing in the 21st century? +HAS_PARENTS = 0x01 +HAS_HEADS = 0x02 + +def versions_to_bytes(versions): + """ INTERNAL: Return raw byte string from hg 40 digit hex + version list. """ + bytes = '' + for version in versions: + try: + raw = unhexlify(version) + if len(raw) != HGVER_SIZE: + raise TypeError() # Hmmm... git'r done. + except TypeError: + # REDFLAG: Test code path. + raise ValueError("Couldn't parse 40 digit hex version from: " + + str(version)) + bytes += raw + return bytes + def top_key_tuple_to_bytes(top_key_tuple, salt_byte=0): """ Returns a binary representation of top_key_tuple. """ - ret = struct.pack(BASE_FMT, HDR_BYTES, salt_byte, len(top_key_tuple[0])) + ret = struct.pack(BASE_FMT, HDR_BYTES, salt_byte, + len(top_key_tuple[0]), len(top_key_tuple[1])) for graph_chk in top_key_tuple[0]: ret += chk_to_bytes(graph_chk) + # Can't find doc. True for all modern Python + assert int(True) == 1 and int(False) == 0 for update in top_key_tuple[1]: - assert len(update[1]) == 40 - assert len(update[2]) == 40 - ret += struct.pack(BASE_UPDATE_FMT, update[0], - unhexlify(update[1]), - unhexlify(update[2]), + flags = (((int(update[4]) * 0xff) & HAS_PARENTS) + | ((int(update[5]) * 0xff) & HAS_HEADS)) + + ret += struct.pack(BASE_UPDATE_FMT, + update[0], flags, + len(update[1]), len(update[2]), len(update[3])) + + ret += versions_to_bytes(update[1]) # parents + ret += versions_to_bytes(update[2]) # heads for chk in update[3]: - chk_bytes = struct.pack(CHK_FMT, chk_to_bytes(chk)) + chk_bytes = chk_to_bytes(chk) assert len(chk_bytes) == CHK_SIZE ret += chk_bytes return ret +def versions_from_bytes(version_bytes): + """ INTERNAL: Parse a list of hg 40 digit hex version strings from + a raw byte block. """ + assert (len(version_bytes) % HGVER_SIZE) == 0 + ret = [] + for count in range(0, len(version_bytes) / HGVER_SIZE): + try: + ret.append(hexlify(version_bytes[count * HGVER_SIZE: + (count + 1) * HGVER_SIZE])) + except TypeError: + raise ValueError("Error parsing an hg version.") + return tuple(ret) + +def bytes_to_update_tuple(bytes): + """ INTERNAL: Read a single update from raw bytes. """ + length, flags, parent_count, head_count, chk_count = struct.unpack( + BASE_UPDATE_FMT, + bytes[:BASE_UPDATE_LEN]) + + bytes = bytes[BASE_UPDATE_LEN:] + + parents = versions_from_bytes(bytes[:HGVER_SIZE * parent_count]) + bytes = bytes[HGVER_SIZE * parent_count:] + + heads = versions_from_bytes(bytes[:HGVER_SIZE * head_count]) + bytes = bytes[HGVER_SIZE * head_count:] + + chks = [] + for dummy in range(0, chk_count): + chks.append(bytes_to_chk(bytes[:CHK_SIZE])) + bytes = bytes[CHK_SIZE:] + + return ((length, parents, heads, tuple(chks), + bool(flags & HAS_PARENTS), bool(flags & HAS_HEADS)), + bytes) + + def bytes_to_top_key_tuple(bytes): - """ Parses the top key data from a byte block and returns a tuple. """ + """ Parses the top key data from a byte block and + returns a (top_key_tuple, header_string, salt_byte) tuple. """ if len(bytes) < BASE_LEN: raise ValueError("Not enough data to parse static fields.") + if not bytes.startswith(HDR_PREFIX): + raise ValueError("Doesn't look like top key binary data.") + # Hmmm... return the salt byte? - hdr, dummy, graph_chk_count = struct.unpack(BASE_FMT, bytes[:BASE_LEN]) + hdr, salt, graph_chk_count, update_count = struct.unpack(BASE_FMT, + bytes[:BASE_LEN]) #print "bytes_to_top_key_data -- salt: ", dummy bytes = bytes[BASE_LEN:] if hdr != HDR_BYTES: - print "bytes_to_top_key_data -- header doesn't match! Expect problems." + if hdr[5] != MAJOR_VERSION: + # DOH! should have done this in initial release. + raise ValueError("Format version mismatch. " + + "Maybe you're running old code?") + print "bytes_to_top_key_data -- minor version mismatch: ", hdr if len(bytes) == 0: print "bytes_to_top_key_data -- No updates?" graph_chks = [] for dummy in range(0, graph_chk_count): - graph_chks.append(bytes_to_chk(struct.unpack(CHK_FMT, - bytes[:CHK_SIZE])[0])) + graph_chks.append(bytes_to_chk(bytes[:CHK_SIZE])) bytes = bytes[CHK_SIZE:] + # REDFLAG: Fix range errors for incomplete / bad data. updates = [] - while len(bytes) > BASE_UPDATE_LEN: - length, raw_parent, raw_latest, chk_count = struct.unpack( - BASE_UPDATE_FMT, - bytes[:BASE_UPDATE_LEN]) + for dummy in range(0, update_count): + update, bytes = bytes_to_update_tuple(bytes) + updates.append(update) - bytes = bytes[BASE_UPDATE_LEN:] - chks = [] - for dummy in range(0, chk_count): - chks.append(bytes_to_chk(struct.unpack(CHK_FMT, - bytes[:CHK_SIZE])[0])) - bytes = bytes[CHK_SIZE:] - - updates.append((length, hexlify(raw_parent), hexlify(raw_latest), - tuple(chks))) - - return (tuple(graph_chks), tuple(updates)) + return ((tuple(graph_chks), tuple(updates)), hdr, salt) def default_out(text): """ Default output function for dump_top_key_tuple(). """ @@ -137,10 +210,18 @@ def dump_top_key_tuple(top_key_tuple, ou for index, chk in enumerate(top_key_tuple[0]): out_func("graph_%s:%s\n" % (chr(ord('a') + index), chk)) for index, update in enumerate(top_key_tuple[1]): - out_func("update[%i]\n" % index) - out_func(" length : %i\n" % update[0]) - out_func(" parent_rev: %s\n" % update[1]) - out_func(" latest_rev: %s\n" % update[2]) + if update[4] and update[5]: + text = "full graph info" + elif not (update[4] or update[5]): + text = "incomplete parent, head lists" + elif not update[4]: + text = "incomplete parent list" + else: + text = "incomplete head list" + out_func("update[%i] (%s)\n" % (index, text)) + out_func(" length : %i\n" % update[0]) + out_func(" parents: %s\n" % ' '.join([ver[:12] for ver in update[1]])) + out_func(" heads : %s\n" % ' '.join([ver[:12] for ver in update[2]])) for index, chk in enumerate(update[3]): out_func(" CHK[%i]:%s\n" % (index, chk)) out_func("binary rep sha1:\n0x00:%s\n0xff:%s\n" % diff --git a/infocalypse/updatesm.py b/infocalypse/updatesm.py --- a/infocalypse/updatesm.py +++ b/infocalypse/updatesm.py @@ -36,12 +36,14 @@ from requestqueue import RequestQueue from chk import clear_control_bytes from bundlecache import make_temp_file, BundleException from graph import INSERT_NORMAL, INSERT_PADDED, INSERT_SALTED_METADATA, \ - minimal_update_graph, graph_to_string, \ - FREENET_BLOCK_LEN, has_version, pull_bundle, parse_graph, hex_version - + FREENET_BLOCK_LEN, has_version, \ + pull_bundle, hex_version +from graphutil import minimal_graph, graph_to_string, parse_graph +from choose import get_top_key_updates from topkey import bytes_to_top_key_tuple, top_key_tuple_to_bytes, \ dump_top_key_tuple + from statemachine import StatefulRequest, RequestQueueState, StateMachine, \ Quiescent, Canceling, RetryingRequestList, CandidateRequest, \ require_state, delete_client_file @@ -55,6 +57,8 @@ HG_MIME_TYPE_FMT = HG_MIME_TYPE + ';%i' METADATA_MARKER = HG_MIME_TYPE + ';' PAD_BYTE = '\xff' +MAX_SSK_LEN = 1024 + class UpdateContext(dict): """ A class to hold inter-state data used while the state machine is running. """ @@ -80,14 +84,23 @@ class UpdateContext(dict): # public key to update the private key. self['IS_KEYPAIR'] = False - self['TARGET_VERSION'] = None + self['TARGET_VERSIONS'] = None self['INSERT_URI'] = 'CHK@' self['REQUEST_URI'] = None - def has_version(self, version): - """ Returns True if version is already in the hg repository, + def has_versions(self, versions): + """ Returns True if all versions are already in the hg repository, False otherwise. """ - return has_version(self.repo, version) + if versions is None: + return False # Allowed. + + assert (type(versions) == type((0, )) or + type(versions) == type([0, ])) + assert len(versions) > 0 + for version in versions: + if not has_version(self.repo, version): + return False + return True def pull(self, file_name): """ Pulls an hg bundle file into the local repository. """ @@ -177,8 +190,10 @@ class UpdateContext(dict): raised = False try: bundle = self.parent.ctx.bundle_cache.make_bundle(self.graph, - edge[:2], - tmp_file) + self.parent.ctx. + version_table, + edge[:2], + tmp_file) if bundle[0] != original_len: raise BundleException("Wrong size. Expected: %i. Got: %i" @@ -376,14 +391,16 @@ class InsertingGraph(StaticRequestList): + '\n') # Create minimal graph that will fit in a 32k block. - self.working_graph = minimal_update_graph(self.parent.ctx.graph, - 31 * 1024, graph_to_string) + assert not self.parent.ctx.version_table is None + self.working_graph = minimal_graph(self.parent.ctx.graph, + self.parent.ctx.repo, + self.parent.ctx.version_table, + 31*1024) if self.parent.params.get('DUMP_GRAPH', False): self.parent.ctx.ui_.status("--- Minimal Graph ---\n") - self.parent.ctx.ui_.status(graph_to_string(minimal_update_graph( - self.working_graph, - 31 * 1024, graph_to_string)) + '\n---\n') + self.parent.ctx.ui_.status(graph_to_string(self.working_graph) + + '\n---\n') # Make sure the string rep is small enough! graph_bytes = graph_to_string(self.working_graph) @@ -408,46 +425,42 @@ class InsertingGraph(StaticRequestList): StaticRequestList.reset(self) self.working_graph = None + # REDFLAG: cache value? not cheap def get_top_key_tuple(self): """ Get the python rep of the data required to insert a new URI with the updated graph CHK(s). """ graph = self.parent.ctx.graph assert not graph is None - return ((self.get_result(0)[1]['URI'], - self.get_result(1)[1]['URI']), - get_top_key_updates(graph)) -def get_top_key_updates(graph): - """ Returns the update tuples needed to build the top key.""" + # REDFLAG: graph redundancy hard coded to 2. + chks = (self.get_result(0)[1]['URI'], self.get_result(1)[1]['URI']) - graph.rep_invariant() + # Slow. + updates = get_top_key_updates(graph, self.parent.ctx.repo) - edges = graph.get_top_key_edges() + # Head revs are more important because they allow us to + # check whether the local repo is up to date. - coalesced_edges = [] - ordinals = {} - for edge in edges: - assert edge[2] >= 0 and edge[2] < 2 - assert edge[2] == 0 or (edge[0], edge[1], 0) in edges - ordinal = ordinals.get(edge[:2]) - if ordinal is None: - ordinal = 0 - coalesced_edges.append(edge[:2]) - ordinals[edge[:2]] = max(ordinal, edge[2]) + # Walk from the oldest to the newest update discarding + # base revs, then head revs until the binary rep will + # fit in an ssk. + index = len(updates) - 1 + zorch_base = True + while (len(top_key_tuple_to_bytes((chks, updates))) >= MAX_SSK_LEN + and index >= 0): + victim = list(updates[index]) + victim[1] = victim[1 + int(zorch_base)][:1] # Discard versions + victim[4 + int(zorch_base)] = False + updates[index] = tuple(victim) + if not zorch_base: + zorch_base = True + index -= 1 + continue + zorch_base = False - ret = [] - for edge in coalesced_edges: - parent_rev = graph.index_table[edge[0]][1] - latest_rev = graph.index_table[edge[1]][1] - length = graph.get_length(edge) - assert len(graph.edge_table[edge][1:]) > 0 + assert len(top_key_tuple_to_bytes((chks, updates))) < MAX_SSK_LEN - #(length, parent_rev, latest_rev, (CHK, ...)) - update = (length, parent_rev, latest_rev, - graph.edge_table[edge][1:]) - ret.append(update) - - return ret + return (chks, updates) class InsertingUri(StaticRequestList): """ A state to insert the top level URI for an Infocalypse repository @@ -579,7 +592,6 @@ class RequestingUri(StaticRequestList): dump_top_key_tuple(self.get_top_key_tuple(), self.parent.ctx.ui_.status) - def get_top_key_tuple(self): """ Get the python rep of the data in the URI. """ top_key_tuple = None @@ -587,7 +599,7 @@ class RequestingUri(StaticRequestList): result = candidate[5] if result is None or result[0] != 'AllData': continue - top_key_tuple = bytes_to_top_key_tuple(result[2]) + top_key_tuple = bytes_to_top_key_tuple(result[2])[0] break assert not top_key_tuple is None return top_key_tuple @@ -700,26 +712,13 @@ class RequestingGraph(StaticRequestList) StaticRequestList.__init__(self, parent, name, success_state, failure_state) - # REDFLAG: remove this? why aren't I just calling get_top_key_tuple - # on REQUESTING_URI_4_INSERT??? - def get_top_key_tuple(self): - """ Returns the Python rep of the data in the request uri. """ - results = [candidate[5] for candidate in - self.parent.get_state(REQUESTING_URI_4_INSERT).ordered] - top_key_tuple = None - for result in results: - if result is None or result[0] != 'AllData': - continue - top_key_tuple = bytes_to_top_key_tuple(result[2]) - break - assert not top_key_tuple is None - return top_key_tuple - def enter(self, from_state): """ Implementation of State virtual. """ require_state(from_state, REQUESTING_URI_4_INSERT) + top_key_tuple = (self.parent.get_state(REQUESTING_URI_4_INSERT). + get_top_key_tuple()) - top_key_tuple = self.get_top_key_tuple() + #top_key_tuple = self.get_top_key_tuple() REDFLAG: remove #print "TOP_KEY_TUPLE", top_key_tuple #[uri, tries, is_insert, raw_data, mime_type, last_msg] for uri in top_key_tuple[0]: @@ -736,7 +735,7 @@ class RequestingGraph(StaticRequestList) result = candidate[5] if not result is None and result[0] == 'AllData': graph = parse_graph(result[2]) - break + assert not graph is None self.parent.ctx.graph = graph @@ -868,19 +867,19 @@ class UpdateStateMachine(RequestQueue, S self.ctx = ctx - def start_inserting(self, graph, to_version, insert_uri='CHK@'): + def start_inserting(self, graph, to_versions, insert_uri='CHK@'): """ Start and insert of the graph and any required new edge CHKs to the insert URI. """ self.require_state(QUIESCENT) self.reset() self.ctx.graph = graph - self.ctx['TARGET_VERSION'] = to_version + self.ctx['TARGET_VERSIONS'] = to_versions self.ctx['INSERT_URI'] = insert_uri self.transition(INSERTING_BUNDLES) # Update a repo USK. # REDFLAG: later, keys_match=False arg - def start_pushing(self, insert_uri, to_version='tip', request_uri=None, + def start_pushing(self, insert_uri, to_versions=('tip',), request_uri=None, is_keypair=False): """ Start pushing local changes up to to_version to an existing @@ -892,7 +891,8 @@ class UpdateStateMachine(RequestQueue, S self.ctx['INSERT_URI'] = insert_uri self.ctx['REQUEST_URI'] = request_uri # Hmmmm... better exception if to_version isn't in the repo? - self.ctx['TARGET_VERSION'] = hex_version(self.ctx.repo, to_version) + self.ctx['TARGET_VERSIONS'] = tuple([hex_version(self.ctx.repo, ver) + for ver in to_versions]) if request_uri is None: self.ctx['IS_KEYPAIR'] = True self.transition(INVERTING_URI_4_INSERT)