|
| 1 | +from subspace import Subspace |
| 2 | +import fdb, fdb.tuple |
| 3 | +import random, struct |
| 4 | + |
| 5 | +fdb.api_version(23) |
| 6 | + |
| 7 | +#TODO: Error class |
| 8 | + |
| 9 | +class HighContentionAllocator (object): |
| 10 | + def __init__(self, subspace): |
| 11 | + self.counters = subspace[0] |
| 12 | + self.recent = subspace[1] |
| 13 | + |
| 14 | + @fdb.transactional |
| 15 | + def allocate( self, tr ): |
| 16 | + """Returns a byte string which |
| 17 | + (1) has never and will never be returned by another call to HighContentionAllocator.allocate() on the same subspace |
| 18 | + (2) is nearly as short as possible given the above""" |
| 19 | + |
| 20 | + [(start, count)] = [ (self.counters.unpack(k)[0],struct.unpack("<q",v)[0]) for k,v in tr.snapshot.get_range( self.counters.range().start, self.counters.range().stop, limit=1, reverse=True ) ] or [ (0,0) ] |
| 21 | + |
| 22 | + window = self._window_size(start) |
| 23 | + if (count+1)*2 >= window: |
| 24 | + # Advance the window |
| 25 | + del tr[ self.counters : self.counters[start].key()+chr(0) ] |
| 26 | + start += window |
| 27 | + del tr[ self.recent : self.recent[start] ] |
| 28 | + window = self._window_size(start) |
| 29 | + |
| 30 | + # Increment the allocation count for the current window |
| 31 | + tr.add( self.counters[start], struct.pack("<q", 1) ) |
| 32 | + |
| 33 | + while True: |
| 34 | + # As of the snapshot we are reading from, the window is less than half full, so |
| 35 | + # this should be expected 2 tries. Under high contention (and when the window advances), |
| 36 | + # there is an additional subsequent risk of conflict for this transaction. |
| 37 | + candidate = random.randint( start, start+window ) |
| 38 | + if tr[ self.recent[candidate] ] == None: |
| 39 | + tr[ self.recent[candidate] ] = "" |
| 40 | + return fdb.tuple.pack( (candidate,) ) |
| 41 | + |
| 42 | + def _window_size(self, start): |
| 43 | + # Larger window sizes are better for high contention, smaller for keeping the keys small. But if |
| 44 | + # there are lots of allocations the keys can't be too small. So start small and scale up. We don't |
| 45 | + # want this to ever get *too* big because we have to store about window_size/2 recent items. |
| 46 | + if start < 255: return 64 |
| 47 | + if start < 65535: return 1024 |
| 48 | + return 8192 |
| 49 | + |
| 50 | +class DirectoryLayer (object): |
| 51 | + def __init__(self, node_subspace = Subspace( rawPrefix="\xfe" ), content_subspace = Subspace() ): |
| 52 | + self.content_subspace = content_subspace |
| 53 | + self.node_subspace = node_subspace |
| 54 | + # The root node is the one whose contents are the node subspace |
| 55 | + self.root_node = self.node_subspace[ self.node_subspace.key() ] |
| 56 | + self.allocator = HighContentionAllocator( self.root_node['hca'] ) |
| 57 | + |
| 58 | + @fdb.transactional |
| 59 | + def create_or_open( self, tr, path, layer=None, prefix=None, allow_create=True, allow_open=True ): |
| 60 | + """Opens the directory with the given path. |
| 61 | + If the directory does not exist, it is created (creating parent directories if necessary). |
| 62 | + If prefix is specified, the directory is created with the given physical prefix; otherwise a prefix is allocated automatically. |
| 63 | + If layer is specified, it is checked against the layer of an existing directory or set as the layer of a new directory.""" |
| 64 | + if isinstance(path, str): path=(path,) |
| 65 | + if not path: raise ValueError( "The root directory may not be opened." ) # Because it contains node metadata! |
| 66 | + existing_node = self._find(tr, path) |
| 67 | + if existing_node: |
| 68 | + if not allow_open: raise ValueError("The directory already exists.") |
| 69 | + existing_layer = tr[ existing_node['layer'].key() ] |
| 70 | + if layer and existing_layer and existing_layer != layer: |
| 71 | + raise ValueError( "The directory exists but was created with an incompatible layer." ) |
| 72 | + return self._contents_of_node(existing_node, path, existing_layer) |
| 73 | + if not allow_create: raise ValueError("The directory does not exist.") |
| 74 | + |
| 75 | + if prefix==None: |
| 76 | + prefix = self.allocator.allocate(tr) |
| 77 | + |
| 78 | + if not self._is_prefix_free(tr, prefix): |
| 79 | + raise ValueError("The given prefix is already in use.") |
| 80 | + |
| 81 | + if path[:-1]: |
| 82 | + parent_node = self._node_with_prefix( self.create_or_open(tr, path[:-1], layer=None).key() ) |
| 83 | + else: |
| 84 | + parent_node = self.root_node |
| 85 | + #parent_node = self._find(tr, path[:-1]) |
| 86 | + if not parent_node: |
| 87 | + print repr(path[:-1]) |
| 88 | + raise ValueError("The parent directory doesn't exist.") |
| 89 | + |
| 90 | + node = self._node_with_prefix(prefix) |
| 91 | + tr[ parent_node[self.SUBDIRS][ path[-1] ].key() ] = prefix |
| 92 | + if layer: tr[ node['layer'].key() ] = layer |
| 93 | + |
| 94 | + return self._contents_of_node(node, path, layer) |
| 95 | + |
| 96 | + def open( self, db_or_tr, path, layer=None ): |
| 97 | + """Opens the directory with the given path. |
| 98 | + If the directory does not exist, an error is raised. |
| 99 | + If layer is specified, and a different layer was specified when the directory was created, an error is raised.""" |
| 100 | + return self.create_or_open(db_or_tr, path, layer, allow_create=False) |
| 101 | + def create( self, db_or_tr, path, layer=None, prefix=None ): |
| 102 | + """Creates a directory with the given path (creating parent directories if necessary). |
| 103 | + If the given directory already exists, an error is raised. |
| 104 | + If prefix is specified, the directory is created with the given physical prefix; otherwise a prefix is allocated automatically. |
| 105 | + If layer is specified, it is recorded with the directory and will be checked by future calls to open.""" |
| 106 | + return self.create_or_open(db_or_tr, path, layer, prefix, allow_open=False) |
| 107 | + |
| 108 | + @fdb.transactional |
| 109 | + def move( self, tr, old_path, new_path ): |
| 110 | + """Moves the directory found at `old_path` to `new_path`. |
| 111 | + There is no effect on the physical prefix of the given directory, or on clients that already have the directory open. |
| 112 | + If the old directory does not exist, a directory already exists at `new_path`, or the parent directory of `new_path` |
| 113 | + does not exist, an error is raised.""" |
| 114 | + if isinstance(old_path, str): old_path=(old_path,) |
| 115 | + if isinstance(new_path, str): new_path=(new_path,) |
| 116 | + if self._find(tr, new_path): raise ValueError( "The destination directory already exists. Remove it first." ) |
| 117 | + old_node = self._find(tr, old_path) |
| 118 | + if not old_node: raise ValueError("The source directory does not exist.") |
| 119 | + parent_node = self._find(tr, new_path[:-1] ) |
| 120 | + if not parent_node: raise ValueError( "The parent of the destination directory does not exist. Create it first." ) |
| 121 | + tr[ parent_node[self.SUBDIRS][ new_path[-1] ].key() ] = self._contents_of_node( old_node, None ).key() |
| 122 | + self._remove_from_parent( tr, old_path ) |
| 123 | + return self._contents_of_node( old_node, new_path, tr[ old_node['layer'].key() ] ) |
| 124 | + |
| 125 | + @fdb.transactional |
| 126 | + def remove( self, tr, path ): |
| 127 | + """Removes the directory, its contents and all subdirectories transactionally. |
| 128 | + Warning: Clients which have already opened the directory might still insert data into its contents after it is removed.""" |
| 129 | + if isinstance(path, str): path=(path,) |
| 130 | + n = self._find(tr, path) |
| 131 | + if not n: raise ValueError( "The directory doesn't exist." ) |
| 132 | + self._remove_recursive(tr, n) |
| 133 | + self._remove_from_parent(tr, path) |
| 134 | + |
| 135 | + @fdb.transactional |
| 136 | + def list( self, tr, path=() ): |
| 137 | + if isinstance(path, str): path=(path,) |
| 138 | + node = self._find( tr, path) |
| 139 | + if not node: |
| 140 | + raise ValueError("The given directory does not exist.") |
| 141 | + return [name for name, cnode in self._subdir_names_and_nodes(tr, node)] |
| 142 | + |
| 143 | + ### IMPLEMENTATION ### |
| 144 | + SUBDIRS=0 |
| 145 | + |
| 146 | + def _node_containing_key(self, tr, key): |
| 147 | + # Right now this is only used for _is_prefix_free(), but if we add parent pointers to directory nodes, |
| 148 | + # it could also be used to find a path based on a key |
| 149 | + if key.startswith(self.node_subspace.key()): |
| 150 | + return self.root_node |
| 151 | + for k,v in tr.get_range( self.node_subspace.range( () ).start, |
| 152 | + self.node_subspace.pack( (key,) )+"\x00", |
| 153 | + reverse=True, |
| 154 | + limit=1 ): |
| 155 | + prev_prefix = self.node_subspace.unpack( k )[0] |
| 156 | + if key.startswith(prev_prefix): |
| 157 | + return Subspace( rawPrefix=k ) # self.node_subspace[prev_prefix] |
| 158 | + return None |
| 159 | + |
| 160 | + def _node_with_prefix( self, prefix ): |
| 161 | + if prefix==None: return None |
| 162 | + return self.node_subspace[prefix] |
| 163 | + |
| 164 | + def _contents_of_node( self, node, path, layer=None ): |
| 165 | + prefix = self.node_subspace.unpack( node.key() )[0] |
| 166 | + return DirectorySubspace( path, prefix, self, layer ) |
| 167 | + |
| 168 | + def _find( self, tr, path ): |
| 169 | + n = self.root_node |
| 170 | + for name in path: |
| 171 | + n = self._node_with_prefix( tr[ n[self.SUBDIRS][name].key() ] ) |
| 172 | + if n == None: |
| 173 | + return None |
| 174 | + return n |
| 175 | + |
| 176 | + def _subdir_names_and_nodes( self, tr, node ): |
| 177 | + sd = node[self.SUBDIRS] |
| 178 | + for k,v in tr[sd.range(())]: |
| 179 | + yield sd.unpack(k)[0], self._node_with_prefix( v ) |
| 180 | + |
| 181 | + def _remove_from_parent( self, tr, path ): |
| 182 | + parent = self._find( tr, path[:-1] ) |
| 183 | + del tr[ parent[self.SUBDIRS][ path[-1] ].key() ] |
| 184 | + |
| 185 | + def _remove_recursive( self, tr, node): |
| 186 | + for name, sn in self._subdir_names_and_nodes(tr, node): |
| 187 | + self._remove_recursive(tr, sn) |
| 188 | + tr.clear_range_startswith( self._contents_of_node(node,None).key() ) |
| 189 | + del tr[ node.range(()) ] |
| 190 | + |
| 191 | + def _is_prefix_free( self, tr, prefix ): |
| 192 | + # Returns true if the given prefix does not intersect any currently allocated prefix |
| 193 | + # (including the root node). This means that it neither contains any other prefix nor |
| 194 | + # is contained by any other prefix. |
| 195 | + return prefix and not self._node_containing_key( tr, prefix ) and not len(list(tr.get_range( self.node_subspace.pack( (prefix,) ), self.node_subspace.pack( (strinc(prefix),) ), limit=1 ))) |
| 196 | + |
| 197 | +directory = DirectoryLayer() |
| 198 | + |
| 199 | +class DirectorySubspace (Subspace): |
| 200 | + # A DirectorySubspace represents the *contents* of a directory, but it also remembers |
| 201 | + # the path it was opened with and offers convenience methods to operate on the directory |
| 202 | + # at that path. |
| 203 | + def __init__(self, path, prefix, directoryLayer=directory, layer=None): |
| 204 | + Subspace.__init__(self, rawPrefix=prefix) |
| 205 | + self.path = path |
| 206 | + self.directoryLayer = directoryLayer |
| 207 | + self.layer = layer |
| 208 | + |
| 209 | + def __repr__(self): |
| 210 | + return 'DirectorySubspace(' + repr(self.path) + ',' + repr(self.rawPrefix) + ')' |
| 211 | + |
| 212 | + def check_layer(self, layer): |
| 213 | + if layer and self.layer and layer!=self.layer: |
| 214 | + raise ValueError("The directory was created with an incompatible layer.") |
| 215 | + |
| 216 | + def create_or_open( self, db_or_tr, name_or_path, layer=None, prefix=None ): |
| 217 | + if not isinstance( name_or_path, tuple ): name_or_path = (name_or_path,) |
| 218 | + return self.directoryLayer.create_or_open( db_or_tr, self.path + name_or_path, layer, prefix ) |
| 219 | + def open( self, db_or_tr, name_or_path, layer=None ): |
| 220 | + if not isinstance( name_or_path, tuple ): name_or_path = (name_or_path,) |
| 221 | + return self.directoryLayer.open( db_or_tr, self.path + name_or_path, layer ) |
| 222 | + def create( self, db_or_tr, name_or_path, layer=None ): |
| 223 | + if not isinstance( name_or_path, tuple ): name_or_path = (name_or_path,) |
| 224 | + return self.directoryLayer.create( db_or_tr, self.path + name_or_path, layer ) |
| 225 | + def move_to( self, db_or_tr, new_path ): |
| 226 | + return self.directoryLayer.moveTo( db_or_tr, self.path, new_path ) |
| 227 | + def remove( self, db_or_tr ): |
| 228 | + return self.directoryLayer.remove( db_or_tr, self.path ) |
| 229 | + def list( self, db_or_tr ): |
| 230 | + return self.directoryLayer.list( db_or_tr, self.path ) |
| 231 | + |
| 232 | +def random_key(): |
| 233 | + return uuid.uuid4().bytes |
| 234 | + |
| 235 | +def strinc(key): |
| 236 | + lastc = (ord(key[-1:]) + 1) % 256 |
| 237 | + if lastc: |
| 238 | + return key[:-1] + chr(lastc) |
| 239 | + else: |
| 240 | + return strinc(key[:-1]) + chr(lastc) |
0 commit comments