Skip to content

Commit 201319c

Browse files
author
David Scherer
committed
directory and subspace
--HG-- branch : feature-directory
1 parent 3c70a53 commit 201319c

2 files changed

Lines changed: 278 additions & 0 deletions

File tree

lib/directory.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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)

lib/subspace.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
###################################
2+
# This defines a Subspace of keys #
3+
###################################
4+
5+
import fdb.tuple
6+
7+
class Subspace (object):
8+
def __init__(self, prefixTuple=tuple(), rawPrefix=""):
9+
self.rawPrefix = rawPrefix + fdb.tuple.pack(prefixTuple)
10+
11+
def __repr__(self):
12+
return 'Subspace(rawPrefix=' + repr(self.rawPrefix) + ')'
13+
14+
def __getitem__(self, name):
15+
return Subspace( (name,), self.rawPrefix )
16+
17+
def key(self):
18+
return self.rawPrefix
19+
20+
def pack(self, t = tuple()):
21+
return self.rawPrefix + fdb.tuple.pack( t )
22+
23+
def unpack(self, key):
24+
assert key.startswith(self.rawPrefix)
25+
return fdb.tuple.unpack(key[len(self.rawPrefix):])
26+
27+
def range(self, t = tuple()):
28+
p = fdb.tuple.range( t )
29+
return slice(self.rawPrefix + p.start, self.rawPrefix + p.stop)
30+
31+
def contains(self, key):
32+
return key.startswith(self.rawPrefix)
33+
34+
def as_foundationdb_key(self):
35+
return self.rawPrefix
36+
37+
def subspace(self, tuple):
38+
return Subspace( tuple, self.rawPrefix )

0 commit comments

Comments
 (0)