Add indirection in the directory state tree so that merged nodes would continue sharing state through future merges.

This commit is contained in:
A.J. Beamon 2018-05-23 14:59:18 -07:00
parent 0b1bd4f765
commit 48bf339843
2 changed files with 100 additions and 87 deletions

View File

@ -124,7 +124,7 @@ class DirectoryTest(Test):
if random.random() < 0.5:
while True:
self.dir_index = random.randrange(0, len(self.dir_list))
if not self.dir_list[self.dir_index].is_partition or not self.dir_list[self.dir_index].deleted:
if not self.dir_list[self.dir_index].state().is_partition or not self.dir_list[self.dir_index].state().deleted:
break
instructions.push_args(self.dir_index)
@ -133,15 +133,15 @@ class DirectoryTest(Test):
dir_entry = self.dir_list[self.dir_index]
choices = op_choices[:]
if dir_entry.is_directory:
if dir_entry.state().is_directory:
choices += directory
if dir_entry.is_subspace:
if dir_entry.state().is_subspace:
choices += subspace
op = random.choice(choices)
# print('%d. Selected %s, dir=%d, dir_id=%d, has_known_prefix=%d, dir_list_len=%d' \
# % (len(instructions), op, self.dir_index, dir_entry.dir_id, dir_entry.has_known_prefix, len(self.dir_list)))
# % (len(instructions), op, self.dir_index, dir_entry.dir_id, dir_entry.state().has_known_prefix, len(self.dir_list)))
if op.endswith('_DATABASE') or op.endswith('_SNAPSHOT'):
root_op = op[0:-9]
@ -194,7 +194,7 @@ class DirectoryTest(Test):
if child_entry is None:
child_entry = DirectoryStateTreeNode(True, True)
child_entry.has_known_prefix = False
child_entry.state().has_known_prefix = False
self.dir_list.append(dir_entry.add_child(path, child_entry))
elif root_op == 'DIRECTORY_CREATE':
@ -224,10 +224,10 @@ class DirectoryTest(Test):
if child_entry is None:
child_entry = DirectoryStateTreeNode(True, True, has_known_prefix=bool(prefix))
elif not bool(prefix):
child_entry.has_known_prefix = False
child_entry.state().has_known_prefix = False
if is_partition:
child_entry.is_partition = True
child_entry.state().is_partition = True
self.dir_list.append(dir_entry.add_child(path, child_entry))
@ -256,7 +256,7 @@ class DirectoryTest(Test):
self.dir_list.append(dir_entry.add_child(new_path, child_entry))
# Make sure that the default directory subspace still exists after moving the specified directory
if dir_entry.is_directory and not dir_entry.is_subspace and old_path == (u'',):
if dir_entry.state().is_directory and not dir_entry.state().is_subspace and old_path == (u'',):
self.ensure_default_directory_subspace(instructions, default_path)
elif root_op == 'DIRECTORY_MOVE_TO':
@ -291,7 +291,7 @@ class DirectoryTest(Test):
dir_entry.delete(path)
# Make sure that the default directory subspace still exists after removing the specified directory
if path == () or (dir_entry.is_directory and not dir_entry.is_subspace and path == (u'',)):
if path == () or (dir_entry.state().is_directory and not dir_entry.state().is_subspace and path == (u'',)):
self.ensure_default_directory_subspace(instructions, default_path)
elif root_op == 'DIRECTORY_LIST' or root_op == 'DIRECTORY_EXISTS':
@ -310,7 +310,7 @@ class DirectoryTest(Test):
instructions.append('DIRECTORY_STRIP_PREFIX')
elif root_op == 'DIRECTORY_UNPACK_KEY' or root_op == 'DIRECTORY_CONTAINS':
if not dir_entry.has_known_prefix or random.random() < 0.2 or root_op == 'DIRECTORY_UNPACK_KEY':
if not dir_entry.state().has_known_prefix or random.random() < 0.2 or root_op == 'DIRECTORY_UNPACK_KEY':
t = self.random.random_tuple(5)
instructions.push_args(*test_util.with_length(t))
instructions.append('DIRECTORY_PACK_KEY')
@ -324,7 +324,7 @@ class DirectoryTest(Test):
instructions.push_args(*test_util.with_length(t))
instructions.append(op)
if root_op == 'DIRECTORY_OPEN_SUBSPACE':
self.dir_list.append(DirectoryStateTreeNode(False, True, dir_entry.has_known_prefix))
self.dir_list.append(DirectoryStateTreeNode(False, True, dir_entry.state().has_known_prefix))
else:
test_util.to_front(instructions, 1)
instructions.append('DIRECTORY_STRIP_PREFIX')
@ -340,10 +340,10 @@ class DirectoryTest(Test):
for i, dir_entry in enumerate(self.dir_list):
instructions.push_args(i)
instructions.append('DIRECTORY_CHANGE')
if dir_entry.is_directory:
if dir_entry.state().is_directory:
instructions.push_args(self.directory_log.key())
instructions.append('DIRECTORY_LOG_DIRECTORY')
if dir_entry.has_known_prefix and dir_entry.is_subspace:
if dir_entry.state().has_known_prefix and dir_entry.state().is_subspace:
# print('%d. Logging subspace: %d' % (i, dir_entry.dir_id))
instructions.push_args(self.subspace_log.key())
instructions.append('DIRECTORY_LOG_SUBSPACE')

View File

@ -1,5 +1,26 @@
import sys
class TreeNodeState:
def __init__(self, dir_id, is_directory, is_subspace, has_known_prefix, root, is_partition):
self.dir_id = dir_id
self.root = root
self.is_directory = is_directory
self.is_subspace = is_subspace
self.has_known_prefix = has_known_prefix
self.children = {}
self.deleted = False
self.is_partition = is_partition
class Pointer:
def __init__(self, obj):
self.obj = obj
def get(self):
return self.obj
def set(self, obj):
self.obj = obj
# Represents an element of the directory hierarchy, which could have multiple states
class DirectoryStateTreeNode:
# A cache of directory layers. We mustn't have multiple entries for the same layer
@ -30,19 +51,15 @@ class DirectoryStateTreeNode:
return DirectoryStateTreeNode.layers[node_subspace_prefix]
def __init__(self, is_directory, is_subspace, has_known_prefix=True, root=None, is_partition=False):
self.root = root or self
self.is_directory = is_directory
self.is_subspace = is_subspace
self.has_known_prefix = has_known_prefix
self.children = {}
self.deleted = False
self.is_partition = is_partition
self.dir_id = DirectoryStateTreeNode.dir_id + 1
self.ptr = Pointer(TreeNodeState(DirectoryStateTreeNode.dir_id + 1, is_directory, is_subspace,
has_known_prefix, root or self, is_partition))
DirectoryStateTreeNode.dir_id += 1
def state(self):
return self.ptr.get()
def __repr__(self):
return '{DirEntry %d: %d}' % (self.dir_id, self.has_known_prefix)
return '{DirEntry %d: %d}' % (self.state().dir_id, self.state().has_known_prefix)
def _get_descendent(self, subpath, default):
if not subpath:
@ -52,9 +69,9 @@ class DirectoryStateTreeNode:
default_child = None
if default is not None:
default_child = default.children.get(subpath[0])
default_child = default.state().children.get(subpath[0])
self_child = self.children.get(subpath[0])
self_child = self.state().children.get(subpath[0])
if self_child is None:
if default_child is None:
@ -68,7 +85,7 @@ class DirectoryStateTreeNode:
return self._get_descendent(subpath, DirectoryStateTreeNode.default_directory)
def add_child(self, subpath, child):
child.root = self.root
child.state().root = self.state().root
if DirectoryStateTreeNode.default_directory:
# print('Adding child %r to default directory at %r' % (child, subpath))
child = DirectoryStateTreeNode.default_directory._add_child_impl(subpath, child)
@ -81,56 +98,52 @@ class DirectoryStateTreeNode:
return c
def _add_child_impl(self, subpath, child):
# print('%d, %d. Adding child (recursive): %r' % (self.dir_id, child.dir_id, subpath))
# print('%d, %d. Adding child %r (recursive): %r' % (self.state().dir_id, child.state().dir_id, child, subpath))
if len(subpath) == 0:
# print('%d, %d. Setting child: %d, %d' % (self.dir_id, child.dir_id, self.has_known_prefix, child.has_known_prefix))
# print('%d, %d. Setting child: %d, %d' % (self.state().dir_id, child.state().dir_id, self.state().has_known_prefix, child.state().has_known_prefix))
self._merge(child)
return self
else:
if not subpath[0] in self.children:
# print('%d, %d. Path %r was absent from %r (%r)' % (self.dir_id, child.dir_id, subpath[0:1], self, self.children))
subdir = DirectoryStateTreeNode(True, True, root=self.root)
self.children[subpath[0]] = subdir
if not subpath[0] in self.state().children:
# print('%d, %d. Path %r was absent from %r (%r)' % (self.state().dir_id, child.state().dir_id, subpath[0:1], self, self.state().children))
subdir = DirectoryStateTreeNode(True, True, root=self.state().root)
self.state().children[subpath[0]] = subdir
else:
subdir = self.children[subpath[0]]
# print('%d, %d. Path was present' % (self.dir_id, child.dir_id))
subdir = self.state().children[subpath[0]]
# print('%d, %d. Path was present' % (self.state().dir_id, child.state().dir_id))
if len(subpath) > 1:
subdir.state().has_known_prefix = False
subdir.has_known_prefix = subdir.has_known_prefix and len(subpath) == 1 # For the last element in the path, the merge will take care of has_known_prefix
return subdir._add_child_impl(subpath[1:], child)
def _merge(self, other):
if self.dir_id == other.dir_id:
if self.state().dir_id == other.state().dir_id:
return
self.is_directory = self.is_directory and other.is_directory
self.is_subspace = self.is_subspace and other.is_subspace
self.has_known_prefix = self.has_known_prefix and other.has_known_prefix
self.deleted = self.deleted or other.deleted
self.is_partition = self.is_partition or other.is_partition
other.root = self.root
other.is_directory = self.is_directory
other.is_subspace = self.is_subspace
other.has_known_prefix = self.has_known_prefix
other.deleted = self.deleted
other.is_partition = self.is_partition
other.dir_id = min(other.dir_id, self.dir_id)
self.dir_id = other.dir_id
self.state().dir_id = min(other.state().dir_id, self.state().dir_id)
self.state().is_directory = self.state().is_directory and other.state().is_directory
self.state().is_subspace = self.state().is_subspace and other.state().is_subspace
self.state().has_known_prefix = self.state().has_known_prefix and other.state().has_known_prefix
self.state().deleted = self.state().deleted or other.state().deleted
self.state().is_partition = self.state().is_partition or other.state().is_partition
other_children = other.children.copy()
for c in other_children:
if c not in self.children:
self.children[c] = other_children[c]
other.children = self.children
other_children = other.state().children.copy()
other.ptr.set(self.state())
other.ptr = self.ptr
for c in other_children:
self.children[c]._merge(other_children[c])
if c not in self.state().children:
self.state().children[c] = other_children[c]
for c in other_children:
self.state().children[c]._merge(other_children[c])
def _delete_impl(self):
if not self.deleted:
self.deleted = True
for c in self.children.values():
if not self.state().deleted:
self.state().deleted = True
for c in self.state().children.values():
c._delete_impl()
def delete(self, path):
@ -139,10 +152,10 @@ class DirectoryStateTreeNode:
child._delete_impl()
def validate_dir(dir, root):
if dir.is_directory:
assert dir.root == root
if dir.state().is_directory:
assert dir.state().root == root
else:
assert dir.root == dir
assert dir.state().root == dir
def run_test():
all_entries = []
@ -177,7 +190,7 @@ def run_test():
# This directory was merged with the default, but both have readable prefixes
entry = root.get_descendent(('2',))
assert entry.has_known_prefix
assert entry.state().has_known_prefix
entry = all_entries[-1]
all_entries.append(entry.add_child(('1',), DirectoryStateTreeNode(True, True, has_known_prefix=True)))
@ -191,59 +204,59 @@ def run_test():
all_entries.extend(child_entries)
# This directory has an known prefix
# This directory has an unknown prefix
entry = root.get_descendent(('1', '2'))
assert not entry.has_known_prefix
assert not entry.state().has_known_prefix
# This directory was default created and should have an unknown prefix
# It will merge with the default directory's child, which is not a subspace
entry = root.get_descendent(('1',))
assert not entry.has_known_prefix
assert not entry.is_subspace
assert not entry.state().has_known_prefix
assert not entry.state().is_subspace
# Multiple merges will have made this prefix unreadable
entry = root.get_descendent(('2',))
assert not entry.has_known_prefix
assert not entry.state().has_known_prefix
# Merge with default directory's child that has an unknown prefix
entry = root.get_descendent(('3',))
assert not entry.has_known_prefix
assert not entry.state().has_known_prefix
# Merge with default directory's child that has an unknown prefix and merged children
entry = root.get_descendent(('1', '3'))
assert set(entry.children.keys()) == {'1', '2', '3', '4', '5', '6'}
assert set(entry.state().children.keys()) == {'1', '2', '3', '4', '5', '6'}
# This child entry should be the combination of ['default', '3'], ['default', '1', '3'], and ['1', '3']
entry = entry.get_descendent(('3',))
assert not entry.has_known_prefix
assert not entry.is_subspace
assert not entry.state().has_known_prefix
assert not entry.state().is_subspace
# Verify the merge of the children
assert not child_entries[0].has_known_prefix
assert not child_entries[0].is_subspace
assert not child_entries[0].state().has_known_prefix
assert not child_entries[0].state().is_subspace
assert not child_entries[1].has_known_prefix
assert child_entries[1].is_subspace
assert not child_entries[1].state().has_known_prefix
assert child_entries[1].state().is_subspace
assert not child_entries[2].has_known_prefix
assert not child_entries[2].is_subspace
assert not child_entries[2].state().has_known_prefix
assert not child_entries[2].state().is_subspace
assert not child_entries[3].has_known_prefix
assert not child_entries[3].is_subspace
assert not child_entries[3].state().has_known_prefix
assert not child_entries[3].state().is_subspace
assert child_entries[4].has_known_prefix
assert not child_entries[4].is_subspace
assert child_entries[4].state().has_known_prefix
assert not child_entries[4].state().is_subspace
assert child_entries[5].has_known_prefix
assert child_entries[5].is_subspace
assert child_entries[5].state().has_known_prefix
assert child_entries[5].state().is_subspace
entry = root.add_child(('3',), entry_to_move)
all_entries.append(entry)
# Test moving an entry
assert not entry.has_known_prefix
assert not entry.is_subspace
assert entry.children.keys() == ['1']
assert not entry.state().has_known_prefix
assert not entry.state().is_subspace
assert entry.state().children.keys() == ['1']
for e in all_entries:
validate_dir(e, root)